diff --git a/AgCloud/.github/CODEOWNERS b/AgCloud/.github/CODEOWNERS new file mode 100644 index 000000000..15f9b0fff --- /dev/null +++ b/AgCloud/.github/CODEOWNERS @@ -0,0 +1 @@ +* @KamaTechOrg @SaraShimon @hadasaGIT @tamarmar @ExtraTech-helper diff --git a/AgCloud/.github/workflows/ci.yml.save b/AgCloud/.github/workflows/ci.yml.save new file mode 100644 index 000000000..18cb2bbd6 --- /dev/null +++ b/AgCloud/.github/workflows/ci.yml.save @@ -0,0 +1,68 @@ +name: CI - Tests (unit + integration) + +on: + push: + branches: ["**"] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + defaults: + run: + working-directory: mqtt_images + + env: + + S3_ENDPOINT: http://localhost:9000 + AWS_ACCESS_KEY_ID: minioadmin + AWS_SECRET_ACCESS_KEY: minioadmin + AWS_REGION: us-east-1 + MQTT_HOST: localhost + MQTT_PORT: 1883 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install Python deps + run: | + python -m pip install --upgrade pip + + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + + if [ -f requirements.txt ]; then pip install -r requirements.txt || true; fi + + + - name: Docker Compose up (MinIO, Mosquitto, etc.) + run: | + docker compose -f docker-compose.yml up -d --build + echo ">> Wait for MinIO to be ready..." + for i in {1..60}; do + curl -fsS http://localhost:9000/minio/health/ready && break + sleep 2 + done + echo ">> Compose services:" + docker compose -f docker-compose.yml ps + + - name: Run tests (pytest) + run: | + pytest -q + + # Show docker compose logs on failure – very useful for debugging + - name: Show docker compose logs on failure + if: failure() + run: docker compose -f docker-compose.yml logs --no-color diff --git a/AgCloud/.github/workflows/daily_pytest_slack.yml b/AgCloud/.github/workflows/daily_pytest_slack.yml new file mode 100644 index 000000000..d8285bf6b --- /dev/null +++ b/AgCloud/.github/workflows/daily_pytest_slack.yml @@ -0,0 +1,83 @@ +name: Daily Pytest + Slack (IL 01:00) + +on: + schedule: + # 01:00 Israel time — 22:00 UTC (summer), 23:00 UTC (winter) + - cron: "0 22 * * *" + - cron: "0 23 * * *" + workflow_dispatch: + +jobs: + run_pytests_and_notify: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Run pytest (and keep log) + run: | + pytest -q --maxfail=50 --disable-warnings -rA \ + --junitxml=pytest-report.xml > pytest.log 2>&1 || true + + - name: Parse results + id: results + run: | + python - <<'PY' + import xml.etree.ElementTree as ET + import os + counts = dict(tests=0, failures=0, errors=0, skipped=0) + try: + tree = ET.parse("pytest-report.xml") + root = tree.getroot() + for suite in root.findall(".//testsuite"): + counts["tests"] += int(suite.attrib.get("tests", 0)) + counts["failures"] += int(suite.attrib.get("failures", 0)) + counts["errors"] += int(suite.attrib.get("errors", 0)) + counts["skipped"] += int(suite.attrib.get("skipped", 0)) + except Exception as e: + print("Parse error:", e) + counts["passed"] = counts["tests"] - counts["failures"] - counts["errors"] - counts["skipped"] + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + for k,v in counts.items(): + f.write(f"{k}={v}\n") + f.write(f"has_failures={'true' if (counts['failures']>0 or counts['errors']>0) else 'false'}\n") + PY + + - name: Send Slack notification (if failures) + if: steps.results.outputs.has_failures == 'true' + uses: slackapi/slack-github-action@v1.25.0 + with: + payload: | + { + "channel": "#vast", + "username": "GitHub Actions", + "icon_emoji": ":rotating_light:", + "text": "🚨 *Pytest Failures Detected!*\n\nRepository: ${{ github.repository }}\nRun: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>\n\n*Passed:* ${{ steps.results.outputs.passed }} / ${{ steps.results.outputs.tests }}\n*Failed:* ${{ steps.results.outputs.failures }}\n*Errors:* ${{ steps.results.outputs.errors }}\n*Skipped:* ${{ steps.results.outputs.skipped }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Send Slack success notification + if: steps.results.outputs.has_failures == 'false' + uses: slackapi/slack-github-action@v1.25.0 + with: + payload: | + { + "channel": "#vast", + "username": "GitHub Actions", + "icon_emoji": ":white_check_mark:", + "text": "✅ All tests passed successfully!\n\nRepository: ${{ github.repository }}\nRun: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>\n\nTotal tests: ${{ steps.results.outputs.tests }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/AgCloud/.github/workflows/soak.yaml b/AgCloud/.github/workflows/soak.yaml new file mode 100644 index 000000000..b3c02dd11 --- /dev/null +++ b/AgCloud/.github/workflows/soak.yaml @@ -0,0 +1,257 @@ +name: Soak Test (Real MQTT→Kafka via Compose) + +on: + workflow_dispatch: + push: + branches: [ "main", "sounds-ruth", "ruthhermelin/soak" ] + +jobs: + soak: + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + ARTIFACT_DIR: artifacts + SOAK_DURATION_SEC: "120" + SOAK_RATE_PER_SEC: "1000" + MQTT_TOPIC: "mqtt/soak" + KAFKA_TOPIC: "dev-robot-alerts" + LOSS_THRESHOLD_PCT: "1.0" # סף כשל: % אובדן מותר (שני לפי הצורך) + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare artifacts dir + run: mkdir -p "$ARTIFACT_DIR" + + - name: Show Docker versions + run: | + docker --version || true + docker compose version || true + + - name: Create dummy MinIO env file (for CI) + run: | + mkdir -p storage_with_mqtt/storage/combined_minio_setup + cat > storage_with_mqtt/storage/combined_minio_setup/.env <<'EOF' + MINIO_ROOT_USER=minioadmin + MINIO_ROOT_PASSWORD=minioadmin123 + EOF + - name: Prepare env for db_api_service + run: | + mkdir -p services/db_api_service + printf '%s\n' \ + 'DB_DSN=postgresql+psycopg://missions_user:pg123@postgres:5432/missions_db' \ + 'ENV=ci' \ + 'JWT_SECRET=change-me-in-ci' \ + 'JWT_ALGO=HS256' \ + 'ACCESS_TTL_MIN=15' \ + 'REFRESH_TTL_DAYS=14' \ + 'DEV_SA_NAME=ci-service' \ + > services/db_api_service/.env + - name: Prepare env for plant_stress + run: | + mkdir -p services/plant_stress + cat > services/plant_stress/.env <<'EOF' + ADDR=0.0.0.0 + PORT=8001 + MINIO_ENDPOINT=minio:9000 + MINIO_ACCESS_KEY=minioadmin + MINIO_SECRET_KEY=minioadmin123 + MINIO_BUCKET=audio + MINIO_PREFIX=samples/ + WINDOW_MIN=5 + EOF + + - name: Start core stack + run: docker compose up -d kafka mosquitto connect + + - name: Wait for Kafka & Mosquitto & Connect + run: | + set -euo pipefail + + echo "== Wait Kafka ==" + for i in $(seq 1 60); do + s=$(docker inspect -f '{{.State.Health.Status}}' kafka || echo unknown) + [ "$s" = "healthy" ] && break + echo "Kafka: $s ($i/60)"; sleep 3 + done + [ "$(docker inspect -f '{{.State.Health.Status}}' kafka)" = "healthy" ] || { echo "Kafka not healthy"; exit 1; } + + echo "== Wait Mosquitto ==" + for i in $(seq 1 40); do + s=$(docker inspect -f '{{.State.Health.Status}}' mosquitto || echo unknown) + [ "$s" = "healthy" ] && break + echo "Mosquitto: $s ($i/40)"; sleep 3 + done + [ "$(docker inspect -f '{{.State.Health.Status}}' mosquitto)" = "healthy" ] || { echo "Mosquitto not healthy"; exit 1; } + + echo "== Wait Connect REST ==" + READY=0 + for i in $(seq 1 60); do + if curl -sf http://localhost:8083/connectors >/dev/null; then + echo "Connect REST is ready!" + READY=1 + break + fi + echo "Connect REST not ready ($i/60)"; sleep 3 + done + if [ "$READY" -ne 1 ]; then + echo "ERROR: Connect REST not ready after timeout" + docker compose logs connect --tail=200 || true + exit 1 + fi + + - name: Create Kafka topic if missing + run: | + set -euo pipefail + docker compose exec -T kafka bash -lc "\ + /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list | grep -qx '${KAFKA_TOPIC}' \ + || /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic '${KAFKA_TOPIC}' --partitions 1 --replication-factor 1" + + - name: Ensure MQTT source connector exists (mosquitto -> ${KAFKA_TOPIC}) + run: | + set -euo pipefail + cat > mqtt-source.json </dev/null; then + curl -sS -X POST -H 'Content-Type: application/json' --data @mqtt-source.json http://localhost:8083/connectors \ + || { echo "Connector create failed"; curl -s http://localhost:8083/connectors; exit 1; } + fi + echo "MQTT connector status:" + curl -sSf http://localhost:8083/connectors/mqtt-source/status || true + + - name: Determine compose network name + id: net + run: | + set -euo pipefail + NET=$(docker network ls --format '{{.Name}}' | grep '_ag_cloud$' | head -1) + echo "net=$NET" >> $GITHUB_OUTPUT + echo "Using network: $NET" + + - name: Start Kafka consumer (kcat) on compose network + run: | + set -euo pipefail + NET="${{ steps.net.outputs.net }}" + docker run --rm --name soak-kcat -i --network "$NET" edenhill/kcat:1.7.1 \ + -b kafka:9092 -t "${KAFKA_TOPIC}" -C -o end -q \ + > "${ARTIFACT_DIR}/kafka-received.log" 2> "${ARTIFACT_DIR}/kafka-consumer.err" & + echo $! > kcat.pid + sleep 3 + + - name: Publish load to MQTT on compose network + run: | + set -euo pipefail + NET="${{ steps.net.outputs.net }}" + D=${SOAK_DURATION_SEC} + RATE=${SOAK_RATE_PER_SEC} + TOPIC=${MQTT_TOPIC} + + echo "Publishing D=$D sec, RATE=$RATE msg/s to $TOPIC" + for sec in $(seq 1 "$D"); do + echo "[$sec/$D] batch $sec" + docker run --rm --network "$NET" alpine:3.20 \ + sh -lc "apk add --no-cache mosquitto-clients >/dev/null \ + && awk -v n=$RATE -v b=$sec 'BEGIN{for(i=0;i/dev/null || true; fi + + D=${SOAK_DURATION_SEC} + RATE=${SOAK_RATE_PER_SEC} + SENT=$((D*RATE)) + RECEIVED=$(wc -l < "${ARTIFACT_DIR}/kafka-received.log" 2>/dev/null || echo 0) + LOSS=$((SENT-RECEIVED)) + LOSS_PCT=$(awk -v s="$SENT" -v r="$RECEIVED" 'BEGIN{ if(s>0){printf "%.2f", (s-r)*100.0/s}else{printf "0.0"} }') + echo "Sent: $SENT" + echo "Received: $RECEIVED" + echo "Lost: $LOSS ($LOSS_PCT%)" + echo "{\"component\":\"mqtt-to-kafka\",\"sent\":$SENT,\"received\":$RECEIVED,\"loss\":$LOSS,\"loss_pct\":$LOSS_PCT}" \ + | tee "${ARTIFACT_DIR}/bridge.json" + + - name: Generate JUnit + run: | + python - <<'PY' + import json, os, xml.etree.ElementTree as ET + art = os.getenv("ARTIFACT_DIR","artifacts") + thr = float(os.getenv("LOSS_THRESHOLD_PCT","1.0")) + r = {"loss_pct":100.0} + p = os.path.join(art, "bridge.json") + if os.path.exists(p): + with open(p) as f: r = json.load(f) + ts = ET.Element("testsuite", name="mqtt-kafka-soak", tests="1") + tc = ET.SubElement(ts, "testcase", classname="soak", name="mqtt->kafka") + if float(r.get("loss_pct",100.0)) > thr: + ET.SubElement(tc, "failure", message=str(r)).text = json.dumps(r) + failures = 1 + else: + failures = 0 + ts.set("failures", str(failures)) + os.makedirs(art, exist_ok=True) + ET.ElementTree(ts).write(os.path.join(art, "junit.xml"), encoding="utf-8", xml_declaration=True) + with open(os.path.join(art, "failures.txt"), "w") as f: f.write(str(failures)) + PY + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: mqtt-kafka-soak-artifacts + path: ${{ env.ARTIFACT_DIR }} + retention-days: 7 + + - name: Fail if tests failed + if: always() + run: | + set -euo pipefail + F=$(cat "$ARTIFACT_DIR/failures.txt" 2>/dev/null || echo 0) + echo "Failures reported: $F" + if [ "$F" -gt 0 ]; then + echo ":x: Tests failed!" + exit 1 + else + echo ":white_check_mark: All tests passed!" + fi + + - name: Cleanup (containers & volumes) + if: always() + run: | + set -euo pipefail + # ודא שהצרכן נעצר + if [ -f kcat.pid ]; then kill -9 "$(cat kcat.pid)" 2>/dev/null || true; fi + # נקה את סביבת ה-compose + docker compose down -v || true + # ניקוי נוסף למקרה של שאריות + docker ps -aq | xargs -r docker rm -f || true + docker network prune -f || true diff --git a/AgCloud/.gitignore b/AgCloud/.gitignore new file mode 100644 index 000000000..7a91a525c --- /dev/null +++ b/AgCloud/.gitignore @@ -0,0 +1,88 @@ +data/rover_samples/** +!data/rover_samples/.gitkeep + +# Ignore data payloads +/data/ +# --- Secrets and Certificates --- +*.env +*.crt +*/certs/ +**/certs/ +**/secrets/ +storage_with_mqtt/secrets/ +storage_with_mqtt/mqtt_images/secrets/ +MQTT_IMAGES/secrets/ +services/sounds/sounds_classifier/src/classification/data/ +services/sounds/sounds_classifier/src/classification/models/panns_data/ + +# Ignore environment and IDE files +.env + +# --- Python --- +__pycache__/ +*.py[cod] +*.pyc +*.pyo +*.pyd +*.pytest_cache/ +.pytest_cache/ + + +.venv/ +venv/ +.coverage + +# --- VSCode / Editor --- +.vscode/ +.idea/ + +# --- Docker / Build --- +*.log +*.pid +*.bak +*.tmp +*.swp +.env.local +.env.* +!.env.example + +# --- OS files --- +.DS_Store +Thumbs.db + + +# ==== Training/experiment outputs (never version) ==== +# Any top-level or nested "runs" folders created by Ultralytics or notebooks +runs*/ +**/runs*/ + +# ==== Model weights from training (PyTorch checkpoints) ==== +# Keep weights out of Git; publish via Releases/Artifacts instead. +!services/fence_hole_detector/weights/ +!services/fence_hole_detector/weights/best.onnx + +runs_fence/**/weights/*.pt + +# ==== Prediction artifacts (images + txt) ==== +# Generic preds folders created by `yolo predict` +runs_fence/**/preds/** +runs_fence/*_preds/** + +# Specific experiment outputs you listed (safe to ignore entirely) +runs_fence/y8n_baseline_no_roi/** +runs_fence/y8n_baseline_vote_soft/** +runs_fence/y8n_realtime_preds_no_roi/** +runs_fence/y8s_cpu_v1_preds/** + +# ==== Logs / plots (reproducible – don’t store) ==== +**/results.png +**/confusion_matrix.png +**/*.log + +# ==== Optional: large exported models (keep if you plan to ship them) ==== +# Uncomment to ignore ONNX as well; otherwise keep the single runtime ONNX in repo. +runs_fence/**/weights/*.onnx + +models/*.pt + +.coverage diff --git a/AgCloud/GUI/.gitignore b/AgCloud/GUI/.gitignore new file mode 100644 index 000000000..389e7967a Binary files /dev/null and b/AgCloud/GUI/.gitignore differ diff --git a/AgCloud/GUI/README.md b/AgCloud/GUI/README.md new file mode 100644 index 000000000..d467ce8f0 --- /dev/null +++ b/AgCloud/GUI/README.md @@ -0,0 +1,272 @@ +# AgCloud +# VAST Dashboard (PyQt6 + Grafana + Orthophoto + Sensors Gateway) + +A desktop monitoring dashboard built with **PyQt6**. It combines three data layers on a single Home screen: + +- **Grafana embeds (demo):** two Grafana panels rendered via QWebEngine (Prometheus demo data; not related to the map sensors). +- **Orthophoto map:** a fast tiled orthophoto viewer (QGraphicsView/QGraphicsScene) with smooth zoom & pan. +- **Live sensor overlay (real):** a DSL → SQL gateway backed by a gRPC runner querying a SQLite database and returning sensor rows that are plotted on the map. + +## Components + +- `src/vast/auth_ui/` – Login/Signup pages and a small in-memory auth service (app starts on Login; successful sign-in opens the main window; Logout returns to Login). +- `src/vast/orthophoto_canvas/` – Tiled viewer (`ui/viewer.py`), sensor overlay (`ui/sensors_layer.py`), data access (`ag_io/sensors_api.py`). +- `src/vast/dsl/` – A compact query DSL (JSON plan). +- `src/vast/runner/` – gRPC server executing SQL against SQLite (`data/app.db`). +- `src/vast/gateway/` – FastAPI app exposing `/runQuery`; translates DSL to SQL, calls the runner, and returns JSON. +- `src/vast/services/` – Flask utilities (Prometheus demo exporter, optional simple web map). +- `grafana/`, `prometheus/` – Dockerized Grafana/Prometheus for the demo panels. +- `src/vast/main.py`, `src/vast/main_window.py`, `src/vast/home_view.py` – Desktop shell (menu, navigation, and Home layout: Grafana on top, orthophoto below). + +## Requirements + +Install everything from the single unified requirements file at the repository root: + +```powershell +py -3.12 -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install --upgrade pip wheel +``` + +```bash +pip install -r requirements.txt +``` + +> Desktop GUI deps (PyQt6 / WebEngine) are installed on **Windows and macOS** only (via environment markers): +> +> ```text +> PyQt6==6.9.1 ; platform_system == "Windows" or platform_system == "Darwin" +> PyQt6-WebEngine==6.9.0 ; platform_system == "Windows" or platform_system == "Darwin" +> ``` +> Linux containers stay slim and avoid Qt/WebEngine system dependencies. If you develop the desktop app on Linux, +> install these two packages explicitly on your host: `pip install PyQt6 PyQt6-WebEngine`. + +--- + +## Run with Docker (recommended) + +Make sure build context is the **repo root** and Dockerfiles are referenced under `src/vast/...` in `docker-compose.yml`. + +```yaml +services: + runner: + build: + context: . + dockerfile: src/vast/runner/Dockerfile + environment: + - RUNNER_MODE=real + - SQLITE_DB=/data/app.db + - LOG_LEVEL=INFO + volumes: + - ./data:/data # RW mount for SQLite + ports: + - "50051:50051" + + gateway: + build: + context: . + dockerfile: src/vast/gateway/Dockerfile + environment: + - RUNNER_ADDR=runner:50051 + ports: + - "9001:9001" + + prometheus: + image: prom/prometheus:latest + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9090:9090" + + grafana: + image: grafana/grafana:latest + environment: + - GF_SECURITY_ALLOW_EMBEDDING=true + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer + - GF_USERS_DEFAULT_THEME=light + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro + ports: + - "3000:3000" + depends_on: + - prometheus +``` + +Build and run: + +```bash +docker compose build runner gateway +docker compose up -d runner gateway prometheus grafana +docker compose ps +``` + +Check services: +- Gateway API: `http://localhost:9001` +- Prometheus: `http://localhost:9090` +- Grafana: `http://localhost:3000` + +--- + +## Run locally (without Docker) + +Create and activate a venv, then run the services and desktop app in separate terminals. + +# Start Grafana/Prometheus (optional but recommended for the demo) +```bash +docker compose up -d prometheus grafana +docker compose ps +``` + + +### Terminal A – Runner (gRPC) +```bash +.\.venv\Scripts\Activate.ps1 +# Change the path according to the current location of the project +$env:SQLITE_DB = "/C:/Users/sara/Documents/login-and-gui/data/app.db" +python -m vast.runner.runner_server +``` + +### Terminal B – Gateway (FastAPI) +```bash +.\.venv\Scripts\Activate.ps1 +python -m uvicorn vast.gateway.app:create_app --factory --host 127.0.0.1 --port 9001 +``` + +### Terminal C – Demo metrics exporter (optional) +```bash +.\.venv\Scripts\Activate.ps1 +python -m vast.services.sensors_metrics_app +``` + +### Terminal D – Desktop app (PyQt6) +**Windows (PowerShell):** +```powershell +.\.venv\Scripts\Activate.ps1 +$env:GATEWAY_URL = "http://127.0.0.1:9001" +python .\src\vast\main.py +``` + +**macOS/Linux (bash):** +```bash +source .venv/bin/activate +export GATEWAY_URL="http://127.0.0.1:9001" +python ./src/vast/main.py +``` + +> Note (Linux/macOS): you may need to install system Qt dependencies for PyQt6 (including WebEngine). + +--- + +## Orthophoto Canvas (PyQt6) + +A tiled orthophoto viewer built with QGraphicsView/QGraphicsScene. It loads only the tiles that enter the viewport, keeps imagery crisp by snapping to native scales, and provides smooth navigation. + +### Features +- Lazy tile loading (only tiles in view are fetched) +- LOD (level-of-detail) with smart `z` selection +- XYZ/TMS auto-detection (flips Y when needed) +- Snap to native scale (crisp imagery, no blur) +- Smooth navigation: wheel to zoom, drag to pan +- Smart initial focus: fits to the real data extent + +### Quick start (desktop app) + +**Windows (PowerShell):** +```powershell +.\.venv\Scripts\Activate.ps1 +$env:GATEWAY_URL = "http://127.0.0.1:9001" +python .\src\vast\main.py +``` + +**macOS/Linux (bash):** +```bash +source .venv/bin/activate +export GATEWAY_URL="http://127.0.0.1:9001" +python ./src/vast/main.py +``` + +> Note (Linux/macOS): you may need to install system Qt dependencies for PyQt6 (including WebEngine). + +### Tile data layout (XYZ/TMS) +```text +tiles/ + {z}/ + {x}/ + {y}.png +``` + +### Hotkeys +- **Wheel**: zoom in/out +- **Drag**: pan +- **Shift + Wheel**: slow, precise zoom +- **F**: fit to data extent +- **W**: fit width (data extent) +- **G**: refocus to a known data tile + + +## Environment variables + +- `RUNNER_MODE` (runner): `real` or simulation mode (if implemented). +- `SQLITE_DB` (runner): path to SQLite DB (default `/data/app.db`). +- `LOG_LEVEL` (both): `INFO`, `DEBUG`, etc. +- `RUNNER_ADDR` (gateway): gRPC target, e.g. `runner:50051`. +- `GATEWAY_URL` (desktop): HTTP endpoint for the gateway, e.g. `http://127.0.0.1:9001`. + +--- + +## Notes + +- Keep Docker `build.context` at repo root so `requirements.txt`, `certs/`, `proto/` and source folders are visible to the build. +- If SQLite errors occur inside containers, ensure `./data` is mounted **RW** (no `:ro`) so WAL/journal files can be created. +- The `version:` key in `docker-compose.yml` is obsolete and can be removed. + +## Prerequisites + +- **Windows + PowerShell** +- **Python 3.12** +- **pip** (latest), **wheel** +- **Docker Desktop** (for Grafana/Prometheus demo) +- **Git** + +--- + +## Setup + +```powershell +# From the repository root +py -3.12 -m venv .venv +.\.venv\Scripts\Activate.ps1 + +python -m pip install --upgrade pip wheel + +# Install all project deps (top-level + module-specific) +pip install -r .\requirements.txt ` + +# Ensure binary wheels for these heavy packages (avoids build issues) +pip install --only-binary=:all: PyQt6 PyQt6-WebEngine grpcio grpcio-tools + + +## Running (4 terminals) +#Start Grafana/Prometheus (optional but recommended for the demo +docker compose up -d prometheus grafana +docker compose ps + +## Terminal A - Runner (gRPC → SQLite) +.\.venv\Scripts\Activate.ps1 +$env:SQLITE_DB = "/C:/Users/sara/Documents/login-and-gui/data/app.db" +python -m vast.runner.runner_server + +## Terminal B – Gateway (FastAPI) +.\.venv\Scripts\Activate.ps1 +python -m uvicorn vast.gateway.app:create_app --factory --host 127.0.0.1 --port 9001 + +## Terminal C - Demo metrics exporter (Flask) +.\.venv\Scripts\Activate.ps1 +python -m vast.services.sensors_metrics_app + +## Terminal D - Desktop app (PyQt6) +.\.venv\Scripts\Activate.ps1 +$env:GATEWAY_URL = "http://127.0.0.1:9001" +python .\src\vast\main.py diff --git a/AgCloud/GUI/data/app.db b/AgCloud/GUI/data/app.db new file mode 100644 index 000000000..bd07c813b Binary files /dev/null and b/AgCloud/GUI/data/app.db differ diff --git a/AgCloud/GUI/docker-compose.yml b/AgCloud/GUI/docker-compose.yml new file mode 100644 index 000000000..faba0316b --- /dev/null +++ b/AgCloud/GUI/docker-compose.yml @@ -0,0 +1,84 @@ + +services: + runner: + build: + context: . + dockerfile: src/vast/runner/Dockerfile + args: + USE_NETFREE: ${USE_NETFREE:-true} + environment: + - RUNNER_MODE=real + - SQLITE_DB=/data/app.db + - LOG_LEVEL=INFO + volumes: + - ./data:/data:ro + ports: + - "50051:50051" + restart: unless-stopped + + gateway: + build: + context: . + dockerfile: src/vast/gateway/Dockerfile + args: + USE_NETFREE: ${USE_NETFREE:-true} + environment: + - RUNNER_ADDR=runner:50051 + ports: + - "8000:8000" + depends_on: + - runner + restart: unless-stopped + + sensors_metrics: + build: + context: . + dockerfile: src/vast/services/Dockerfile + environment: + - SQLITE_DB=/data/app.db + volumes: + - ./data:/data:ro + depends_on: + - gateway + restart: unless-stopped + + prometheus: + image: prom/prometheus:latest + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + + grafana: + image: grafana/grafana:latest + ports: + - "3000:3000" + environment: + - GF_SECURITY_ALLOW_EMBEDDING=true + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer + - GF_USERS_DEFAULT_THEME=light + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro + depends_on: + - prometheus + + + desktop_app: + build: + context: . + dockerfile: src/vast/desktop/Dockerfile + environment: + - NO_VNC_PORT=8080 + - DISPLAY=host.docker.internal:0.0 + ports: + - "5900:5900" + - "8080:8080" + depends_on: + - gateway + - runner + volumes: + - ./src/vast:/app/src/vast + restart: unless-stopped + \ No newline at end of file diff --git a/AgCloud/GUI/generate_protos.py b/AgCloud/GUI/generate_protos.py new file mode 100644 index 000000000..3d1f2e6ee --- /dev/null +++ b/AgCloud/GUI/generate_protos.py @@ -0,0 +1,9 @@ +from grpc_tools import protoc + +protoc.main([ + 'protoc', + '-I./vast/proto', + '--python_out=./vast/proto/generated', + '--grpc_python_out=./vast/proto/generated', + './vast/proto/query.proto' +]) diff --git a/AgCloud/GUI/grafana/dashboards/grafana-dashboard-simple.json b/AgCloud/GUI/grafana/dashboards/grafana-dashboard-simple.json new file mode 100644 index 000000000..8dc444f1c --- /dev/null +++ b/AgCloud/GUI/grafana/dashboards/grafana-dashboard-simple.json @@ -0,0 +1,56 @@ +{ + "id": null, + "title": "Postgres – WAL, Replication, BRIN", + "tags": ["postgres", "prometheus"], + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "5s", + "panels": [ + { + "type": "graph", + "title": "WAL Throughput (bytes/s)", + "targets": [ + { + "expr": "rate(pg_wal_stats_wal_bytes[30s])", + "legendFormat": "WAL bytes/s" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "Replication Lag (bytes)", + "targets": [ + { + "expr": "pg_replication_lag_bytes_primary", + "legendFormat": "{{application_name}}" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "Replication Lag (seconds)", + "targets": [ + { + "expr": "pg_replication_replay_lag_seconds_standby", + "legendFormat": "standby lag" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "BRIN Hit Ratio by Index", + "targets": [ + { + "expr": "pg_brin_index_io_brin_hit_ratio", + "legendFormat": "{{schemaname}}.{{index_name}}" + } + ], + "datasource": "Prometheus" + } + ] + } + \ No newline at end of file diff --git a/AgCloud/GUI/grafana/dashboards/sensors.json b/AgCloud/GUI/grafana/dashboards/sensors.json new file mode 100644 index 000000000..0546e5d2f --- /dev/null +++ b/AgCloud/GUI/grafana/dashboards/sensors.json @@ -0,0 +1,29 @@ +{ + "uid": "agcloud-sensors", + "title": "Sensors", + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "10s", + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Active Sensors", + "gridPos": { "h": 6, "w": 8, "x": 0, "y": 0 }, + "targets": [ + { "expr": "sum(sensor_status)", "instant": true, "refId": "A" } + ] + }, + { + "id": 2, + "type": "bargauge", + "title": "Sensor Status (1=active, 0=inactive)", + "gridPos": { "h": 10, "w": 16, "x": 8, "y": 0 }, + "options": { "displayMode": "lcd", "orientation": "horizontal" }, + "targets": [ + { "expr": "sensor_status", "instant": true, "refId": "A", "legendFormat": "{{sensor}}" } + ] + } + ] +} diff --git a/AgCloud/GUI/grafana/provisioning/dashboards/dashboards.yaml b/AgCloud/GUI/grafana/provisioning/dashboards/dashboards.yaml new file mode 100644 index 000000000..6300c162c --- /dev/null +++ b/AgCloud/GUI/grafana/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'agcloud-dashboards' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards diff --git a/AgCloud/GUI/grafana/provisioning/datasources/prometheus.yaml b/AgCloud/GUI/grafana/provisioning/datasources/prometheus.yaml new file mode 100644 index 000000000..168dd4594 --- /dev/null +++ b/AgCloud/GUI/grafana/provisioning/datasources/prometheus.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + isDefault: true + url: http://prometheus:9090 + jsonData: + httpMethod: GET diff --git a/AgCloud/GUI/prometheus/prometheus.yml b/AgCloud/GUI/prometheus/prometheus.yml new file mode 100644 index 000000000..4ceb7fe8b --- /dev/null +++ b/AgCloud/GUI/prometheus/prometheus.yml @@ -0,0 +1,15 @@ +# prometheus.yml +global: + scrape_interval: 15s + +alerting: + alertmanagers: + - static_configs: + - targets: ['alertmanager:9093'] + +scrape_configs: + # Sensors metrics (GUI) + - job_name: 'sensors-demo' + static_configs: + - targets: ['sensors_metrics:8000'] + diff --git a/AgCloud/GUI/pyproject.toml b/AgCloud/GUI/pyproject.toml new file mode 100644 index 000000000..dd3fbcd0f --- /dev/null +++ b/AgCloud/GUI/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["setuptools>=69", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "agcloud-gui" +version = "0.1.0" +description = "AgCloud GUI (src layout)" +requires-python = ">=3.9" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/AgCloud/GUI/pytest.ini b/AgCloud/GUI/pytest.ini new file mode 100644 index 000000000..b8757d170 --- /dev/null +++ b/AgCloud/GUI/pytest.ini @@ -0,0 +1,3 @@ +# pytest.ini +[pytest] +pythonpath = . diff --git a/AgCloud/GUI/requirements.txt b/AgCloud/GUI/requirements.txt new file mode 100644 index 000000000..e22e519b4 --- /dev/null +++ b/AgCloud/GUI/requirements.txt @@ -0,0 +1,51 @@ + +PyQt6==6.9.1 +PyQt6-WebEngine==6.9.0 +PyQt6-Charts==6.9.0 +PyQt6-Qt6==6.9.1 +PyQt6-sip>=13.6 +python-vlc + + +# ───── Web/API ───── +fastapi>=0.110 +uvicorn[standard]>=0.29 +flask + +# ───── Metrics & HTTP ───── +prometheus-client>=0.20 +requests>=2.31 +httpx==0.27.0 # only needed when you switch to real Flink REST + +# ───── gRPC & Protobuf ───── +grpcio>=1.56,<2 +grpcio-tools>=1.56,<2 +protobuf>=6,<7 + +# ───── Validation / crypto / auth ───── +pydantic>=2.9,<3 +argon2-cffi +PyJWT>=2.9.0 + +# ───── Geospatial / Math ───── +shapely + +# ───── Async / misc ───── +aiohttp +folium +plotly +shapely +PyJWT>=2.9.0 +sip + +SQLAlchemy>=2.0 +geoalchemy2>=0.15.0 + +tenacity>=8.2 +openai>=1.0.0 +python-dotenv>=1.0.0 +jsonschema>=4.0.0 +psycopg2-binary>=2.9.0 +matplotlib>=3.7.0 + + diff --git a/AgCloud/GUI/sanity_qt.py b/AgCloud/GUI/sanity_qt.py new file mode 100644 index 000000000..a9ee23180 --- /dev/null +++ b/AgCloud/GUI/sanity_qt.py @@ -0,0 +1,10 @@ +# sanity_qt.py +import sys +from PyQt6.QtWidgets import QApplication, QWidget + +app = QApplication(sys.argv) +w = QWidget() +w.setWindowTitle("PyQt6 sanity") +w.resize(400, 300) +w.show() +app.exec() diff --git a/AgCloud/GUI/src/vast/__init__.py b/AgCloud/GUI/src/vast/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/alerts/alert_client.py b/AgCloud/GUI/src/vast/alerts/alert_client.py new file mode 100644 index 000000000..7212a10c8 --- /dev/null +++ b/AgCloud/GUI/src/vast/alerts/alert_client.py @@ -0,0 +1,57 @@ + +from PyQt6.QtCore import QObject, pyqtSignal, QUrl, QTimer +from PyQt6.QtWebSockets import QWebSocket +from PyQt6.QtNetwork import QAbstractSocket # ✅ add this +import json + + +class AlertClient(QObject): + """ + Connects to the alerts WebSocket gateway and emits signals + when new alerts or snapshots arrive. + """ + snapshotReceived = pyqtSignal(list) + alertReceived = pyqtSignal(dict) + connectionLost = pyqtSignal() + + def __init__(self, ws_url: str, parent=None): + super().__init__(parent) + self.url = QUrl(ws_url) + self.socket = QWebSocket() + self.socket.connected.connect(self._on_connected) + self.socket.disconnected.connect(self._on_disconnected) + self.socket.textMessageReceived.connect(self._on_message) + self.reconnect_timer = QTimer() + self.reconnect_timer.timeout.connect(self._try_reconnect) + self.reconnect_interval_ms = 5000 # retry every 5s + self._connect() + + def _connect(self): + print(f"[AlertClient] Connecting to {self.url.toString()}") + self.socket.open(self.url) + + def _try_reconnect(self): + # ✅ Use QAbstractSocket.SocketState instead of QWebSocket.SocketState + if self.socket.state() == QAbstractSocket.SocketState.ConnectedState: + self.reconnect_timer.stop() + return + print("[AlertClient] Attempting reconnect...") + self._connect() + + + def _on_connected(self): + print("[AlertClient] Connected to alerts gateway.") + self.reconnect_timer.stop() + + def _on_disconnected(self): + print("[AlertClient] Disconnected from alerts gateway.") + self.connectionLost.emit() + self.reconnect_timer.start(self.reconnect_interval_ms) + + def _on_message(self, msg: str): + try: + payload = json.loads(msg) + if payload["type"] == "alert": + self.alertReceived.emit(payload["data"]) + except Exception as e: + print("[AlertClient] Invalid message:", e, msg) diff --git a/AgCloud/GUI/src/vast/alerts/alert_service.py b/AgCloud/GUI/src/vast/alerts/alert_service.py new file mode 100644 index 000000000..8014e0f86 --- /dev/null +++ b/AgCloud/GUI/src/vast/alerts/alert_service.py @@ -0,0 +1,260 @@ +import yaml +import json, ast +from string import Template +from PyQt6.QtCore import QObject, pyqtSignal +from vast.alerts.alert_client import AlertClient +from concurrent.futures import ThreadPoolExecutor + +class AlertService(QObject): + alertsUpdated = pyqtSignal(list) + alertAdded = pyqtSignal(dict) + alertRemoved = pyqtSignal(str) + + def __init__(self, ws_url, api, templates_path="/app/templates/templates.yml"): + super().__init__() + self.api = api + self.device_locations = {} + self.templates = self._load_templates(templates_path) + self.load_devices() + + self.client = AlertClient(ws_url) + self.client.alertReceived.connect(self._on_realtime) + + self.alerts = [] + + # ──────────────────────────────── + # Load YAML templates + # ──────────────────────────────── + def _load_templates(self, path): + try: + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + print(f"[AlertService] Loaded templates from {path}") + return data.get("templates", {}) + except Exception as e: + print("[AlertService] Failed to load templates:", e) + return {} + + # ──────────────────────────────── + # Fetch devices from DB + # ──────────────────────────────── + def load_devices(self): + try: + url = f"{self.api.base}/api/tables/devices?limit=500" + r = self.api.http.get(url, timeout=10) + r.raise_for_status() + data = r.json() + devices = data.get("rows", data) + + self.device_locations = { + d["device_id"]: (d.get("location_lat"), d.get("location_lon")) + for d in devices if d.get("device_id") + } + print(f"[AlertService] Cached {len(self.device_locations)} device locations.") + except Exception as e: + print("[AlertService] Failed to fetch devices:", e) + + # ──────────────────────────────── + # Fetch alerts from DB and enrich with templates + # ──────────────────────────────── + def load_initial(self): + try: + url = f"{self.api.base}/api/tables/alerts?limit=500" + r = self.api.http.get(url, timeout=10) + r.raise_for_status() + data = r.json() + alerts = data.get("rows", data) + + for a in alerts: + device_id = a.get("device_id") + alert_type = a.get("alert_type") + + # Add lat/lon if missing + if device_id in self.device_locations: + lat, lon = self.device_locations[device_id] + if not a.get("lat") and lat: + a["lat"] = lat + if not a.get("lon") and lon: + a["lon"] = lon + + # ───────────── ENRICH ALERT WITH TEMPLATE ───────────── + tmpl = self.templates.get(alert_type) + if tmpl: + raw_meta = a.get("meta", {}) or {} + meta = {} + + # FIX: correct meta parsing + if isinstance(raw_meta, dict): + meta = raw_meta + elif isinstance(raw_meta, str): + try: + meta = json.loads(raw_meta) + except Exception: + try: + meta = ast.literal_eval(raw_meta) + except Exception: + meta = {} + + subject = meta.get("subject", "animal") + severity = int(a.get("severity", meta.get("severity", 1))) + + # build context + context = { + "device_id": device_id, + "area": a.get("area", "unknown area"), + "confidence": a.get("confidence", "?"), + "timestamp": a.get("started_at", ""), + "subject": subject, + "severity": severity, + "started_at": a.get("startsAt", ""), + } + + # enrich record + a["category"] = tmpl.get("category") + a["severity"] = severity + a["subject"] = subject + a["summary"] = Template(tmpl.get("summary", "")).safe_substitute(context) + a["recommendation"] = Template(tmpl.get("recommendation", "")).safe_substitute(context) + + self.alerts = alerts + self.alerts.sort( + key=lambda a: a.get("started_at") or a.get("startsAt") or "", + ) + self.alertsUpdated.emit(self.alerts) + print(f"[AlertService] Loaded {len(alerts)} enriched alerts.") + except Exception as e: + print("[AlertService] Failed to fetch alerts:", e) + + # ──────────────────────────────── + # Handle incoming WebSocket alerts + # ──────────────────────────────── + def _on_realtime(self, alert_msg): + alerts = alert_msg.get("alerts", []) + + for a in alerts: + labels = a.get("labels", {}) + ann = a.get("annotations", {}) + alert_id = labels.get("alert_id") + device_id = labels.get("device") + alert_type = labels.get("alertname") + ends_at = a.get("endsAt") + is_resolved = ends_at and not ends_at.startswith("0001-01-01") + + # Find existing alert + existing = next((al for al in self.alerts if al.get("alert_id") == alert_id), None) + + if is_resolved: + if existing: + existing["endedAt"] = ends_at + self.alertRemoved.emit(alert_id) + else: + fake_alert = {"alert_id": alert_id, "endedAt": ends_at} + self.alerts.append(fake_alert) + self.alertRemoved.emit(alert_id) + continue + + # ACTIVE alert + lat = ann.get("lat") + lon = ann.get("lon") + + # Fill missing coordinates + if (not lat or not lon) and device_id in self.device_locations: + lat, lon = self.device_locations[device_id] + print(f"[AlertService] Filled missing coords for {device_id}: ({lat}, {lon})") + + # ──────────────────────────────── + # FIXED meta parsing + # ──────────────────────────────── + tmpl = self.templates.get(alert_type, {}) + raw_meta = ann.get("meta", {}) or {} + meta = {} + + if isinstance(raw_meta, dict): + meta = raw_meta + elif isinstance(raw_meta, str): + try: + meta = json.loads(raw_meta) + except Exception: + try: + meta = ast.literal_eval(raw_meta) + except Exception: + meta = {} + + subject = meta.get("subject", "animal") + severity = int(ann.get("severity", 1)) + started_at = a.get("startsAt") or "" + + summary = Template(tmpl.get("summary", "")).safe_substitute( + device_id=device_id, + area=ann.get("area", ""), + confidence=ann.get("confidence", ""), + subject=subject, + severity=severity, + started_at=started_at, + ) + + recommendation = Template(tmpl.get("recommendation", "")).safe_substitute( + device_id=device_id, + area=ann.get("area", ""), + subject=subject, + severity=severity, + ) + + category = tmpl.get("category") + + normalized = { + "alert_id": alert_id, + "alert_type": alert_type, + "device_id": device_id, + "lat": lat, + "lon": lon, + "severity": severity, + "summary": summary, + "recommendation": recommendation, + "category": category, + "hls": ann.get("hls"), + "vod": ann.get("vod"), + "image_url": ann.get("image_url"), + "startsAt": a.get("startsAt"), + } + + if existing: + existing.update(normalized) + else: + self.alerts.append(normalized) + + self.alerts.sort( + key=lambda a: a.get("started_at") or a.get("startsAt") or "", + ) + + self.alertAdded.emit(normalized) + + # ──────────────────────────────── + # Mark all as acknowledged + # ──────────────────────────────── + def mark_all_acknowledged(self): + unacked = [a for a in self.alerts if not a.get("ack", False)] + if not unacked: + return + + for a in unacked: + a["ack"] = True + + def _patch_ack(alert): + try: + url = f"{self.api.base}/api/tables/alerts?limit=500" + payload = { + "keys": {"alert_id": alert["alert_id"]}, + "data": {"ack": True}, + } + r = self.api.http.patch(url, json=payload, timeout=5) + r.raise_for_status() + except Exception as e: + print(f"[AlertService] Failed to PATCH ack for {alert['alert_id']}: {e}") + + with ThreadPoolExecutor(max_workers=4) as pool: + for a in unacked: + pool.submit(_patch_ack, a) + + self.alertsUpdated.emit(self.alerts) + print(f"[AlertService] Marked {len(unacked)} alerts as acknowledged.") \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/auth_ui/__init__.py b/AgCloud/GUI/src/vast/auth_ui/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/auth_ui/login_page.py b/AgCloud/GUI/src/vast/auth_ui/login_page.py new file mode 100644 index 000000000..825a0de19 --- /dev/null +++ b/AgCloud/GUI/src/vast/auth_ui/login_page.py @@ -0,0 +1,87 @@ +from __future__ import annotations +from typing import Callable +from PyQt6.QtCore import Qt, QSettings, pyqtSignal +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import ( + QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, + QFormLayout, QStackedWidget, QCheckBox, QSpacerItem, QSizePolicy +) +from .service import AuthService +from .widgets import ErrorBanner + + +class LoginPage(QWidget): + logged_in = pyqtSignal(str, str) # username, password (or token) + def __init__(self, on_login: Callable, on_go_signup: Callable, auth: AuthService, parent=None) -> None: + super().__init__(parent) + self.auth = auth + self.on_login = on_login + self.on_go_signup = on_go_signup + self.settings = QSettings("AgriSense", "FarmMonitor") + self._build() + + def _build(self) -> None: + title = QLabel("Welcome back 👋") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: 600;") + + self.banner = ErrorBanner() + + self.email = QLineEdit() + self.email.setPlaceholderText("you@example.com") + self.email.setClearButtonEnabled(True) + + self.password = QLineEdit() + self.password.setEchoMode(QLineEdit.EchoMode.Password) + self.password.setPlaceholderText("Your password") + self.password.setClearButtonEnabled(True) + + toggle_action = QAction("Show") + toggle_action.setCheckable(True) + toggle_action.toggled.connect(self._toggle_password) + self.password.addAction(toggle_action, QLineEdit.ActionPosition.TrailingPosition) + + self.remember = QCheckBox("Remember me") + + form = QFormLayout() + form.addRow("Email", self.email) + form.addRow("Password", self.password) + form.addRow("", self.remember) + + login_btn = QPushButton("Sign in") + login_btn.clicked.connect(self._do_login) + login_btn.setDefault(True) + + go_signup = QPushButton("Create an account") + go_signup.setFlat(True) + go_signup.clicked.connect(lambda: self.on_go_signup()) + + main = QVBoxLayout(self) + main.addWidget(title) + main.addWidget(self.banner) + main.addLayout(form) + main.addWidget(login_btn) + main.addItem(QSpacerItem(0, 12, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed)) + main.addWidget(go_signup, alignment=Qt.AlignmentFlag.AlignCenter) + main.addStretch(1) + + last_email = self.settings.value("last_email", "") + if last_email: + self.email.setText(last_email) + self.remember.setChecked(True) + + def _toggle_password(self, checked: bool) -> None: + self.password.setEchoMode(QLineEdit.EchoMode.Normal if checked else QLineEdit.EchoMode.Password) + + def _do_login(self) -> None: + try: + email = self.email.text().strip() + password = self.password.text() + user = self.auth.login(email, password) + if self.remember.isChecked(): + self.settings.setValue("last_email", email) + else: + self.settings.remove("last_email") + self.on_login(user) + except Exception as e: + self.banner.show_message(str(e)) diff --git a/AgCloud/GUI/src/vast/auth_ui/models.py b/AgCloud/GUI/src/vast/auth_ui/models.py new file mode 100644 index 000000000..2f5eb1e3f --- /dev/null +++ b/AgCloud/GUI/src/vast/auth_ui/models.py @@ -0,0 +1,8 @@ +from __future__ import annotations +from dataclasses import dataclass + +@dataclass +class User: + email: str + full_name: str + pw_encoded: str # Argon2 encoded hash string diff --git a/AgCloud/GUI/src/vast/auth_ui/service.py b/AgCloud/GUI/src/vast/auth_ui/service.py new file mode 100644 index 000000000..f109c7c54 --- /dev/null +++ b/AgCloud/GUI/src/vast/auth_ui/service.py @@ -0,0 +1,48 @@ +from __future__ import annotations +import os +import re +from typing import Dict +from argon2 import PasswordHasher, exceptions as argon2_exc +from .models import User + +EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$") + +# Tune to your hardware so hashing ~100–300 ms +ph = PasswordHasher(time_cost=3, memory_cost=102_400, parallelism=2) # ~100 MiB + +# Optional pepper (defense-in-depth). Store outside DB (env var / keychain) +PEPPER = os.getenv("FARMMONITOR_PWD_PEPPER", "") + +def _pepper(pw: str) -> str: + return pw + PEPPER + +class AuthService: + """Demo auth service using Argon2id. + TODO: Replace with real API calls and persistent storage. + """ + def __init__(self) -> None: + self._users: Dict[str, User] = {} + + def register(self, email: str, password: str, full_name: str) -> None: + email_lc = email.strip().lower() + if not EMAIL_RE.match(email_lc): + raise ValueError("Please enter a valid email address.") + if len(password) < 8: + raise ValueError("Password must be at least 8 characters.") + if email_lc in self._users: + raise ValueError("This email is already registered.") + encoded = ph.hash(_pepper(password)) + self._users[email_lc] = User(email=email_lc, full_name=full_name.strip(), pw_encoded=encoded) + + def login(self, email: str, password: str) -> User: + email_lc = email.strip().lower() + u = self._users.get(email_lc) + if not u: + raise ValueError("No account found for this email.") + try: + ph.verify(u.pw_encoded, _pepper(password)) + except argon2_exc.VerifyMismatchError: + raise ValueError("Incorrect password.") + if ph.check_needs_rehash(u.pw_encoded): + u.pw_encoded = ph.hash(_pepper(password)) + return u diff --git a/AgCloud/GUI/src/vast/auth_ui/signup_page.py b/AgCloud/GUI/src/vast/auth_ui/signup_page.py new file mode 100644 index 000000000..3ac48d572 --- /dev/null +++ b/AgCloud/GUI/src/vast/auth_ui/signup_page.py @@ -0,0 +1,105 @@ +from __future__ import annotations +import re +from typing import Callable +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import ( + QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout, + QFormLayout, QStackedWidget, QCheckBox +) +from .service import AuthService, EMAIL_RE +from .widgets import ErrorBanner + +class SignupPage(QWidget): + def __init__(self, on_signed_up: Callable, on_go_login: Callable, auth: AuthService, parent=None) -> None: + super().__init__(parent) + self.auth = auth + self.on_signed_up = on_signed_up + self.on_go_login = on_go_login + self._build() + + def _build(self) -> None: + title = QLabel("Create your account") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: 600;") + + self.banner = ErrorBanner() + + self.full_name = QLineEdit() + self.full_name.setPlaceholderText("Full name") + self.full_name.setClearButtonEnabled(True) + + self.email = QLineEdit() + self.email.setPlaceholderText("you@example.com") + self.email.setClearButtonEnabled(True) + + self.password = QLineEdit() + self.password.setEchoMode(QLineEdit.EchoMode.Password) + self.password.setPlaceholderText("At least 8 characters") + self.password.setClearButtonEnabled(True) + + self.confirm = QLineEdit() + self.confirm.setEchoMode(QLineEdit.EchoMode.Password) + self.confirm.setPlaceholderText("Repeat password") + self.confirm.setClearButtonEnabled(True) + + toggle_action = QAction("Show") + toggle_action.setCheckable(True) + toggle_action.toggled.connect(self._toggle_passwords) + self.confirm.addAction(toggle_action, QLineEdit.ActionPosition.TrailingPosition) + + self.terms = QCheckBox("I agree to the Terms of Service and Privacy Policy") + + form = QFormLayout() + form.addRow("Full name", self.full_name) + form.addRow("Email", self.email) + form.addRow("Password", self.password) + form.addRow("Confirm", self.confirm) + form.addRow("", self.terms) + + signup_btn = QPushButton("Sign up") + signup_btn.clicked.connect(self._do_signup) + signup_btn.setDefault(True) + + go_login = QPushButton("Already have an account? Sign in") + go_login.setFlat(True) + go_login.clicked.connect(lambda: self.on_go_login()) + + main = QVBoxLayout(self) + main.addWidget(title) + main.addWidget(self.banner) + main.addLayout(form) + main.addWidget(signup_btn) + main.addWidget(go_login, alignment=Qt.AlignmentFlag.AlignCenter) + main.addStretch(1) + + def _toggle_passwords(self, checked: bool) -> None: + mode = QLineEdit.EchoMode.Normal if checked else QLineEdit.EchoMode.Password + self.password.setEchoMode(mode) + self.confirm.setEchoMode(mode) + + def _do_signup(self) -> None: + try: + full_name = self.full_name.text().strip() + email = self.email.text().strip() + password = self.password.text() + confirm = self.confirm.text() + + if not full_name: + raise ValueError("Please enter your full name.") + if not EMAIL_RE.match(email): + raise ValueError("Please enter a valid email address.") + if len(password) < 8: + raise ValueError("Password must be at least 8 characters.") + if password != confirm: + raise ValueError("Passwords do not match.") + if not self.terms.isChecked(): + raise ValueError("You must accept the Terms to continue.") + + self.auth.register(email=email, password=password, full_name=full_name) + from PyQt6.QtWidgets import QMessageBox # local import to avoid circulars in stubs + QMessageBox.information(self, "Account created", "Your account has been created. Please sign in.") + self.on_go_login() + self.on_signed_up() + except Exception as e: + self.banner.show_message(str(e)) diff --git a/AgCloud/GUI/src/vast/auth_ui/widgets.py b/AgCloud/GUI/src/vast/auth_ui/widgets.py new file mode 100644 index 000000000..83d4b6671 --- /dev/null +++ b/AgCloud/GUI/src/vast/auth_ui/widgets.py @@ -0,0 +1,21 @@ +from __future__ import annotations +from PyQt6.QtWidgets import QFrame, QLabel, QPushButton, QHBoxLayout + +class ErrorBanner(QFrame): + def __init__(self) -> None: + super().__init__() + self.setObjectName("errorBanner") + self.setFrameShape(QFrame.Shape.StyledPanel) + self.setVisible(False) + self._label = QLabel() + self._label.setWordWrap(True) + close_btn = QPushButton("×") + close_btn.setFixedWidth(28) + close_btn.clicked.connect(lambda: self.setVisible(False)) + lay = QHBoxLayout(self) + lay.addWidget(self._label, 1) + lay.addWidget(close_btn, 0) + + def show_message(self, text: str) -> None: + self._label.setText(text) + self.setVisible(True) diff --git a/AgCloud/GUI/src/vast/dashboard_api.py b/AgCloud/GUI/src/vast/dashboard_api.py new file mode 100644 index 000000000..845ff093b --- /dev/null +++ b/AgCloud/GUI/src/vast/dashboard_api.py @@ -0,0 +1,617 @@ +# -*- coding: utf-8 -*- +from __future__ import annotations + +import os +import json +import time +import base64 +import pathlib +from typing import Dict, List, Optional, Tuple, Union + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +# ---- Optional deps (do not crash if missing) ---- +try: + from minio import Minio + from minio.error import S3Error +except Exception: # pragma: no cover + Minio = None # type: ignore + S3Error = Exception # type: ignore + +try: + from vast.rel_db import RelDB +except Exception: # pragma: no cover + RelDB = None # type: ignore + + +# ========================= +# CONFIG +# ========================= +# --- HTTP API --- +DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001") +DB_API_AUTH_MODE = os.getenv("DB_API_AUTH_MODE", "service") # "service" | "bearer" +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/app/secrets/db_api_token") +DB_API_TOKEN = os.getenv("DB_API_TOKEN", "auto") +DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "GUI_H") + +# --- RelDB (used inside RelDB class; here only for reference/env) --- +DB_HOST = os.getenv("DB_HOST", "127.0.0.1") +DB_PORT = int(os.getenv("DB_PORT", "5432")) +DB_USER = os.getenv("DB_USER", "missions_user") +DB_PASS = os.getenv("DB_PASS", "pg123") +DB_NAME = os.getenv("DB_NAME", "missions_db") + +# --- MinIO --- +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "127.0.0.1:9001") # host:exposed_port +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" + +DEFAULT_GROUND_BUCKET = os.getenv("GROUND_BUCKET", "ground") +DEFAULT_GROUND_PREFIX = os.getenv("GROUND_PREFIX", "") + + +# ========================= +# TOKEN BOOTSTRAP HELPERS +# ========================= +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + +def _read_token_from_file(path: str) -> Optional[str]: + p = pathlib.Path(path) + if p.exists(): + token = p.read_text(encoding="utf-8").strip() + return token or None + return None + +def _fetch_token_via_dev_bootstrap(base: str, retries: int = 3, backoff: float = 0.8) -> Optional[str]: + """ + Calls /auth/_dev_bootstrap to mint/rotate a service token for this client. + """ + url = _safe_join_url(base, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": True} + last_exc: Optional[Exception] = None + for attempt in range(1, retries + 1): + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code in (200, 201): + data = r.json() if r.content else {} + raw = (data.get("service_account", {}) or {}).get("raw_token") \ + or (data.get("service_account", {}) or {}).get("token") + if raw and isinstance(raw, str) and "***" not in raw: + return raw.strip() + except Exception as e: + last_exc = e + time.sleep(backoff * attempt) + if last_exc: + print(f"[BOOTSTRAP][WARN] last error: {last_exc}") + return None + +def get_or_bootstrap_token() -> Optional[str]: + if DB_API_TOKEN and DB_API_TOKEN.lower() != "auto": + print("[DEBUG] Using static token from DB_API_TOKEN", flush=True) + return DB_API_TOKEN + + token = _read_token_from_file(DB_API_TOKEN_FILE) + if token: + print(f"[DEBUG] Loaded token from {DB_API_TOKEN_FILE}", flush=True) + return token + + print(f"[DEBUG] No token found, bootstrapping via {DB_API_BASE}/auth/_dev_bootstrap", flush=True) + token = _fetch_token_via_dev_bootstrap(DB_API_BASE) + if token: + p = pathlib.Path(DB_API_TOKEN_FILE) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + print(f"[BOOTSTRAP] wrote token to {DB_API_TOKEN_FILE}", flush=True) + return token + + print("[BOOTSTRAP][ERROR] Failed to obtain token.", flush=True) + return None + + +# ========================= +# UTILITIES +# ========================= +def _image_id_from_object_key(object_key: str) -> str: + """ + 'some/prefix/image (3).jpg' -> 'image (3)' + """ + base = os.path.basename(object_key or "") + return base.rsplit(".", 1)[0] if "." in base else base + + +# ========================= +# DASHBOARD API +# ========================= +class DashboardApi: + """ + Unified client: + - REST to DB-API (with token bootstrap/refresh) + - Optional MinIO helper + - Optional RelDB helper + """ + + def __init__(self) -> None: + # ---- HTTP session ---- + self.base = DB_API_BASE.rstrip("/") + self.http = requests.Session() + + # Attach robust retries + retry = Retry( + total=5, + backoff_factor=0.5, + status_forcelist=[500, 502, 503, 504], + allowed_methods=frozenset(["HEAD", "GET", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"]) + ) + self.http.mount("http://", HTTPAdapter(max_retries=retry)) + self.http.mount("https://", HTTPAdapter(max_retries=retry)) + self.http.headers.update({"Content-Type": "application/json"}) + + # ---- Auth ---- + token = get_or_bootstrap_token() + self.token: Optional[str] = token + self.token_type = "service" if DB_API_AUTH_MODE == "service" else "bearer" + self._apply_auth_header(token) + + # ---- MinIO (optional) ---- + self.minio: Optional[Minio] = None + if Minio is not None: + try: + self.minio = Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=MINIO_SECURE, + ) + except Exception as e: # pragma: no cover + print(f"[MINIO][INIT][WARN] {e}") + + # ---- RelDB (optional) ---- + self.rdb: Optional[RelDB] = None + if RelDB is not None: + try: + self.rdb = RelDB() + except Exception as e: # pragma: no cover + print(f"[RelDB][INIT][WARN] {e}") + + # --------------------------- + # Auth helpers + # --------------------------- + def _apply_auth_header(self, token: Optional[str]) -> None: + # Clean previous header variants + for h in ["X-Service-Token", "Authorization"]: + if h in self.http.headers: + del self.http.headers[h] + if token: + if DB_API_AUTH_MODE == "service": + self.http.headers.update({"X-Service-Token": token}) + else: + self.http.headers.update({"Authorization": f"Bearer {token}"}) + + def get_token_info(self) -> dict: + """ + Tries to decode JWT payload. If not a JWT, returns basic info. + """ + t = self.token + if not t: + return {"type": self.token_type, "status": "missing"} + + if "." in t: + try: + payload_b64 = t.split(".")[1] + padded = payload_b64 + "=" * (-len(payload_b64) % 4) + data = json.loads(base64.urlsafe_b64decode(padded)) + exp = data.get("exp") + secs_left = exp - int(time.time()) if exp else None + return {"type": "jwt", "exp": exp, "secs_left": secs_left, "payload": data} + except Exception: + pass + return {"type": self.token_type, "token_length": len(t)} + + def refresh_token(self) -> bool: + """ + Fetches a new service token via dev bootstrap and updates headers + file. + """ + new_token = _fetch_token_via_dev_bootstrap(self.base) + if new_token: + try: + pathlib.Path(DB_API_TOKEN_FILE).parent.mkdir(parents=True, exist_ok=True) + pathlib.Path(DB_API_TOKEN_FILE).write_text(new_token, encoding="utf-8") + except Exception as e: + print(f"[TOKEN][WARN] Could not persist new token: {e}") + self.token = new_token + self._apply_auth_header(new_token) + print("[TOKEN] refreshed", flush=True) + return True + print("[TOKEN][ERROR] refresh failed", flush=True) + return False + + # --------------------------- + # REST: examples / utilities + # --------------------------- + def list_devices(self, model: Optional[str] = None) -> List[dict]: + """ + Tries modern path /api/devices; falls back to /api/tables/devices for older servers. + """ + paths = ["/api/devices", "/api/tables/devices"] + last_err: Optional[str] = None + for path in paths: + url = f"{self.base}{path}" + if model: + sep = "&" if "?" in url else "?" + url = f"{url}{sep}model={model}" + try: + r = self.http.get(url, timeout=10) + if r.status_code == 200: + try: + return r.json() + except Exception: + print("[API WARN] devices response is not JSON", flush=True) + return [] + if r.status_code in (404, 405): + last_err = f"http-{r.status_code}" + continue + print(f"[API ERROR] {r.status_code}: {r.text[:200]}") + return [] + except Exception as e: + last_err = str(e) + continue + if last_err: + print(f"[API FAIL] list_devices: {last_err}") + return [] + + def bulk_set_task_thresholds_labeled( + self, + mapping: Dict[Tuple[str, str], float] | List[dict], + updated_by: str = "gui", + ) -> dict: + """ + Unified + fallback: + 1) POST /api/task_thresholds/batch + 2) if 404/405 -> POST /api/thresholds/batch + Body shape is normalized to: {"task": str, "label": str, "threshold": float, "updated_by": str} + """ + items = ( + [ + {"task": t, "label": l or "", "threshold": thr, "updated_by": updated_by} + for (t, l), thr in mapping.items() + ] + if isinstance(mapping, dict) else mapping + ) + + paths = ["/api/task_thresholds/batch", "/api/thresholds/batch"] + last_err: Optional[str] = None + for path in paths: + url = f"{self.base}{path}" + try: + r = self.http.post(url, json=items, timeout=20) + if r.status_code in (200, 201): + data = r.json() if r.content else {} + return {"ok": list(data.get("ok", [])), "fail": list(data.get("fail", []))} + if r.status_code in (404, 405): + last_err = f"http-{r.status_code}" + continue + return { + "ok": [], + "fail": [[[i.get("task"), i.get("label","")], f"http-{r.status_code} {r.text[:200]}"] for i in items], + } + except Exception as e: + last_err = str(e) + continue + return {"ok": [], "fail": [[[i.get("task"), i.get("label","")], last_err or "unknown"] for i in items]} + + # --------------------------- + # MinIO helpers (optional) + # --------------------------- + def list_minio_objects(self, bucket: str, prefix: str = "", limit: int = 100) -> List[dict]: + """ + Returns: [{'key': 'path/file.jpg', 'size': int, 'last_modified': iso}, ...] + """ + if not self.minio: + print("[MINIO][WARN] MinIO client not available") + return [] + out: List[dict] = [] + try: + for i, obj in enumerate(self.minio.list_objects(bucket, prefix=prefix, recursive=True)): + if i >= limit: + break + lm = getattr(obj, "last_modified", None) + out.append({ + "key": getattr(obj, "object_name", None) or getattr(obj, "name", None), + "size": getattr(obj, "size", None), + "last_modified": lm.isoformat() if lm else None, + }) + except Exception as e: + print(f"[MINIO LIST FAIL] {e}") + return out + + def get_latest_minio_key(self, bucket: str, prefix: str = "") -> Optional[str]: + objs = self.list_minio_objects(bucket, prefix=prefix, limit=200) + if not objs: + return None + objs_sorted = sorted(objs, key=lambda o: o.get("last_modified") or "", reverse=True) + key = objs_sorted[0].get("key") + return key if isinstance(key, str) and key.strip() else None + + def get_image_bytes_from_minio(self, key: str, bucket: Optional[str] = None) -> Optional[bytes]: + if not self.minio: + print("[MINIO][WARN] MinIO client not available") + return None + bucket_name = bucket or DEFAULT_GROUND_BUCKET + try: + response = self.minio.get_object(bucket_name, key) + data = response.read() + response.close() + response.release_conn() + print(f"[DEBUG] Got {len(data)} bytes from {bucket_name}/{key}") + return data + except Exception as e: + print(f"[MINIO GET FAIL] {e}") + return None + + # --------------------------- + # RelDB delegates (optional) + # --------------------------- + def _rdb_guard(self) -> bool: + if not self.rdb: + print("[RelDB][WARN] RelDB client not available") + return False + return True + + def get_weekly_phi(self) -> dict: + if not self._rdb_guard(): return {} + return self.rdb.get_weekly_phi() + + def get_latest_rows(self, limit: int = 20) -> List[dict]: + if not self._rdb_guard(): return [] + return self.rdb.get_latest_anomalies(limit=limit) + + def get_latest_detections(self, limit: int = 20) -> List[dict]: + if not self._rdb_guard(): return [] + return self.rdb.get_latest_anomalies(limit=limit) + + def get_rows_by_image(self, image_name: str, limit: int = 50) -> List[dict]: + """ + image_name is image_id without extension. + """ + if not self._rdb_guard(): return [] + return self.rdb.get_anomalies_by_image(image_name, limit=limit) + + def get_last_row_by_image(self, image_name: str) -> Optional[dict]: + if not self._rdb_guard(): return None + return self.rdb.get_last_anomaly_by_image(image_name) + + def get_rows_by_day(self, date_iso: str, limit: int = 1000) -> List[dict]: + if not self._rdb_guard(): return [] + return self.rdb.get_anomalies_by_day(date_iso, limit=limit) + + # --------------------------- + # Image-centric (MinIO→image_id→RelDB) + # --------------------------- + def get_latest_image_key(self) -> Optional[str]: + """ + Prefer the newest in MinIO; if none—fallback to DB (if available). + """ + key = None + if self.minio: + key = self.get_latest_minio_key(DEFAULT_GROUND_BUCKET, DEFAULT_GROUND_PREFIX) + if key: + return key + if self.rdb: + try: + return self.rdb.get_latest_image_key() + except Exception as e: + print(f"[RelDB][WARN] get_latest_image_key fallback failed: {e}") + return None + + def get_anomalies_for_image_key(self, object_key: str, limit: int = 50) -> List[dict]: + if not self._rdb_guard(): return [] + image_id = _image_id_from_object_key(object_key) + return self.rdb.get_anomalies_by_image(image_id, limit=limit) + + def get_anomalies_for_current_image(self, limit: int = 100) -> List[dict]: + if not self._rdb_guard(): return [] + key = self.get_latest_image_key() + if not key: + return [] + image_id = _image_id_from_object_key(key) + return self.rdb.get_anomalies_by_image(image_id, limit=limit) + + def get_last_anomaly_for_current_image(self) -> Optional[dict]: + if not self._rdb_guard(): return None + key = self.get_latest_image_key() + if not key: + return None + image_id = _image_id_from_object_key(key) + return self.rdb.get_last_anomaly_by_image(image_id) + + def get_phi_for_image(self, image_name_or_key: str) -> dict: + if not self._rdb_guard(): + return {"phi": None, "severity_avg": None, "density": None, "coverage": None, "trend": None} + image_id = _image_id_from_object_key(image_name_or_key) + return self.rdb.get_phi_for_image(image_id) + + def get_phi_for_current_image(self) -> dict: + if not self._rdb_guard(): + return {"phi": None, "severity_avg": None, "density": None, "coverage": None, "trend": None} + key = self.get_latest_image_key() + if not key: + return {"phi": None, "severity_avg": None, "density": None, "coverage": None, "trend": None} + image_id = _image_id_from_object_key(key) + return self.rdb.get_phi_for_image(image_id) + + + # ===================================================== + # ===== ADDED: AUDIO ANALYTICS METHODS ===== + # ===================================================== + def get_audio_stats(self, time_range: str = 'all') -> Dict: + """ + Get aggregated audio classification statistics. + """ + time_filter = { + 'all': '', + 'hour': "AND r.started_at > NOW() - INTERVAL '1 hour'", + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'" + }.get(time_range, '') + + query = f""" + SELECT + COUNT(*) AS total_files, + SUM(CASE WHEN fa.head_is_another = true THEN 1 ELSE 0 END) AS unknown_count, + AVG(fa.head_pred_prob) AS avg_confidence, + AVG(fa.processing_ms) AS avg_processing_ms + FROM agcloud_audio.file_aggregates fa + JOIN agcloud_audio.runs r + ON fa.run_id = r.run_id + WHERE 1=1 {time_filter} + """ + results = self.run_query(query) + return results[0] if results else {} + + def get_audio_distribution(self, time_range: str = 'all', limit: int = 10) -> List[Dict]: + """ + Get distribution of audio classifications (for pie chart). + """ + time_filter = { + 'all': '', + 'hour': "AND r.started_at > NOW() - INTERVAL '1 hour'", + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'" + }.get(time_range, '') + + query = f""" + SELECT + fa.head_pred_label, + COUNT(*) AS count + FROM agcloud_audio.file_aggregates fa + JOIN agcloud_audio.runs r + ON fa.run_id = r.run_id + WHERE fa.head_pred_label IS NOT NULL + {time_filter} + GROUP BY fa.head_pred_label + ORDER BY count DESC + LIMIT {limit} + """ + return self.run_query(query) + + def get_audio_confidence_by_class(self, time_range: str = 'all', limit: int = 10) -> List[Dict]: + """ + Get average confidence levels by classification (for bar chart). + """ + time_filter = { + 'all': '', + 'hour': "AND r.started_at > NOW() - INTERVAL '1 hour'", + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'" + }.get(time_range, '') + + query = f""" + SELECT + fa.head_pred_label, + AVG(fa.head_pred_prob) AS avg_confidence + FROM agcloud_audio.file_aggregates fa + JOIN agcloud_audio.runs r + ON fa.run_id = r.run_id + WHERE fa.head_pred_label IS NOT NULL + AND fa.head_pred_prob IS NOT NULL + {time_filter} + GROUP BY fa.head_pred_label + ORDER BY avg_confidence DESC + LIMIT {limit} + """ + return self.run_query(query) + + def get_audio_critical_events(self, time_range: str = 'day', limit: int = 100) -> List[Dict]: + """ + Get critical sound events (fire, screaming, shotgun, predatory animals). + """ + time_filter = { + 'hour': "AND r.started_at > NOW() - INTERVAL '1 hour'", + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'" + }.get(time_range, "AND r.started_at > NOW() - INTERVAL '24 hours'") + + query = f""" + SELECT + r.run_id, + r.started_at, + snsc.file_name, + snsc.key AS s3_key, + sm.device_id, + sm.capture_time, + fa.head_pred_label AS event_type, + fa.head_pred_prob AS confidence, + fa.head_probs_json + FROM agcloud_audio.file_aggregates fa + JOIN agcloud_audio.runs r + ON fa.run_id = r.run_id + JOIN public.sound_new_sounds_connections snsc + ON fa.file_id = snsc.id + LEFT JOIN public.sounds_metadata sm + ON snsc.file_name = sm.file_name + WHERE fa.head_pred_label IN ('fire', 'screaming', 'shotgun', 'predatory_animals') + {time_filter} + ORDER BY r.started_at DESC, fa.head_pred_prob DESC + LIMIT {limit} + """ + return self.run_query(query) + + + + # ===================================================== + # ===== ADDED: HELPER METHODS FOR OTHER VIEWS ===== + # ===================================================== + def get_sensors(self) -> List[Dict]: + """Get all sensors from the sensors table""" + query = "SELECT * FROM sensors ORDER BY sensor_name" + return self.run_query(query) + def get_sensor_status(self, sensor_name: str) -> Dict: + """Get status of a specific sensor""" + query = "SELECT * FROM sensors WHERE sensor_name = %s" + results = self.run_query(query, (sensor_name,)) + return results[0] if results else {} + def get_alerts(self, limit: int = 50) -> List[Dict]: + """Get recent alerts""" + query = """ + SELECT * FROM alerts + ORDER BY started_at DESC + LIMIT %s + """ + return self.run_query(query, (limit,)) + + def acknowledge_alert(self, alert_id: str) -> bool: + """Mark an alert as acknowledged""" + conn = None + cursor = None + try: + conn = self._get_connection() + cursor = conn.cursor() + query = "UPDATE alerts SET ack = true WHERE alert_id = %s" + cursor.execute(query, (alert_id,)) + conn.commit() + print(f"[DashboardApi] Alert {alert_id} acknowledged", flush=True) + return True + except Exception as e: + print(f"[DashboardApi] Error acknowledging alert: {e}", flush=True) + return False + finally: + if cursor: + cursor.close() + if conn: + conn.close() + def get_ripeness_stats(self) -> Dict: + """Get ripeness prediction statistics""" + query = """ + SELECT + COUNT(*) as total_predictions, + SUM(CASE WHEN ripeness_label = 'ripe' THEN 1 ELSE 0 END) as ripe_count, + SUM(CASE WHEN ripeness_label = 'unripe' THEN 1 ELSE 0 END) as unripe_count, + SUM(CASE WHEN ripeness_label = 'overripe' THEN 1 ELSE 0 END) as overripe_count + FROM ripeness_predictions + """ + results = self.run_query(query) + return results[0] if results else {} \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/desktop/Dockerfile b/AgCloud/GUI/src/vast/desktop/Dockerfile new file mode 100644 index 000000000..56a4c5d04 --- /dev/null +++ b/AgCloud/GUI/src/vast/desktop/Dockerfile @@ -0,0 +1,84 @@ +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app + +# ───────── system dependencies ───────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 libegl1 libx11-6 libxcomposite1 libxext6 libxi6 libxtst6 libsm6 \ + libxkbcommon0 libxkbcommon-x11-0 libxkbfile1 libxrender1 libxrandr2 \ + libxfixes3 libxdamage1 libwayland-cursor0 libwayland-egl1 libxcb-cursor0 \ + libxcb-icccm4 libxcb-util1 libxcb-image0 libxcb-keysyms1 libxcb-render0 \ + libxcb-render-util0 libxcb-shape0 libxcb-xkb1 libfontconfig1 libfreetype6 \ + libpango-1.0-0 libharfbuzz0b libatk1.0-0 libatk-bridge2.0-0 libnss3 \ + libnspr4 libdbus-1-3 libkrb5-3 libgssapi-krb5-2 libasound2 libpulse0 \ + fluxbox x11vnc xvfb wget net-tools python3-tk ca-certificates \ + procps iproute2 xauth git vlc libvlc5 libvlccore9 \ + fonts-dejavu-core fonts-noto-core fonts-noto-color-emoji\ + && rm -rf /var/lib/apt/lists/* + +# (optional) minimal extra XCB deps for PyQt +RUN apt-get update && apt-get install -y --no-install-recommends \ + libxcb-xinerama0 libxcb-cursor0 libxcb-keysyms1 libxcb-render-util0 \ + libxcb-randr0 && rm -rf /var/lib/apt/lists/* + +# ───────── optional CA certs ───────── +COPY certs /app/certs +RUN if [ -d ./certs ] && [ "$(ls ./certs/*.crt 2>/dev/null)" ]; then \ + echo "Configuring NetFree certificates..."; \ + cp ./certs/*.crt /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# ───────── noVNC for remote GUI ───────── +RUN mkdir -p /opt && \ + wget --tries=3 --timeout=30 -O /tmp/novnc.tar.gz https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz && \ + tar xzf /tmp/novnc.tar.gz -C /opt && \ + mv /opt/noVNC-1.4.0 /opt/noVNC && \ + rm /tmp/novnc.tar.gz && \ + git clone --depth 1 https://github.com/novnc/websockify /opt/noVNC/utils/websockify + +# ───────── Python deps ───────── +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir \ + "PyQt6==6.9.0" \ + "PyQt6-WebEngine==6.9.0" \ + "argon2-cffi" \ + "requests" \ + "numpy" \ + --extra-index-url https://pypi.org/simple \ + --prefer-binary \ + --break-system-packages \ + && pip show PyQt6 PyQt6-WebEngine argon2-cffi +RUN pip install plotly +RUN pip install PyJWT +# ───────── app setup ───────── +RUN useradd -m -s /bin/bash appuser \ + && mkdir -p /app /tmp/.X11-unix \ + && chown -R appuser:appuser /app /tmp /opt/noVNC /var/tmp + +RUN apt-get update && apt-get install -y --no-install-recommends gosu && rm -rf /var/lib/apt/lists/* + +COPY src/vast /app/src/vast +COPY src/vast/desktop/start.sh /app/start.sh +RUN sed -i 's/\r$//' /app/start.sh && chmod +x /app/start.sh && chown -R appuser:appuser /app + +RUN mkdir -p /app/secrets && chmod -R 777 /app/secrets + +USER appuser +EXPOSE 5900 6080 +ENV PYTHONPATH=/app/src:/app +ENV DISPLAY=:0 +ENV NO_VNC_PORT=6080 +ENV PORT=19100 +ENV MEDIA_BASE=http://media-proxy:8080 + +CMD ["/app/start.sh"] + + + diff --git a/AgCloud/GUI/src/vast/desktop/noVNC-1.4.0.tar.gz b/AgCloud/GUI/src/vast/desktop/noVNC-1.4.0.tar.gz new file mode 100644 index 000000000..4ea63d6e1 Binary files /dev/null and b/AgCloud/GUI/src/vast/desktop/noVNC-1.4.0.tar.gz differ diff --git a/AgCloud/GUI/src/vast/desktop/start.sh b/AgCloud/GUI/src/vast/desktop/start.sh new file mode 100644 index 000000000..460dfcafe --- /dev/null +++ b/AgCloud/GUI/src/vast/desktop/start.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -e +set -x + +export DISPLAY=:0 +rm -f /tmp/.X0-lock + +echo "[INFO] Starting Xvfb..." +Xvfb :0 -screen 0 1920x1080x24 & +sleep 3 + +echo "[INFO] Starting fluxbox..." +fluxbox & +sleep 1 + +echo "[INFO] Starting x11vnc..." +x11vnc -display :0 -nopw -forever -shared & +sleep 1 + +echo "[INFO] Starting noVNC..." +/opt/noVNC/utils/novnc_proxy --vnc localhost:5900 --listen ${NO_VNC_PORT:-8080} & + +echo "[INFO] Starting PyQt application..." +exec python /app/src/vast/main.py + + + +# # ------------------------------ +# # 🚀 Launch the main PyQt application +# # ------------------------------ +# exec /opt/venv/bin/python /app/src/vast/main.py diff --git a/AgCloud/GUI/src/vast/dsl/__init__.py b/AgCloud/GUI/src/vast/dsl/__init__.py new file mode 100644 index 000000000..6d077959d --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/__init__.py @@ -0,0 +1,19 @@ +"""Public package surface for the DSL. + +Re-exports the most common types so users can do: + from dsl import Query, Col, Literal, AND, OR, SQLBuilder, SQLiteDialect, PostgresDialect +""" + +from __future__ import annotations +from .dialects import Dialect, SQLiteDialect, PostgresDialect +from .expr import Expr, Col, Literal, Predicate, Cond, AND, OR, ALLOWED_BIN_OPS +from .ir import Plan +from .builder import SQLBuilder +from .query import Query + + +__all__ = [ + "Dialect", "SQLiteDialect", "PostgresDialect", + "Expr", "Col", "Literal", "Predicate", "Cond", "AND", "OR", "ALLOWED_BIN_OPS", + "Plan", "SQLBuilder", "Query", +] \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/dsl/binops.py b/AgCloud/GUI/src/vast/dsl/binops.py new file mode 100644 index 000000000..bd0d2e276 --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/binops.py @@ -0,0 +1,17 @@ +# dsl/binops.py (new) +from enum import Enum + +# Works on Python 3.8+; on 3.11+ you can import from enum import StrEnum +class StrEnum(str, Enum): + pass + +class BinOp(StrEnum): + EQ = "=" + NE = "!=" + LT = "<" + LE = "<=" + GT = ">" + GE = ">=" + + def __str__(self) -> str: + return self.value diff --git a/AgCloud/GUI/src/vast/dsl/builder.py b/AgCloud/GUI/src/vast/dsl/builder.py new file mode 100644 index 000000000..8636ddf6c --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/builder.py @@ -0,0 +1,95 @@ +"""SQL builder pipeline. + +1) SQLBuilder.compile takes a Plan or dict, instantiates SQLState with a dialect. +2) For each item in plan._ops, it looks up the Op subclass in the registry and applies it. +3) SQLState.build: + - injects FROM (always required) and a default SELECT if missing, + - renders phases in a stable order (SELECT → FROM → WHERE), + - collects the final SQL string and the bound params from CompileCtx. +""" + +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Any, Dict, List +from collections import defaultdict +from .dialects import Dialect, SQLiteDialect +from .runtime import CompileCtx +from .clauses import Clause, SelectClause, FromClause, WhereClause +from .ops import Op, SelectOp, WhereOp # ensure registry +from .ir import Plan + +@dataclass +class SQLState: + """Mutable assembly state for a single SQL statement.""" + source: str + dialect: Dialect + clauses: Dict[str, List[Clause]] = field(default_factory=lambda: defaultdict(list)) + CLAUSE_ORDER: List[str] = field(default_factory=lambda: [ + "select", "from", "where", "group_by", "having", "order_by", "limit", "offset" +]) + + # Helper methods to avoid importing Clause classes in ops + def add_select(self, columns: List[str]) -> None: + self.clauses["select"].append(SelectClause(columns)) + def add_where(self, cond) -> None: + self.clauses["where"].append(WhereClause(cond)) + def add_clause(self, clause: Clause) -> None: + self.clauses[clause.phase].append(clause) + + def has_phase(self, name: str) -> bool: + return name in self.clauses and len(self.clauses[name]) > 0 + + def build(self) -> tuple[str, List[Any]]: + """Render all collected clauses into a SQL string and parameter list.""" + ctx = CompileCtx(self.dialect) + parts: List[str] = [] + + # Ensure FROM exists for this plan + self.add_clause(FromClause(self.source)) + # Ensure SELECT exists (default "*") if none provided + if not self.has_phase("select"): + self.add_select(["*"]) + + # Render ordered phases, then any extras not in the order list + seen = set() + def render_phase(phase: str): + items = self.clauses.get(phase, []) + if not items: return + kw = items[0].keyword() + joiner = items[0].joiner() + frags = [c.fragment(ctx) for c in items] + if frags: parts.append(f"{kw} {joiner.join(frags)}") + seen.add(phase) + + for ph in self.CLAUSE_ORDER: render_phase(ph) + for ph in list(self.clauses.keys()): + if ph not in seen: render_phase(ph) + + sql = " ".join(parts) + return sql, ctx.params + +class SQLBuilder: + """Facade that compiles a strict Plan (or dict) into (sql, params) using a dialect.""" + def __init__(self, dialect: Dialect | None = None) -> None: + self.dialect = dialect or SQLiteDialect() + def compile(self, plan: Plan | Dict[str, Any]) -> tuple[str, List[Any]]: + """Validate ops, apply them to SQLState, and produce final SQL + params.""" + p = plan if isinstance(plan, Plan) else Plan.from_dict(plan) + st = SQLState(source=p.source, dialect=self.dialect) + for op in p._ops: + if not isinstance(op, dict): + raise TypeError(f"Each operation must be a dict, got {type(op).__name__}: {op}") + + allowed_keys = {"op", "columns", "cond", "directions", "limit", "offset"} + extra = set(op.keys()) - allowed_keys + if extra: + raise ValueError(f"Unknown keys in op: {op}") + + op_type = op.get("op") + if op_type not in Op.registry: + raise ValueError(f"Unsupported op {op_type!r}. Allowed: {sorted(Op.registry)}") + + # Pass all keys except "op" as kwargs + Op.registry[op_type](**{k: v for k, v in op.items() if k != "op"}).apply(st) + + return st.build() \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/dsl/clauses.py b/AgCloud/GUI/src/vast/dsl/clauses.py new file mode 100644 index 000000000..cc7cfddc9 --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/clauses.py @@ -0,0 +1,157 @@ +"""Low-level SQL clause objects. + +Each Clause knows: +- its logical phase (e.g., 'select', 'from', 'where'), +- how to render its keyword and fragment, +- and how to join multiple instances within the same phase. +""" + +from __future__ import annotations +from dataclasses import dataclass +from typing import List +from abc import ABC, abstractmethod +from .runtime import CompileCtx +from .expr import Cond + +class Clause(ABC): + @property + @abstractmethod + def phase(self) -> str: ... # e.g., "select", "from", "where", "order_by" + @abstractmethod + def keyword(self) -> str: ... # e.g., "SELECT", "FROM", "WHERE" + @abstractmethod + def joiner(self) -> str: ... # how to join multiple fragments in same phase + @abstractmethod + def fragment(self, ctx: CompileCtx) -> str: ... + +@dataclass +class SelectClause(Clause): + columns: List[str] + + @property + def phase(self) -> str: + return "select" + + def keyword(self) -> str: + return "SELECT" + + def joiner(self) -> str: + return ", " + + def fragment(self, ctx: CompileCtx) -> str: + def star_aware_quote(col: str) -> str: + # Skip quoting if expression or alias (COUNT(*), AVG(...), AS, etc.) + if any(token in col.upper() for token in ("(", ")", " AS ")): + return col # treat as SQL expression + + # Support "*", "tbl.*", and dotted names that may end with * + parts = col.split(".") + quoted = [] + for p in parts: + if p == "*": + quoted.append("*") + else: + quoted.append(ctx.dialect.quote_ident(p)) + return ".".join(quoted) + + if not self.columns: + return "*" # default + return ", ".join(star_aware_quote(c) for c in self.columns) + + + +@dataclass +class FromClause(Clause): + source: str + @property + def phase(self) -> str: return "from" + def keyword(self) -> str: return "FROM" + def joiner(self) -> str: return " " + def fragment(self, ctx: CompileCtx) -> str: + return ctx.dialect.quote_ident(self.source) + +@dataclass +class WhereClause(Clause): + cond: Cond + @property + def phase(self) -> str: return "where" + def keyword(self) -> str: return "WHERE" + def joiner(self) -> str: return " AND " + def fragment(self, ctx: CompileCtx) -> str: + return self.cond.compile(ctx) + + +@dataclass +class OrderByClause(Clause): + columns: list[str] + directions: list[str] + + @property + def phase(self): return "order_by" + def keyword(self): return "ORDER BY" + def joiner(self): return ", " + + def fragment(self, ctx): + def quote_or_passthrough(col: str) -> str: + # Skip quoting if it's clearly an SQL expression or aggregate + if any(token in col.upper() for token in ( + "(", ")", " AS ", "COUNT", "AVG", "SUM", "MAX", "MIN" + )): + return col # leave expressions as-is + return ctx.dialect.quote_ident(col) + + cols = [quote_or_passthrough(c) for c in self.columns] + dirs = self.directions or ["ASC"] * len(cols) + return ", ".join(f"{c} {d.upper()}" for c, d in zip(cols, dirs)) + + + +@dataclass +class LimitClause(Clause): + limit: int + @property + def phase(self): return "limit" + def keyword(self): return "LIMIT" + def joiner(self): return " " + def fragment(self, ctx): return str(self.limit) + + +@dataclass +class OffsetClause(Clause): + offset: int + @property + def phase(self): return "offset" + def keyword(self): return "OFFSET" + def joiner(self): return " " + def fragment(self, ctx): return str(self.offset) + + +@dataclass +class GroupByClause(Clause): + columns: list[str] + + @property + def phase(self): return "group_by" + def keyword(self): return "GROUP BY" + def joiner(self): return ", " + + def fragment(self, ctx): + def quote_or_passthrough(col: str) -> str: + # Skip quoting if it's a SQL expression or function call + if any(token in col.upper() for token in ( + "(", ")", " AS ", "COUNT", "AVG", "SUM", "MAX", "MIN", "DATE_TRUNC" + )): + return col + return ctx.dialect.quote_ident(col) + return ", ".join(quote_or_passthrough(c) for c in self.columns) + + + +@dataclass +class HavingClause(Clause): + cond: Cond + @property + def phase(self): return "having" + def keyword(self): return "HAVING" + def joiner(self): return " AND " + def fragment(self, ctx): return self.cond.compile(ctx) diff --git a/AgCloud/GUI/src/vast/dsl/dialects.py b/AgCloud/GUI/src/vast/dsl/dialects.py new file mode 100644 index 000000000..36bcce02b --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/dialects.py @@ -0,0 +1,85 @@ +"""SQL dialect abstraction. + +Each dialect decides: +- how to quote identifiers (tables/columns), +- how to normalize Python booleans to DB-native values, +- and what placeholder syntax to use for bound parameters. +""" + + +from __future__ import annotations +from abc import ABC, abstractmethod +from typing import Any + +class Dialect(ABC): + @abstractmethod + def quote_ident(self, name: str) -> str: + """Quote an identifier, supporting dotted paths like schema.table or table.column.""" + ... + @abstractmethod + def normalize_bool(self, v: Any) -> Any: + """Convert Python bools to dialect-appropriate values (e.g., 0/1 for SQLite).""" + ... + @abstractmethod + def placeholder(self, idx: int) -> str: + """Return a placeholder string for the given 1-based parameter index.""" + ... + +class SQLiteDialect(Dialect): + def quote_ident(self, name: str) -> str: + parts = name.split(".") + return ".".join('"' + p.replace('"', '""') + '"' for p in parts) + def normalize_bool(self, v: Any) -> Any: + return int(v) if isinstance(v, bool) else v + def placeholder(self, idx: int) -> str: + return "?" # qmark style + +# class PostgresDialect(Dialect): +# def __init__(self, style: str = "psycopg"): +# """style: +# - 'psycopg' → %s style placeholders (psycopg2/3) +# - 'numeric' → $1, $2, ... style placeholders (asyncpg) +# """ + +# if style not in ("psycopg", "numeric"): +# raise ValueError("PostgresDialect.style must be 'psycopg' or 'numeric'") +# self.style = style +# def quote_ident(self, name: str) -> str: +# parts = name.split(".") +# return ".".join('"' + p.replace('"', '""') + '"' for p in parts) +# def normalize_bool(self, v: Any) -> Any: +# return v # PostgreSQL has a real boolean type +# def placeholder(self, idx: int) -> str: +# return "%s" if self.style == "psycopg" else f"${idx}" +# dialects.py +class PostgresDialect(Dialect): + def __init__(self, style: str = "named"): + """ + style: + - 'psycopg' → %s placeholders (for psycopg2) + - 'numeric' → $1, $2 placeholders (for asyncpg) + - 'named' → :p1, :p2 placeholders (for SQLAlchemy.text) + """ + if style not in ("psycopg", "numeric", "named"): + raise ValueError("PostgresDialect.style must be 'psycopg', 'numeric', or 'named'") + self.style = style + + def quote_ident(self, name: str) -> str: + parts = name.split(".") + escaped = [] + for p in parts: + escaped_name = p.replace('"', '""') + escaped.append(f'"{escaped_name}"') + return ".".join(escaped) + + + def normalize_bool(self, v: Any) -> Any: + return v + + def placeholder(self, idx: int) -> str: + if self.style == "psycopg": + return "%s" + elif self.style == "numeric": + return f"${idx}" + else: # named + return f":p{idx}" diff --git a/AgCloud/GUI/src/vast/dsl/expr.py b/AgCloud/GUI/src/vast/dsl/expr.py new file mode 100644 index 000000000..4e9cffc70 --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/expr.py @@ -0,0 +1,171 @@ +"""Expression and condition trees for the DSL. + +Two layers: +- Expr: scalar leaves (columns, literals) used inside predicates. +- Cond: boolean expressions (AND/OR/predicates) used in WHERE. + +We purposely restrict binary predicates to compare only Col and Literal +to keep compilation and parameterization simple and safe. +""" + +from __future__ import annotations +from dataclasses import dataclass +from typing import Any, Dict +from abc import ABC, abstractmethod +from .runtime import CompileCtx +from .binops import BinOp + +ALLOWED_BIN_OPS = {"=", "!=", "<", "<=", ">", ">="} + +class Expr(ABC): + @abstractmethod + def compile(self, ctx: CompileCtx) -> str: + """Render this scalar expression to SQL, possibly producing a placeholder.""" + ... + + @abstractmethod + def to_ir(self) -> Dict[str, Any]: + """Serialize to strict JSON IR (e.g., {'col': 'name'} or {'literal': 42}).""" + ... + + # Operator sugar → Predicate + def __eq__(self, other: Any) -> "Predicate": return Predicate(self, BinOp.EQ, ensure_expr(other)) + def __ne__(self, other: Any) -> "Predicate": return Predicate(self, BinOp.NE, ensure_expr(other)) + def __lt__(self, other: Any) -> "Predicate": return Predicate(self, BinOp.LT, ensure_expr(other)) + def __le__(self, other: Any) -> "Predicate": return Predicate(self, BinOp.LE, ensure_expr(other)) + def __gt__(self, other: Any) -> "Predicate": return Predicate(self, BinOp.GT, ensure_expr(other)) + def __ge__(self, other: Any) -> "Predicate": return Predicate(self, BinOp.GE, ensure_expr(other)) + +@dataclass +class Col(Expr): + """A column reference (optionally dotted, e.g., 'schema.table' or 'table.col').""" + name: str + def compile(self, ctx: CompileCtx) -> str: return ctx.dialect.quote_ident(self.name) + def to_ir(self) -> Dict[str, Any]: return {"col": self.name} + +@dataclass +class Literal(Expr): + """A literal value that becomes a bound parameter (with a placeholder).""" + value: Any + + def compile(self, ctx: CompileCtx) -> str: + # Detect SQL expressions that should be inlined (not parameterized) + if isinstance(self.value, str) and any( + kw in self.value.lower() + for kw in ("now()", "interval", "date_trunc", "current_date", "current_timestamp") + ): + return self.value # inline directly as SQL expression + + # Default: treat as bound parameter + return ctx.add_param(self.value) + + def to_ir(self) -> Dict[str, Any]: + return {"literal": self.value} + + +def ensure_expr(x: Any) -> Expr: + """Coerce Python values to Literal, leave Expr as-is.""" + return x if isinstance(x, Expr) else Literal(x) + +# ---- Boolean conditions ---- +class Cond(ABC): + @abstractmethod + def compile(self, ctx: CompileCtx) -> str: + """Render this boolean condition to SQL text.""" + ... + @abstractmethod + def to_ir(self) -> Dict[str, Any]: + """Serialize to strict JSON IR.""" + ... + +@dataclass +class Predicate(Cond): + """Binary predicate: (left right).""" + left: Expr + op: BinOp + right: Expr + def __post_init__(self): + if not isinstance(self.left, (Col, Literal)) or not isinstance(self.right, (Col, Literal)): + raise TypeError("Predicate must compare columns and/or literals only") + def compile(self, ctx: CompileCtx) -> str: + return f"({self.left.compile(ctx)} {self.op.value} {self.right.compile(ctx)})" + def to_ir(self) -> Dict[str, Any]: + return {"op": self.op.value, "left": self.left.to_ir(), "right": self.right.to_ir()} + + +class All(Cond): # AND + parts: list[Cond] + def __init__(self, *parts: Cond): self.parts = list(parts) + def compile(self, ctx: CompileCtx) -> str: return "(" + " AND ".join(p.compile(ctx) for p in self.parts) + ")" + def to_ir(self) -> Dict[str, Any]: return {"all": [p.to_ir() for p in self.parts]} + + +class Any(Cond): # OR + parts: list[Cond] + def __init__(self, *parts: Cond): self.parts = list(parts) + def compile(self, ctx: CompileCtx) -> str: return "(" + " OR ".join(p.compile(ctx) for p in self.parts) + ")" + def to_ir(self) -> Dict[str, Any]: return {"any": [p.to_ir() for p in self.parts]} + +# ---- Strict IR decoding ---- + +def expr_from_ir(d: Dict[str, Any]) -> Expr: + """Decode a strict Expr IR object into Expr.""" + if not isinstance(d, dict): raise TypeError("Expr leaf must be an object") + keys = set(d.keys()) + if keys == {"col"}: return Col(d["col"]) + if keys == {"literal"}: return Literal(d["literal"]) + raise ValueError("Expr leaf must be either {\"col\": name} or {\"literal\": value}") + + +def cond_from_ir(d: Dict[str, Any]) -> Cond: + """Decode a strict Cond IR object into Cond with defensive validation.""" + # ───────────── DEBUG LOG ───────────── + import traceback + print("\n[DEBUG cond_from_ir] called with:", repr(d)) + # Optional: print call stack to see which Op invoked this + # print("".join(traceback.format_stack(limit=3))) + + if not isinstance(d, dict): + raise TypeError(f"Cond node must be an object, got {type(d).__name__}: {d}") + + keys = set(d.keys()) + + # ───── Logical AND + if keys == {"all"}: + arr = d["all"] + if not isinstance(arr, list): + raise TypeError(f"'all' must be a list of condition objects, got {type(arr).__name__}") + for i, item in enumerate(arr): + if not isinstance(item, dict): + raise TypeError(f"List argument at index {i} must be a dict, got {type(item).__name__}: {item}") + return All(*[cond_from_ir(x) for x in arr]) + + # ───── Logical OR + if keys == {"any"}: + arr = d["any"] + if not isinstance(arr, list): + raise TypeError(f"'any' must be a list of condition objects, got {type(arr).__name__}") + for i, item in enumerate(arr): + if not isinstance(item, dict): + raise TypeError(f"List argument at index {i} must be a dict, got {type(item).__name__}: {item}") + return Any(*[cond_from_ir(x) for x in arr]) + + # ───── Predicate + if keys == {"op", "left", "right"}: + try: + op = BinOp(d["op"]) + except ValueError as e: + allowed = ", ".join(o.value for o in BinOp) + raise ValueError(f"Operator {d['op']!r} not allowed. Allowed: [{allowed}]") from e + return Predicate(expr_from_ir(d["left"]), op, expr_from_ir(d["right"])) + + if "col" in d and "literal" in d: + return Predicate(expr_from_ir({"col": d["col"]}), BinOp.EQ, expr_from_ir({"literal": d["literal"]})) + + raise ValueError(f"Invalid condition node: {d}") + + + +# Convenience aliases +AND = All +OR = Any \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/dsl/ir.py b/AgCloud/GUI/src/vast/dsl/ir.py new file mode 100644 index 000000000..e5d678806 --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/ir.py @@ -0,0 +1,25 @@ +from __future__ import annotations +"""Strict JSON IR container for query plans. + +A Plan is intentionally minimal: a source (table/view) and a list of ops. +Serialization uses a stable shape to keep validation simple. +""" + +from dataclasses import dataclass, field +from typing import Any, Dict, List + +@dataclass +class Plan: + source: str + _ops: List[Dict[str, Any]] = field(default_factory=list) + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a minimal, stable dict shape.""" + return {"source": self.source, "_ops": self._ops} + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "Plan": + """Validate and construct a Plan from a dict.""" + if set(d.keys()) != {"source", "_ops"}: + raise ValueError("Plan must contain exactly 'source' and '_ops'") + return Plan(source=d["source"], _ops=list(d.get("_ops", []))) \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/dsl/ops.py b/AgCloud/GUI/src/vast/dsl/ops.py new file mode 100644 index 000000000..f92ca0528 --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/ops.py @@ -0,0 +1,81 @@ +"""Plan operations → SQLState mutations. + +Ops transform high-level IR steps into concrete Clause objects and attach them +to the SQLState. A small registry maps 'op' strings to Op subclasses. +""" + +from __future__ import annotations +from typing import Any, Dict, List, Type +from abc import ABC, abstractmethod +from .clauses import SelectClause, WhereClause,OrderByClause,LimitClause,HavingClause,OffsetClause,GroupByClause +from .expr import cond_from_ir +# from .builder import SQLState + +class Op(ABC): + registry: Dict[str, Type["Op"]] = {} + def __init_subclass__(cls, **kwargs): + """Automatically register subclasses that declare `op_type`.""" + super().__init_subclass__(**kwargs) + op_type = getattr(cls, "op_type", None) + if op_type: Op.registry[op_type] = cls + def __init__(self, **payload: Any): self.payload = payload + + @abstractmethod + def apply(self, st: "SQLState") -> None: + """Mutate SQLState by adding clauses based on this operation.""" + ... + +class SelectOp(Op): + op_type = "select" + def apply(self, st: "SQLState") -> None: + cols = self.payload.get("columns") + st.add_select(cols or []) + +class WhereOp(Op): + op_type = "where" + def apply(self, st: "SQLState") -> None: + cond_ir = self.payload.get("cond") + if not isinstance(cond_ir, dict): + raise TypeError( + f"Invalid WHERE condition: expected dict, got {type(cond_ir).__name__} → {cond_ir}" + ) + st.add_where(cond_from_ir(cond_ir)) + + +class HavingOp(Op): + op_type = "having" + def apply(self, st): + cond_ir = self.payload.get("cond") + if not isinstance(cond_ir, dict): + raise TypeError( + f"Invalid HAVING condition: expected dict, got {type(cond_ir).__name__} → {cond_ir}" + ) + st.add_clause(HavingClause(cond_from_ir(cond_ir))) + + + +class OrderByOp(Op): + op_type = "order_by" + def apply(self, st): + st.add_clause(OrderByClause( + self.payload.get("columns", []), + self.payload.get("directions", []) + )) + +class LimitOp(Op): + op_type = "limit" + def apply(self, st): + st.add_clause(LimitClause(self.payload["limit"])) + +class OffsetOp(Op): + op_type = "offset" + def apply(self, st): + st.add_clause(OffsetClause(self.payload["offset"])) + +class GroupByOp(Op): + op_type = "group_by" + def apply(self, st): + st.add_clause(GroupByClause(self.payload.get("columns", []))) + + + diff --git a/AgCloud/GUI/src/vast/dsl/query.py b/AgCloud/GUI/src/vast/dsl/query.py new file mode 100644 index 000000000..b21965bae --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/query.py @@ -0,0 +1,57 @@ +"""Fluent, chainable Python DSL that produces a strict Plan and compiles to SQL. + +Example: + q = Query("sensors").select("sensor_id").where(Col("status") == "ok") + sql, params = q.to_sql() +""" + +from __future__ import annotations +from .ir import Plan +from .expr import Cond +from .builder import SQLBuilder +from .dialects import Dialect +from typing import List, Tuple + +class Query: + def __init__(self, source: str): + self._plan = Plan(source=source) + + def select(self, *columns: str) -> "Query": + """Add a SELECT op (empty → '*').""" + self._plan._ops.append({"op": "select", "columns": list(columns)}) + return self + + def where(self, cond: Cond) -> "Query": + """Add a WHERE op by serializing the Cond tree to strict IR.""" + self._plan._ops.append({"op": "where", "cond": cond.to_ir()}) + return self + + def order_by(self, *columns: str, directions: list[str] | None = None) -> "Query": + directions = directions or ["ASC"] * len(columns) + self._plan._ops.append({"op": "order_by", "columns": list(columns), "directions": directions}) + return self + + def limit(self, n: int) -> "Query": + self._plan._ops.append({"op": "limit", "limit": n}) + return self + + def offset(self, n: int) -> "Query": + self._plan._ops.append({"op": "offset", "offset": n}) + return self + + def group_by(self, *columns: str) -> "Query": + self._plan._ops.append({"op": "group_by", "columns": list(columns)}) + return self + + def having(self, cond: Cond) -> "Query": + self._plan._ops.append({"op": "having", "cond": cond.to_ir()}) + return self + + + def to_plan(self) -> Plan: + """Return the underlying Plan object (e.g., for transport).""" + return self._plan + + def to_sql(self, dialect: Dialect | None = None): + """Compile the plan with the chosen dialect (SQLite by default).""" + return SQLBuilder(dialect).compile(self._plan) \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/dsl/runtime.py b/AgCloud/GUI/src/vast/dsl/runtime.py new file mode 100644 index 000000000..15fdaefb8 --- /dev/null +++ b/AgCloud/GUI/src/vast/dsl/runtime.py @@ -0,0 +1,21 @@ +"""Compilation-time context. + +Holds the evolving list of bound parameters and the active dialect, +and exposes a helper to push a parameter and get the correct placeholder. +""" +from __future__ import annotations +from typing import Any, List +from .dialects import Dialect + +class CompileCtx: + def __init__(self, dialect: Dialect) -> None: + self.params: dict[str, Any] = {} + self.dialect = dialect + + def add_param(self, v: Any) -> str: + """Append a value and return its dialect-specific placeholder.""" + idx = len(self.params) + 1 + name = f"p{idx}" + self.params[name] = self.dialect.normalize_bool(v) + return self.dialect.placeholder(idx) + diff --git a/AgCloud/GUI/src/vast/gateway/.dockerignore b/AgCloud/GUI/src/vast/gateway/.dockerignore new file mode 100644 index 000000000..6333d2e75 --- /dev/null +++ b/AgCloud/GUI/src/vast/gateway/.dockerignore @@ -0,0 +1,22 @@ +# VCS / editors +.git/ +.gitignore +.vscode/ +.idea/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.pytest_cache/ +.venv/ +venv/ + +# OS cruft +.DS_Store + +# Misc +*.log +dist/ +build/ diff --git a/AgCloud/GUI/src/vast/gateway/Dockerfile b/AgCloud/GUI/src/vast/gateway/Dockerfile new file mode 100644 index 000000000..f14d3fa70 --- /dev/null +++ b/AgCloud/GUI/src/vast/gateway/Dockerfile @@ -0,0 +1,74 @@ +# syntax=docker/dockerfile:1.6 + +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app +# build arg +# ARG USE_NETFREE=true + +# Toggle NetFree handling at build time: +# docker build --build-arg USE_NETFREE=true -t image:netfree . +# docker build --build-arg USE_NETFREE=false -t image:default . +ARG USE_NETFREE=false + +# Base system tools (certificates + curl) +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# Conditionally load extra CA certs from build context ./certs (if exists) +# - With BuildKit, the mount is optional (required=false). +# - If USE_NETFREE=false or no *.crt files exist, nothing happens. +RUN --mount=type=bind,source=certs,target=/tmp/certs,required=false \ + set -eux; \ + if [ "${USE_NETFREE}" = "true" ] \ + && [ -d /tmp/certs ] \ + && ls /tmp/certs/*.crt >/dev/null 2>&1; then \ + echo "Adding extra CA certs from /tmp/certs ..."; \ + cp /tmp/certs/*.crt /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ + else \ + echo "No extra CA certs configured (USE_NETFREE=${USE_NETFREE})."; \ + fi + +# System-wide SSL env (works with or without extra CAs) +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# Python dependencies + + +# # System CA + add NetFree certs +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* +COPY certs/*.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates || true +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# Python deps +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt \ + --index-url https://pypi.org/simple \ + --trusted-host pypi.org \ + --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org + +# App code +COPY src/vast/gateway /app/vast/gateway +COPY src/vast/proto /app/vast/proto + +# Generate gRPC stubs +RUN python -m grpc_tools.protoc -I./vast/proto \ + --python_out=. --grpc_python_out=. \ + ./vast/proto/query.proto + +ENV PYTHONPATH=/app/vast/proto/generated:/app +ENV RUNNER_ADDR=runner:50051 + +EXPOSE 8000 +CMD ["uvicorn", "vast.gateway.gateway_main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/AgCloud/GUI/src/vast/gateway/__init__.py b/AgCloud/GUI/src/vast/gateway/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/gateway/app.py b/AgCloud/GUI/src/vast/gateway/app.py new file mode 100644 index 000000000..2e02bbe87 --- /dev/null +++ b/AgCloud/GUI/src/vast/gateway/app.py @@ -0,0 +1,199 @@ +""" +Gateway service (FastAPI → gRPC) with an app factory. + +Strict plan validation for the DSL: +- Plan: {"source": str, "_ops": [SelectOp | WhereOp]} +- SelectOp: {"op":"select","columns":[str,...]} +- WhereOp: {"op":"where","cond": Cond} +- Cond: {"all":[Cond,...]} | {"any":[Cond,...]} | {"op":OP,"left":Leaf,"right":Leaf} +- Leaf: {"col": str} | {"literal": JSON} +- OP: one of "=", "!=", "<", "<=", ">", ">=" +""" + +from __future__ import annotations + +import os, uuid, json, logging +from typing import Any, Union, List, Mapping +from fastapi import FastAPI, HTTPException, Request, Body +from fastapi import status as http_status +from pydantic import BaseModel, Field, ConfigDict, ValidationError +from contextlib import asynccontextmanager +import grpc +from vast.proto.generated import query_pb2, query_pb2_grpc + +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() +logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s %(message)s") +log = logging.getLogger("gateway") + +# ---------- Strict Plan Models (Pydantic v2) ---------- + +ALLOWED_BIN_OPS = {"=", "!=", "<", "<=", ">", ">="} + +class ColLeaf(BaseModel): + model_config = ConfigDict(extra="forbid") + col: str + +class LiteralLeaf(BaseModel): + model_config = ConfigDict(extra="forbid") + literal: Any + +Leaf = Union[ColLeaf, LiteralLeaf] + +class PredicateCond(BaseModel): + model_config = ConfigDict(extra="forbid") + op: str + left: Leaf + right: Leaf + + def model_post_init(self, _context): + if self.op not in ALLOWED_BIN_OPS: + raise ValueError(f"Operator {self.op!r} not allowed. Allowed: {sorted(ALLOWED_BIN_OPS)}") + +class AllCond(BaseModel): + model_config = ConfigDict(extra="forbid") + all: List["Cond"] # forward ref + +class AnyCond(BaseModel): + model_config = ConfigDict(extra="forbid") + any: List["Cond"] + +Cond = Union[AllCond, AnyCond, PredicateCond] +AllCond.model_rebuild() +AnyCond.model_rebuild() + +class SelectOpModel(BaseModel): + model_config = ConfigDict(extra="forbid") + op: str + columns: List[str] + + def model_post_init(self, _context): + if self.op != "select": + raise ValueError("Select op 'op' must be 'select'") + if not self.columns: + raise ValueError("Select op requires non-empty 'columns'") + +class WhereOpModel(BaseModel): + model_config = ConfigDict(extra="forbid") + op: str + cond: Cond + + def model_post_init(self, _context): + if self.op != "where": + raise ValueError("Where op 'op' must be 'where'") + +OpModel = Union[SelectOpModel, WhereOpModel] + +class PlanModel(BaseModel): + """ + Strict Plan: exactly {"source": str, "_ops": [OpModel,...]}. + We forbid extras and use alias for `_ops`. + """ + model_config = ConfigDict(extra="forbid", populate_by_name=True) + source: str + ops_: List[OpModel] = Field(alias="_ops") + +# ---------- Response model (unchanged) ---------- + +class SensorModel(BaseModel): + sensor_id: str + lat: float + lon: float + model_config = ConfigDict(extra="allow") # passthrough props + + +# ---------- Utilities ---------- + +_ERROR_MAP: dict[grpc.StatusCode, tuple[int, str | None]] = { + grpc.StatusCode.INVALID_ARGUMENT: (400, None), + grpc.StatusCode.DEADLINE_EXCEEDED: (504, "Runner deadline exceeded"), + grpc.StatusCode.UNAVAILABLE: (503, "Runner unavailable"), + grpc.StatusCode.UNIMPLEMENTED: (501, "Runner feature not implemented"), +} + +def _map_grpc_error(e: grpc.aio.AioRpcError) -> HTTPException: + code = e.code() + detail = e.details() or "" + http_status_code, static_msg = _ERROR_MAP.get(code, (502, None)) + message = static_msg if static_msg is not None else detail or f"Runner error: {code.name}" + return HTTPException(status_code=http_status_code, detail=message) + +# ---------- App Factory ---------- + +def create_app(*, runner_addr: str | None = None, stub: query_pb2_grpc.QueryRunnerStub | None = None) -> FastAPI: + """ + Build a FastAPI app. + - If `stub` is provided (tests), it's used directly. + - Else we create a grpc.aio channel to `runner_addr` (or $RUNNER_ADDR / default). + """ + runner_addr = runner_addr or os.getenv("RUNNER_ADDR", "localhost:50051") + + @asynccontextmanager + async def lifespan(app: FastAPI): + if stub is not None: + app.state.channel = None + app.state.stub = stub + log.info("gateway_startup (injected_stub)") + yield + log.info("gateway_shutdown (injected_stub)") + return + + channel = grpc.aio.insecure_channel(runner_addr) + app.state.channel = channel + app.state.stub = query_pb2_grpc.QueryRunnerStub(channel) + log.info("gateway_startup (runner_addr=%s)", runner_addr) + try: + yield + finally: + try: + await channel.close() + except Exception as e: + log.warning("gateway_shutdown_error: %s", e) + log.info("gateway_shutdown") + + app = FastAPI(title="Query Gateway (FastAPI → gRPC Runner)", lifespan=lifespan) + + @app.post("/runQuery", response_model=List[SensorModel], status_code=http_status.HTTP_200_OK) + async def run_query(request: Request, plan: PlanModel = Body(...)) -> List[SensorModel]: + """ + Accept a strict JSON plan, forward to the runner via gRPC, and return sensors. + - Propagates/creates X-Request-Id (also returned as a response header by infra, if desired). + - Sends plan as query_pb2.Plan(json=...). + - 30s timeout on the RPC (configurable via env / edit here). + - Maps gRPC errors to HTTP codes. + """ + request_id = (request.headers.get("X-Request-Id") if request else None) or str(uuid.uuid4()) + + # Validate & serialize STRICT plan (by_alias=True to emit "_ops") + try: + payload_dict = plan.model_dump(by_alias=True) + payload = json.dumps(payload_dict, separators=(",", ":")) + except ValidationError as e: + log.error("plan_validation_error request_id=%s err=%s", request_id, e) + raise HTTPException(status_code=400, detail="Invalid plan") + except Exception as e: + log.error("plan_serialization_error request_id=%s err=%s", request_id, e) + raise HTTPException(status_code=400, detail="Invalid plan") + + log.info("run_query_received request_id=%s size=%dB", request_id, len(payload)) + + try: + metadata = (("x-request-id", request_id),) + resp = await app.state.stub.RunQuery( + query_pb2.Plan(json=payload), + metadata=metadata, + timeout=30.0, + ) + except grpc.aio.AioRpcError as e: + log.error("runner_rpc_error request_id=%s code=%s detail=%s", + request_id, e.code().name, e.details() or "") + raise _map_grpc_error(e) + + # Convert gRPC SensorList -> list[dict] (merge props map) + sensors: List[SensorModel] = [] + for s in resp.sensors: + row: Mapping[str, Any] = {"sensor_id": s.sensor_id, "lat": s.lat, "lon": s.lon, **dict(s.props)} + sensors.append(SensorModel.model_validate(row)) + log.info("run_query_success request_id=%s sensors=%d", request_id, len(sensors)) + return sensors + + return app \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/gateway/gateway_main.py b/AgCloud/GUI/src/vast/gateway/gateway_main.py new file mode 100644 index 000000000..19efabc00 --- /dev/null +++ b/AgCloud/GUI/src/vast/gateway/gateway_main.py @@ -0,0 +1,5 @@ +""" +Legacy entry that exposes `app` for uvicorn and imports the factory. +""" +from .app import create_app +app = create_app() diff --git a/AgCloud/GUI/src/vast/home_view.py b/AgCloud/GUI/src/vast/home_view.py new file mode 100644 index 000000000..b82ef2ef1 --- /dev/null +++ b/AgCloud/GUI/src/vast/home_view.py @@ -0,0 +1,176 @@ +from __future__ import annotations +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtCore import QUrl, pyqtSignal +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QSizePolicy, QPushButton +) +from orthophoto_canvas.ui.viewer_factory import create_orthophoto_viewer +from vast.orthophoto_canvas.ui.sensors_layer import SensorLayer, add_sensors_by_gps_bulk +from orthophoto_canvas.ag_io import sensors_api +import os + +from vast.orthophoto_canvas.ui.alert_layer import AlertLayer + + +class HomeView(QWidget): + openSensorsRequested = pyqtSignal() + + def __init__(self, api, alert_service, parent: QWidget | None = None): + super().__init__(parent) + self.api = api + self.alert_service = alert_service + + # ───────────────────────────── + # Root vertical layout + # ───────────────────────────── + root = QVBoxLayout(self) + root.setContentsMargins(12, 12, 12, 12) + root.setSpacing(10) + + # Header + header = QLabel("Sensors Dashboard (Grafana)") + header.setStyleSheet("font-size: 20px; font-weight: 600; margin-bottom: 8px;") + root.addWidget(header) + + # ───────────────────────────── + # Main content: Map (left) + Panels (right) + # ───────────────────────────── + main_layout = QHBoxLayout() + main_layout.setSpacing(12) + root.addLayout(main_layout, stretch=1) + + # ───── Map on the left ───── + tiles_root = "./src/vast/orthophoto_canvas/data/tiles" + self.viewer = create_orthophoto_viewer(tiles_root, forced_scheme=None, parent=self) + self.viewer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.viewer.setMinimumSize(700, 700) # Ensures it's visibly large and square + main_layout.addWidget(self.viewer, stretch=3) + + # ───── Grafana panels on the right ───── + right_box = QVBoxLayout() + right_box.setSpacing(10) + main_layout.addLayout(right_box, stretch=2) + + grafana_host = os.getenv("GRAFANA_HOST", "grafana") + base = f"http://{grafana_host}:3000" + panel_urls = [ + QUrl(f"{base}/d-solo/agcloud-sensors/sensors?orgId=1&panelId=1&from=now-6h&to=now&refresh=10s&theme=light"), + QUrl(f"{base}/d-solo/agcloud-sensors/sensors?orgId=1&panelId=2&from=now-6h&to=now&refresh=10s&theme=light"), + ] + + for url in panel_urls: + view = QWebEngineView(self) + view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + view.setFixedHeight(300) + view.setUrl(url) + right_box.addWidget(view) + + # ───────────────────────────── + # Load sensors layer + # ───────────────────────────── + gateway_url = os.getenv("GATEWAY_URL", "http://gateway:8000") + sensors_api.GATEWAY_URL = gateway_url + rows = sensors_api.get_sensors() + + self.sensor_layer = SensorLayer(self.viewer) + add_sensors_by_gps_bulk( + self.sensor_layer, + rows, + center_on_first=True, + default_radius_px=0.2 + ) + + # ───────────────────────────── + # Alerts layer setup + # ───────────────────────────── + self.alert_layer = AlertLayer(self.viewer) + self.alert_service.alertsUpdated.connect(self._on_alerts_updated) + self.alert_service.alertAdded.connect(self._on_alert_added) + self.alert_service.alertRemoved.connect(self._on_alert_removed) + self.alert_service.load_initial() + + # ───────────────────────────── + # Footer button + # ───────────────────────────── + self.sensor_types_btn = QPushButton("Sensor Types") + self.sensor_types_btn.setStyleSheet("padding: 8px 12px; font-weight: 500;") + self.sensor_types_btn.clicked.connect(self.openSensorsRequested.emit) + root.addWidget(self.sensor_types_btn) + + # ───────────────────────────── + # Keep the map square on resize + # ───────────────────────────── + def resizeEvent(self, event): + super().resizeEvent(event) + if self.viewer: + # Square size = min(available height, available width fraction) + left_width = int(self.width() * 0.6) + height = self.height() - 100 + size = min(left_width, height) + if size > 400: + self.viewer.setFixedSize(size-50, size-50) + + # ───────────────────────────── + # Alerts Handlers + # ───────────────────────────── + def _on_alerts_updated(self, alerts: list): + print(f"[HomeView] Full alert update: {len(alerts)} alerts") + + active_alerts = [a for a in alerts if not a.get("ended_at") and not a.get("endedAt")] + print(f"[HomeView] Displaying {len(active_alerts)} active alerts on map") + + self.alert_layer.clear_alerts() + for alert in active_alerts: + self.alert_layer.add_or_update_alert(alert) + + def _on_alert_added(self, alert: dict): + print(f"[HomeView] New alert added: {alert.get('alert_id')}") + self.alert_layer.add_or_update_alert(alert) + + def _on_alert_removed(self, alert_id: str): + print(f"[HomeView] Removing alert: {alert_id}") + self.alert_layer.remove_alert(alert_id) + + # ───────────────────────────── + # Real-time alert normalization + # ───────────────────────────── + def _on_alert_realtime(self, alert: dict): + alerts = alert.get("alerts", []) + if not alerts: + print("[HomeView] No alerts in payload.") + return + + for a in alerts: + labels = a.get("labels", {}) + ann = a.get("annotations", {}) + + normalized = { + "alert_id": labels.get("alert_id"), + "alert_type": labels.get("alertname"), + "device_id": labels.get("device"), + "lat": float(ann.get("lat")) if ann.get("lat") else None, + "lon": float(ann.get("lon")) if ann.get("lon") else None, + "severity": int(ann.get("severity", 1)), + "confidence": float(ann.get("confidence", 0)), + "area": ann.get("area"), + "summary": ann.get("summary"), + "category": ann.get("category"), + "recommendation": ann.get("recommendation"), + "meta": ann.get("meta"), + "startsAt": a.get("startsAt"), + "endsAt": a.get("endsAt"), + } + + alert_id = normalized.get("alert_id") + ended_at = normalized.get("endsAt") + is_resolved = ended_at and not ended_at.startswith("0001-01-01") + + if is_resolved: + print(f"[HomeView] Removing resolved alert: {alert_id}") + self.alert_layer.remove_alert(alert_id) + continue + + print(f"[HomeView] Active alert: {normalized['alert_type']} " + f"from {normalized['device_id']} ({normalized['lat']}, {normalized['lon']})") + self.alert_layer.add_or_update_alert(normalized) diff --git a/AgCloud/GUI/src/vast/main.py b/AgCloud/GUI/src/vast/main.py new file mode 100644 index 000000000..4517b96d6 --- /dev/null +++ b/AgCloud/GUI/src/vast/main.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import os +import sys +import traceback +import inspect +from pathlib import Path + +import PyQt6 # import first; no QtWebEngine yet + +# Wipe any inherited QT_* env vars (avoid old Qt5 overrides) +for k in list(os.environ): + if k.startswith(("QTWEBENGINE", "QT_QPA", "QT_PLUGIN", "QT_")): + os.environ.pop(k, None) + +# Debug only: show Qt6 paths (do NOT set env vars) +qt6_dir = Path(inspect.getfile(PyQt6)).with_name("Qt6") +plugins = qt6_dir / "plugins" +bin_dir = qt6_dir / "bin" +resources = qt6_dir / "resources" +print("[Qt debug]") +print(" qt_root:", qt6_dir) +print(" plugins:", plugins) +print(" bin:", bin_dir) +print(" resources:", resources) +print(" icudtl.dat exists?:", (resources / "icudtl.dat").exists()) + +from PyQt6.QtWebEngineWidgets import QWebEngineView # ensures WebEngine is available +from PyQt6.QtWidgets import QApplication +from PyQt6.QtWidgets import QStackedWidget +from auth_ui.service import AuthService +from auth_ui.login_page import LoginPage +from auth_ui.signup_page import SignupPage +from dashboard_api import DashboardApi +from main_window import MainWindow + + +def excepthook(exctype, value, tb): + print("\n=== Uncaught exception ===") + traceback.print_exception(exctype, value, tb) + print("==========================\n") + sys.__excepthook__(exctype, value, tb) + + +sys.excepthook = excepthook + +class AuthShell(QStackedWidget): + """Holds Login and Signup pages and switches between them.""" + def __init__(self, parent=None): + super().__init__(parent) + self.auth = AuthService() + + # placeholders for pages (will be created below) + self.login_page = None + self.signup_page = None + + # build pages + self._build_pages() + + def _build_pages(self): + # callbacks + def go_signup(): + self.setCurrentWidget(self.signup_page) + + def go_login(): + self.setCurrentWidget(self.login_page) + + def on_signed_up(): + # after successful signup, move back to login + self.setCurrentWidget(self.login_page) + + def on_login_success(user): + # bubble up to whoever created the shell + # we’ll set this attribute from main() + if hasattr(self, "on_login_success") and callable(self.on_login_success): + self.on_login_success(user) + + # create pages + self.login_page = LoginPage(on_login=on_login_success, + on_go_signup=go_signup, + auth=self.auth) + self.signup_page = SignupPage(on_signed_up=on_signed_up, + on_go_login=go_login, + auth=self.auth) + + # add to stack + self.addWidget(self.login_page) + self.addWidget(self.signup_page) + self.setCurrentWidget(self.login_page) + + def reset(self): + """Optional: clear fields on logout if you want.""" + # for example: + # self.login_page.email.clear() + # self.login_page.password.clear() + self.setCurrentWidget(self.login_page) + + +def main() -> int: + print("[main] starting QApplication") + app = QApplication(sys.argv) + + + # 1) create the auth shell but do NOT show it + shell = AuthShell() + shell.setWindowTitle("Sign in") + # shell.show() # disabled to skip the login window + + + # 2) when login succeeds -> open MainWindow + def open_main(user): + api = DashboardApi() # create API instance (user not required) + win = MainWindow(api) + # connect logout back to login + win.logoutRequested.connect(lambda: on_logout(win)) + win.show() + shell.hide() + + + def on_logout(win): + win.close() + shell.reset() + shell.show() + # wire callback + shell.on_login_success = open_main + + + # open the main window directly (skip login) + open_main(None) + + print("[main] window shown, entering event loop") + rc = app.exec() + print(f"[main] event loop exited with code {rc}") + return rc +if __name__ == "__main__": + sys.exit(main()) diff --git a/AgCloud/GUI/src/vast/main_window.py b/AgCloud/GUI/src/vast/main_window.py new file mode 100644 index 000000000..eb31fe66f --- /dev/null +++ b/AgCloud/GUI/src/vast/main_window.py @@ -0,0 +1,556 @@ +from PyQt6.QtCore import Qt, pyqtSignal, QSize +from PyQt6.QtWidgets import ( + QMainWindow, QDockWidget, QListWidget, QListWidgetItem, QStatusBar, + QStackedWidget, QToolButton, QLabel, QWidget, QHBoxLayout, QVBoxLayout, + QGraphicsDropShadowEffect, QPushButton, + QMainWindow, QDockWidget, QListWidget, QListWidgetItem, QStatusBar, + QStackedWidget, QToolButton, QLabel, QWidget, QHBoxLayout, QVBoxLayout, + QGraphicsDropShadowEffect, QPushButton +) +from PyQt6.QtGui import QAction, QIcon, QFont, QColor +import os + +from PyQt6.QtGui import QAction, QIcon, QFont, QColor +import os + +from home_view import HomeView +from views.sensors_view import SensorsView +from views.alerts_panel import AlertsPanel +from views.notification_view import NotificationView +from views.fruits_view import FruitsView +from views.sound.sound_view import SoundView +from views.ground_view import GroundView +from views.auth_status_view import AuthStatusView +from views.ground_view import GroundView +from views.auth_status_view import AuthStatusView +from dashboard_api import DashboardApi +from vast.alerts.alert_service import AlertService +from views.leaf_diseases import LeafDiseaseView + + +# === New Sensors GUI imports === +from views.sensorsMainView import SensorsMainView +from views.sensorsMapView import SensorsMapView +from views.sensorDetailsTab import SensorDetailsTab +from views.sensors_status_summary import SensorsStatusSummary + +from views.security.incident_player_vlc import IncidentPlayerVLC +# === New Sensors GUI imports === +from views.sensorsMainView import SensorsMainView +from views.sensorsMapView import SensorsMapView +from views.sensorDetailsTab import SensorDetailsTab +from views.sensors_status_summary import SensorsStatusSummary + + +class MainWindow(QMainWindow): + logoutRequested = pyqtSignal() + + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.setWindowTitle("AgCloud – Dashboard") + self.resize(1280, 760) + self.setWindowTitle("AgCloud – Dashboard") + self.resize(1280, 760) + self.api = api + + # ─────────────────────────────── + # GLOBAL STYLE + # ─────────────────────────────── + self.setStyleSheet(""" + QMainWindow { background-color: #f9fafb; } + QMenuBar { background-color: #e5e7eb; font-size: 11.5pt; padding: 4px 10px; } + QToolBar { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f3f4f6); + border-bottom: 1px solid #d1d5db; padding: 2px 10px; min-height: 42px; + } + QToolButton { background-color: transparent; border: none; padding: 4px; border-radius: 8px; font-size: 20px; } + QToolButton:hover { background-color: #e5e7eb; } + QListWidget { background-color: #ffffff; border: none; font-size: 12pt; color: #111827; } + QListWidget::item { padding: 10px; border-radius: 6px; } + QListWidget::item:selected { background-color: #10b981; color: white; } + QStatusBar { background-color: #f3f4f6; font-size: 10pt; } + """) + + # ─────────────────────────────── + # MENU + # ─────────────────────────────── + # ─────────────────────────────── + # GLOBAL STYLE + # ─────────────────────────────── + self.setStyleSheet(""" + QMainWindow { background-color: #f9fafb; } + QMenuBar { background-color: #e5e7eb; font-size: 11.5pt; padding: 4px 10px; } + QToolBar { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #ffffff, stop:1 #f3f4f6); + border-bottom: 1px solid #d1d5db; padding: 2px 10px; min-height: 42px; + } + QToolButton { background-color: transparent; border: none; padding: 4px; border-radius: 8px; font-size: 20px; } + QToolButton:hover { background-color: #e5e7eb; } + QListWidget { background-color: #ffffff; border: none; font-size: 12pt; color: #111827; } + QListWidget::item { padding: 10px; border-radius: 6px; } + QListWidget::item:selected { background-color: #10b981; color: white; } + QStatusBar { background-color: #f3f4f6; font-size: 10pt; } + """) + + # ─────────────────────────────── + # MENU + # ─────────────────────────────── + file_menu = self.menuBar().addMenu("&File") + self.back_action = QAction(QIcon.fromTheme("go-previous"), "Back", self) + self.back_action.setShortcut("Alt+Left") + self.back_action.triggered.connect(self.go_back) + file_menu.addAction(self.back_action) + self.logout_action = QAction("Log out", self) + self.logout_action = QAction("Log out", self) + self.logout_action.triggered.connect(self._logout) + file_menu.addAction(self.logout_action) + + # ─────────────────────────────── + # TOP BAR (toolbar) + # ─────────────────────────────── + # ─────────────────────────────── + # TOP BAR (toolbar) + # ─────────────────────────────── + toolbar = self.addToolBar("Main Toolbar") + toolbar.setMovable(False) + toolbar.setFloatable(False) + toolbar.setIconSize(QSize(32, 32)) + + top_bar = QWidget() + top_bar_layout = QHBoxLayout(top_bar) + top_bar_layout.setContentsMargins(8, 0, 8, 0) + top_bar_layout.setSpacing(10) + + # Logout button + logout_btn = QPushButton("Logout") + logout_btn.setToolTip("Log out") + logout_btn.setCursor(Qt.CursorShape.PointingHandCursor) + logout_btn.setStyleSheet(""" + QPushButton { + background-color: #10b981; + color: white; + border: none; + border-radius: 8px; + padding: 6px 16px; + font-size: 11pt; + font-weight: 600; + } + QPushButton:hover { background-color: #059669; } + QPushButton:pressed { background-color: #047857; } + """) + logout_btn.clicked.connect(self._logout) + + # Alert bell + toolbar.setIconSize(QSize(32, 32)) + + top_bar = QWidget() + top_bar_layout = QHBoxLayout(top_bar) + top_bar_layout.setContentsMargins(8, 0, 8, 0) + top_bar_layout.setSpacing(10) + + # Logout button + logout_btn = QPushButton("Logout") + logout_btn.setToolTip("Log out") + logout_btn.setCursor(Qt.CursorShape.PointingHandCursor) + logout_btn.setStyleSheet(""" + QPushButton { + background-color: #10b981; + color: white; + border: none; + border-radius: 8px; + padding: 6px 16px; + font-size: 11pt; + font-weight: 600; + } + QPushButton:hover { background-color: #059669; } + QPushButton:pressed { background-color: #047857; } + """) + logout_btn.clicked.connect(self._logout) + + # Alert bell + self.alert_button = QToolButton() + self.alert_button.setToolTip("Show alerts") + self.alert_button.setText("🔔") + self.alert_button.setIconSize(QSize(40, 40)) + self.alert_button.setIconSize(QSize(40, 40)) + self.alert_button.setStyleSheet(""" + QToolButton { + font-size: 30px; + font-size: 30px; + border: none; + background: transparent; + padding: 4px; + border-radius: 8px; + border-radius: 8px; + } + QToolButton:hover { background-color: #e5e7eb; } + QToolButton:hover { background-color: #e5e7eb; } + """) + + # Alert badge + # Alert badge + self.alert_badge = QLabel("0", self.alert_button) + self.alert_badge.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.alert_badge.setFixedSize(24, 24) + self.alert_badge.setFixedSize(24, 24) + self.alert_badge.setStyleSheet(""" + QLabel { + background-color: #3b82f6; + background-color: #3b82f6; + color: white; + font-size: 10pt; + font-size: 10pt; + font-weight: bold; + border-radius: 12px; + border: 2px solid white; + border-radius: 12px; + border: 2px solid white; + } + """) + self.alert_badge.hide() + + def reposition_badge(): + btn_w = self.alert_button.width() + self.alert_badge.move(btn_w - 22, 2) + self.alert_badge.move(btn_w - 22, 2) + self.alert_badge.raise_() + + self.alert_button.resizeEvent = lambda e: ( + QToolButton.resizeEvent(self.alert_button, e), + reposition_badge() + ) + reposition_badge() + + # ─────────────────────────────── + # TITLE AREA (Updated) + # ─────────────────────────────── + title_container = QWidget() + title_layout = QVBoxLayout(title_container) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + main_title = QLabel("AgCloud") + main_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + main_title.setStyleSheet(""" + QLabel { + font-size: 22pt; + font-weight: 700; + color: #047857; + letter-spacing: 1px; + } + """) + + subtitle = QLabel("The Smart Platform that Protects and Optimizes Your Field") + subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) + subtitle.setStyleSheet(""" + QLabel { + font-size: 11pt; + font-weight: 500; + color: #374151; + margin-top: 2px; + } + """) + + title_layout.addWidget(main_title) + title_layout.addWidget(subtitle) + + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(8) + shadow.setColor(QColor(0, 0, 0, 35)) + shadow.setOffset(0, 2) + top_bar.setGraphicsEffect(shadow) + + top_bar_layout.addWidget(logout_btn) + top_bar_layout.addWidget(self.alert_button) + top_bar_layout.addStretch() + top_bar_layout.addWidget(title_container) + top_bar_layout.addStretch() + toolbar.addWidget(top_bar) + + # ─────────────────────────────── + # NAVIGATION + # ─────────────────────────────── + # ─────────────────────────────── + # TITLE AREA (Updated) + # ─────────────────────────────── + title_container = QWidget() + title_layout = QVBoxLayout(title_container) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + main_title = QLabel("AgCloud") + main_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + main_title.setStyleSheet(""" + QLabel { + font-size: 22pt; + font-weight: 700; + color: #047857; + letter-spacing: 1px; + } + """) + + subtitle = QLabel("The Smart Platform that Protects and Optimizes Your Field") + subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) + subtitle.setStyleSheet(""" + QLabel { + font-size: 11pt; + font-weight: 500; + color: #374151; + margin-top: 2px; + } + """) + + title_layout.addWidget(main_title) + title_layout.addWidget(subtitle) + + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(8) + shadow.setColor(QColor(0, 0, 0, 35)) + shadow.setOffset(0, 2) + top_bar.setGraphicsEffect(shadow) + + top_bar_layout.addWidget(logout_btn) + top_bar_layout.addWidget(self.alert_button) + top_bar_layout.addStretch() + top_bar_layout.addWidget(title_container) + top_bar_layout.addStretch() + toolbar.addWidget(top_bar) + + # ─────────────────────────────── + # NAVIGATION + # ─────────────────────────────── + self.nav_dock = QDockWidget("Navigation", self) + self.nav_dock.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) + self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.nav_dock) + self.nav_list = QListWidget(self.nav_dock) + self.nav_dock.setWidget(self.nav_list) + self.nav_dock.setMinimumWidth(220) + self.nav_dock.setMinimumWidth(220) + + font = QFont(); font.setPointSize(12) + self.nav_list.setFont(font) + + for main_item in ["Home", "Sensors", "Sound", "Ground Image", "Aerial Image", "Fruits", "Security", "Settings", "Notifications", "Auth", "Leaf Diseases"]: + item.setData(Qt.ItemDataRole.UserRole, {"type": "main"}) + self.nav_list.addItem(item) + if main_item == "Sensors": + for sub in ["Live Data", "Sensor Health", "Location Map"]: + sub_item = QListWidgetItem(f" ↳ {sub}") + sub_item.setData(Qt.ItemDataRole.UserRole, {"type": "sub", "parent": main_item, "name": sub}) + sub_item.setHidden(True) + self.nav_list.addItem(sub_item) + + font = QFont(); font.setPointSize(12) + self.nav_list.setFont(font) + for main_item in ["Home", "Sensors", "Sound", "Ground Image", "Aerial Image", "Fruits", "Security", "Settings", "Notifications", "Auth", "Leaf Diseases"]: + item = QListWidgetItem(main_item) + item.setData(Qt.ItemDataRole.UserRole, {"type": "main"}) + self.nav_list.addItem(item) + if main_item == "Sensors": + for sub in ["Live Data", "Sensor Health", "Location Map"]: + sub_item = QListWidgetItem(f" ↳ {sub}") + sub_item.setData(Qt.ItemDataRole.UserRole, {"type": "sub", "parent": main_item, "name": sub}) + sub_item.setHidden(True) + self.nav_list.addItem(sub_item) + + self.nav_list.currentRowChanged.connect(self._on_nav_change) + self.nav_list.itemClicked.connect(self._on_nav_click) + self.nav_list.itemClicked.connect(self._on_nav_click) + + # ─────────────────────────────── + # ALERT SERVICE + PANEL + # ─────────────────────────────── + # ─────────────────────────────── + # ALERT SERVICE + PANEL + # ─────────────────────────────── + ws_url = os.getenv("ALERTS_WS", "ws://alerts-gateway:8000/ws/alerts") + self.alert_service = AlertService(ws_url, api) + self.alert_service.alertsUpdated.connect(self.update_alert_badge) + self.alert_service.alertAdded.connect(lambda _: self.update_alert_badge()) + + self.alerts_panel = AlertsPanel(self.alert_service) + self.alerts_panel.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.Tool) + self.alerts_panel.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.alerts_panel.setStyleSheet(""" + QWidget { + background-color: #ffffff; + border: 1px solid #d1d5db; + border: 1px solid #d1d5db; + border-radius: 10px; + } + """) + self.alerts_panel.hide() + self.alert_button.clicked.connect(self.toggle_alert_panel) + + # ─────────────────────────────── + # CENTRAL STACKED VIEWS + # ─────────────────────────────── + # ─────────────────────────────── + # CENTRAL STACKED VIEWS + # ─────────────────────────────── + self.home = HomeView(api, self.alert_service, self) + self.sensors_view = SensorsView(api, self) + self.notification_view = NotificationView(self) + self.fruits_view = FruitsView(api, self) + self.sound_view = SoundView(api, self) + self.ground_view = GroundView(api, self) + self.auth_status = AuthStatusView(api, self) + self.leaf_diseases_view = LeafDiseaseView(api, self) + self.sensors_status_summary = SensorsStatusSummary(api, self) + self.sensors_health = SensorsView(api, self) + self.sensors_main = SensorsMainView(api, self) + self.security_view = IncidentPlayerVLC(api, self.alert_service, self) + self.ground_view = GroundView(api, self) + self.auth_status = AuthStatusView(api, self) + + self.sensors_status_summary = SensorsStatusSummary(api, self) + self.sensors_health = SensorsView(api, self) + self.sensors_main = SensorsMainView(api, self) + + self.stack = QStackedWidget() + self.setCentralWidget(self.stack) + self.views = { + "Home": self.home, + "Sensors": self.sensors_view, + "Sound": self.sound_view, + "Sensors - Live Data": self.sensors_status_summary, + "Sensors - Sensor Health": self.sensors_health, + "Sensors - Location Map": self.sensors_main, + "Notifications": self.notification_view, + "Leaf Diseases": self.leaf_diseases_view, + "Fruits": self.fruits_view, + "Ground Image": self.ground_view, + "Auth": self.auth_status, + "Security": self.security_view, + } + + for view in self.views.values(): + self.stack.addWidget(view) + self.stack.setCurrentWidget(self.home) + self.history = [] + self.history = [] + + # ─────────────────────────────── + # STATUS BAR + # ─────────────────────────────── + # ─────────────────────────────── + # STATUS BAR + # ─────────────────────────────── + sb = QStatusBar(self) + sb.setStyleSheet("QStatusBar { background-color: #f3f4f6; color: #374151; font-size: 10.5pt; }") + sb.setStyleSheet("QStatusBar { background-color: #f3f4f6; color: #374151; font-size: 10.5pt; }") + self.setStatusBar(sb) + sb.showMessage("Ready") + + # ─────────────────────────────── + # ALERT BADGE + # ─────────────────────────────── + # ─────────────────────────────── + # ALERT BADGE + # ─────────────────────────────── + def update_alert_badge(self): + unacked = sum(1 for a in self.alert_service.alerts if not a.get("ack", False)) + if unacked > 0: + self.alert_badge.setText(str(unacked)) + self.alert_badge.show() + else: + self.alert_badge.hide() + + def toggle_alert_panel(self): + if self.alerts_panel.isVisible(): + self.alerts_panel.hide() + return + + panel_width, panel_height = 420, 540 + panel_width, panel_height = 420, 540 + self.alerts_panel.resize(panel_width, panel_height) + rect = self.alert_button.geometry() + bottom_left = self.alert_button.mapToGlobal(rect.bottomLeft()) + bottom_right = self.alert_button.mapToGlobal(rect.bottomRight()) + center_x = (bottom_left.x() + bottom_right.x()) // 2 - (panel_width // 2) + pos_y = bottom_left.y() + 8 + pos_y = bottom_left.y() + 8 + self.alerts_panel.move(center_x, pos_y) + self.alerts_panel.show() + self.alerts_panel.raise_() + + if hasattr(self.alert_service, "mark_all_acknowledged"): + self.alert_service.mark_all_acknowledged() + self.update_alert_badge() + + # ─────────────────────────────── + # NAVIGATION + # ─────────────────────────────── + # ─────────────────────────────── + # NAVIGATION + # ─────────────────────────────── + def _on_nav_change(self, row: int) -> None: + name = self.nav_list.item(row).text().strip() + name = self.nav_list.item(row).text().strip() + if name in self.views: + self.navigate_to(self.views[name]) + else: + self.statusBar().showMessage(f"Section '{name}' not implemented yet.") + + def _on_nav_click(self, item): + data = item.data(Qt.ItemDataRole.UserRole) + if data and data.get("type") == "main": + parent = item.text() + expanded = False + for i in range(self.nav_list.count()): + sub_item = self.nav_list.item(i) + sub_data = sub_item.data(Qt.ItemDataRole.UserRole) + if sub_data and sub_data.get("type") == "sub" and sub_data.get("parent") == parent: + expanded = sub_item.isHidden() + break + for i in range(self.nav_list.count()): + sub_item = self.nav_list.item(i) + sub_data = sub_item.data(Qt.ItemDataRole.UserRole) + if sub_data and sub_data.get("type") == "sub" and sub_data.get("parent") == parent: + sub_item.setHidden(not expanded) + elif data and data.get("type") == "sub": + parent = data.get("parent") + sub_name = data.get("name") + key = f"{parent} - {sub_name}" + if key in self.views: + self.stack.setCurrentWidget(self.views[key]) + + def _on_nav_click(self, item): + data = item.data(Qt.ItemDataRole.UserRole) + if data and data.get("type") == "main": + parent = item.text() + expanded = False + for i in range(self.nav_list.count()): + sub_item = self.nav_list.item(i) + sub_data = sub_item.data(Qt.ItemDataRole.UserRole) + if sub_data and sub_data.get("type") == "sub" and sub_data.get("parent") == parent: + expanded = sub_item.isHidden() + break + for i in range(self.nav_list.count()): + sub_item = self.nav_list.item(i) + sub_data = sub_item.data(Qt.ItemDataRole.UserRole) + if sub_data and sub_data.get("type") == "sub" and sub_data.get("parent") == parent: + sub_item.setHidden(not expanded) + elif data and data.get("type") == "sub": + parent = data.get("parent") + sub_name = data.get("name") + key = f"{parent} - {sub_name}" + if key in self.views: + self.stack.setCurrentWidget(self.views[key]) + + def navigate_to(self, widget): + current = self.stack.currentWidget() + if current not in self.history: + self.history.append(current) + self.stack.setCurrentWidget(widget) + + def go_back(self): + if self.history: + last = self.history.pop() + self.stack.setCurrentWidget(last) + else: + self.statusBar().showMessage("No previous view to go back to.") + + def _logout(self) -> None: + self.statusBar().showMessage("Logged out (demo)") + self.logoutRequested.emit() diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/__init__.py b/AgCloud/GUI/src/vast/orthophoto_canvas/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ag_io/sensors_api.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ag_io/sensors_api.py new file mode 100644 index 000000000..dae37b853 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ag_io/sensors_api.py @@ -0,0 +1,83 @@ +from __future__ import annotations +import os +import uuid +import typing as t + +try: + import requests +except Exception as e: + raise RuntimeError("Please install 'requests' (pip install requests)") from e + +GATEWAY_URL = os.getenv("GATEWAY_URL", "http://gateway:8000") +SENSORS_PATH = os.getenv("SENSORS_PATH", "") + +def _normalize(rows: t.Any) -> list[dict]: + if isinstance(rows, dict): + rows = rows.get("sensors", []) + out = [] + if isinstance(rows, list): + for r in rows: + if not isinstance(r, dict): + continue + sid = r.get("sensor_id") or r.get("id") or r.get("sensorId") + lat = r.get("lat") or r.get("latitude") + lon = r.get("lon") or r.get("lng") or r.get("longitude") + if sid is None or lat is None or lon is None: + continue + out.append({ + "sensor_id": str(sid), + "lat": float(lat), + "lon": float(lon), + "label": r.get("label") or r.get("name") or str(sid), + "status": r.get("status", ""), + "battery": r.get("battery"), + "moisture": r.get("moisture"), + "last_seen": r.get("last_seen"), + "data": r, + }) + return out + +def get_sensors() -> list[dict]: + """ + Try the current service endpoint first (GET /api/sensors). + If not available, fallback to legacy POST /runQuery. + Returns a normalized list of dicts with at least: sensor_id, lat, lon. + """ + # --- 1) try GET /api/sensors (services/webmap.py / sensors_metrics_app.py) --- + path = SENSORS_PATH or "/api/sensors" + try: + resp = requests.get(f"{GATEWAY_URL}{path}", timeout=5) + resp.raise_for_status() + return _normalize(resp.json()) + except Exception as e: + print(f"[SENSORS] GET {path} failed: {e}") + + # --- 2) fallback: legacy gateway DSL via POST /runQuery --- + plan = { + "source": "sensors", + "_ops": [ + {"op": "select", "columns": [ + "sensor_id", "lat", "lon", "status", "name", "label", "battery", "moisture" + ]}, + {"op": "where", "cond": { + "any": [ + {"op": "=", "left": {"col": "status"}, "right": {"literal": "ok"}}, + {"op": "=", "left": {"col": "status"}, "right": {"literal": "warning"}} + ] + }} + ] + } + headers = { + "Content-Type": "application/json", + "X-Request-Id": str(uuid.uuid4()), + } + try: + resp = requests.post(f"{GATEWAY_URL}/runQuery", json=plan, headers=headers, timeout=10) + resp.raise_for_status() + return _normalize(resp.json()) + except requests.exceptions.ConnectionError: + print("WARNING: Could not connect to sensors API! Returning empty list.") + return [] + except Exception as e: + print(f"ERROR: Failed to fetch sensors via /runQuery: {e}") + return [] diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ag_io/tileset.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ag_io/tileset.py new file mode 100644 index 000000000..7b5c398f7 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ag_io/tileset.py @@ -0,0 +1,138 @@ +# orthophoto_canvas/ag_io/tileset.py +from __future__ import annotations + +import os +from pathlib import Path +from typing import Dict, List, Optional, Tuple + + +class TileStore: + """ + Minimal tileset adapter for OrthophotoViewer. + + Expects folder layout: + ///.png|jpg|jpeg + + Builds: + - existing_zooms: list[int] + - min_zoom, max_zoom: ints + - z_ranges[z]: (x_min, x_max, y_min, y_max) + - is_tms: bool (true if scheme == 'TMS') + And provides: + - tile_path(z, x, y) -> str | None + """ + + def __init__(self, root: Path | str, scheme: Optional[str] = None) -> None: + self.root = str(root) + self.scheme = (scheme or self._detect_scheme()).upper() + if self.scheme not in ("XYZ", "TMS"): + self.scheme = "XYZ" + + self.existing_zooms: List[int] = [] + self.z_ranges: Dict[int, Tuple[int, int, int, int]] = {} + + # Scan the directory tree to discover available tiles and ranges + self._scan_tree() + + if self.existing_zooms: + self.min_zoom = min(self.existing_zooms) + self.max_zoom = max(self.existing_zooms) + else: + # fallbacks to allow the viewer to start even on empty sets + self.min_zoom = 0 + self.max_zoom = 0 + + # ---- helpers ---- + + def _detect_scheme(self) -> str: + """ + If a text file '/scheme.txt' exists, read first token (XYZ/TMS). + Otherwise default to 'XYZ'. + """ + cand = os.path.join(self.root, "scheme.txt") + try: + with open(cand, "r", encoding="utf-8") as f: + token = f.read().strip().upper() + if token in ("XYZ", "TMS"): + return token + except Exception: + pass + return "XYZ" + + def _scan_tree(self) -> None: + """ + Populate existing_zooms and z_ranges by scanning the filesystem. + """ + if not os.path.isdir(self.root): + return + + for z_name in os.listdir(self.root): + if not z_name.isdigit(): + continue + z = int(z_name) + z_dir = os.path.join(self.root, z_name) + if not os.path.isdir(z_dir): + continue + + # collect x folders + xs: List[int] = [int(d) for d in os.listdir(z_dir) + if d.isdigit() and os.path.isdir(os.path.join(z_dir, d))] + if not xs: + continue + + x_min, x_max = min(xs), max(xs) + + # collect y files + ys: List[int] = [] + for x in xs: + x_dir = os.path.join(z_dir, str(x)) + if not os.path.isdir(x_dir): + continue + for fname in os.listdir(x_dir): + stem, ext = os.path.splitext(fname) + if stem.isdigit() and ext.lower() in (".png", ".jpg", ".jpeg"): + ys.append(int(stem)) + + if not ys: + continue + + y_min, y_max = min(ys), max(ys) + self.existing_zooms.append(z) + self.z_ranges[z] = (x_min, x_max, y_min, y_max) + + # keep zooms sorted for nicer behavior + self.existing_zooms.sort() + + # ---- properties expected by the viewer ---- + + @property + def is_tms(self) -> bool: + return self.scheme == "TMS" + + # ---- tile lookup ---- + + def tile_path(self, z: int, x: int, y: int) -> Optional[str]: + """ + Return existing file path for (z, x, y), respecting XYZ vs TMS. + """ + base = os.path.join(self.root, str(z), str(x)) + + def first_existing(candidates: List[str]) -> Optional[str]: + for p in candidates: + if os.path.exists(p): + return p + return None + + if self.scheme == "TMS": + # flip y + y = ((1 << z) - 1) - y + + candidates = [ + os.path.join(base, f"{y}.png"), + os.path.join(base, f"{y}.jpg"), + os.path.join(base, f"{y}.jpeg"), + ] + return first_existing(candidates) + + +__all__ = ["TileStore"] diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ag_types.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ag_types.py new file mode 100644 index 000000000..aaaca88bd --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ag_types.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Literal, Optional, Tuple + +TileScheme = Literal["XYZ", "TMS"] + +@dataclass(frozen=True) +class Sensor: + id: str + lon: float + lat: float + value: Optional[float] = None + label: Optional[str] = None + +# Optional: screen / scene points for Qt, +PointF = Tuple[float, float] diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/app.py b/AgCloud/GUI/src/vast/orthophoto_canvas/app.py new file mode 100644 index 000000000..22fd84086 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/app.py @@ -0,0 +1,32 @@ +from __future__ import annotations +import sys +from pathlib import Path +from PyQt6.QtWidgets import QApplication +from .ui.viewer import OrthophotoViewer +from .ag_io.sensors_api import get_sensors + +def _patch_qt_plugin_paths(): + pass + +def main(): + # _patch_qt_plugin_paths() + + app = QApplication(sys.argv) + + tiles_folder = r".\orthophoto_canvas\data\tiles" + viewer = OrthophotoViewer(tiles_folder) + + # Fetch sensors from the API and display them + try: + sensors = get_sensors() + viewer.set_sensors(sensors) + except Exception as e: + print(f"[SENSORS] failed to fetch: {e}") + + viewer.setWindowTitle("Orthophoto Viewer") + viewer.resize(1200, 900) + viewer.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/10/611/416.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/10/611/416.png new file mode 100644 index 000000000..4bb59ab3b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/10/611/416.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/11/1222/832.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/11/1222/832.png new file mode 100644 index 000000000..0e1d5af65 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/11/1222/832.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/12/2444/1664.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/12/2444/1664.png new file mode 100644 index 000000000..8dac7157b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/12/2444/1664.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/13/4888/3329.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/13/4888/3329.png new file mode 100644 index 000000000..e7fc96f10 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/13/4888/3329.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/13/4889/3329.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/13/4889/3329.png new file mode 100644 index 000000000..a12d5ec81 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/13/4889/3329.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9777/6658.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9777/6658.png new file mode 100644 index 000000000..67ef0a8b6 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9777/6658.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9777/6659.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9777/6659.png new file mode 100644 index 000000000..9e8e6884e Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9777/6659.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9778/6658.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9778/6658.png new file mode 100644 index 000000000..a15f66b4f Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9778/6658.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9778/6659.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9778/6659.png new file mode 100644 index 000000000..527e812cd Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/14/9778/6659.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19555/13317.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19555/13317.png new file mode 100644 index 000000000..86a8c8bd2 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19555/13317.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19555/13318.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19555/13318.png new file mode 100644 index 000000000..8b8fc8765 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19555/13318.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19556/13317.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19556/13317.png new file mode 100644 index 000000000..8f4bef162 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19556/13317.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19556/13318.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19556/13318.png new file mode 100644 index 000000000..c54c1af70 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/15/19556/13318.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26634.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26634.png new file mode 100644 index 000000000..f74e4798c Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26634.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26635.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26635.png new file mode 100644 index 000000000..5577bc645 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26635.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26636.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26636.png new file mode 100644 index 000000000..6f7fa6264 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39111/26636.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26634.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26634.png new file mode 100644 index 000000000..d18482b38 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26634.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26635.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26635.png new file mode 100644 index 000000000..d00cb752a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26635.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26636.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26636.png new file mode 100644 index 000000000..fca06dcb2 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39112/26636.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26634.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26634.png new file mode 100644 index 000000000..b1e07c6f2 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26634.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26635.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26635.png new file mode 100644 index 000000000..0f05f40d3 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26635.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26636.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26636.png new file mode 100644 index 000000000..0c6c18ab9 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/16/39113/26636.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53268.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53268.png new file mode 100644 index 000000000..b36e38a58 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53268.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53269.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53269.png new file mode 100644 index 000000000..64820ad61 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53269.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53270.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53270.png new file mode 100644 index 000000000..56be414e6 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53270.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53271.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53271.png new file mode 100644 index 000000000..f0613ff85 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53271.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53272.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53272.png new file mode 100644 index 000000000..7301615ea Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78222/53272.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53268.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53268.png new file mode 100644 index 000000000..fe0fe746a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53268.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53269.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53269.png new file mode 100644 index 000000000..a3fdf5f6e Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53269.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53270.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53270.png new file mode 100644 index 000000000..f47470d7b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53270.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53271.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53271.png new file mode 100644 index 000000000..cba63798b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53271.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53272.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53272.png new file mode 100644 index 000000000..fd4d92d1c Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78223/53272.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53268.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53268.png new file mode 100644 index 000000000..549cd829c Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53268.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53269.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53269.png new file mode 100644 index 000000000..d8ab7111a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53269.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53270.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53270.png new file mode 100644 index 000000000..f52f2fefa Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53270.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53271.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53271.png new file mode 100644 index 000000000..0b37a0779 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53271.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53272.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53272.png new file mode 100644 index 000000000..211976928 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78224/53272.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53268.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53268.png new file mode 100644 index 000000000..82d77d88c Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53268.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53269.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53269.png new file mode 100644 index 000000000..34b6d31bd Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53269.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53270.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53270.png new file mode 100644 index 000000000..fceaa6702 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53270.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53271.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53271.png new file mode 100644 index 000000000..31813a8dc Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53271.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53272.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53272.png new file mode 100644 index 000000000..9395eb23b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78225/53272.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53268.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53268.png new file mode 100644 index 000000000..42ad3451a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53268.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53269.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53269.png new file mode 100644 index 000000000..f9e89a344 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53269.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53270.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53270.png new file mode 100644 index 000000000..cc2971ec6 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53270.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53271.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53271.png new file mode 100644 index 000000000..0f141de7d Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53271.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53272.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53272.png new file mode 100644 index 000000000..f966eb17d Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/17/78226/53272.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106537.png new file mode 100644 index 000000000..e1879311a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106538.png new file mode 100644 index 000000000..f117c896a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106539.png new file mode 100644 index 000000000..9621b3ad0 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106540.png new file mode 100644 index 000000000..1d877367e Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106541.png new file mode 100644 index 000000000..c665a5f81 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106542.png new file mode 100644 index 000000000..3a205cb48 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106543.png new file mode 100644 index 000000000..bdc9283c9 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106544.png new file mode 100644 index 000000000..7cea1cfb7 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156445/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106537.png new file mode 100644 index 000000000..c92c7bcfa Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106538.png new file mode 100644 index 000000000..f5f65965b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106539.png new file mode 100644 index 000000000..ccd0b22a2 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106540.png new file mode 100644 index 000000000..26a17e9de Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106541.png new file mode 100644 index 000000000..ad0888967 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106542.png new file mode 100644 index 000000000..91d19bc5b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106543.png new file mode 100644 index 000000000..67df0c8d1 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106544.png new file mode 100644 index 000000000..bda12a46a Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156446/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106537.png new file mode 100644 index 000000000..f85756dfa Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106538.png new file mode 100644 index 000000000..58211597c Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106539.png new file mode 100644 index 000000000..e7913b8f4 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106540.png new file mode 100644 index 000000000..2ceb3e395 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106541.png new file mode 100644 index 000000000..bf189c17f Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106542.png new file mode 100644 index 000000000..5f435c20b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106543.png new file mode 100644 index 000000000..a713b8c14 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106544.png new file mode 100644 index 000000000..14756bbb7 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156447/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106537.png new file mode 100644 index 000000000..366af322f Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106538.png new file mode 100644 index 000000000..a4fee79d7 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106539.png new file mode 100644 index 000000000..ef9470707 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106540.png new file mode 100644 index 000000000..f9a0b0562 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106541.png new file mode 100644 index 000000000..dbcf634a6 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106542.png new file mode 100644 index 000000000..99e22254f Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106543.png new file mode 100644 index 000000000..cdcc49f19 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106544.png new file mode 100644 index 000000000..019bf9c9b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156448/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106537.png new file mode 100644 index 000000000..f9f2a3da3 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106538.png new file mode 100644 index 000000000..ea973ee13 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106539.png new file mode 100644 index 000000000..bfe62804d Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106540.png new file mode 100644 index 000000000..a9cc211a9 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106541.png new file mode 100644 index 000000000..a654fc1a0 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106542.png new file mode 100644 index 000000000..35b2cf64b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106543.png new file mode 100644 index 000000000..b7e625f05 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106544.png new file mode 100644 index 000000000..da533fc7d Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156449/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106537.png new file mode 100644 index 000000000..f13c52c80 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106538.png new file mode 100644 index 000000000..efcf53f4f Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106539.png new file mode 100644 index 000000000..a74fbc882 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106540.png new file mode 100644 index 000000000..70b5842e3 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106541.png new file mode 100644 index 000000000..0909ae9a0 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106542.png new file mode 100644 index 000000000..853013a26 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106543.png new file mode 100644 index 000000000..246ba5b7b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106544.png new file mode 100644 index 000000000..2121a8fb7 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156450/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106537.png new file mode 100644 index 000000000..a72889992 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106538.png new file mode 100644 index 000000000..95cfaeab4 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106539.png new file mode 100644 index 000000000..42c3923fc Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106540.png new file mode 100644 index 000000000..eec1b7730 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106541.png new file mode 100644 index 000000000..e969ae264 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106542.png new file mode 100644 index 000000000..ee43a0261 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106543.png new file mode 100644 index 000000000..a4671b819 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106544.png new file mode 100644 index 000000000..8e105e9b4 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156451/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106537.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106537.png new file mode 100644 index 000000000..5680b198b Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106537.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106538.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106538.png new file mode 100644 index 000000000..236748650 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106538.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106539.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106539.png new file mode 100644 index 000000000..2a344f550 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106539.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106540.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106540.png new file mode 100644 index 000000000..9dae98116 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106540.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106541.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106541.png new file mode 100644 index 000000000..6c40f2b2d Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106541.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106542.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106542.png new file mode 100644 index 000000000..8dd67bee9 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106542.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106543.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106543.png new file mode 100644 index 000000000..3f2c523ad Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106543.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106544.png b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106544.png new file mode 100644 index 000000000..34676e632 Binary files /dev/null and b/AgCloud/GUI/src/vast/orthophoto_canvas/data/tiles/18/156452/106544.png differ diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/geo/__init__.py b/AgCloud/GUI/src/vast/orthophoto_canvas/geo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/geo/tiling.py b/AgCloud/GUI/src/vast/orthophoto_canvas/geo/tiling.py new file mode 100644 index 000000000..12ec18fb3 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/geo/tiling.py @@ -0,0 +1,29 @@ +from typing import Tuple + +TILE_SIZE = 512 # pixels + +def eff_tile_scene(z: int, z_base: int) -> float: + """ + Size (in scene units) of a single tile at zoom z, when scene is anchored to z_base. + """ + return TILE_SIZE / float(1 << (z - z_base)) + +def anchor_indices_to_scene(z: int, z_base: int, x_min_base: int, y_min_base: int, + x_idx: int, y_idx: int) -> Tuple[int, int]: + """ + Convert (z/x/y) indices to scene grid indices (tx, ty), anchored to z_base. + """ + scale = 1 << (z - z_base) + x0 = x_min_base * scale + y0 = y_min_base * scale + tx = x_idx - x0 + ty = y_idx - y0 + return tx, ty + +def scene_pos_from_indices(tx: int, ty: int, eff_tile: float) -> Tuple[int, int]: + """ + Convert tile grid indices (tx, ty) to scene pixel coordinates (sx, sy). + """ + sx = int(tx * eff_tile) + sy = int(ty * eff_tile) + return sx, sy diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/scripts/make_tiles.py b/AgCloud/GUI/src/vast/orthophoto_canvas/scripts/make_tiles.py new file mode 100644 index 000000000..76e9c1a4f --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/scripts/make_tiles.py @@ -0,0 +1,103 @@ +# make_tiles.py — run this from the OSGeo4W Shell +import os +import sys +import subprocess +from datetime import datetime + +try: + from osgeo import gdal +except ImportError: + print("ERROR: Could not import GDAL. Please run this script from the OSGeo4W Shell.") + sys.exit(1) + +# ==== CONFIG ==== +INPUT_TIF = r".\field_x10.tif" +OUTPUT_DIR = r".\tiles" +ZOOM_RANGE = "10-18" # change if needed +RESAMPLING = "bilinear" # 'near'/'bilinear'/'cubic'/... +TARGET_SRS = "EPSG:3857" # Web Mercator for XYZ +TILESIZE_PREFERRED = 512 # try 512 first; fall back to 256 +NUM_THREADS = "ALL_CPUS" # speed-up for warp +# =============== + +def run_cmd(args): + # show nice, quoted command for readability + print(">>", " ".join(f'"{a}"' if " " in a else a for a in args)) + cp = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) + print(cp.stdout) + cp.check_returncode() + return cp + +def warp_to_3857(src_path, dst_path): + print(f"[{datetime.now():%H:%M:%S}] Reprojecting to {TARGET_SRS} ...") + os.makedirs(os.path.dirname(dst_path), exist_ok=True) + + warp_opts = gdal.WarpOptions( + dstSRS=TARGET_SRS, + resampleAlg=RESAMPLING, + multithread=True, + warpOptions=[f"NUM_THREADS={NUM_THREADS}"] + ) + ds = gdal.Warp(dst_path, src_path, options=warp_opts) + if ds is None: + raise RuntimeError("gdal.Warp failed") + ds = None + print(f"[{datetime.now():%H:%M:%S}] Reprojected -> {dst_path}") + +def gdal2tiles_xyz(src_path, out_dir, zoom_range, tilesize=None): + """ + Call GDAL's tiler via module execution to avoid Win32 exec issues: + python -m osgeo_utils.gdal2tiles --xyz ... + """ + cmd = [sys.executable, "-m", "osgeo_utils.gdal2tiles", "--xyz", + "-z", zoom_range, "-r", RESAMPLING, "-w", "none"] + if tilesize: + cmd += ["--tilesize", str(tilesize)] + cmd += [src_path, out_dir] + return run_cmd(cmd) + +def main(): + if not os.path.isfile(INPUT_TIF): + print(f"ERROR: Input not found: {INPUT_TIF}") + sys.exit(1) + + os.makedirs(OUTPUT_DIR, exist_ok=True) + temp_3857 = os.path.join(os.path.dirname(OUTPUT_DIR), "_tmp_3857.tif") + + # 1) Reproject to Web Mercator + warp_to_3857(INPUT_TIF, temp_3857) + + # 2) Generate tiles (try 512; fallback to 256 if unsupported) + chosen_tile_size = TILESIZE_PREFERRED + try: + print(f"[{datetime.now():%H:%M:%S}] Generating tiles (XYZ) at {TILESIZE_PREFERRED}×{TILESIZE_PREFERRED} ...") + gdal2tiles_xyz(temp_3857, OUTPUT_DIR, ZOOM_RANGE, tilesize=TILESIZE_PREFERRED) + except subprocess.CalledProcessError as e: + print("WARN: Your GDAL may not support --tilesize. Falling back to 256×256 ...") + chosen_tile_size = 256 + # rerun without --tilesize (defaults to 256) + gdal2tiles_xyz(temp_3857, OUTPUT_DIR, ZOOM_RANGE, tilesize=None) + + print(f"[{datetime.now():%H:%M:%S}] DONE. Tiles at: {OUTPUT_DIR}") + print(f"Tile size used: {chosen_tile_size} × {chosen_tile_size}") + print("Scheme: XYZ (no Y flip).") + + # Quick sanity listing + try: + zs = sorted(int(d) for d in os.listdir(OUTPUT_DIR) if d.isdigit()) + if zs: + top = zs[-1] + x_root = os.path.join(OUTPUT_DIR, str(top)) + xs = [d for d in os.listdir(x_root) if d.isdigit()] + print(f"Highest zoom: z={top}, sample X: {xs[:5]}") + if xs: + y_root = os.path.join(x_root, xs[0]) + ys = [f for f in os.listdir(y_root) if f.lower().endswith(('.png','.jpg','.jpeg'))] + print(f"Sample files at z={top}/{xs[0]}: {ys[:5]}") + else: + print("No zoom folders found—check the logs above.") + except Exception as e: + print("Sanity-list failed:", e) + +if __name__ == "__main__": + main() diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ui/__init__.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/__init__.py new file mode 100644 index 000000000..08982cb31 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/__init__.py @@ -0,0 +1 @@ +# ui package marker diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ui/alert_layer.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/alert_layer.py new file mode 100644 index 000000000..defc7612e --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/alert_layer.py @@ -0,0 +1,203 @@ +from PyQt6.QtWidgets import ( + QGraphicsTextItem, QLabel, QVBoxLayout, QWidget, QGraphicsDropShadowEffect +) +from PyQt6.QtCore import Qt, QPoint +from PyQt6.QtGui import QColor, QFont +from src.vast.orthophoto_canvas.ui.sensors_layer import _latlon_to_xy_at_max_zoom, TILE_SIZE + + +# ───────────────────────────────────────────── +# Frameless Popup Widget +# ───────────────────────────────────────────── +class AlertPopupWidget(QWidget): + """Frameless popup with rounded corners, colored border, and drop shadow.""" + + def __init__(self, html: str, border_color: str = "#444", parent=None): + super().__init__(parent) + self.setWindowFlags(Qt.WindowType.ToolTip | Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + label = QLabel() + label.setTextFormat(Qt.TextFormat.RichText) + label.setText(html) + label.setWordWrap(True) + label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) + label.setStyleSheet(f""" + QLabel {{ + background-color: #ffffff; + border: 2px solid {border_color}; + border-radius: 12px; + padding: 10px 12px; + font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif; + font-size: 12px; + color: #111; + }} + """) + layout.addWidget(label) + + shadow = QGraphicsDropShadowEffect(self) + shadow.setBlurRadius(18) + shadow.setOffset(0, 4) + shadow.setColor(QColor(0, 0, 0, 70)) + self.setGraphicsEffect(shadow) + + self.adjustSize() + + def show_near(self, global_pos: QPoint): + """Show popup slightly above and to the right of the marker.""" + self.adjustSize() + self.move(global_pos + QPoint(12, -self.height() - 12)) + self.show() + + +# ───────────────────────────────────────────── +# Marker Item +# ───────────────────────────────────────────── +class _AlertMarker(QGraphicsTextItem): + """A single alert marker (emoji icon) that shows a modern popup on hover.""" + + def __init__(self, alert_id, alert_data, *args, **kwargs): + severity = int(alert_data.get("severity", 1)) + icon = {1: "⚠️", 2: "🚨"}.get(severity, "🚨") + super().__init__(icon, *args, **kwargs) + + self.alert_id = alert_id + self.alert_data = alert_data + self._popup = None + + self.setZValue(1_000_000) + self.setFont(QFont("Noto Color Emoji", 12)) + self.setDefaultTextColor(QColor("#222")) + self.setFlag(QGraphicsTextItem.GraphicsItemFlag.ItemIgnoresTransformations, True) + self.setAcceptHoverEvents(True) + + def hoverEnterEvent(self, event): + alert = self.alert_data + severity = int(alert.get("severity", 1)) + alert_type = alert.get("alert_type", "Alert").replace("_", " ") + device_id = alert.get("device_id", "unknown") + summary = alert.get("summary") or "No additional details." + started_at = alert.get("startsAt", "") + + border_color = {1: "#f1c232", 2: "#f39c12", 3: "#e67e22", + 4: "#cc0000", 5: "#8b0000"}.get(severity, "#999") + + tooltip_html = f""" +
+
+ {self.toPlainText()} + {alert_type.capitalize()} detected +
+
+
+ 💬 + {summary} +
+ {f'
🕒 {started_at}
' if started_at else ''} +
+ """ + + view = self.scene().views()[0] if self.scene().views() else None + if view: + global_pos = view.mapToGlobal(view.mapFromScene(self.scenePos())) + self._popup = AlertPopupWidget(tooltip_html, border_color=border_color) + self._popup.show_near(global_pos) + + super().hoverEnterEvent(event) + + def hoverLeaveEvent(self, event): + if self._popup: + self._popup.close() + self._popup = None + super().hoverLeaveEvent(event) + + +# ───────────────────────────────────────────── +# Alert Layer +# ───────────────────────────────────────────── +class AlertLayer: + """Draws alert markers on the orthophoto scene, consistent with RegionLayer projection.""" + + def __init__(self, viewer): + self.viewer = viewer + self.scene = viewer.scene + self.alerts = {} + + # Use same base tile coordinates as RegionLayer (max zoom) + z = viewer.max_zoom_fs + self._x_min_base = viewer.ts.z_ranges[z][0] + self._y_min_base = viewer.ts.z_ranges[z][2] + + def add_or_update_alert(self, alert: dict): + """Add or update a marker for the given alert.""" + if not alert: + return + + alert_id = alert.get("alert_id") or alert.get("id") or alert.get("alertId") + if not alert_id: + print("[AlertLayer] ⚠️ Skipping alert without ID:", alert) + return + + # Parse coordinates + lat = alert.get("lat") or alert.get("latitude") or alert.get("location_lat") + lon = alert.get("lon") or alert.get("longitude") or alert.get("location_lon") + try: + lat = float(lat) + lon = float(lon) + except Exception: + print(f"[AlertLayer] ⚠️ Invalid lat/lon for {alert_id}: {lat}, {lon}") + return + + pos = _latlon_to_xy_at_max_zoom(self.viewer, lat, lon) + if not pos: + print(f"[AlertLayer] ⚠️ Alert {alert_id} outside dataset bounds") + return + + xb, yb = pos + scene_x = (xb - self._x_min_base) * TILE_SIZE + scene_y = (yb - self._y_min_base) * TILE_SIZE + print(f"[AlertLayer] Alert {alert_id}: scene=({scene_x:.1f}, {scene_y:.1f})") + + # Remove old marker if exists + if alert_id in self.alerts: + old_marker, _ = self.alerts.pop(alert_id) + self.scene.removeItem(old_marker) + + severity = int(alert.get("severity", 1)) + normalized = { + "alert_id": alert_id, + "alert_type": alert.get("alert_type") or "alert", + "device_id": alert.get("device_id") or "unknown", + "area": alert.get("area") or "", + "severity": severity, + "confidence": alert.get("confidence") or 0, + "summary": alert.get("summary") or alert.get("meta") or "", + "startsAt": alert.get("started_at") or alert.get("startsAt") or "", + } + + marker = _AlertMarker(alert_id, normalized) + marker.setPos(scene_x, scene_y) + self.scene.addItem(marker) + self.alerts[alert_id] = (marker, None) + + def clear_alerts(self): + print("[AlertLayer] Clearing all alert markers") + for marker, _ in self.alerts.values(): + self.scene.removeItem(marker) + self.alerts.clear() + + def remove_alert(self, alert_id: str): + """Remove a specific alert marker from the scene.""" + if alert_id not in self.alerts: + print(f"[AlertLayer] ⚠️ Tried to remove unknown alert_id: {alert_id}") + return + marker, _ = self.alerts.pop(alert_id) + if marker: + self.scene.removeItem(marker) + print(f"[AlertLayer] ❌ Removed alert marker: {alert_id}") diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py new file mode 100644 index 000000000..4ef7808d6 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py @@ -0,0 +1,408 @@ +from dataclasses import dataclass, field +from typing import Optional, Tuple, Dict, Any, List, TYPE_CHECKING +import math +from PyQt6.QtWidgets import ( + QGraphicsEllipseItem, QGraphicsSimpleTextItem, QGraphicsItem, + QGraphicsPathItem, QGraphicsTextItem, QToolTip +) +from PyQt6.QtGui import QBrush, QPen, QColor, QPainterPath, QLinearGradient, QFont +from PyQt6.QtCore import Qt, QPointF + +TILE_SIZE = 512 +MERCATOR_MAX_LAT = 85.05112878 + +KNOWN_SPEC_KEYS = frozenset({"label", "color", "radius_px", "min_z", "max_z", "hover"}) +ID_KEYS = ("sensor_id", "id", "sensorId") +LAT_KEYS = ("lat", "latitude") +LON_KEYS = ("lon", "lng", "longitude") + +@dataclass +class SensorSpec: + sensor_id: str + label: str = "" + color: QColor = field(default_factory=lambda: QColor(0, 120, 255)) + radius_px: float = 8.0 + min_z: Optional[int] = None + max_z: Optional[int] = None + data: Dict[str, Any] = field(default_factory=dict) + # Mode 1 (base coords) + xb: Optional[float] = None + yb: Optional[float] = None + # Mode 2 (tile + pixel offset) + z: Optional[int] = None + x: Optional[int] = None + y: Optional[int] = None + offset_px: Tuple[float, float] = (TILE_SIZE / 2.0, TILE_SIZE / 2.0) + +class _HoverPopup: + def __init__(self, scene): + self._bg = QGraphicsPathItem() + self._bg.setZValue(1_000_000_000) + self._bg.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations, True) + + self._radius = 10.0 + self._pad_w, self._pad_h = 18, 16 + self._border_pen = QPen(QColor(0, 0, 0, 40), 1) + + self._text = QGraphicsTextItem(self._bg) + self._text.setDefaultTextColor(QColor(24, 24, 24)) + self._text.setZValue(1_000_000_001) + self._text.setPos(14, 12) + self._text.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations, True) + + scene.addItem(self._bg) + self._bg.setVisible(False) + self._for_sensor_id = None + + def _update_bg_path(self, w: float, h: float, accent: QColor): + path = QPainterPath() + path.addRoundedRect(0, 0, w, h, self._radius, self._radius) + path.moveTo(26.0, h); path.lineTo(34.0, h + 10.0); path.lineTo(42.0, h); path.closeSubpath() + self._bg.setPath(path) + + grad = QLinearGradient(0, 0, 0, h) + grad.setColorAt(0.0, QColor(255, 255, 255, 250)) + grad.setColorAt(1.0, QColor(245, 247, 250, 240)) + self._bg.setBrush(QBrush(grad)) + self._bg.setPen(self._border_pen) + + def show_for(self, sensor_id: str, html: str, anchor_scene_pos: QPointF, accent: Optional[QColor] = None): + self._for_sensor_id = sensor_id + self._text.setHtml(html or f"{sensor_id}") + br = self._text.boundingRect() + w = max(br.width() + self._pad_w + 16, 320.0) + h = max(br.height() + self._pad_h + 20, 140.0) + self._update_bg_path(w, h, accent or QColor(0, 120, 255)) + + sr = self._bg.scene().sceneRect() + desired_x = anchor_scene_pos.x() + 16 + desired_y = anchor_scene_pos.y() - (h + 18) + x = min(max(sr.left() + 4, desired_x), sr.right() - w - 4) + y = min(max(sr.top() + 4, desired_y), sr.bottom() - h - 4) + self._bg.setPos(QPointF(x, y)) + self._bg.setVisible(True) + self._bg.update() + + def hide_if_for(self, sensor_id: Optional[str] = None): + if sensor_id is None or sensor_id == self._for_sensor_id: + self._bg.setVisible(False) + self._for_sensor_id = None + + def visible_for(self) -> Optional[str]: + return self._for_sensor_id + +class _SensorDot(QGraphicsEllipseItem): + def __init__(self, layer_ref, spec, *args, **kwargs): + super().__init__(*args, **kwargs) + self._layer = layer_ref + self._spec = spec + self.setAcceptHoverEvents(True) + + def hoverEnterEvent(self, event): + self._layer._on_sensor_hover_enter(self, self._spec) + super().hoverEnterEvent(event) + + def hoverLeaveEvent(self, event): + self._layer._on_sensor_hover_leave(self, self._spec) + super().hoverLeaveEvent(event) + +class SensorLayer: + """ + Draws interactive sensor dots with hover popups and labels. + """ + def __init__(self, viewer): + self.viewer = viewer + self._items: Dict[str, Tuple[QGraphicsEllipseItem, Optional[QGraphicsSimpleTextItem], SensorSpec]] = {} + self._popup = _HoverPopup(self.viewer.scene) + self.viewer.update_timer.timeout.connect(self._on_view_update) + + def specs(self): + return [tpl[2] for tpl in self._items.values()] + + def add_sensor(self, spec: SensorSpec): + if spec.sensor_id in self._items: + self.remove_sensor(spec.sensor_id) + + sx, sy = self._sensor_scene_pos(spec) + print(f"add_sensor: sensor_id={spec.sensor_id}, radius_px={spec.radius_px}, sx={sx}, sy={sy}") + r = float(spec.radius_px) + dot = _SensorDot(self, spec, -r, -r, 2.0 * r, 2.0 * r) + dot.setBrush(QBrush(spec.color)) + dot.setPen(QPen(Qt.PenStyle.NoPen)) + dot.setPos(QPointF(sx, sy)) + dot.setZValue(1_000_000) + + self.viewer.scene.addItem(dot) + self._items[spec.sensor_id] = (dot, None, spec) + self._apply_visibility_for_current_zoom(spec.sensor_id) + + def add_sensors(self, specs: List[SensorSpec]): + for s in specs: + self.add_sensor(s) + + def remove_sensor(self, sensor_id: str): + tpl = self._items.pop(sensor_id, None) + if not tpl: + return + self._popup.hide_if_for(sensor_id) + dot, label_item, _ = tpl + if label_item: self.viewer.scene.removeItem(label_item) + self.viewer.scene.removeItem(dot) + + def clear(self): + for sid in list(self._items.keys()): + self.remove_sensor(sid) + + # Hover popup + + def _on_sensor_hover_enter(self, dot_item: QGraphicsEllipseItem, spec: SensorSpec): + html = self._format_popup_html(spec) + view = self.viewer + gp = view.mapToGlobal(view.mapFromScene(dot_item.scenePos())) + QToolTip.showText(gp, html) + + def _on_sensor_hover_leave(self, _dot_item: QGraphicsEllipseItem, spec: SensorSpec): + self._popup.hide_if_for(spec.sensor_id) + + # placement + + def _sensor_scene_pos(self, spec: SensorSpec) -> Tuple[float, float]: + v = self.viewer + if spec.xb is not None and spec.yb is not None: + return (spec.xb - v.ts.z_ranges[v.min_zoom_fs][0]) * TILE_SIZE, (spec.yb - v.ts.z_ranges[v.min_zoom_fs][2]) * TILE_SIZE + if spec.z is not None and spec.x is not None and spec.y is not None: + eff_scene = TILE_SIZE / float(1 << (spec.z - v.min_zoom_fs)) + sx, sy, _ = v._tile_scene_pos(spec.z, spec.x, spec.y, eff_scene) + dx, dy = spec.offset_px + return sx + float(dx) * (eff_scene / TILE_SIZE), sy + float(dy) * (eff_scene / TILE_SIZE) + raise ValueError(f"SensorSpec {spec.sensor_id}: provide either (xb,yb) or (z,x,y[+offset_px])") + + def _on_view_update(self): + for sid in list(self._items.keys()): + self._apply_visibility_for_current_zoom(sid) + vis_for = self._popup.visible_for() + if vis_for is not None: + tpl = self._items.get(vis_for) + if tpl: + _, _, spec = tpl + z_now = self.viewer._calc_zoom_level() + if (spec.min_z is not None and z_now < spec.min_z) or \ + (spec.max_z is not None and z_now > spec.max_z): + self._popup.hide_if_for(vis_for) + + def _apply_visibility_for_current_zoom(self, sensor_id: str): + tpl = self._items.get(sensor_id) + if not tpl: return + dot, label_item, spec = tpl + z_now = self.viewer._calc_zoom_level() + visible = True + if spec.min_z is not None and z_now < spec.min_z: visible = False + if spec.max_z is not None and z_now > spec.max_z: visible = False + dot.setVisible(visible) + if label_item: label_item.setVisible(visible) + if not visible: self._popup.hide_if_for(sensor_id) + + def _format_popup_html(self, spec: SensorSpec) -> str: + d = spec.data or {} + title = str(d.get("name") or spec.label or spec.sensor_id) + c = spec.color; color_hex = f"#{c.red():02X}{c.green():02X}{c.blue():02X}" + def row(lbl, key, icon=""): + val = d.get(key); + if not val: return "" + ic = f"{icon}" if icon else "" + return ( "" + f"{ic}{lbl}:" + f"{val}" + "" ) + rows = [ + row("Moisture","moisture","💧"), + row("Battery","battery","🔋"), + row("Last seen","last_seen","⏱"), + row("Temp","temp","🌡"), + row("Status","status","•"), + ] + preferred = {"name","moisture","battery","last_seen","temp","status"} + for k,v in d.items(): + if k in preferred or v in (None,""): continue + rows.append(row(k.replace("_"," ").title(), k)) + status_val = (d.get("status","") or "").strip().lower() + status_bg = "#10B981" if status_val in ("ok","online","running","good","active") else \ + "#F59E0B" if status_val in ("degraded","warning") else \ + "#EF4444" if status_val in ("offline","error","fault","down") else color_hex + html = ( + "
" + f"
" + f"" + f"{title}" + f"#{spec.sensor_id}" + "
" + "" + + "".join(r for r in rows if r) + + "
" + + (f"
{status_val.capitalize() or 'Sensor'}
" + if status_val else "") + + "
" + ) + return html + +# ========================= +# Helpers for GPS/bulk +# ========================= + +def _split_known_from_data(kwargs: dict): + known = {k: v for k, v in kwargs.items() if k in KNOWN_SPEC_KEYS} + data = {k: v for k, v in kwargs.items() if k not in KNOWN_SPEC_KEYS} + return known, data + +def _first_key(d: dict, keys): + for k in keys: + if k in d: return d[k] + return None + +def _y_xyz_to_disk(viewer, z: int, y_xyz: float) -> float: + return ((1 << z) - 1 - y_xyz) if getattr(viewer, "is_tms", False) else y_xyz + +def _y_disk_bounds_to_xyz_bounds(viewer, z: int, y_min_disk: int, y_max_disk: int) -> Tuple[int, int]: + if getattr(viewer, "is_tms", False): + n = 1 << z + y1 = n - 1 - y_max_disk + y2 = n - 1 - y_min_disk + return (min(y1, y2), max(y1, y2)) + return (y_min_disk, y_max_disk) + +def _color_for_status(status: str) -> QColor: + s = (status or "").strip().lower() + if s in ("ok","online","running","good","active"): return QColor(16,185,129) + if s in ("warning","degraded"): return QColor(245,158,11) + if s in ("error","offline","fault","down"): return QColor(239,68,68) + return QColor(0,120,255) + +def _latlon_to_base_xy_if_inside(viewer, lat: float, lon: float, z: int = None) -> Optional[Tuple[float, float]]: + if z is None: + z = viewer.max_zoom_fs + if z not in viewer.z_ranges: + return None + try: + lat = float(lat); lon = float(lon) + except Exception: + return None + if not (-90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0): + return None + + lat = max(min(lat, MERCATOR_MAX_LAT), -MERCATOR_MAX_LAT) + n = 1 << z + # Web mercator math + lat_rad = math.radians(lat) + xtile_f = (lon + 180.0) / 360.0 * n + ytile_f_xyz = (1.0 - math.log(math.tan(lat_rad) + 1.0 / math.cos(lat_rad)) / math.pi) / 2.0 * n + ytile_f_disk = _y_xyz_to_disk(viewer, z, ytile_f_xyz) + + scale = 1 << (z - viewer.min_zoom_fs) + xb = xtile_f / float(scale) + yb = ytile_f_disk / float(scale) + + x_min_base = viewer.ts.z_ranges[viewer.min_zoom_fs][0] + x_max_base = viewer.ts.z_ranges[viewer.min_zoom_fs][1] + y_min_base = viewer.ts.z_ranges[viewer.min_zoom_fs][2] + y_max_base = viewer.ts.z_ranges[viewer.min_zoom_fs][3] + + if x_min_base <= xb < x_min_base + x_max_base + 1 and \ + y_min_base <= yb < y_min_base + y_max_base + 1: + return xb, yb + return None +import math +MERCATOR_MAX_LAT = 85.05112878 + +def _latlon_to_xy_at_max_zoom(viewer, lat: float, lon: float) -> Optional[tuple[float, float]]: + """ + Convert WGS84 (lat, lon) → fractional tile coordinates (x, y) + aligned with viewer.max_zoom_fs scene orientation. + """ + z = viewer.max_zoom_fs + if z not in viewer.z_ranges: + return None + + try: + lat = float(lat) + lon = float(lon) + except Exception: + return None + + lat = max(min(lat, MERCATOR_MAX_LAT), -MERCATOR_MAX_LAT) + n = 1 << z + lat_rad = math.radians(lat) + xtile = (lon + 180.0) / 360.0 * n + ytile = (1.0 - math.log(math.tan(lat_rad) + 1 / math.cos(lat_rad)) / math.pi) / 2.0 * n + + # 🔁 Flip if your tile store is TMS (bottom origin) + if getattr(viewer, "is_tms", False): + ytile = n - ytile - 1 + + # 🧭 XYZ tiles: (0,0) top-left, y increases downward + # But our scene uses top-left origin (same), so no additional flip! + + x_min, x_max, y_min, y_max = viewer.ts.z_ranges[z] + if not (x_min <= xtile <= x_max and y_min <= ytile <= y_max): + return None + + return xtile, ytile + + +def add_sensor_by_gps_strict(layer: SensorLayer, sensor_id: str, lat: float, lon: float, + z: int = None, center: bool = False, **kwargs) -> Optional[SensorSpec]: + v = layer.viewer + pos = _latlon_to_base_xy_if_inside(v, lat, lon, z=z) + if pos is None: + return None + xb, yb = pos + known, data = _split_known_from_data(kwargs) + if "color" not in known and "status" in data: + known["color"] = _color_for_status(data.get("status")) + spec = SensorSpec(sensor_id=sensor_id, xb=xb, yb=yb, **known, data=data) + layer.add_sensor(spec) + if center: + v.centerOn((xb - v._x_min_base) * TILE_SIZE, (yb - v._y_min_base) * TILE_SIZE) + return spec + +def add_sensors_by_gps_bulk(layer: SensorLayer, sensors: list, z: int = None, + center_on_first: bool = False, default_radius_px: float = 0.1) -> Dict[str, list]: + placed, skipped = [], [] + first_spec: Optional[SensorSpec] = None + for s in sensors: + sid = _first_key(s, ID_KEYS) + lat = _first_key(s, LAT_KEYS) + lon = _first_key(s, LON_KEYS) + if sid is None or lat is None or lon is None: + skipped.append(sid or "") + continue + known = {k: s[k] for k in KNOWN_SPEC_KEYS if k in s} + known.setdefault("radius_px", default_radius_px) + data = {k: v for k, v in s.items() if k not in KNOWN_SPEC_KEYS and k not in ID_KEYS + LAT_KEYS + LON_KEYS} + spec = add_sensor_by_gps_strict(layer, sid, lat, lon, z=z, center=False, **known, **data) + if spec: + placed.append(sid) + if first_spec is None: first_spec = spec + else: + skipped.append(sid) + if center_on_first and first_spec: + v = layer.viewer + layer.viewer.centerOn((first_spec.xb - v.ts.z_ranges[v.min_zoom_fs][0]) * TILE_SIZE, (first_spec.yb - v.ts.z_ranges[v.min_zoom_fs][2]) * TILE_SIZE) + print(f"[BULK] placed={len(placed)} skipped={len(skipped)}") + return {"placed": placed, "skipped": skipped} + +def dataset_bbox_latlon(viewer, z: int = None) -> Tuple[float, float, float, float]: + if z is None: + z = viewer.max_zoom_fs + if z not in viewer.z_ranges: + raise ValueError(f"Zoom z={z} not available.") + x_min, x_max, y_min_disk, y_max_disk = viewer.z_ranges[z] + n = 1 << z + y_xyz_min, y_xyz_max = _y_disk_bounds_to_xyz_bounds(viewer, z, y_min_disk, y_max_disk) + def tile2lon(x): return x / n * 360.0 - 180.0 + def tile2lat(y): + t = math.pi * (1.0 - 2.0 * (y / n)); return math.degrees(math.atan(math.sinh(t))) + lon_min = tile2lon(x_min); lon_max = tile2lon(x_max + 1) + lat_max = tile2lat(y_xyz_min); lat_min = tile2lat(y_xyz_max + 1) + print(f"[COVERAGE z={z}] lon:[{lon_min:.6f}..{lon_max:.6f}] lat:[{lat_min:.6f}..{lat_max:.6f}]") + return (lat_min, lat_max, lon_min, lon_max) diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ui/viewer.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/viewer.py new file mode 100644 index 000000000..34f0fc4a2 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/viewer.py @@ -0,0 +1,298 @@ +from __future__ import annotations +from pathlib import Path +from ..utils.tiles import TileStore +from .sensors_layer import SensorLayer, add_sensors_by_gps_bulk, dataset_bbox_latlon +from ..ag_io.sensors_api import get_sensors + +import math +from typing import Optional, Tuple, Union + +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor +from PyQt6.QtWidgets import ( + QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsRectItem +) + +# ==== Tunables ==== +TILE_SIZE = 512 +TARGET_TILE_PX_FOR_LOD = 512.0 +SNAP_CHOICES = (512.0, 384.0, 320.0, 256.0, 192.0, 128.0) + + +class OrthophotoViewer(QGraphicsView): + """Stable orthophoto tile viewer that perfectly fits its container.""" + + def __init__(self, tiles: Union[TileStore, str, Path]) -> None: + super().__init__() + + # ───────────────────────────── + # Load tiles + # ───────────────────────────── + # if isinstance(tiles, TileStore): + # self.ts = tiles + # else: + # self.ts = TileStore(Path(tiles)) + + # self.min_zoom_fs = self.ts.min_zoom + # self.max_zoom_fs = self.ts.max_zoom + # self.z_ranges = self.ts.z_ranges + # self.is_tms = self.ts.is_tms + # ───────────────────────────── +# Load tiles +# ───────────────────────────── + if isinstance(tiles, TileStore): + self.ts = tiles + else: + tiles_path = Path(tiles) + if not tiles_path.exists(): + raise FileNotFoundError(f"[OrthophotoViewer] Tile root not found: {tiles_path}") + self.ts = TileStore(tiles_path) + + # Safety: ensure scheme attribute exists + if not hasattr(self.ts, "scheme"): + self.ts.scheme = "XYZ" + + self.min_zoom_fs = self.ts.min_zoom + self.max_zoom_fs = self.ts.max_zoom + self.z_ranges = self.ts.z_ranges + self.is_tms = self.ts.is_tms + + print(f"[DEBUG] Tile root: {self.ts.root}") + print(f"[DEBUG] Tile scheme: {self.ts.scheme}") + print(f"[DEBUG] is_tms: {self.ts.is_tms}") + print(f"[DEBUG] Zoom levels: {self.ts.existing_zooms or 'none found'}") + print(f"[DEBUG] z_ranges: {self.ts.z_ranges or 'empty'}") + + + + + # ───────────────────────────── + # Scene setup + # ───────────────────────────── + self.scene = QGraphicsScene(self) + self.setScene(self.scene) + self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True) + self.setRenderHint(QPainter.RenderHint.Antialiasing, True) + self.setCacheMode(QGraphicsView.CacheModeFlag.CacheBackground) + self.setOptimizationFlag(QGraphicsView.OptimizationFlag.DontSavePainterState, True) + self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) + self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) + self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.SmartViewportUpdate) + + # 🔹 Light gray background (no border) + self.setBackgroundBrush(QColor("#d1d5db")) # soft gray background + self.setStyleSheet("background-color: #d1d5db; border: none;") + + # No scrollbars + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + # ───────────────────────────── + # Internal state + # ───────────────────────────── + self.current_zoom = self.ts.max_zoom + self.placeholder_color = QColor("#d1d5db") + self.tile_items: dict[Tuple[int, int, int], QGraphicsPixmapItem | QGraphicsRectItem] = {} + + # ───────────────────────────── + # Timed updates + # ───────────────────────────── + self.update_timer = QTimer(self) + self.update_timer.setSingleShot(True) + self.update_timer.timeout.connect(self.update_tiles) + + # ───────────────────────────── + # Optional sensor overlay + # ───────────────────────────── + self.sensor_layer = SensorLayer(self) + dataset_bbox_latlon(self, z=self.max_zoom_fs) + + try: + add_sensors_by_gps_bulk(self.sensor_layer, get_sensors(), z=self.max_zoom_fs, default_radius_px=0.2) + except Exception as e: + print(f"[Sensors] skipped: {e}") + + # ───────────────────────────── + # Scene geometry from MAX zoom + # ───────────────────────────── + self._init_scene_rect_from_max_zoom() + + # ───────────────────────────── + # Initial zoom and centering + # ───────────────────────────── + self._fit_scene_exactly() + + # ───────────────────────────── + # Initial tile rendering + # ───────────────────────────── + self.update_tiles() + + + # ───────────────────────────── + # Scene geometry + # ───────────────────────────── + def _init_scene_rect_from_max_zoom(self) -> None: + """Build scene rect from the max zoom level (actual dataset size).""" + z_max = self.ts.max_zoom + x_min, x_max, y_min, y_max = self.ts.z_ranges[z_max] + width = (x_max - x_min + 1) * TILE_SIZE + height = (y_max - y_min + 1) * TILE_SIZE + self._x_min_base = x_min + self._y_min_base = y_min + + # Add tiny margin to prevent borders + margin = 2 + self.scene.setSceneRect(-margin, -margin, width + margin * 2, height + margin * 2) + print(f"[BASE] z={z_max} scene={width}x{height}px") + + # ───────────────────────────── + # Fit helper + # ───────────────────────────── + def _fit_scene_exactly(self): + """Reset zoom and fit map exactly to the view size.""" + self.resetTransform() + self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) + self.centerOn(self.scene.sceneRect().center()) + + # ───────────────────────────── + # Events + # ───────────────────────────── + def resizeEvent(self, e): + """Fit perfectly when resized, no cumulative zoom.""" + super().resizeEvent(e) + self._fit_scene_exactly() + self._debounced_update() + + + def wheelEvent(self, event) -> None: + """Zoom with mouse wheel.""" + factor = 1.25 + if event.angleDelta().y() > 0: + self.scale(factor, factor) + else: + self.scale(1.0 / factor, 1.0 / factor) + self._debounced_update() + + def mouseReleaseEvent(self, event) -> None: + super().mouseReleaseEvent(event) + if event.button() in (Qt.MouseButton.LeftButton, Qt.MouseButton.MiddleButton): + self._debounced_update() + + def _debounced_update(self) -> None: + self.update_timer.start(50) + + # ───────────────────────────── + # Level-of-detail (LOD) + # ───────────────────────────── + def _calc_zoom_level(self) -> int: + """Force max zoom for small coverage sets.""" + return self.ts.max_zoom + + def update_tiles(self) -> None: + """Compute visible tiles and render them.""" + z = self._calc_zoom_level() + self.current_zoom = z + + eff_tile_scene = TILE_SIZE / float(1 << (z - self.ts.max_zoom)) + eff_tile_screen = eff_tile_scene * max(self.transform().m11(), 1e-6) + print(f"[LODDBG] z={z} tile_on_screen≈{eff_tile_screen:.1f}px") + + view_rect = self.mapToScene(self.viewport().rect()).boundingRect() + x_min_z, x_max_z, y_min_z, y_max_z = self.ts.z_ranges[z] + + start_tx = int(math.floor(view_rect.left() / eff_tile_scene)) + end_tx = int(math.floor(view_rect.right() / eff_tile_scene)) + start_ty = int(math.floor(view_rect.top() / eff_tile_scene)) + end_ty = int(math.floor(view_rect.bottom() / eff_tile_scene)) + + scale_factor = 1 << (z - self.ts.max_zoom) + want: set[Tuple[int, int, int]] = set() + for tx in range(start_tx, end_tx + 1): + for ty in range(start_ty, end_ty + 1): + x_idx = self._x_min_base * scale_factor + tx + y_idx = self._y_min_base * scale_factor + ty + if x_idx < x_min_z or x_idx > x_max_z or y_idx < y_min_z or y_idx > y_max_z: + continue + want.add((z, x_idx, y_idx)) + + # Create or upgrade + for key in want: + if key not in self.tile_items: + ph = self._create_placeholder_item_at(key, eff_tile_scene) + self.tile_items[key] = ph + self.scene.addItem(ph) + self._try_upgrade_tile_to_pixmap(key, eff_tile_scene) + + # Unload tiles that are no longer visible + for key in list(self.tile_items.keys()): + if key not in want: + self.scene.removeItem(self.tile_items.pop(key)) + + # ───────────────────────────── + # Tile placement / upgrade + # ───────────────────────────── + def _create_placeholder_item_at(self, key: Tuple[int, int, int], eff_tile_scene: float): + z, x, y = key + scale_factor = 1 << (z - self.ts.max_zoom) + x0 = self._x_min_base * scale_factor + y0 = self._y_min_base * scale_factor + tx = x - x0 + ty = y - y0 + sx = tx * eff_tile_scene + sy = ty * eff_tile_scene + + rect = QGraphicsRectItem(sx, sy, eff_tile_scene, eff_tile_scene) + rect.setBrush(QColor("#d1d5db")) # same gray as background + rect.setPen(QPen(Qt.PenStyle.NoPen)) + return rect + + def _place_pixmap_item(self, pm: QPixmap, key: Tuple[int, int, int], eff_tile_scene: float): + z, x, y = key + scale_factor = 1 << (z - self.ts.max_zoom) + x0 = self._x_min_base * scale_factor + y0 = self._y_min_base * scale_factor + tx = x - x0 + ty = y - y0 + sx = tx * eff_tile_scene + sy = ty * eff_tile_scene + + item = QGraphicsPixmapItem(pm) + item.setPos(sx, sy) + s = eff_tile_scene / float(pm.width()) + item.setScale(s) + item.setTransformationMode(Qt.TransformationMode.SmoothTransformation) + return item + + def _try_upgrade_tile_to_pixmap(self, key: Tuple[int, int, int], eff_tile_scene: float) -> None: + z, x, y = key + # print(f"[TRY] tile z={z} x={x} y={y}") + zz, xx, yy = z, x, y + pm: Optional[QPixmap] = None + while zz >= self.ts.min_zoom: + p = self.ts.tile_path(zz, xx, yy) + if p: + pm0 = QPixmap(str(p)) + if not pm0.isNull(): + pm = pm0 + if zz < z: + k = z - zz + seg = 1 << k + w = pm.width() // seg + h = pm.height() // seg + u = (x % seg) * w + v = (y % seg) * h + pm = pm.copy(u, v, w, h) + break + xx //= 2 + yy //= 2 + zz -= 1 + + if not pm: + return + + old = self.tile_items.get(key) + if old: + self.scene.removeItem(old) + item = self._place_pixmap_item(pm, key, eff_tile_scene) + self.scene.addItem(item) + self.tile_items[key] = item diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/ui/viewer_factory.py b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/viewer_factory.py new file mode 100644 index 000000000..44c2f602f --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/ui/viewer_factory.py @@ -0,0 +1,46 @@ +# orthophoto_canvas/ui/viewer_factory.py +from __future__ import annotations + +from pathlib import Path +from typing import Optional, Union + +from PyQt6.QtWidgets import QWidget + +from .viewer import OrthophotoViewer +from vast.orthophoto_canvas.ag_io.tileset import TileStore + + +def create_orthophoto_viewer( + tiles_root: Union[str, Path], + forced_scheme: Optional[str] = None, + parent: Optional[QWidget] = None, +) -> OrthophotoViewer: + """ + Build and return an OrthophotoViewer widget. + No QApplication is created here. + """ + tiles_path = Path(tiles_root) + + # Let the viewer create the TileStore internally. + viewer = OrthophotoViewer(tiles=tiles_path) + + # Optionally force tile scheme (TMS / XYZ). + if forced_scheme: + scheme = forced_scheme.lower().strip() + if scheme in ("tms", "xyz"): + is_tms = (scheme == "tms") + # Try to update both the viewer and its underlying TileStore. + try: + viewer.is_tms = is_tms + except Exception: + pass + try: + if isinstance(viewer.ts, TileStore): + viewer.ts.is_tms = is_tms + except Exception: + pass + + if parent is not None: + viewer.setParent(parent) + + return viewer diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/utils/__init__.py b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/__init__.py new file mode 100644 index 000000000..ecee43dd1 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/__init__.py @@ -0,0 +1 @@ +# utils package marker diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/utils/geometry.py b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/geometry.py new file mode 100644 index 000000000..4f01cec71 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/geometry.py @@ -0,0 +1,9 @@ +from typing import Tuple + +# Minimal helpers + +def clamp(v: float, vmin: float, vmax: float) -> float: + return max(vmin, min(v, vmax)) + +def lerp(a: float, b: float, t: float) -> float: + return a + (b - a) * t diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/utils/qt.py b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/qt.py new file mode 100644 index 000000000..f442ffdd6 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/qt.py @@ -0,0 +1,30 @@ +from PyQt6.QtCore import QPointF +from PyQt6.QtGui import QPen, QBrush, QColor, QFont +from PyQt6.QtWidgets import QGraphicsScene, QGraphicsEllipseItem, QGraphicsSimpleTextItem + +def add_round_marker(scene: QGraphicsScene, pos: QPointF, radius: float = 4.0, + color: QColor = QColor(220, 30, 30)) -> QGraphicsEllipseItem: + """ + Add a simple circle marker to the scene. + TODO: expose pen/brush via params if needed. + """ + r = radius + item = QGraphicsEllipseItem(pos.x() - r, pos.y() - r, 2 * r, 2 * r) + item.setPen(QPen(color)) + item.setBrush(QBrush(color.lighter(140))) + scene.addItem(item) + return item + +def add_label(scene: QGraphicsScene, pos: QPointF, text: str, + color: QColor = QColor(20, 20, 20)) -> QGraphicsSimpleTextItem: + """ + Add a small text label near a point. + """ + t = QGraphicsSimpleTextItem(text) + t.setBrush(QBrush(color)) + font = QFont() + font.setPointSize(8) + t.setFont(font) + t.setPos(pos.x() + 6, pos.y() - 6) + scene.addItem(t) + return t diff --git a/AgCloud/GUI/src/vast/orthophoto_canvas/utils/tiles.py b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/tiles.py new file mode 100644 index 000000000..a17908c96 --- /dev/null +++ b/AgCloud/GUI/src/vast/orthophoto_canvas/utils/tiles.py @@ -0,0 +1,200 @@ +# utils/tiles.py +from __future__ import annotations +from pathlib import Path +import os +from typing import Dict, List, Tuple, Optional + +_EXTS = (".png", ".jpg", ".jpeg") + + +class TileStore: + """ + Small helper around a tiles root folder laid out as {root}/{z}/{x}/{y}.ext + (XYZ or TMS). + Scans available zoom levels and x/y ranges, detects XYZ vs TMS, and + provides helpers to locate tile files. + """ + + def __init__(self, root: Path | str) -> None: + self.root = Path(root) + if not self.root.exists() or not self.root.is_dir(): + raise FileNotFoundError(f"Tiles root does not exist or is not a directory: {self.root}") + + self.existing_zooms: List[int] = self._scan_existing_zooms() + if not self.existing_zooms: + raise FileNotFoundError(f"No zoom folders found under: {self.root}") + + self.min_zoom: int = min(self.existing_zooms) + self.max_zoom: int = max(self.existing_zooms) + + # map[z] = (x_min, x_max, y_min, y_max) + self.z_ranges: Dict[int, Tuple[int, int, int, int]] = self._scan_index_ranges() + + # detect XYZ/TMS + self.is_tms: bool = self._detect_scheme() + + # ---------- scans ---------- + + def _scan_existing_zooms(self) -> List[int]: + """Return sorted zooms (dir names that are all digits) under root.""" + zs: List[int] = [] + try: + with os.scandir(self.root) as it: + for e in it: + if e.is_dir() and e.name.isdigit(): + zs.append(int(e.name)) + except OSError as ex: + raise OSError(f"Failed to scan tiles root: {self.root}") from ex + zs.sort() + return zs + + def _scan_index_ranges(self) -> Dict[int, Tuple[int, int, int, int]]: + """ + For each zoom z, compute: + x_min..x_max from subfolders, and y_min..y_max from filenames inside those x folders. + """ + ranges: Dict[int, Tuple[int, int, int, int]] = {} + for z in self.existing_zooms: + zdir = self.root / str(z) + if not zdir.is_dir(): + continue + + xs: List[int] = [] + try: + with os.scandir(zdir) as it: + for e in it: + if e.is_dir() and e.name.isdigit(): + xs.append(int(e.name)) + except OSError: + continue + + if not xs: + continue + + x_min, x_max = min(xs), max(xs) + + # collect all y values (numeric stems with allowed image extensions) + ys: List[int] = [] + for x in xs: + xdir = zdir / str(x) + try: + with os.scandir(xdir) as it: + for e in it: + if not e.is_file(): + continue + stem, ext = os.path.splitext(e.name) + if stem.isdigit() and ext.lower() in _EXTS: + ys.append(int(stem)) + except OSError: + continue + + if ys: + y_min, y_max = min(ys), max(ys) + else: + # no y found – degrade gracefully (still allow positioning by x) + y_min = y_max = 0 + + ranges[z] = (x_min, x_max, y_min, y_max) + + if not ranges: + raise FileNotFoundError( + f"No x/y tiles found under zoom folders in: {self.root}" + ) + return ranges + + # ---------- scheme detection (XYZ vs TMS) ---------- + + def _detect_scheme(self) -> bool: + """ + Returns True if TMS (Y is flipped), otherwise False (XYZ). + Heuristic: pick high zoom z, choose an x with files, grab a y from it, + probe both XYZ and TMS paths — prefer XYZ if both exist. + """ + z = self.max_zoom + zdir = self.root / str(z) + + xs = self.list_existing_x(z) + # try a few centered xs + xs_sorted = sorted(xs, key=lambda xv: abs(xv - ((min(xs) + max(xs)) // 2))) if xs else xs + + for x in xs_sorted[:10]: # probe a few + ys = self.list_existing_y(z, x) + if not ys: + continue + # pick a "middle" y to avoid edge bias + y = ys[len(ys) // 2] + + if self._tile_path_xyz(z, x, y) is not None: + # XYZ found — prefer it + return False + + # Check TMS flip + y_max = (1 << z) - 1 + y_tms = y_max - y + if self._tile_path_tms(z, x, y_tms) is not None: + return True + + # default to XYZ if unsure + return False + + # ---------- public helpers ---------- + + def list_existing_x(self, z: int) -> List[int]: + zdir = self.root / str(z) + if not zdir.is_dir(): + return [] + xs: List[int] = [] + try: + with os.scandir(zdir) as it: + for e in it: + if e.is_dir() and e.name.isdigit(): + xs.append(int(e.name)) + except OSError: + return [] + xs.sort() + return xs + + def list_existing_y(self, z: int, x: int) -> List[int]: + xdir = self.root / str(z) / str(x) + if not xdir.is_dir(): + return [] + ys: List[int] = [] + try: + with os.scandir(xdir) as it: + for e in it: + if not e.is_file(): + continue + stem, ext = os.path.splitext(e.name) + if stem.isdigit() and ext.lower() in _EXTS: + ys.append(int(stem)) + except OSError: + return [] + ys.sort() + return ys + + def tile_path(self, z: int, x: int, y: int) -> Optional[str]: + """Return absolute file path (str) to an existing tile, or None.""" + if self.is_tms: + return self._tile_path_tms(z, x, y) + return self._tile_path_xyz(z, x, y) + + # ---------- path building ---------- + + def _tile_path_xyz(self, z: int, x: int, y: int) -> Optional[str]: + base = self.root / str(z) / str(x) + for ext in _EXTS: + p = base / f"{y}{ext}" + if p.exists(): + return str(p) + return None + + def _tile_path_tms(self, z: int, x: int, y: int) -> Optional[str]: + # flip Y: y_tms = (2^z - 1) - y + y_max = (1 << z) - 1 + y_tms = y_max - y + base = self.root / str(z) / str(x) + for ext in _EXTS: + p = base / f"{y_tms}{ext}" + if p.exists(): + return str(p) + return None diff --git a/AgCloud/GUI/src/vast/proto/__init__.py b/AgCloud/GUI/src/vast/proto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/proto/generated/__init__.py b/AgCloud/GUI/src/vast/proto/generated/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/proto/generated/query_pb2.py b/AgCloud/GUI/src/vast/proto/generated/query_pb2.py new file mode 100644 index 000000000..54e1ec5bb --- /dev/null +++ b/AgCloud/GUI/src/vast/proto/generated/query_pb2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: query.proto +# Protobuf Python Version: 6.31.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 31, + 1, + '', + 'query.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bquery.proto\x12\x05query\"\x14\n\x04Plan\x12\x0c\n\x04json\x18\x01 \x01(\t\"\x8c\x01\n\x06Sensor\x12\x11\n\tsensor_id\x18\x01 \x01(\t\x12\x0b\n\x03lat\x18\x02 \x01(\x01\x12\x0b\n\x03lon\x18\x03 \x01(\x01\x12\'\n\x05props\x18\x04 \x03(\x0b\x32\x18.query.Sensor.PropsEntry\x1a,\n\nPropsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\",\n\nSensorList\x12\x1e\n\x07sensors\x18\x01 \x03(\x0b\x32\r.query.Sensor29\n\x0bQueryRunner\x12*\n\x08RunQuery\x12\x0b.query.Plan\x1a\x11.query.SensorListb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'query_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_SENSOR_PROPSENTRY']._loaded_options = None + _globals['_SENSOR_PROPSENTRY']._serialized_options = b'8\001' + _globals['_PLAN']._serialized_start=22 + _globals['_PLAN']._serialized_end=42 + _globals['_SENSOR']._serialized_start=45 + _globals['_SENSOR']._serialized_end=185 + _globals['_SENSOR_PROPSENTRY']._serialized_start=141 + _globals['_SENSOR_PROPSENTRY']._serialized_end=185 + _globals['_SENSORLIST']._serialized_start=187 + _globals['_SENSORLIST']._serialized_end=231 + _globals['_QUERYRUNNER']._serialized_start=233 + _globals['_QUERYRUNNER']._serialized_end=290 +# @@protoc_insertion_point(module_scope) diff --git a/AgCloud/GUI/src/vast/proto/generated/query_pb2_grpc.py b/AgCloud/GUI/src/vast/proto/generated/query_pb2_grpc.py new file mode 100644 index 000000000..ebf680015 --- /dev/null +++ b/AgCloud/GUI/src/vast/proto/generated/query_pb2_grpc.py @@ -0,0 +1,97 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +from vast.proto.generated import query_pb2 as query__pb2 + +GRPC_GENERATED_VERSION = '1.74.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in query_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class QueryRunnerStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.RunQuery = channel.unary_unary( + '/query.QueryRunner/RunQuery', + request_serializer=query__pb2.Plan.SerializeToString, + response_deserializer=query__pb2.SensorList.FromString, + _registered_method=True) + + +class QueryRunnerServicer(object): + """Missing associated documentation comment in .proto file.""" + + def RunQuery(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_QueryRunnerServicer_to_server(servicer, server): + rpc_method_handlers = { + 'RunQuery': grpc.unary_unary_rpc_method_handler( + servicer.RunQuery, + request_deserializer=query__pb2.Plan.FromString, + response_serializer=query__pb2.SensorList.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'query.QueryRunner', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('query.QueryRunner', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class QueryRunner(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def RunQuery(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/query.QueryRunner/RunQuery', + query__pb2.Plan.SerializeToString, + query__pb2.SensorList.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/AgCloud/GUI/src/vast/proto/query.proto b/AgCloud/GUI/src/vast/proto/query.proto new file mode 100644 index 000000000..c8668840c --- /dev/null +++ b/AgCloud/GUI/src/vast/proto/query.proto @@ -0,0 +1,21 @@ +// query.proto +syntax = "proto3"; +package query; + +message Plan { string json = 1; } + +// Extra sensor fields live in props to stay flexible +message Sensor { + string sensor_id = 1; + double lat = 2; + double lon = 3; + map props = 4; // e.g. battery, status, name... +} + +message SensorList { + repeated Sensor sensors = 1; +} + +service QueryRunner { + rpc RunQuery(Plan) returns (SensorList); +} diff --git a/AgCloud/GUI/src/vast/rel_db.py b/AgCloud/GUI/src/vast/rel_db.py new file mode 100644 index 000000000..8e92c49c1 --- /dev/null +++ b/AgCloud/GUI/src/vast/rel_db.py @@ -0,0 +1,324 @@ +# rel_db.py +from __future__ import annotations +import os +import datetime as dt +from contextlib import contextmanager +from typing import Optional, List, Dict, Tuple +from functools import lru_cache + +import psycopg2 +from psycopg2.extras import RealDictCursor + + +# ---- ENV (Docker Compose defaults) ---- +DB_HOST = os.getenv("DB_HOST", "127.0.0.1") +DB_PORT = int(os.getenv("DB_PORT", "5432")) +DB_USER = os.getenv("DB_USER", "missions_user") +DB_PASS = os.getenv("DB_PASS", "pg123") +DB_NAME = os.getenv("DB_NAME", "missions_db") + + +@contextmanager +def _pg_conn(): + conn = psycopg2.connect( + host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASS, dbname=DB_NAME + ) + try: + yield conn + finally: + conn.close() + + +def _query(sql: str, params: tuple = ()) -> List[Dict]: + try: + with _pg_conn() as conn: + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(sql, params) + return [dict(r) for r in cur.fetchall()] + except Exception as e: + print(f"[RelDB][QUERY FAIL] {e}\n | SQL={sql!r} | params={params!r}") + return [] + + +# ---------- Dynamic schema ---------- +@lru_cache(maxsize=1) +def _anomalies_cols() -> set[str]: + rows = _query( + "SELECT column_name FROM information_schema.columns " + "WHERE table_schema='public' AND table_name='anomalies'" + ) + return {r["column_name"] for r in rows} if rows else set() + +def _has_col(name: str) -> bool: + return name in _anomalies_cols() + +def _img_expr() -> str: + """ + Adaptive image column: + image_id (if exists) -> else tile_id -> else details->>'image_id' + """ + if _has_col("image_id"): + return "image_id" + if _has_col("tile_id"): + return "tile_id" + return "(details->>'image_id')" + +def _bbox_expr() -> str: + """Adaptive bbox column: bbox or JSON.""" + if _has_col("bbox"): + return "bbox" + return "(details->'bbox')" + +def _select_projection() -> str: + """ + Returns SELECT column list with aliases to always include: + anomaly_id, mission_id, device_id, ts, anomaly_type_id, severity, + bbox, area, label, image_id, confidence, geom, details + Even if some do not physically exist (extracted from JSON). + """ + cols = [ + "anomaly_id", + "mission_id", + "device_id", + "ts", + "anomaly_type_id", + "severity", + f"{_bbox_expr()} AS bbox", + # Derived from JSON (even if they exist physically, it’s fine; but we avoid name conflicts) + "(details->>'area')::float AS area", + "(details->>'label') AS label", + f"{_img_expr()} AS image_id", + "(details->>'confidence')::float AS confidence", + "geom", + "details", + ] + # If a physical column with the same name exists, prefer it (remove alias to avoid collision) + if _has_col("area"): + cols[cols.index("(details->>'area')::float AS area")] = "area" + if _has_col("label"): + cols[cols.index("(details->>'label') AS label")] = "label" + if _has_col("confidence"): + cols[cols.index("(details->>'confidence')::float AS confidence")] = "confidence" + return ", ".join(cols) + + +class RelDB: + """ + Thin data-access layer for the anomalies table. + Works even when image_id/bbox are missing by using details(JSONB). + """ + + # ---------- Utilities ---------- + @staticmethod + def _split_object_key(object_key: str) -> Tuple[str, str]: + if not isinstance(object_key, str): + return "", "" + name = object_key.replace("\\", "/").split("/")[-1] + if "." in name: + base = ".".join(name.split(".")[:-1]) + ext = name.split(".")[-1] + return base, ext + return name, "" + + @staticmethod + def _image_name_from_object_key(object_key: str) -> str: + base, _ = RelDB._split_object_key(object_key) + return base.strip() + + # ---------- Latest N ---------- + def get_latest_anomalies(self, limit: int = 20) -> List[Dict]: + limit = max(1, min(int(limit or 20), 1000)) + cols = _select_projection() + q_ts = f"SELECT {cols} FROM public.anomalies ORDER BY ts DESC LIMIT %s" + rows = _query(q_ts, (limit,)) + if rows: + return rows + q_id = f"SELECT {cols} FROM public.anomalies ORDER BY anomaly_id DESC LIMIT %s" + return _query(q_id, (limit,)) + + # ---------- By image ---------- + def get_anomalies_by_image(self, image_name: str, limit: int = 50) -> List[Dict]: + if not image_name: + return [] + limit = max(1, min(int(limit or 50), 1000)) + cols = _select_projection() + img_col = _img_expr() + q_ts = f""" + SELECT {cols} + FROM public.anomalies + WHERE {img_col} = %s + ORDER BY ts DESC + LIMIT %s + """ + rows = _query(q_ts, (image_name, limit)) + if rows: + return rows + q_id = f""" + SELECT {cols} + FROM public.anomalies + WHERE {img_col} = %s + ORDER BY anomaly_id DESC + LIMIT %s + """ + return _query(q_id, (image_name, limit)) + + def get_last_anomaly_by_image(self, image_name: str) -> Optional[Dict]: + rows = self.get_anomalies_by_image(image_name, limit=1) + return rows[0] if rows else None + + # ---------- From object key ---------- + def get_anomalies_for_image_key(self, object_key: str, limit: int = 50) -> List[Dict]: + image_name = self._image_name_from_object_key(object_key) + if not image_name: + return [] + return self.get_anomalies_by_image(image_name, limit=limit) + + # ---------- Latest image present in DB ---------- + def get_latest_image_key(self) -> Optional[str]: + img_col = _img_expr() + if img_col.startswith("(") and "details" in img_col: + # Can still filter based on the expression + pass + q_ts = f""" + SELECT {img_col} AS img + FROM public.anomalies + WHERE {img_col} IS NOT NULL AND {img_col} <> '' + ORDER BY ts DESC + LIMIT 50 + """ + rows = _query(q_ts) + if not rows: + q_id = f""" + SELECT {img_col} AS img + FROM public.anomalies + WHERE {img_col} IS NOT NULL AND {img_col} <> '' + ORDER BY anomaly_id DESC + LIMIT 50 + """ + rows = _query(q_id) + for r in rows or []: + v = r.get("img") + if isinstance(v, str) and v.strip(): + return v.strip() + return None + + # ---------- By day ---------- + def get_anomalies_by_day(self, date_iso: str, limit: int = 1000) -> List[Dict]: + try: + day = dt.date.fromisoformat(date_iso) + except Exception: + print(f"[RelDB][DAY WARN] invalid date {date_iso!r}") + return [] + start = dt.datetime.combine(day, dt.time.min) + end = start + dt.timedelta(days=1) + cols = _select_projection() + q = f""" + SELECT {cols} + FROM public.anomalies + WHERE ts >= %s AND ts < %s + ORDER BY ts DESC + LIMIT %s + """ + rows = _query(q, (start, end, limit)) + if rows: + return rows + return self.get_latest_anomalies(limit=limit) + + # ---------- PHI helpers ---------- + @staticmethod + def _sev_norm(x) -> Optional[float]: + try: + s = float(x) + except Exception: + return None + if s < 0: + return None + return s if s <= 1.0 else min(s, 10.0) / 10.0 + + @staticmethod + def _phi_from(sev_avg_norm: Optional[float]) -> Optional[float]: + if sev_avg_norm is None: + return None + return max(0.0, min(100.0, 100.0 * (1.0 - max(0.0, min(1.0, sev_avg_norm))))) + + # --- PHI per image --- + def get_phi_for_image(self, image_name: str) -> Dict[str, Optional[float | str]]: + if not image_name: + return {"phi": None, "severity_avg": None, "image_id": None} + img_col = _img_expr() + q = f""" + SELECT + AVG( + CASE + WHEN severity <= 1.0 THEN severity + WHEN severity > 1.0 THEN LEAST(severity, 10.0)/10.0 + ELSE NULL + END + ) AS sev_avg_norm, + COUNT(*) AS n_rows + FROM public.anomalies + WHERE {img_col} = %s + """ + rows = _query(q, (image_name,)) + sev_avg = rows[0].get("sev_avg_norm") if rows else None + phi = self._phi_from(sev_avg) + return { + "phi": phi, + "severity_avg": float(sev_avg) if sev_avg is not None else None, + "image_id": image_name, + } + + def get_phi_for_current_image(self) -> Dict[str, Optional[float | str]]: + image_name = self.get_latest_image_key() + if not image_name: + return {"phi": None, "severity_avg": None, "image_id": None} + return self.get_phi_for_image(image_name) + + # --- Weekly PHI (backward compatibility) --- + def get_weekly_phi(self) -> Dict[str, Optional[float | str]]: + today = dt.date.today() + week_start = today - dt.timedelta(days=today.weekday()) # Monday + prev_week_start = week_start - dt.timedelta(days=7) + week_end = week_start + dt.timedelta(days=7) + prev_week_end = week_start + + def _week_stats(a: dt.date, b: dt.date): + q = """ + SELECT + AVG( + CASE + WHEN severity <= 1.0 THEN severity + WHEN severity > 1.0 THEN LEAST(severity, 10.0)/10.0 + ELSE NULL + END + ) AS sev_avg_norm, + COUNT(*) AS n_rows + FROM public.anomalies + WHERE ts >= %s AND ts < %s + """ + rows = _query(q, ( + dt.datetime.combine(a, dt.time.min), + dt.datetime.combine(b, dt.time.min), + )) + return rows[0] if rows else {"sev_avg_norm": None, "n_rows": 0} + + cur = _week_stats(week_start, week_end) + prev = _week_stats(prev_week_start, prev_week_end) + + sev_avg = cur.get("sev_avg_norm") + phi = self._phi_from(sev_avg) + + n_rows = (cur.get("n_rows") or 0) + density = (n_rows / 7.0) if n_rows else None + + prev_phi = self._phi_from(prev.get("sev_avg_norm")) + trend = (phi - prev_phi) if (phi is not None and prev_phi is not None) else None + + return { + "phi": phi, + "severity_avg": float(sev_avg) if sev_avg is not None else None, + "density": float(density) if density is not None else None, + "coverage": None, + "trend": float(trend) if trend is not None else None, + "week_start": str(week_start), + } diff --git a/AgCloud/GUI/src/vast/runner/.dockerignore b/AgCloud/GUI/src/vast/runner/.dockerignore new file mode 100644 index 000000000..550f2fffc --- /dev/null +++ b/AgCloud/GUI/src/vast/runner/.dockerignore @@ -0,0 +1,23 @@ +# VCS / editors +.git/ +.gitignore +.vscode/ +.idea/ + +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +*.pytest_cache/ +.venv/ +venv/ + +# OS cruft +.DS_Store + +# Misc +*.log +dist/ +build/ + diff --git a/AgCloud/GUI/src/vast/runner/Dockerfile b/AgCloud/GUI/src/vast/runner/Dockerfile new file mode 100644 index 000000000..1307a1d59 --- /dev/null +++ b/AgCloud/GUI/src/vast/runner/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app + +# build arg +ARG USE_NETFREE=true + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* +COPY certs /app/certs +# System CA + add NetFree certs +RUN if [ "$USE_NETFREE" = "true" ] && [ -d ./certs ] && [ "$(ls ./certs/*.crt 2>/dev/null)" ]; then \ + echo "Configuring NetFree certificates..."; \ + cp ./certs/*.crt /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ + fi + +# SSL certs env +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt + + +# Python deps from repo root +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +# Copy source under /app/vast/* +COPY src/vast/runner /app/vast/runner +COPY src/vast/dsl /app/vast/dsl +COPY src/vast/proto /app/vast/proto + +# Generate stubs +RUN python -m grpc_tools.protoc -I./vast/proto \ + --python_out=. --grpc_python_out=. \ + ./vast/proto/query.proto + +ENV RUNNER_MODE=real SQLITE_DB=/data/app.db LOG_LEVEL=INFO PYTHONPATH=/app +ENV PYTHONPATH=/app/vast/proto/generated:/app + +EXPOSE 50051 +CMD ["python", "vast/runner/runner_server.py"] \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/runner/__init__.py b/AgCloud/GUI/src/vast/runner/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/runner/runner_server.py b/AgCloud/GUI/src/vast/runner/runner_server.py new file mode 100644 index 000000000..768e64d28 --- /dev/null +++ b/AgCloud/GUI/src/vast/runner/runner_server.py @@ -0,0 +1,171 @@ +# runner_server.py +""" +gRPC runner that executes a JSON DSL plan on SQLite (or returns mock data). + +RPC +- QueryRunner.RunQuery({ json: str }) -> SensorList + +Env +- RUNNER_MODE: 'real' | 'mock' +- SQLITE_DB : path to SQLite file +- PORT : gRPC port (default 50051) +- LOG_LEVEL : logging level +""" + +import os, json, uuid, logging, sqlite3 +from concurrent import futures +from typing import Dict, Any, List +import grpc +from vast.proto.generated import query_pb2, query_pb2_grpc + +# === Config === +RUNNER_MODE = os.getenv("RUNNER_MODE", "real") # "mock" or "real" +SQLITE_DB = os.getenv("SQLITE_DB", "./app.db") # path to your SQLite file +PORT = int(os.getenv("PORT", "50051")) +LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() + +logging.basicConfig(level=LOG_LEVEL, format="%(asctime)s %(levelname)s %(name)s %(message)s") +log = logging.getLogger("runner") + +# === Import the DSL compiler (strict JSON IR → SQL) === +# Assumes you added the multi-file package from earlier under 'dsl/' on PYTHONPATH. +from vast.dsl import SQLBuilder, SQLiteDialect + +# ---------------- gRPC Service ---------------- # + +class QueryRunnerImpl(query_pb2_grpc.QueryRunnerServicer): + """Validates JSON; mock mode returns fixtures, real mode compiles plan→SQL and executes.""" + + def RunQuery(self, request, context): + md = dict(context.invocation_metadata()) + request_id = md.get("x-request-id", str(uuid.uuid4())) + log.info("RunQuery request_id=%s mode=%s", request_id, RUNNER_MODE) + + # Parse the JSON plan (strict format only) + try: + if not request.json: + context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Empty request.json") + plan: Dict[str, Any] = json.loads(request.json) + except Exception as e: + log.error("Invalid plan JSON req=%s err=%s", request_id, e) + context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid plan JSON") + + if RUNNER_MODE == "mock": + sensors = [ + {"sensor_id":"A-101", "lat":31.8998, "lon":34.849524, "name":"Soil Probe A", + "battery":"3.86V", "moisture":"21%", "status":"ok"}, + {"sensor_id":"D-404", "lat":31.8978, "lon":34.849524, "name":"Soil Probe B", + "battery":"3.86V", "moisture":"21%", "status":"ok"}, + {"sensor_id":"E-505", "lat":31.8978, "lon":34.850924, "name":"Soil Probe C", + "battery":"3.86V", "moisture":"21%", "status":"ok"}, + {"sensor_id":"B-202", "lat":31.8996, "lon":34.849524, "label":"Valve East 4", + "battery":"3.55V", "status":"warning"}, + {"sensor_id":"C-303", "lat":31.8999, "lon":34.851734, "status":"offline"} + ] + return _pack_sensors(sensors) + + # compile plan → SQL (SQLite) and execute --- + try: + sql, params = SQLBuilder(SQLiteDialect()).compile(plan) + except Exception as e: + log.exception("Plan compilation failed req=%s", request_id) + context.abort(grpc.StatusCode.INVALID_ARGUMENT, f"Plan compilation failed: {e}") + + log.debug("SQL req=%s: %s", request_id, sql) + log.debug("Params req=%s: %s", request_id, params) + + try: + # Read-only connection: file:... uri + mode=ro + uri = f"file:{SQLITE_DB}?mode=ro" + with sqlite3.connect(uri, uri=True) as conn: + conn.row_factory = sqlite3.Row + cur = conn.execute(sql, params) + rows = cur.fetchall() + except sqlite3.OperationalError as e: + log.exception("SQLite error req=%s", request_id) + context.abort(grpc.StatusCode.FAILED_PRECONDITION, f"SQLite error: {e}") + except Exception as e: + log.exception("Query execution failed req=%s", request_id) + context.abort(grpc.StatusCode.INTERNAL, f"Execution failed: {e}") + + # Map rows → SensorList + sensors_list = _rows_to_sensors(rows) + return sensors_list + + +# ---------------- Helpers ---------------- # + +def _rows_to_sensors(rows: List[sqlite3.Row]) -> query_pb2.SensorList: + """ + Convert query result rows → SensorList. + We look for common columns: sensor_id, lat/latitude, lon/lng/longitude. + Any other columns go into props (as strings). + """ + result = query_pb2.SensorList() + for row in rows: + # Column name lookup (case-insensitive) + colmap = {k.lower(): k for k in row.keys()} + sid_key = _first_key(colmap, ["sensor_id", "id", "sensorid"]) + lat_key = _first_key(colmap, ["lat", "latitude", "y"]) + lon_key = _first_key(colmap, ["lon", "lng", "long", "longitude", "x"]) + + sensor = query_pb2.Sensor( + sensor_id=str(row[sid_key]) if sid_key else "", + lat=float(row[lat_key]) if lat_key and row[lat_key] is not None else 0.0, + lon=float(row[lon_key]) if lon_key and row[lon_key] is not None else 0.0, + ) + + # Put remaining columns into props (strings) + skip = set(k for k in [sid_key, lat_key, lon_key] if k) + for k in row.keys(): + if k in skip: + continue + v = row[k] + sensor.props[k] = "" if v is None else str(v) + + result.sensors.append(sensor) + return result + + +def _first_key(colmap: Dict[str, str], candidates) -> str | None: + """Return original column name matching any candidate (case-insensitive).""" + for c in candidates: + if c in colmap: + return colmap[c] + return None + + +def _pack_sensors(sensors_list): + """(kept for mock mode) list[dict] → SensorList.""" + result = query_pb2.SensorList() + for s in sensors_list: + sensor = query_pb2.Sensor( + sensor_id=str(s.get("sensor_id", "")), + lat=float(s.get("lat", 0.0)), + lon=float(s.get("lon", 0.0)), + ) + for k, v in s.items(): + if k in ("sensor_id", "lat", "lon"): + continue + sensor.props[k] = "" if v is None else str(v) + result.sensors.append(sensor) + return result + + +def serve(port: int = PORT): + """Start the gRPC server and block.""" + server = grpc.server(futures.ThreadPoolExecutor(max_workers=8)) + query_pb2_grpc.add_QueryRunnerServicer_to_server(QueryRunnerImpl(), server) + server.add_insecure_port(f"[::]:{port}") + server.start() + log.info("Runner gRPC listening on :%s (mode=%s, db=%s)", port, RUNNER_MODE, SQLITE_DB) + server.wait_for_termination() + + +if __name__ == "__main__": + serve() + + + + + diff --git a/AgCloud/GUI/src/vast/services/Dockerfile b/AgCloud/GUI/src/vast/services/Dockerfile new file mode 100644 index 000000000..3b3c03a35 --- /dev/null +++ b/AgCloud/GUI/src/vast/services/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.11-slim +ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +WORKDIR /app +# # System CA + NetFree +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* +COPY certs/*.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates || true +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt +# Python deps from repo root +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt +# Copy source under /app/vast/* +COPY src/vast/services /app/vast/services +# Set environment variable for DB if needed +ENV SQLITE_DB=/data/app.db +ENV PYTHONPATH=/app/vast/proto/generated:/app +# Expose port if metrics serve HTTP (optional) +EXPOSE 8001 +# Run the metrics app +CMD ["python", "-m", "vast.services.sensors_metrics_app"] \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/services/__init__.py b/AgCloud/GUI/src/vast/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/services/sensors_metrics_app.py b/AgCloud/GUI/src/vast/services/sensors_metrics_app.py new file mode 100644 index 000000000..3e4983fa3 --- /dev/null +++ b/AgCloud/GUI/src/vast/services/sensors_metrics_app.py @@ -0,0 +1,133 @@ +from __future__ import annotations +import random, time, threading +from datetime import datetime, timezone +from flask import Flask, Response, send_from_directory, jsonify +from pathlib import Path +from prometheus_client import CollectorRegistry, Gauge, generate_latest, CONTENT_TYPE_LATEST +# from webmap import webmap_bp +from .webmap import webmap_bp + +app = Flask(__name__) +app.register_blueprint(webmap_bp) # routes: /map, /tiles, /api/sensors +# TILES_ROOT = Path(__file__).resolve().parents[1] / "orthophoto_canvas" / "data" / "tiles" +REG = CollectorRegistry() + +SENSOR_STATUS = Gauge("sensor_status", "1=active, 0=inactive", ["sensor"], registry=REG) +SENSORS_ACTIVE_TOTAL = Gauge("sensors_active_total", "Active sensors total", registry=REG) +SENSOR_LAST_SEEN = Gauge("sensor_last_seen_timestamp_seconds", "Last seen (unix ts)", ["sensor"], registry=REG) + +SENSORS = [f"sensor_{i:03d}" for i in range(1, 41)] # 40 sensors demo + +def _tick(): + while True: + active_count = 0 + now = datetime.now(timezone.utc).timestamp() + for s in SENSORS: + val = 1 if random.random() < 0.8 else 0 # ~80% active + SENSOR_STATUS.labels(sensor=s).set(val) + if val == 1: + active_count += 1 + SENSOR_LAST_SEEN.labels(sensor=s).set(now) + SENSORS_ACTIVE_TOTAL.set(active_count) + time.sleep(5) + +@app.route("/metrics") +def metrics(): + return Response(generate_latest(REG), mimetype=CONTENT_TYPE_LATEST) + +@app.get("/tiles///") +def tiles(z: int, x: int, y_png: str): + p = TILES_ROOT / str(z) / str(x) / y_png + if p.is_file(): + return send_from_directory(p.parent, p.name, mimetype="image/png") + try: + y = int(y_png.replace(".png", "")) + y_tms = (1 << z) - 1 - y + p2 = TILES_ROOT / str(z) / str(x) / f"{y_tms}.png" + if p2.is_file(): + return send_from_directory(p2.parent, p2.name, mimetype="image/png") + except Exception: + pass + return Response("Tile not found", status=404) + +@app.get("/map") +def map_page(): + html = """ + + + + Orthophoto Map + + + + + + + + + +
+
+ + +
+
Lat, Lon
+ + + + + + + +""" + return Response(html, mimetype="text/html") + +# Optional, only if you want markers from an API: +@app.get("/api/sensors") +def api_sensors(): + return jsonify([ + {"id": "S-001", "lat": 31.778, "lon": 35.235}, + {"id": "S-002", "lat": 31.785, "lon": 35.220}, + {"id": "S-003", "lat": 31.760, "lon": 35.250}, + ]) + + +if __name__ == "__main__": + threading.Thread(target=_tick, daemon=True).start() + app.run(host="0.0.0.0", port=8000) diff --git a/AgCloud/GUI/src/vast/services/webmap.py b/AgCloud/GUI/src/vast/services/webmap.py new file mode 100644 index 000000000..969a9233f --- /dev/null +++ b/AgCloud/GUI/src/vast/services/webmap.py @@ -0,0 +1,124 @@ +# services/webmap.py +from __future__ import annotations +import os +import uuid +from pathlib import Path +from typing import Any + +import requests +from flask import Blueprint, send_from_directory, Response, jsonify + +webmap_bp = Blueprint("webmap", __name__) + +TILES_ROOT = Path(__file__).resolve().parents[1] / "orthophoto_canvas" / "data" / "tiles" +GATEWAY_URL = os.getenv("GATEWAY_URL", "http://127.0.0.1:9001") + +@webmap_bp.get("/tiles///") +def tiles(z: int, x: int, y_png: str): + p = TILES_ROOT / str(z) / str(x) / y_png + if p.is_file(): + return send_from_directory(p.parent, p.name, mimetype="image/png") + try: + y = int(y_png.replace(".png", "")) + y_tms = (1 << z) - 1 - y + p2 = TILES_ROOT / str(z) / str(x) / f"{y_tms}.png" + if p2.is_file(): + return send_from_directory(p2.parent, p2.name, mimetype="image/png") + except Exception: + pass + return Response("Tile not found", status=404) + +@webmap_bp.get("/api/sensors") +def api_sensors(): + plan = { + "source": "sensors", + "_ops": [ + {"op": "select", "columns": [ + "sensor_id", "lat", "lon", "status", "name", "label", "battery", "moisture" + ]}, + {"op": "where", "cond": {"any": [ + {"op": "=", "left": {"col": "status"}, "right": {"literal": "ok"}}, + {"op": "=", "left": {"col": "status"}, "right": {"literal": "warning"}} + ]}} + ] + } + headers = {"Content-Type": "application/json", "X-Request-Id": str(uuid.uuid4())} + try: + r = requests.post(f"{GATEWAY_URL.rstrip('/')}/runQuery", json=plan, headers=headers, timeout=20) + r.raise_for_status() + data: Any = r.json() + except Exception as e: + print(f"[webmap] ERROR fetching sensors from gateway: {e}") + data = [] + + if isinstance(data, dict): + data = data.get("sensors", []) + if not isinstance(data, list): + data = [] + return jsonify(data) + +@webmap_bp.get("/map") +def map_page(): + html = """ + + + + + Orthophoto Map + + + + + + +
+
+ +
+
Lat, Lon
+ + + + + + + """.strip() + return Response(html, mimetype="text/html") diff --git a/AgCloud/GUI/src/vast/session_manager.py b/AgCloud/GUI/src/vast/session_manager.py new file mode 100644 index 000000000..9590a2c88 --- /dev/null +++ b/AgCloud/GUI/src/vast/session_manager.py @@ -0,0 +1,42 @@ +from __future__ import annotations +from typing import Optional +from PyQt6.QtCore import QObject, pyqtSignal + +class SessionManager(QObject): + """ + Simple session holder (Singleton-like). + Stores current user and role; emits 'sessionChanged' when updated. + """ + sessionChanged = pyqtSignal() + + _instance: "SessionManager | None" = None + + def __new__(cls) -> "SessionManager": + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance # type: ignore[return-value] + + def __init__(self) -> None: + super().__init__() + if not hasattr(self, "_initialized"): + self._initialized = True + self._user: Optional[str] = None + self._role: Optional[str] = None + + @property + def user(self) -> Optional[str]: + return self._user + + @property + def role(self) -> Optional[str]: + return self._role + + def login(self, user: str, role: str) -> None: + self._user = user + self._role = role + self.sessionChanged.emit() + + def logout(self) -> None: + self._user = None + self._role = None + self.sessionChanged.emit() \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/status_card.py b/AgCloud/GUI/src/vast/status_card.py new file mode 100644 index 000000000..775eaaecb --- /dev/null +++ b/AgCloud/GUI/src/vast/status_card.py @@ -0,0 +1,55 @@ +from PyQt6.QtCore import pyqtSignal, Qt +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton +from PyQt6.QtGui import QFont + +class StatusCard(QWidget): + openRequested = pyqtSignal() + + def __init__(self, title: str, value: str, description: str = "", parent: QWidget | None = None): + super().__init__(parent) + self.setObjectName("StatusCard") + + self.title_label = QLabel(title) + title_font = QFont() + title_font.setPointSize(10) + title_font.setBold(True) + self.title_label.setFont(title_font) + + self.value_label = QLabel(value) + value_font = QFont() + value_font.setPointSize(22) + value_font.setBold(True) + self.value_label.setFont(value_font) + self.value_label.setAlignment(Qt.AlignmentFlag.AlignLeft) + + self.desc_label = QLabel(description) + desc_font = QFont() + desc_font.setPointSize(9) + self.desc_label.setFont(desc_font) + self.desc_label.setStyleSheet("color: gray;") + + self.open_btn = QPushButton("Open") + self.open_btn.clicked.connect(self.openRequested.emit) + + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(8) + layout.addWidget(self.title_label) + layout.addWidget(self.value_label) + layout.addWidget(self.desc_label) + layout.addWidget(self.open_btn, alignment=Qt.AlignmentFlag.AlignRight) + + self.setStyleSheet(""" + #StatusCard { + border: 1px solid #E0E0E0; + border-radius: 12px; + background: #FFFFFF; + } + """) + + def set_value(self, value: str) -> None: + self.value_label.setText(value) + + def set_description(self, text: str) -> None: + self.desc_label.setText(text) + diff --git a/AgCloud/GUI/src/vast/views/__init__.py b/AgCloud/GUI/src/vast/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/views/alerts_panel.py b/AgCloud/GUI/src/vast/views/alerts_panel.py new file mode 100644 index 000000000..e9cfd549d --- /dev/null +++ b/AgCloud/GUI/src/vast/views/alerts_panel.py @@ -0,0 +1,250 @@ +# views/alerts_panel.py +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QLabel, QScrollArea, QFrame, QHBoxLayout +) +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QFont +from datetime import datetime, timezone +import re + + +# ──────────────────────────────────────────────── +# Helper: parse timestamps from DB or realtime +# ──────────────────────────────────────────────── +def _parse_time(value: str): + """Safely parse a timestamp from DB or Alertmanager format.""" + if not value: + return None + + v = value.strip().replace("Z", "+00:00") + + # Try ISO format first + try: + return datetime.fromisoformat(v) + except Exception: + pass + + # Try common fallback formats (Postgres or plain) + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"): + try: + return datetime.strptime(v.split("+")[0], fmt) + except Exception: + continue + return None + + +# ──────────────────────────────────────────────── +# AlertItem Widget +# ──────────────────────────────────────────────── +class AlertItem(QFrame): + """Compact alert box with one-line layout that expands for longer text.""" + + def __init__(self, alert): + super().__init__() + self.alert = alert + self._build_ui() + + def _build_ui(self): + color = "#FFC107" # default amber tone + + layout = QHBoxLayout(self) + layout.setContentsMargins(10, 6, 10, 6) + layout.setSpacing(10) + + # Left colored bar + bar = QFrame() + bar.setFixedWidth(5) + bar.setStyleSheet(f"background-color: {color}; border-radius: 2px;") + layout.addWidget(bar) + + # Alert details + alert_type = self.alert.get("alert_type", "Unknown") + device = self.alert.get("device_id", "") + summary = self.alert.get("summary", "No summary") + + # Remove ISO timestamps from summary text + summary = re.sub( + r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:\+\d{2}:\d{2}|Z)?", + "", + summary + ).strip() + + # --- Parse and format time --- + start_raw = ( + self.alert.get("startsAt") + or self.alert.get("started_at") + or self.alert.get("startedAt") + ) + dt = _parse_time(start_raw) + time_str = dt.strftime("%Y-%m-%d %H:%M") if dt else "–" + + # --- Alert text --- + is_unack = not self.alert.get("ack", False) + font_weight = "font-weight:600;" if is_unack else "font-weight:normal;" + text = QLabel( + f"{alert_type} " + f"on {device} — {summary} " + f"🕒 {time_str}" + ) + text.setWordWrap(True) + text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + text.setFont(QFont("Segoe UI", 9)) + layout.addWidget(text, 1) + + # Right status label + self.status_label = QLabel("ACTIVE") + self.status_label.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold)) + self.status_label.setStyleSheet(f"color:{color};") + layout.addWidget(self.status_label, alignment=Qt.AlignmentFlag.AlignRight) + + # Allow box to expand vertically if needed + self.setMinimumHeight(65) + self.setMaximumHeight(130) + + # Style + self.setStyleSheet(""" + QFrame { + background-color: #ffffff; + border: 1px solid #ddd; + border-radius: 8px; + } + """) + + # ──────────────────────────────────────────────── + # Mark alert as resolved + # ──────────────────────────────────────────────── + def mark_resolved(self, ended_at): + """Change color and show duration when resolved.""" + try: + start_str = ( + self.alert.get("startsAt") + or self.alert.get("started_at") + or self.alert.get("startedAt") + ) + end_str = ended_at or self.alert.get("endedAt") or self.alert.get("ended_at") + start = _parse_time(start_str) + end = _parse_time(end_str) + + if start and end: + dur = end - start + mins = int(dur.total_seconds() // 60) + secs = int(dur.total_seconds() % 60) + duration = f"{mins}m {secs}s" + else: + duration = "" + except Exception: + duration = "" + + self.status_label.setText(f"✓ {duration}") + self.status_label.setStyleSheet("color:#2E7D32; font-weight:bold;") + self.setStyleSheet(""" + QFrame { + background-color: #f6fff6; + border: 1px solid #b8e5b8; + border-radius: 8px; + } + """) + + +# ──────────────────────────────────────────────── +# AlertsPanel Widget +# ──────────────────────────────────────────────── +class AlertsPanel(QWidget): + """Floating list of alert boxes (like a modern notification dropdown).""" + + def __init__(self, alert_service): + super().__init__() + self.alert_service = alert_service + self.items = {} + + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(8) + + # Scrollable area + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) + scroll.setStyleSheet(""" + QScrollArea { + border: none; + background: transparent; + } + QScrollBar:vertical { + width: 8px; + background: #f0f0f0; + margin: 2px; + border-radius: 4px; + } + QScrollBar::handle:vertical { + background: #bbb; + border-radius: 4px; + } + QScrollBar::handle:vertical:hover { + background: #999; + } + """) + layout.addWidget(scroll) + + # Inner container + container = QWidget() + self.vbox = QVBoxLayout(container) + self.vbox.setContentsMargins(6, 6, 6, 6) + self.vbox.setSpacing(8) + self.vbox.setAlignment(Qt.AlignmentFlag.AlignTop) + scroll.setWidget(container) + + # Connect signals + self.alert_service.alertsUpdated.connect(self._populate) + self.alert_service.alertAdded.connect(self._add_alert) + self.alert_service.alertRemoved.connect(self._mark_resolved) + + # Load initial alerts + QTimer.singleShot(500, self.alert_service.load_initial) + + # ──────────────────────────────────────────────── + # Populate panel + # ──────────────────────────────────────────────── + def _populate(self, alerts): + # Remove all existing widgets + for i in reversed(range(self.vbox.count())): + widget = self.vbox.itemAt(i).widget() + if widget: + widget.deleteLater() + self.items.clear() + + # Add in reverse chronological order + for a in reversed(alerts): + self._add_alert(a) + + # ──────────────────────────────────────────────── + # Add single alert + # ──────────────────────────────────────────────── + def _add_alert(self, alert): + alert_id = alert.get("alert_id") + if not alert_id or alert_id in self.items: + return + + item = AlertItem(alert) + self.vbox.insertWidget(0, item) + self.items[alert_id] = item + + # ✅ If alert is resolved already, mark as resolved + ended_at = alert.get("ended_at") or alert.get("endedAt") + if ended_at: + item.mark_resolved(ended_at) + + # ──────────────────────────────────────────────── + # Mark resolved by ID + # ──────────────────────────────────────────────── + def _mark_resolved(self, alert_id): + item = self.items.get(alert_id) + if item: + for a in self.alert_service.alerts: + if a.get("alert_id") == alert_id: + ended_at = a.get("endedAt") or a.get("ended_at") + break + else: + ended_at = datetime.now(timezone.utc).isoformat() + item.mark_resolved(ended_at) diff --git a/AgCloud/GUI/src/vast/views/assets/fields.png b/AgCloud/GUI/src/vast/views/assets/fields.png new file mode 100644 index 000000000..b26f5ecda Binary files /dev/null and b/AgCloud/GUI/src/vast/views/assets/fields.png differ diff --git a/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet-heat.js b/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet-heat.js new file mode 100644 index 000000000..aa8031ab5 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet-heat.js @@ -0,0 +1,11 @@ +/* + (c) 2014, Vladimir Agafonkin + simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas + https://github.com/mourner/simpleheat +*/ +!function(){"use strict";function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;e>s;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;e>s;s+=4)a=4*t[s],a&&(t[s-3]=i[a],t[s-2]=i[a+1],t[s-1]=i[a+2])}},window.simpleheat=t}(),/* + (c) 2014, Vladimir Agafonkin + Leaflet.heat, a tiny and fast heatmap plugin for Leaflet. + https://github.com/Leaflet/Leaflet.heat +*/ +L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!this._heat||this._frame||this._map._animating||(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient),this.options.max&&this._heat.max(this.options.max)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){var t,i,a,s,e,n,h,o,r,d=[],_=this._heat._r,l=this._map.getSize(),m=new L.Bounds(L.point([-_,-_]),l.add([_,_])),c=void 0===this.options.max?1:this.options.max,u=void 0===this.options.maxZoom?this._map.getMaxZoom():this.options.maxZoom,f=1/Math.pow(2,Math.max(0,Math.min(u-this._map.getZoom(),12))),g=_/2,p=[],v=this._map._getMapPanePos(),w=v.x%g,y=v.y%g;for(t=0,i=this._latlngs.length;i>t;t++)if(a=this._map.latLngToContainerPoint(this._latlngs[t]),m.contains(a)){e=Math.floor((a.x-w)/g)+2,n=Math.floor((a.y-y)/g)+2;var x=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;r=x*f,p[n]=p[n]||[],s=p[n][e],s?(s[0]=(s[0]*s[2]+a.x*r)/(s[2]+r),s[1]=(s[1]*s[2]+a.y*r)/(s[2]+r),s[2]+=r):p[n][e]=[a.x,a.y,r]}for(t=0,i=p.length;i>t;t++)if(p[t])for(h=0,o=p[t].length;o>h;h++)s=p[t][h],s&&d.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],c)]);this._heat.data(d).draw(this.options.minOpacity),this._frame=null},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)}; \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet.css b/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet.css new file mode 100644 index 000000000..9ade8dc49 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet.css @@ -0,0 +1,661 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + } diff --git a/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet.js b/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet.js new file mode 100644 index 000000000..a3bf693d0 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/assets/leaflet/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1= 0) + handlers.splice(idx, 1); + if (handlers.length === 0) { + delete this.__objectSignals__[signalIdx]; + this.__transport__.send(JSON.stringify({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: this.__id__, + signal: signalIdx + })); + } +}; + +QObject.prototype.__signalEmitted__ = function(signalName, args) { + var handlers = this.__objectSignals__[signalName]; + if (handlers) { + handlers.forEach(function(cb) { cb.apply(null, args); }); + } +}; + +function QWebChannel(transport, initCallback) { + this.transport = transport; + this.objects = {}; + + var channel = this; + this.transport.onmessage = function(message) { + var data = JSON.parse(message.data); + switch (data.type) { + case QWebChannelMessageTypes.init: + Object.keys(data.data).forEach(function(name) { + channel.objects[name] = new QObject(name, data.data[name], transport); + }); + if (initCallback) + initCallback(channel); + break; + case QWebChannelMessageTypes.signal: + var object = channel.objects[data.object]; + if (object) + object.__signalEmitted__(data.signal, data.args); + break; + case QWebChannelMessageTypes.propertyUpdate: + Object.keys(data.data).forEach(function(objName) { + var obj = channel.objects[objName]; + var props = data.data[objName]; + Object.keys(props).forEach(function(propName) { + obj[propName] = props[propName]; + }); + }); + break; + case QWebChannelMessageTypes.response: + break; + } + }; + + this.exec = function(data) { + this.transport.send(JSON.stringify(data)); + }; +} + +// Export +if (typeof module !== "undefined" && module.exports) { + module.exports = QWebChannel; +} else { + window.QWebChannel = QWebChannel; +} +})(); diff --git a/AgCloud/GUI/src/vast/views/assets/sensors_map.html b/AgCloud/GUI/src/vast/views/assets/sensors_map.html new file mode 100644 index 000000000..3a711258b --- /dev/null +++ b/AgCloud/GUI/src/vast/views/assets/sensors_map.html @@ -0,0 +1,238 @@ + + + + + AgCloud – Sensor Map + + + + + +
+
+
Sensor Dashboard
+
+
Normal
+
Soil Moisture
+
Temperature
+
Humidity
+
+
+ +
+
+
+
+ + + + + + + + + + + + + diff --git a/AgCloud/GUI/src/vast/views/assets/zones.geojson b/AgCloud/GUI/src/vast/views/assets/zones.geojson new file mode 100644 index 000000000..bddcf348b --- /dev/null +++ b/AgCloud/GUI/src/vast/views/assets/zones.geojson @@ -0,0 +1,24 @@ + + + +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"name": "Zone A"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[34.75, 32.00], [34.90, 32.00], [34.90, 32.10], [34.75, 32.10], [34.75, 32.00]]] + } + }, + { + "type": "Feature", + "properties": {"name": "Zone B"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[34.90, 31.95], [35.05, 31.95], [35.05, 32.05], [34.90, 32.05], [34.90, 31.95]]] + } + } + ] +} diff --git a/AgCloud/GUI/src/vast/views/auth_status_view.py b/AgCloud/GUI/src/vast/views/auth_status_view.py new file mode 100644 index 000000000..1a4ea8563 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/auth_status_view.py @@ -0,0 +1,316 @@ + +from __future__ import annotations +import os, time, jwt, requests, json +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, + QComboBox, QTableWidget, QTableWidgetItem, QTextEdit, QFrame, + QMessageBox, QProgressDialog +) +from PyQt6.QtCore import Qt, QTimer +from PyQt6 import sip + + +def _line(): + line = QFrame() + line.setFrameShape(QFrame.Shape.HLine) + line.setFrameShadow(QFrame.Shadow.Sunken) + return line + + +class AuthStatusView(QWidget): + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + self.access_token = None + self.refresh_token = None + self.expiry_ts = None + self.all_data = [] + + self.setStyleSheet(""" + QWidget { + background-color: #101010; + color: #e6e6e6; + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + } + QLineEdit, QComboBox { + background-color: #1a1a1a; + color: #e6e6e6; + border: 1px solid #333; + border-radius: 4px; + padding: 6px; + } + QPushButton { + background-color: #2d89ef; + color: white; + border: none; + padding: 8px 14px; + border-radius: 6px; + font-weight: 600; + } + QPushButton:hover { background-color: #1e5fb4; } + QTableWidget { + background-color: #1a1a1a; + gridline-color: #333; + color: #e6e6e6; + border: 1px solid #333; + border-radius: 6px; + } + QTextEdit { + background-color: #181818; + border: 1px solid #333; + color: #cccccc; + font-family: Consolas, monospace; + font-size: 12px; + } + QLabel#Title { + font-size: 22px; + font-weight: 700; + color: #00bcd4; + } + QFrame#Card { + background-color: #141414; + border: 1px solid #333; + border-radius: 10px; + padding: 14px; + } + """) + + layout = QVBoxLayout(self) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(15) + + title = QLabel("User Data Dashboard") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setObjectName("Title") + layout.addWidget(title) + layout.addWidget(_line()) + + login_card = QFrame() + login_card.setObjectName("Card") + login_layout = QHBoxLayout(login_card) + self.user_edit = QLineEdit() + self.user_edit.setPlaceholderText("Username") + self.pass_edit = QLineEdit() + self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password) + self.pass_edit.setPlaceholderText("Password") + self.btn_login = QPushButton("Login") + login_layout.addWidget(self.user_edit) + login_layout.addWidget(self.pass_edit) + login_layout.addWidget(self.btn_login) + layout.addWidget(login_card) + + token_card = QFrame() + token_card.setObjectName("Card") + token_layout = QVBoxLayout(token_card) + self.tokens_display = QTextEdit() + self.tokens_display.setReadOnly(True) + token_layout.addWidget(self.tokens_display) + layout.addWidget(token_card) + layout.addWidget(_line()) + + tables_env = os.getenv("TABLES_LIST", "devices") + self.tables = [t.strip() for t in tables_env.split(",") if t.strip()] + select_card = QFrame() + select_card.setObjectName("Card") + select_layout = QHBoxLayout(select_card) + self.table_combo = QComboBox() + self.table_combo.addItems(self.tables) + self.btn_load = QPushButton("Load Table Data") + select_layout.addWidget(QLabel("Select Table:")) + select_layout.addWidget(self.table_combo, 1) + select_layout.addWidget(self.btn_load) + layout.addWidget(select_card) + + search_card = QFrame() + search_card.setObjectName("Card") + search_layout = QHBoxLayout(search_card) + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Search in table...") + search_layout.addWidget(self.search_edit) + layout.addWidget(search_card) + + self.table_widget = QTableWidget() + layout.addWidget(self.table_widget, 1) + + self.progress = QProgressDialog("Loading data...", None, 0, 0, self) + self.progress.setWindowTitle("Please Wait") + self.progress.setCancelButton(None) + self.progress.setWindowModality(Qt.WindowModality.ApplicationModal) + self.progress.setStyleSheet(""" + QProgressDialog { + background-color: #222; + color: white; + border: 2px solid #00bcd4; + border-radius: 10px; + font-size: 16px; + padding: 15px; + } + """) + self.progress.close() + + self.btn_login.clicked.connect(self._login) + self.btn_load.clicked.connect(self._load_table) + self.search_edit.textChanged.connect(self._filter_table) + + self.timer = QTimer(self) + self.timer.timeout.connect(self._update_expiry_timer) + self.timer.start(1000) + + def _login(self): + user = self.user_edit.text().strip() + password = self.pass_edit.text().strip() + if not user or not password: + QMessageBox.warning(self, "Missing Data", "Please enter both username and password.") + return + try: + url = f"{self.api.base}/auth/login" + data = {"username": user, "password": password} + r = requests.post(url, data=data, timeout=10) + if r.status_code == 200: + js = r.json() + old_token = self.access_token + self.access_token = js.get("access_token") + self.refresh_token = js.get("refresh_token") + self.api.http.headers.update({"Authorization": f"Bearer {self.access_token}"}) + try: + payload = jwt.decode(self.access_token, options={"verify_signature": False}) + self.expiry_ts = payload.get("exp") + except Exception: + self.expiry_ts = None + msg_prefix = "✅ Access Token updated!\n\n" if old_token and self.access_token != old_token else "" + self.tokens_display.setPlainText( + f"{msg_prefix}" + f"Access Token:\n{self.access_token}\n\n" + f"Refresh Token:\n{self.refresh_token}" + ) + QMessageBox.information(self, "Login Successful", "User authenticated successfully.") + else: + QMessageBox.warning(self, "Login Failed", f"Error {r.status_code}: {r.text[:200]}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to login:\n{e}") + + def _update_expiry_timer(self): + if not self.expiry_ts or sip.isdeleted(self.tokens_display): + return + now = int(time.time()) + secs_left = self.expiry_ts - now + if secs_left < 0: + msg = "⚠ Token expired." + else: + mins, secs = divmod(secs_left, 60) + msg = f"Token expires in {mins:02d}:{secs:02d}" + self.tokens_display.setToolTip(msg) + + def _load_table(self): + if not self.access_token: + if not sip.isdeleted(self): + QMessageBox.warning(self, "Not Authenticated", "Please login first.") + return + + table_name = self.table_combo.currentText() + url = f"{self.api.base}/api/tables/{table_name}" + + try: + if sip.isdeleted(self): + return + self.progress.show() + self.repaint() + + r = self.api.http.get(url, timeout=20) + + if r.status_code == 200: + data = r.json() + if not sip.isdeleted(self) and self.isVisible(): + self._populate_table(data) + elif not sip.isdeleted(self): + QMessageBox.warning(self, "Request Failed", f"{r.status_code}: {r.text[:200]}") + + except Exception as e: + if not sip.isdeleted(self): + QMessageBox.critical(self, "Error", f"Request failed:\n{e}") + finally: + if hasattr(self, "progress") and not sip.isdeleted(self.progress): + self.progress.close() + + + def _populate_table(self, data): + if sip.isdeleted(self) or sip.isdeleted(self.table_widget) or not self.isVisible(): + return + + # --- normalize input --- + if isinstance(data, str): + try: + data = json.loads(data) + except Exception: + data = [{"value": data}] + if isinstance(data, dict) and "rows" in data: + data = data["rows"] + if not isinstance(data, list): + data = [data] if data else [] + + # --- handle empty --- + if not data: + self.table_widget.clear() + self.table_widget.setRowCount(0) + self.table_widget.setColumnCount(0) + if not sip.isdeleted(self): + QMessageBox.information(self, "Empty", "No data found for this table.") + return + + # --- normalize rows to dicts --- + normalized = [] + for row in data: + if not isinstance(row, dict): + try: + row = dict(row) + except Exception: + row = {"value": str(row)} + normalized.append(row) + data = normalized + + # --- build header keys --- + keys = sorted({k for row in data for k in row.keys()}) + if sip.isdeleted(self.table_widget): + return + + self.table_widget.setColumnCount(len(keys)) + self.table_widget.setRowCount(len(data)) + self.table_widget.setHorizontalHeaderLabels(keys) + + # --- fill cells safely --- + for i, row in enumerate(data): + if sip.isdeleted(self.table_widget): + return + for j, key in enumerate(keys): + val = row.get(key, "") + try: + if isinstance(val, (dict, list)): + val = json.dumps(val, ensure_ascii=False, indent=2) + elif val is None: + val = "" + else: + val = str(val) + + if len(val) > 1000: + val = val[:997] + "..." + item = QTableWidgetItem(val) + item.setToolTip(val[:3000]) + self.table_widget.setItem(i, j, item) + except Exception as e: + if not sip.isdeleted(self.table_widget): + self.table_widget.setItem(i, j, QTableWidgetItem(f"[error: {e}]")) + + # --- finish up --- + self.table_widget.resizeColumnsToContents() + self.all_data = data + + def _filter_table(self): + if sip.isdeleted(self.table_widget): + return + text = self.search_edit.text().lower().strip() + if not text: + self._populate_table(self.all_data) + return + filtered = [r for r in self.all_data if any(text in str(v).lower() for v in r.values())] + self._populate_table(filtered) diff --git a/AgCloud/GUI/src/vast/views/fruits_view.py b/AgCloud/GUI/src/vast/views/fruits_view.py new file mode 100644 index 000000000..c77ded97d --- /dev/null +++ b/AgCloud/GUI/src/vast/views/fruits_view.py @@ -0,0 +1,399 @@ +# views/fruits_view.py +from __future__ import annotations +from typing import Optional, Tuple, Dict, List +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, + QTableWidget, QTableWidgetItem, QAbstractItemView, QDoubleSpinBox, + QMessageBox, QHeaderView, QDialog, QLineEdit, QFrame +) + +from dashboard_api import DashboardApi + + +# ---------- thresholds ---------- +class ThresholdsEditorDialog(QDialog): + thresholdsSaved = pyqtSignal(dict) # {(task,label): threshold} + + TASK_OPTIONS = ["ripeness", "disease", "size", "color"] + + def __init__(self, api: DashboardApi, parent: QWidget | None = None): + super().__init__(parent) + self.api = api + self.setWindowTitle("Fruits — Task Thresholds") + self.setModal(True) + self.resize(820, 560) + + + self.setStyleSheet(""" + +QLineEdit#search { + padding: 10px 12px; border: 1px solid #e8dccc; border-radius: 10px; background: #ffffff; +} + + +/* ====== status====== */ +QLabel#status { color: #6b7280; } +QLabel.status-ok { color: #17803a; } +QLabel.status-warn { color: #b25a00; } +QLabel.status-err { color: #cc0022; } + + +QPushButton, QToolButton { + padding: 10px 16px; border-radius: 12px; color: white; border: none; font-weight: 700; +} +QPushButton:disabled { background: #c8c8c8; color: #f5f5f5; } + +/* Add (🍌) */ +QPushButton#btn_add { + background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #f8e27a, stop:1 #d8c94a); + color: #3a3a00; +} +QPushButton#btn_add:hover { background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #ffef87, stop:1 #e3d65a); } + +/* Delete (🍒) */ +QPushButton#btn_delete { + background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #ff6a7a, stop:1 #e03d4f); +} +QPushButton#btn_delete:hover { background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #ff7f8d, stop:1 #ea5666); } + +/* Save (🥝) */ +QPushButton#btn_save { + background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #4bd27c, stop:1 #2fb765); +} +QPushButton#btn_save:hover { background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #5fe08b, stop:1 #3fcb75); } + +/* Close (🫐) */ +QPushButton#btn_close { + background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #6a7bff, stop:1 #4757e6); +} +QPushButton#btn_close:hover { background: qlineargradient(x1:0,y1:0,x2:1,y2:0, stop:0 #7d8bff, stop:1 #5b6cf0); } +""") + + root = QVBoxLayout(self) + root.setSpacing(12) + + # Title + title = QLabel("Fruits — Task Thresholds (per task/label)") + title.setObjectName("title") + root.addWidget(title) + + # Toolbar: search + actions + toolbar = QFrame() + toolbar.setObjectName("toolbar") + tl = QHBoxLayout(toolbar) + tl.setContentsMargins(12, 12, 12, 12) + tl.setSpacing(8) + + self.txt_search = QLineEdit(placeholderText="Search by task or label…") + self.txt_search.setObjectName("search") + + self.btn_add = QPushButton("🍌 Add row") + self.btn_delete = QPushButton("🍒 Delete selected") + self.btn_save = QPushButton("🥝 Save all") + + self.btn_add.setObjectName("btn_add") + self.btn_delete.setObjectName("btn_delete") + self.btn_save.setObjectName("btn_save") + + tl.addWidget(self.txt_search, 1) + tl.addStretch(0) + tl.addWidget(self.btn_add) + tl.addWidget(self.btn_delete) + tl.addWidget(self.btn_save) + + root.addWidget(toolbar) + + # Table + self.tbl = QTableWidget(0, 4, self) + self.tbl.setAlternatingRowColors(True) + self.tbl.setHorizontalHeaderLabels([ + "Task", "Label (optional)", "Threshold (0..1)", "Updated By" + ]) + hdr = self.tbl.horizontalHeader() + hdr.setStretchLastSection(True) + hdr.setSectionResizeMode(QHeaderView.ResizeMode.Interactive) + self.tbl.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.tbl.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.tbl.setEditTriggers( + QAbstractItemView.EditTrigger.DoubleClicked + | QAbstractItemView.EditTrigger.SelectedClicked + | QAbstractItemView.EditTrigger.EditKeyPressed + ) + + self.tbl.verticalHeader().setDefaultSectionSize(36) + + root.addWidget(self.tbl, 1) + + # Status + Close + bottom = QHBoxLayout() + self.lbl_status = QLabel("Add rows and click Save.") + self.lbl_status.setObjectName("status") + bottom.addWidget(self.lbl_status) + bottom.addStretch(1) + self.btn_close = QPushButton("🫐 Close") + self.btn_close.setObjectName("btn_close") + bottom.addWidget(self.btn_close) + root.addLayout(bottom) + + # Signals + self.btn_add.clicked.connect(self.add_row) + self.btn_delete.clicked.connect(self.delete_selected) + self.btn_save.clicked.connect(self.save_all) + self.btn_close.clicked.connect(self.accept) + self.txt_search.textChanged.connect(self._apply_filter) + + # Start with one empty row + self.add_row() + + def load_rows(self, rows: List[Tuple[str, str, float, str]]): + + self.tbl.setRowCount(0) + for t, l, thr, upd in rows: + self.add_row(t, l, thr, upd) + self.lbl_status.setText(f"Loaded {len(rows)} rows.") + + # -------- Row ops -------- + def add_row( + self, + task: str = "", + label: str = "", + threshold: float = 0.5, + updated_by: str = "gui" + ): + r = self.tbl.rowCount() + self.tbl.insertRow(r) + + # Task (combobox) + cmb = QComboBox(self.tbl) + cmb.addItems(self.TASK_OPTIONS) + if task in self.TASK_OPTIONS: + cmb.setCurrentText(task) + self.tbl.setCellWidget(r, 0, cmb) + + # Label (editable) + self._set_text_cell(r, 1, label) + + # Threshold (spinbox) + spn = QDoubleSpinBox(self.tbl) + spn.setRange(0.0, 1.0) + spn.setSingleStep(0.01) + spn.setDecimals(2) + spn.setValue(float(threshold)) + spn.setAlignment(Qt.AlignmentFlag.AlignRight) + self.tbl.setCellWidget(r, 2, spn) + + # Updated By + self._set_text_cell(r, 3, updated_by or "gui") + + self.lbl_status.setText("Row added.") + + def delete_selected(self): + sel = self.tbl.selectionModel().selectedRows() + if not sel: + self._set_status("No row selected.", "warn") + return + for m in sel: + self.tbl.removeRow(m.row()) + self._set_status("Row deleted.", "ok") + + # -------- Helpers -------- + def _set_text_cell(self, row: int, col: int, text: str): + item = QTableWidgetItem(text or "") + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) + self.tbl.setItem(row, col, item) + + def _read_row(self, r: int) -> Tuple[str, str, float, str]: + # Task + cmb = self.tbl.cellWidget(r, 0) + task = cmb.currentText() if isinstance(cmb, QComboBox) else "" + + # Label (optional) + label_item = self.tbl.item(r, 1) + label = (label_item.text().strip() if label_item else "") + + # Threshold + spn = self.tbl.cellWidget(r, 2) + threshold = float(spn.value()) if isinstance(spn, QDoubleSpinBox) else 0.0 + + # Updated By + updated_item = self.tbl.item(r, 3) + updated_by = (updated_item.text().strip() if updated_item else "") + + return task, label, threshold, updated_by + + def _apply_filter(self): + q = self.txt_search.text().strip().lower() + for r in range(self.tbl.rowCount()): + t, l, _, _ = self._read_row(r) + show = (q in t.lower()) or (q in (l or "").lower()) or (q == "") + self.tbl.setRowHidden(r, not show) + + def _set_status(self, text: str, level: str = "info"): + # level: ok|warn|err|info + self.lbl_status.setText(text) + for cls in ["status-ok", "status-warn", "status-err"]: + self.lbl_status.setProperty("class", "") + if level == "ok": + self.lbl_status.setProperty("class", "status-ok") + elif level == "warn": + self.lbl_status.setProperty("class", "status-warn") + elif level == "err": + self.lbl_status.setProperty("class", "status-err") + self.lbl_status.style().unpolish(self.lbl_status) + self.lbl_status.style().polish(self.lbl_status) + + def _validate(self) -> Tuple[bool, str, List[int]]: + """ + Rules: + - Task: required + - Label: optional (dedup by (task,label)) + - Threshold: 0..1 + - No duplicate (task, label) + - At least one row + Returns: (ok, msg, bad_rows_indices) + """ + seen = set() + bad_rows = [] + + for r in range(self.tbl.rowCount()): + t, l, thr, _ = self._read_row(r) + if not t: + bad_rows.append(r) + return False, f"Row {r+1}: Task is empty.", bad_rows + if not (0.0 <= thr <= 1.0): + bad_rows.append(r) + return False, f"Row {r+1}: Threshold must be between 0 and 1.", bad_rows + + key = (t, l or "") + if key in seen: + bad_rows.append(r) + return False, f"Row {r+1}: Duplicate (task,label)=({t},{l or '∅'}).", bad_rows + seen.add(key) + + if self.tbl.rowCount() == 0: + return False, "No rows to save.", [] + + return True, "", [] + + # -------- Save -------- + def save_all(self): + ok, msg, bad_rows = self._validate() + + if not ok: + if bad_rows: + self.tbl.selectRow(bad_rows[0]) + QMessageBox.warning(self, "Validation error", msg) + self._set_status(msg, "err") + return + + # Build mapping: {(task,label): threshold} + mapping: Dict[Tuple[str, str], float] = {} + for r in range(self.tbl.rowCount()): + t, l, thr, _ = self._read_row(r) + key = (t, l or "") + mapping[key] = thr + + # Disable buttons during save + self.btn_save.setEnabled(False) + self.btn_add.setEnabled(False) + self.btn_delete.setEnabled(False) + + def _normalize_ok_set(ok_raw) -> set[tuple[str, str]]: + ok: set[tuple[str, str]] = set() + for item in ok_raw or []: + if isinstance(item, (list, tuple)): + ok.add((str(item[0]) if len(item)>0 else "", str(item[1]) if len(item)>1 else "")) + return ok + + def _normalize_fail_list(fail_raw): + pairs = [] + for item in fail_raw or []: + if isinstance(item, (list, tuple)) and len(item) >= 1: + key = item[0] + reason = item[1] if len(item) > 1 else "unknown" + if isinstance(key, (list, tuple)) and len(key) >= 1: + key_str = f"{key[0]},{key[1] if len(key)>1 else ''}" + else: + key_str = str(key) + pairs.append((key_str, str(reason))) + else: + pairs.append((str(item), "unknown")) + return pairs + + try: + report = self.api.bulk_set_task_thresholds_labeled(mapping, updated_by="gui") + ok_keys = _normalize_ok_set(report.get("ok", [])) + fail_pairs = _normalize_fail_list(report.get("fail", [])) + + total = len(mapping) + failed = len(fail_pairs) + succeeded = len(ok_keys) if ok_keys else (total - failed) + + if failed == 0: + self._set_status(f"Saved {succeeded}/{total} thresholds ✓", "ok") + QMessageBox.information(self, "Saved", f"All {total} thresholds saved successfully.") + self.thresholdsSaved.emit({k: v for k, v in mapping.items()}) + else: + lines = "\n".join(f"- {k}: {reason}" for k, reason in fail_pairs[:10]) + more = "" if failed <= 10 else f"\n(+{failed-10} more...)" + self._set_status(f"Partial save: {succeeded}/{total} saved, {failed} failed.", "warn") + QMessageBox.warning(self, "Partial save", f"Saved {succeeded}/{total}.\nFailed:\n{lines}{more}") + + except Exception as e: + import traceback + traceback.print_exc() + QMessageBox.critical(self, "Error", f"Failed to save thresholds:\n{type(e).__name__}: {e}") + self._set_status("Failed to save thresholds.", "err") + finally: + self.btn_save.setEnabled(True) + self.btn_add.setEnabled(True) + self.btn_delete.setEnabled(True) + + + +class FruitsView(QWidget): + thresholdsSaved = pyqtSignal(dict) # {(task,label): threshold} + + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + + root = QVBoxLayout(self) + root.setSpacing(10) + + title = QLabel("Fruits") + title.setStyleSheet("font-size: 22px; font-weight: 700;") + root.addWidget(title) + + + row = QHBoxLayout() + lbl = QLabel("Manage task thresholds per task/label.") + self.btn_open_editor = QPushButton("Change thresholds…") + row.addWidget(lbl, 1) + row.addStretch(0) + row.addWidget(self.btn_open_editor) + root.addLayout(row) + + line = QFrame() + line.setFrameShape(QFrame.Shape.HLine) + line.setStyleSheet("color:#e5e7eb;") + root.addWidget(line) + + + self.lbl_status = QLabel("Click “Change thresholds…” to edit.") + self.lbl_status.setStyleSheet("color:#555;") + root.addWidget(self.lbl_status) + + + self.btn_open_editor.clicked.connect(self.open_thresholds_dialog) + + def open_thresholds_dialog(self): + dlg = ThresholdsEditorDialog(self.api, self) + + # rows = self.api.get_current_thresholds() -> List[Tuple[str,str,float,str]] + # dlg.load_rows(rows) + + dlg.thresholdsSaved.connect(self.thresholdsSaved.emit) + dlg.exec() # modal + self.lbl_status.setText("Threshold editor closed.") diff --git a/AgCloud/GUI/src/vast/views/ground_view.py b/AgCloud/GUI/src/vast/views/ground_view.py new file mode 100644 index 000000000..09db68187 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/ground_view.py @@ -0,0 +1,589 @@ +from __future__ import annotations +import os +from dataclasses import dataclass +from typing import Optional, Any, Dict, List + +from PyQt6.QtCore import Qt, QTimer, QSize, QRectF +from PyQt6.QtGui import QPixmap, QKeyEvent, QPainter, QColor, QPen, QFont +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QProgressBar, QMessageBox, QSizePolicy, QFrame +) + +# GUI never touches MinIO directly – it uses DashboardApi only. +from vast.dashboard_api import DashboardApi + +GROUND_BUCKET = os.getenv("GROUND_BUCKET", "ground") +GROUND_PREFIX = os.getenv("GROUND_PREFIX", "") + +# ---------------------------- +# PHI data model +# ---------------------------- +@dataclass +class PhiSnapshot: + phi: Optional[float] # 0..100 or None + density: Optional[float] + coverage: Optional[float] + severity_avg: Optional[float] # usually 0..1 (clamped) + trend: Optional[float] + week_start: Optional[str] + source: str = "" # textual hint of data source (NOT shown in UI) + + +def _phi_band_color(v: float) -> str: + # 80..100 = green, 50..79 = amber, else red + if v >= 80: + return "#16a34a" # green-600 + if v >= 50: + return "#f59e0b" # amber-500 + return "#dc2626" # red-600 + + +def _safe_float(x) -> Optional[float]: + try: + if x is None: + return None + return float(x) + except Exception: + return None + + +# ---------------------------- +# Visual PHI circle (pie) +# ---------------------------- +class PhiCircleWidget(QWidget): + """ + Draws a pie: + - red slice = severity in [0..1] + - green slice = 1 - severity (healthy remainder) + Always draws red on top so it's never hidden. + Also draws the severity percentage text centered on the pie. + """ + def __init__(self, parent=None): + super().__init__(parent) + self._severity = 0.0 # 0..1 + self.setAttribute(Qt.WidgetAttribute.WA_OpaquePaintEvent, True) + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) + self.setToolTip("Red slice = severity (0..1). Green = healthy remainder.") + + def sizeHint(self): + return QSize(120, 120) + + def minimumSizeHint(self): + return QSize(100, 100) + + def setSeverity(self, value: float) -> None: + try: + v = float(value) + except Exception: + v = 0.0 + self._severity = max(0.0, min(1.0, v)) + self.update() # ensure repaint + + def paintEvent(self, event) -> None: + try: + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) + painter.setPen(Qt.PenStyle.NoPen) + + pad = 10 + size = min(self.width(), self.height()) - 2 * pad + if size <= 0: + return + cx = (self.width() - size) / 2 + cy = (self.height() - size) / 2 + rect = QRectF(cx, cy, size, size) + + start = 90 * 16 # 12 o'clock (Qt: 0° is 3 o'clock) + full = -360 * 16 + + s = self._severity + # Degenerate cases + if s <= 1e-6: + painter.setBrush(QColor("#16a34a")) + painter.drawEllipse(rect) + elif s >= 1 - 1e-6: + painter.setBrush(QColor("#dc2626")) + painter.drawEllipse(rect) + else: + span_red = int(round(full * s)) # negative (clockwise) + span_green = full - span_red # the remainder + + # Draw green remainder first + painter.setBrush(QColor("#16a34a")) + painter.drawPie(rect, start + span_red, span_green) + + # Draw red slice on top (so it's always visible) + painter.setBrush(QColor("#dc2626")) + painter.drawPie(rect, start, span_red) + + # Outline + pen = QPen(QColor("#334155")) + pen.setWidth(2) + painter.setPen(pen) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawEllipse(rect) + + # Percentage text (centered) + percent_text = f"{int(round(s * 100))}%" + font = QFont() + font.setBold(True) + font.setPointSize(int(size * 0.22)) # responsive sizing + painter.setFont(font) + + # Soft shadow for readability + painter.setPen(QColor(0, 0, 0, 160)) + painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, percent_text) + # Foreground text + painter.setPen(QColor("#ffffff")) + painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, percent_text) + + except Exception as e: + print(f"[PhiCircleWidget] paintEvent error: {e}") + + +# ---------------------------- +# GroundView +# ---------------------------- +class GroundView(QWidget): + """ + Gallery mode: + - Loads all object keys from MinIO via DashboardApi. + - Keeps current index; supports Prev/Next buttons and keyboard arrows. + - On image change, fetches bytes and refreshes PHI for that key. + """ + + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + + # State for gallery + self._keys: List[str] = [] + self._idx: int = -1 + + # ---------- UI ---------- + root = QVBoxLayout(self) + root.setContentsMargins(12, 12, 12, 12) + root.setSpacing(10) + + title = QLabel("🌿 Ground — Gallery & PHI") + title.setStyleSheet("font-size:20px;font-weight:800;color:#0f172a;") + root.addWidget(title) + + # Toolbar + toolbar = QHBoxLayout() + self.btn_refresh_list = QPushButton("Reload list") + self.btn_refresh_list.clicked.connect(self.reload_keys) + + self.btn_prev = QPushButton("◀ Prev") + self.btn_prev.clicked.connect(self.prev_image) + self.btn_next = QPushButton("Next ▶") + self.btn_next.clicked.connect(self.next_image) + + self.btn_show_phi = QPushButton("Show PHI") + self.btn_show_phi.clicked.connect(self.refresh_phi_current) + + self.counter_label = QLabel("(0 / 0)") + self.counter_label.setStyleSheet("color:#475569;font-size:12px;") + + toolbar.addWidget(self.btn_refresh_list) + toolbar.addSpacing(8) + toolbar.addWidget(self.btn_prev) + toolbar.addWidget(self.btn_next) + toolbar.addSpacing(16) + toolbar.addWidget(self.btn_show_phi) + toolbar.addStretch(1) + toolbar.addWidget(self.counter_label) + root.addLayout(toolbar) + + # Image frame + img_frame = QFrame() + img_frame.setStyleSheet("background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;") + img_layout = QVBoxLayout(img_frame) + img_layout.setContentsMargins(8, 8, 8, 8) + img_layout.setSpacing(6) + + self.image_label = QLabel("(No image loaded yet)") + self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.image_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.image_label.setMinimumHeight(260) + img_layout.addWidget(self.image_label) + + self.img_meta = QLabel("") + self.img_meta.setStyleSheet("color:#475569;font-size:12px;") + img_layout.addWidget(self.img_meta) + + root.addWidget(img_frame, stretch=2) + + # PHI area + phi_frame = QFrame() + phi_frame.setStyleSheet("background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;") + phi_layout = QVBoxLayout(phi_frame) + phi_layout.setContentsMargins(12, 12, 12, 12) + phi_layout.setSpacing(8) + + # Row with headline + (trimmed) details + row = QHBoxLayout() + self.phi_label = QLabel("PHI: –") + self.phi_label.setStyleSheet("font-size:16px;font-weight:700;color:#0f172a;") + row.addWidget(self.phi_label) + row.addStretch(1) + self.phi_details = QLabel("") # will show severity/coverage/trend (without src) + self.phi_details.setStyleSheet("color:#475569;font-size:12px;") + row.addWidget(self.phi_details) + phi_layout.addLayout(row) + + # PHI progress (axis-like) + pie at its side + self.phi_bar = QProgressBar() + self.phi_bar.setRange(0, 100) + self.phi_bar.setValue(0) + self.phi_bar.setFormat("%v") + self._style_phi_bar(None) + + phi_row2 = QHBoxLayout() + phi_row2.setContentsMargins(0, 0, 0, 0) + phi_row2.setSpacing(10) + phi_row2.addWidget(self.phi_bar, stretch=1) + + self.phi_circle = PhiCircleWidget() + self.phi_circle.setFixedSize(120, 120) + phi_row2.addWidget(self.phi_circle) + + phi_layout.addLayout(phi_row2) + + legend = QLabel("אדום = Severity | ירוק = Healthy") + legend.setStyleSheet("color:#64748b;font-size:11px;") + phi_layout.addWidget(legend) + + root.addWidget(phi_frame, stretch=1) + + # Auto-refresh PHI every 2 min + self.timer = QTimer(self) + self.timer.setInterval(120_000) + self.timer.timeout.connect(self.refresh_phi_current) + self.timer.start() + + # Initial load + QTimer.singleShot(200, self.reload_keys) + + # So arrow keys work even without inner focus + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + + # ---------------------------- + # Styling helpers + # ---------------------------- + def _style_phi_bar(self, value: Optional[float]) -> None: + color = "#64748b" if value is None else _phi_band_color(float(value)) + self.phi_bar.setStyleSheet( + f"QProgressBar {{ border:1px solid #cbd5e1;border-radius:6px;height:18px; }} " + f"QProgressBar::chunk {{ background:{color}; border-radius:6px; }}" + ) + + def _warn(self, msg: str) -> None: + # Non-blocking warning box + try: + def _show(): + try: + box = QMessageBox(self) + box.setIcon(QMessageBox.Icon.Warning) + box.setWindowTitle("Ground") + box.setText(str(msg)) + box.setStandardButtons(QMessageBox.StandardButton.Ok) + box.setWindowModality(Qt.WindowModality.NonModal) + box.show() + except BaseException: + print(f"[GroundView] WARN(fallback): {msg}") + QTimer.singleShot(0, _show) + except BaseException: + print(f"[GroundView] WARN: {msg}") + + def _try_api(self, names: List[str], *args, **kwargs) -> Any: + # Try a list of API method names until one succeeds. + for name in names: + fn = getattr(self.api, name, None) + if callable(fn): + try: + return fn(*args, **kwargs) + except Exception as e: + print(f"[GroundView] API call {name} failed: {e}") + return None + + # ---------------------------- + # Gallery: load keys & navigation + # ---------------------------- + def reload_keys(self) -> None: + """Load all object keys from MinIO (sorted newest→oldest).""" + try: + objs = self._try_api( + ["list_minio_objects", "list_objects"], + bucket=GROUND_BUCKET, prefix=GROUND_PREFIX, limit=1000 + ) + keys: List[str] = [] + if isinstance(objs, list): + # Sort by last_modified/LastModified desc when available + def _lm(o): + if not isinstance(o, dict): + return "" + return o.get("last_modified") or o.get("LastModified") or "" + try: + objs = sorted(objs, key=_lm, reverse=True) + except Exception: + pass + for o in objs: + if isinstance(o, dict): + for f in ("key", "name", "object_name", "path"): + v = o.get(f) + if isinstance(v, str) and v.strip(): + keys.append(v.strip()) + break + + self._keys = keys + self._idx = 0 if self._keys else -1 + self._update_counter() + self._update_nav_buttons() + if self._idx >= 0: + self.load_current_image() + else: + self._set_image(None) + self.img_meta.setText("No objects found in MinIO.") + self._render_phi_none() + + except Exception as e: + self._warn(f"reload_keys error: {e}") + + def _update_counter(self) -> None: + total = len(self._keys) + pos = (self._idx + 1) if self._idx >= 0 else 0 + self.counter_label.setText(f"({pos} / {total})") + + def _update_nav_buttons(self) -> None: + has = bool(self._keys) + for b in (self.btn_prev, self.btn_next, self.btn_show_phi): + b.setEnabled(has) + + def prev_image(self) -> None: + if not self._keys: + return + self._idx = (self._idx - 1) % len(self._keys) + self._update_counter() + self.load_current_image() + + def next_image(self) -> None: + if not self._keys: + return + self._idx = (self._idx + 1) % len(self._keys) + self._update_counter() + self.load_current_image() + + def keyPressEvent(self, event: QKeyEvent) -> None: + if event.key() in (Qt.Key.Key_Left, Qt.Key.Key_A): + self.prev_image() + event.accept() + return + if event.key() in (Qt.Key.Key_Right, Qt.Key.Key_D): + self.next_image() + event.accept() + return + super().keyPressEvent(event) + + # ---------------------------- + # Image load + PHI for current key + # ---------------------------- + def _set_image(self, pix: Optional[QPixmap]) -> None: + if pix is None or pix.isNull(): + self.image_label.setText("(No image)") + self.image_label.setPixmap(QPixmap()) + return + target_size: QSize = self.image_label.size() + if target_size.width() <= 4 or target_size.height() <= 4: + self.image_label.setPixmap(pix) + self.image_label.setText("") + return + scaled = pix.scaled( + target_size.width(), + target_size.height(), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + self.image_label.setPixmap(scaled) + self.image_label.setText("") + + def resizeEvent(self, e): + super().resizeEvent(e) + pix = self.image_label.pixmap() + if pix is not None and not pix.isNull(): + self._set_image(pix) + + def load_current_image(self) -> None: + """Load image bytes for current key and refresh PHI.""" + try: + if self._idx < 0 or self._idx >= len(self._keys): + self._set_image(None) + self.img_meta.setText("No selection.") + self._render_phi_none() + return + + key = self._keys[self._idx] + getter = getattr(self.api, "get_image_bytes_from_minio", None) + if not callable(getter): + self._warn("DashboardApi.get_image_bytes_from_minio is missing.") + self._set_image(None) + self._render_phi_none() + return + + data = None + try: + data = getter(key, bucket=GROUND_BUCKET) + except TypeError: + data = getter(key) + except Exception as e: + self._warn(f"Failed fetching image bytes: {e}") + data = None + + if not data: + self._set_image(None) + self.img_meta.setText(f"Failed to read: {GROUND_BUCKET}/{key}") + self._render_phi_none() + return + + pix = QPixmap() + if not pix.loadFromData(data): + self._set_image(None) + self.img_meta.setText(f"Unsupported bytes: {GROUND_BUCKET}/{key}") + self._render_phi_none() + return + + self._set_image(pix) + self.img_meta.setText(f"{GROUND_BUCKET}/{key}") + + # After image displayed, refresh PHI + self._refresh_phi_for_key(key) + + except Exception as e: + self._warn(f"load_current_image error: {e}") + self._render_phi_none() + + # ---------------------------- + # PHI flow + # ---------------------------- + def _map_phi_dict(self, d: Dict[str, Any], source: str) -> PhiSnapshot: + return PhiSnapshot( + phi=_safe_float(d.get("phi")), + density=_safe_float(d.get("density")), + coverage=_safe_float(d.get("coverage")), + severity_avg=_safe_float(d.get("severity_avg")), + trend=_safe_float(d.get("trend")), + week_start=str(d.get("week_start")) if d.get("week_start") is not None else None, + source=source, + ) + + def _render_phi_none(self) -> None: + self.phi_label.setText("PHI: –") + self.phi_details.setText("No PHI available.") + self.phi_bar.setValue(0) + self._style_phi_bar(None) + self.phi_circle.setSeverity(0.0) + + def _refresh_phi_for_key(self, key: str) -> None: + """Best-effort PHI for the specific image key. Fully guarded.""" + try: + # 1) Exact by key + d = self._try_api(["get_phi_for_image"], key) + if isinstance(d, dict) and (d.get("phi") is not None or d.get("severity_avg") is not None): + return self._render_phi(self._map_phi_dict(d, "phi_by_key")) + + # 2) Current image + d = self._try_api(["get_phi_for_current_image"]) + if isinstance(d, dict) and (d.get("phi") is not None or d.get("severity_avg") is not None): + return self._render_phi(self._map_phi_dict(d, "phi_current")) + + # 3) Weekly/global + d = self._try_api(["get_weekly_phi"]) + if isinstance(d, dict) and (d.get("phi") is not None or d.get("severity_avg") is not None): + return self._render_phi(self._map_phi_dict(d, "weekly")) + + # 4) Derive roughly from latest rows as last resort + rows = self._try_api(["get_latest_rows", "get_latest_detections", "get_latest_ground_rows"], limit=1) or [] + if rows and isinstance(rows, list) and isinstance(rows[0], dict): + sev = None + cov = None + for k in ("severity_avg", "severity", "mean_severity"): + sev = _safe_float(rows[0].get(k)) + if sev is not None: + break + for k in ("coverage", "plant_coverage"): + cov = _safe_float(rows[0].get(k)) + if cov is not None: + break + + phi_val = None + if sev is not None: + s = sev if sev <= 1.0 else min(sev, 10.0) / 10.0 + phi_val = max(0.0, min(100.0, 100.0 * (1.0 - s))) + elif cov is not None: + c = max(0.0, min(1.0, cov)) + phi_val = 100.0 * c + + if phi_val is not None: + snap = PhiSnapshot( + phi=phi_val, density=None, coverage=cov, severity_avg=sev, + trend=None, week_start=None, source="derived_from_rows" + ) + return self._render_phi(snap) + + # Nothing available + self._render_phi_none() + self._warn("No PHI available for this image.") + except Exception as e: + self._warn(f"_refresh_phi_for_key error: {e}") + self._render_phi_none() + + def _render_phi(self, snap: PhiSnapshot) -> None: + if snap is None or snap.phi is None: + self._render_phi_none() + return + + # Progress bar + label + val = max(0, min(100, int(round(snap.phi)))) + self.phi_label.setText(f"PHI: {val}") + parts = [] + # keep useful metrics, but DO NOT show 'src=' anymore + if snap.density is not None: + parts.append(f"density={snap.density:.2f}") + if snap.coverage is not None: + parts.append(f"coverage={snap.coverage:.2f}") + if snap.severity_avg is not None: + parts.append(f"severity={snap.severity_avg:.2f}") + if snap.trend is not None: + parts.append(f"trend={snap.trend:+.2f}") + if snap.week_start: + parts.append(f"week={snap.week_start}") + self.phi_details.setText(" | ".join(parts)) # no src here + self.phi_bar.setValue(val) + self._style_phi_bar(val) + + # Circle severity (normalized to 0..1) + sev = snap.severity_avg + try: + sev = float(sev) if sev is not None else 0.0 + except Exception: + sev = 0.0 + sev_norm = sev if sev <= 1.0 else min(sev, 10.0) / 10.0 + self.phi_circle.setSeverity(max(0.0, min(1.0, sev_norm))) + + def refresh_phi_current(self) -> None: + """Called by the 'Show PHI' button; uses current image key safely.""" + try: + if 0 <= self._idx < len(self._keys): + key = self._keys[self._idx] + if not isinstance(key, str) or not key.strip(): + self._render_phi_none() + self._warn("No valid image key selected.") + return + self._refresh_phi_for_key(key) + else: + self._render_phi_none() + self._warn("No image selected yet. Click 'Reload list' or 'Next'.") + except Exception as e: + self._render_phi_none() + self._warn(f"Show PHI failed: {e}") diff --git a/AgCloud/GUI/src/vast/views/leaf_diseases.py b/AgCloud/GUI/src/vast/views/leaf_diseases.py new file mode 100644 index 000000000..3f387dab8 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/leaf_diseases.py @@ -0,0 +1,675 @@ +# leaf_diseases.py (compact, fixed + Grafana integration) +# UI: English, Comments: English, 4 spaces indent. + +from __future__ import annotations +from collections import Counter, defaultdict +from datetime import datetime +from typing import Iterable, Optional, Tuple + +from PyQt6.QtCore import QDate, QTimer, Qt, QPointF, pyqtSignal, QUrl +from PyQt6.QtGui import QPainter, QPen, QColor +from PyQt6.QtWidgets import ( + QCalendarWidget, QDialog, QDialogButtonBox, QFrame, QHBoxLayout, QLabel, + QPushButton, QScrollArea, QSizePolicy, QSpacerItem, QVBoxLayout, QWidget, QDateEdit, QProgressBar +) +from PyQt6.QtWebEngineWidgets import QWebEngineView + +from dashboard_api import DashboardApi + +# ────────────────────────────── +# Small utils +# ────────────────────────────── + +def _color_for_pct(pct: float) -> str: + """Traffic-light color by percentage.""" + return "#22c55e" if pct < 20 else ("#f59e0b" if pct <= 50 else "#ef4444") + +def _is_sick(v) -> bool: + """Normalize truthy sick flags from DB.""" + return v in [True, "true", "t", "1", 1, "yes", "y"] + +def _ts_to_date(ts: str) -> Optional[datetime.date]: + """Parse ISO ts (supports 'Z').""" + try: + return datetime.fromisoformat(ts.replace("Z", "+00:00")).date() + except Exception: + return None + +def _json_rows(payload) -> list: + """Accept list or any common envelope {data/items/rows/...}.""" + if isinstance(payload, list): + return payload + if isinstance(payload, dict): + for k in ("data", "results", "items", "records", "rows"): + v = payload.get(k) + if isinstance(v, list): + return v + return [] + +def _clear_layout(layout) -> None: + """Remove all widgets from a layout.""" + while layout.count(): + itm = layout.takeAt(0) + w = itm.widget() + if w: + w.deleteLater() + +# Styles (one place) +APP_STYLES = """ +QWidget { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #0a0f1f, stop:1 #060913); + color: #e5e7eb; font-family: ui-sans-serif, system-ui, 'Segoe UI', Roboto, sans-serif; +} +QScrollArea { border: none; background: transparent; } +QLabel { color: #e5e7eb; background: transparent; } + +QPushButton { + background: #0f1a33; border: 1px solid #1f2b49; border-radius: 12px; + color: #e5e7eb; padding: 10px 20px; font-weight: 600; font-size: 13px; +} +QPushButton:hover { background: #1a2a4a; border: 1px solid #3d5a8f; } +QPushButton:pressed { background: #0a1220; } + +QDateEdit { background: transparent; border: none; color: #e5e7eb; padding: 6px 10px; font-size: 13px; } +QDateEdit::drop-down { border: none; background: transparent; width: 20px; } + +QFrame.card { + background: qradialgradient(cx:0.9, cy:-0.1, radius:1.2, fx:0.9, fy:-0.1, + stop:0 #243b55, stop:0.35 #111827, stop:1 #0b1220); + border: 1px solid #1f2937; border-radius: 16px; padding: 16px; +} + +/* nicer disease chips */ +.Tag { + background: #0b1220; border: 1px solid #1f2937; + border-radius: 999px; padding: 6px 12px; font-size: 12px; +} +.Tag:hover { border-color: #334155; box-shadow: 0 0 0 2px rgba(99,102,241,.15); } + +/* progress bars */ +QProgressBar { + background: #0b1220; border: 1px solid #1f2937; border-radius: 999px; + min-height: 14px; max-height: 14px; text-align: center; +} +QProgressBar::chunk { + border-radius: 999px; +} +""" + + +# ────────────────────────────── +# Clickables +# ────────────────────────────── + +class ClickableRow(QFrame): + clicked = pyqtSignal(str) + def __init__(self, key: str, parent: Optional[QWidget] = None) -> None: + super().__init__(parent) + self.key = key + self.setStyleSheet("QFrame { background: transparent; }") + self.setCursor(Qt.CursorShape.PointingHandCursor) + def mousePressEvent(self, e): # noqa: N802 + self.clicked.emit(self.key) + super().mousePressEvent(e) + +ClickableTag = ClickableRow # identical behavior + +# ────────────────────────────── +# Tiny sparkline +# ────────────────────────────── + +class TrendSparkline(QFrame): + """Minimal line chart for percentages (0..100).""" + def __init__(self, parent: Optional[QWidget] = None) -> None: + super().__init__(parent) + self._series: list[float] = [] + self.setMinimumHeight(160) + self.setStyleSheet("QFrame { background: #0b1220; border: 1px solid #1f2937; border-radius: 16px; }") + def set_series(self, values: Iterable[float]) -> None: + self._series = list(values) if values else [] + self.update() + def paintEvent(self, _): # noqa: N802 + super().paintEvent(_) + if not self._series: + return + p = QPainter(self); p.setRenderHints(QPainter.RenderHint.Antialiasing, True) + w, h = self.width(), self.height() + L, T, R, B = 16, 12, 16, 20 + cw, ch = max(1, w - L - R), max(1, h - T - B) + pen_axis = QPen(QColor("#1f2937")); pen_axis.setWidth(1); p.setPen(pen_axis) + p.drawLine(L, h - B, w - R, h - B); p.drawLine(L, T, L, h - B) + n = len(self._series); dx = cw / max(1, n - 1); pts = [] + for i, val in enumerate(self._series): + v = max(0.0, min(100.0, float(val))) + pts.append(QPointF(L + i * dx, T + (100.0 - v) / 100.0 * ch)) + pen_line = QPen(QColor("#6366f1")); pen_line.setWidth(2); p.setPen(pen_line) + for i in range(1, len(pts)): p.drawLine(pts[i - 1], pts[i]) + p.setPen(Qt.PenStyle.NoPen); p.setBrush(QColor("#6366f1")) + for idx in (0, len(pts) - 1): p.drawEllipse(pts[idx], 3.5, 3.5) + +# ────────────────────────────── +# Date range dialog +# ────────────────────────────── + +class DateRangeDialog(QDialog): + """Two calendars (From/To) + clear/today/OK/Cancel.""" + def __init__(self, parent: Optional[QWidget] = None, start: Optional[QDate] = None, end: Optional[QDate] = None): + super().__init__(parent) + self.setWindowTitle("Select Date Range") + self.setModal(True) + self.setMinimumWidth(460) + self.setStyleSheet(""" + QDialog { background: #0b1220; color: #e5e7eb; } + QLabel { background: transparent; color: #e5e7eb; } + QCalendarWidget QWidget { background: transparent; color: #e5e7eb; } + QCalendarWidget QAbstractItemView { selection-background-color: #1e3a5f; } + QDialogButtonBox QPushButton { + background: #0f1a33; border: 1px solid #1f2b49; border-radius: 8px; + color: #e5e7eb; padding: 6px 12px; font-weight: 600; + } + QDialogButtonBox QPushButton:hover { background: #1a2a4a; border-color: #3d5a8f; } + """) + main = QVBoxLayout(self); row = QHBoxLayout(); main.addLayout(row) + min_allowed, max_allowed = QDate(2000, 1, 1), QDate(2100, 12, 31) + + def _calendar(lbl: str) -> QCalendarWidget: + box = QVBoxLayout(); box.addWidget(QLabel(lbl)) + cal = QCalendarWidget(self) + cal.setNavigationBarVisible(True); cal.setGridVisible(True) + cal.setMinimumDate(min_allowed); cal.setMaximumDate(max_allowed) + box.addWidget(cal); return box, cal + + left_box, self.cal_from = _calendar("From Date") + right_box, self.cal_to = _calendar("To Date") + row.addLayout(left_box); row.addItem(QSpacerItem(16, 1, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)); row.addLayout(right_box) + today = QDate.currentDate(); self.cal_from.setSelectedDate(start or today.addDays(-7)); self.cal_to.setSelectedDate(end or today) + buttons = QDialogButtonBox(self) + self._btn_clear = buttons.addButton("Clear", QDialogButtonBox.ButtonRole.ActionRole) + self._btn_today = buttons.addButton("Today", QDialogButtonBox.ButtonRole.ActionRole) + buttons.addButton(QDialogButtonBox.StandardButton.Ok); buttons.addButton(QDialogButtonBox.StandardButton.Cancel) + self._btn_clear.clicked.connect(self._on_clear); self._btn_today.clicked.connect(self._on_today) + buttons.accepted.connect(self.accept); buttons.rejected.connect(self.reject) + main.addWidget(buttons) + + def _on_today(self): t = QDate.currentDate(); self.cal_from.setSelectedDate(t); self.cal_to.setSelectedDate(t) + def _on_clear(self): self.cal_from.setSelectedDate(QDate()); self.cal_to.setSelectedDate(QDate()) + + def selectedRange(self) -> Tuple[Optional[QDate], Optional[QDate]]: + f, t = self.cal_from.selectedDate(), self.cal_to.selectedDate() + if not f.isValid() or not t.isValid(): return None, None + return (t, f) if f > t else (f, t) + +# ────────────────────────────── +# Main View +# ────────────────────────────── + +class LeafDiseaseView(QWidget): + """ + Compact dashboard: date filtering, top devices, top diseases, + device details (per-disease % of total device images). + When clicking on disease or device, shows details below inline. + """ + + def __init__(self, api: DashboardApi, parent: Optional[QWidget] = None) -> None: + super().__init__(parent) + self.api = api + self._raw: list[dict] = [] + self._types: dict[str, str] = {} # { "1": "Blight", ... } + self._filters_timer = QTimer(self); self._filters_timer.setSingleShot(True) + self._filters_timer.setInterval(250); self._filters_timer.timeout.connect(self._apply_filters_and_update) + self._setup_ui() + self._load_types(); self._load_reports() + + # ───────── UI / build ───────── + + def _setup_ui(self) -> None: + self.setStyleSheet(APP_STYLES) + scroll = QScrollArea(); scroll.setWidgetResizable(True); scroll.setFrameShape(QFrame.Shape.NoFrame) + container = QWidget(); main = QVBoxLayout(container); main.setSpacing(20); main.setContentsMargins(24, 24, 24, 24) + + # Top section: header, controls, KPIs, cards + main.addWidget(self._header()) + main.addWidget(self._controls()) + main.addWidget(self._kpis()) + main.addWidget(self._cards()) + + # Details section (shown below when clicking) + self.device_card = self._device_details_card() + self.device_card.setVisible(False) + main.addWidget(self.device_card) + + self.grafana_card = self._grafana_details_card() + self.grafana_card.setVisible(False) + main.addWidget(self.grafana_card) + + main.addStretch() + scroll.setWidget(container) + wrap = QVBoxLayout(self); wrap.setContentsMargins(0, 0, 0, 0); wrap.addWidget(scroll) + + def _header(self) -> QWidget: + frame = QFrame(); h = QHBoxLayout(frame) + h.setContentsMargins(0, 0, 0, 0) + title = QLabel("Field Status: Report") + title.setStyleSheet("font-size: 22px; font-weight: 700; letter-spacing: 0.3px;") + btn = QPushButton("🔄 Refresh"); btn.clicked.connect(self._refresh) + h.addWidget(title); h.addStretch(); h.addWidget(btn); return frame + + def _refresh(self): self._load_types(); self._load_reports() + + def _controls(self) -> QWidget: + box = QFrame(); box.setObjectName("controls"); box.setStyleSheet( + "QFrame#controls { background: #0f1a33; border: 1px solid #1f2b49; border-radius: 12px; padding: 10px; }" + ) + h = QHBoxLayout(box); h.setSpacing(20) + min_allowed, max_allowed = QDate(2000, 1, 1), QDate(2100, 12, 31) + h.addWidget(QLabel("Date Range:")) + self.date_from = QDateEdit(); self.date_from.setCalendarPopup(True); self.date_from.setDisplayFormat("dd/MM/yyyy") + self.date_from.setDate(QDate.currentDate().addDays(-7)); self.date_from.setMinimumDate(min_allowed); self.date_from.setMaximumDate(max_allowed) + self.date_to = QDateEdit(); self.date_to.setCalendarPopup(True); self.date_to.setDisplayFormat("dd/MM/yyyy") + self.date_to.setDate(QDate.currentDate()); self.date_to.setMinimumDate(min_allowed); self.date_to.setMaximumDate(max_allowed) + self.range_btn = QPushButton("📅 Select Range"); self.range_btn.clicked.connect(self._open_range_dialog) + for w in (self.date_from, QLabel("–"), self.date_to, self.range_btn): h.addWidget(w) + h.addStretch(); self.range_info = QLabel("Last 7 Days"); self.range_info.setStyleSheet("color: #94a3b8; font-size: 12px;"); h.addWidget(self.range_info) + self.date_from.dateChanged.connect(self._range_changed); self.date_to.dateChanged.connect(self._range_changed) + self._update_range_info(); return box + + def _kpis(self) -> QWidget: + frame = QFrame(); h = QHBoxLayout(frame); h.setSpacing(16); h.setContentsMargins(0, 0, 0, 0) + self.kpi_healthy = self._kpi_card("Overall Healthy %", "0%", "#22c55e") + self.kpi_images = self._kpi_card("Images Analyzed", "0", "#6366f1") + self.kpi_devices = self._kpi_card("Active Devices", "0", "#f59e0b") + for k in (self.kpi_healthy, self.kpi_images, self.kpi_devices): h.addWidget(k) + return frame + + def _cards(self) -> QWidget: + frame = QFrame(); h = QHBoxLayout(frame); h.setSpacing(16); h.setContentsMargins(0, 0, 0, 0) + self.devices_card = self._devices_card(); self.diseases_card = self._diseases_card() + h.addWidget(self.devices_card, 2); h.addWidget(self.diseases_card, 1); return frame + + # Small UI helpers + def _kpi_card(self, title: str, value: str, color: str) -> QWidget: + card = QFrame(); card.setProperty("class", "card"); v = QVBoxLayout(card); v.setSpacing(8) + t = QLabel(title); t.setStyleSheet("font-size: 13px; color: #94a3b8;") + val = QLabel(value); val.setObjectName("value"); val.setStyleSheet(f"font-size: 28px; font-weight: 700; color: {color};") + v.addWidget(t); v.addWidget(val); return card + + def _devices_card(self) -> QWidget: + card = QFrame(); card.setProperty("class", "card"); v = QVBoxLayout(card); v.setSpacing(12) + title = QLabel("Disease Severity by Device (Top 5)") + title.setStyleSheet("font-size: 16px; font-weight: 700; letter-spacing: .2px;") + self.devices_container = QWidget(); self.devices_layout = QVBoxLayout(self.devices_container) + self.devices_layout.setSpacing(10); self.devices_layout.setContentsMargins(0,0,0,0) + note = QLabel("Bar color by severity: Green/Orange/Red • Length represents percentage") + note.setStyleSheet("font-size: 12px; color: #94a3b8; margin-top: 8px;") + v.addWidget(title); v.addWidget(self.devices_container); v.addStretch(); v.addWidget(note); return card + + def _diseases_card(self) -> QWidget: + card = QFrame(); card.setProperty("class", "card"); v = QVBoxLayout(card); v.setSpacing(12) + title = QLabel("Top 3 Most Common Diseases") + title.setStyleSheet("font-size: 16px; font-weight: 700; letter-spacing: .2px;") + self.diseases_container = QWidget(); self.diseases_layout = QVBoxLayout(self.diseases_container) + self.diseases_layout.setSpacing(10); self.diseases_layout.setContentsMargins(0,0,0,0) + note = QLabel("Click on a disease to view detailed Grafana analytics below") + note.setStyleSheet("font-size: 12px; color: #94a3b8; margin-top: 8px;") + v.addWidget(title); v.addWidget(self.diseases_container); v.addStretch(); v.addWidget(note); return card + + def _device_details_card(self) -> QWidget: + """Card that shows disease breakdown for selected device""" + card = QFrame(); card.setProperty("class", "card"); v = QVBoxLayout(card); v.setSpacing(12) + + # Header with close button + header = QHBoxLayout() + self.device_title = QLabel("Device Details") + self.device_title.setStyleSheet("font-size: 16px; font-weight: 700;") + header.addWidget(self.device_title) + header.addStretch() + + close_btn = QPushButton("✕ Close") + close_btn.setStyleSheet("padding: 4px 8px; font-size: 11px;") + close_btn.clicked.connect(lambda: self.device_card.setVisible(False)) + header.addWidget(close_btn) + + v.addLayout(header) + + self.device_box = QWidget(); self.device_layout = QVBoxLayout(self.device_box) + self.device_layout.setSpacing(10); self.device_layout.setContentsMargins(0,0,0,0) + note = QLabel("% represents sick images with this disease out of ALL images from this device") + note.setStyleSheet("font-size: 12px; color: #94a3b8; margin-top: 8px;") + v.addWidget(self.device_box); v.addStretch(); v.addWidget(note) + return card + + def _grafana_details_card(self) -> QWidget: + """Card that shows embedded Grafana dashboard for selected disease""" + card = QFrame(); card.setProperty("class", "card"); v = QVBoxLayout(card); v.setSpacing(12) + v.setContentsMargins(16, 16, 16, 16) + + # Header with title and close button + header = QHBoxLayout() + self.grafana_title = QLabel("Disease Analysis") + self.grafana_title.setStyleSheet("font-size: 16px; font-weight: 700;") + header.addWidget(self.grafana_title) + header.addStretch() + + refresh_btn = QPushButton("🔄 Refresh") + refresh_btn.setStyleSheet("padding: 4px 8px; font-size: 11px;") + refresh_btn.clicked.connect(self._refresh_grafana) + header.addWidget(refresh_btn) + + close_btn = QPushButton("✕ Close") + close_btn.setStyleSheet("padding: 4px 8px; font-size: 11px;") + close_btn.clicked.connect(lambda: self.grafana_card.setVisible(False)) + header.addWidget(close_btn) + + v.addLayout(header) + + # Grafana WebView (embedded) + self.grafana_web = QWebEngineView() + self.grafana_web.setMinimumHeight(700) # Good size for viewing + v.addWidget(self.grafana_web) + + # Status label + self.grafana_status = QLabel("📊 Loading dashboard...") + self.grafana_status.setStyleSheet("padding: 5px; color: #94a3b8; font-size: 11px;") + v.addWidget(self.grafana_status) + + self.grafana_web.loadFinished.connect(self._on_grafana_loaded) + + return card + + def _refresh_grafana(self): + """Refresh the embedded Grafana dashboard""" + self.grafana_status.setText("📊 Refreshing dashboard...") + self.grafana_web.reload() + + def _on_grafana_loaded(self, success: bool): + """Called when Grafana page finishes loading""" + if success: + self.grafana_status.setText("✅ Dashboard loaded | Auto-refresh every 10 seconds") + else: + self.grafana_status.setText("❌ Error loading dashboard - Ensure Grafana is running") + + # ───────── Data fetch ───────── + + def _load_types(self) -> None: + """Fetch leaf_disease_types -> self._types {id:str -> name:str}.""" + try: + url = f"{self.api.base}/api/tables/leaf_disease_types" + resp = self.api.http.get(url, timeout=10) + mapping = { + str(r.get("id")): str(r.get("name")) + for r in _json_rows(resp.json()) + if isinstance(r, dict) and r.get("id") is not None and r.get("name") + } + if mapping: + self._types = mapping + print(f"[LeafDiseaseView] Loaded {len(self._types)} disease types.") + except Exception as e: + print(f"[LeafDiseaseView] Failed loading disease types: {e}") + + def _load_reports(self) -> None: + """Fetch reports -> self._raw and update.""" + try: + url = f"{self.api.base}/api/tables/leaf_reports" + print(f"[LeafDiseaseView] Requesting: {url}") + resp = self.api.http.get(url) + rows = _json_rows(resp.json()) if resp.status_code == 200 else [] + if rows and isinstance(rows[0], str): # stringified JSON rows support + import json + rows = [json.loads(x) if isinstance(x, str) else x for x in rows] + self._raw = [r for r in rows if isinstance(r, dict)] + self._update_all(self._filter_by_dates(self._raw)) + except Exception as e: + print(f"[LeafDiseaseView] Error: {e}") + self._update_all([]) + + # ───────── Filters & triggers ───────── + + def _range_changed(self, *_): + self._update_range_info() + self._filters_timer.start() + + def _open_range_dialog(self): + dlg = DateRangeDialog(self, start=self.date_from.date(), end=self.date_to.date()) + if dlg.exec() == QDialog.DialogCode.Accepted: + f, t = dlg.selectedRange() + if f and t: + self.date_from.setDate(f) + self.date_to.setDate(t) + else: + self.date_from.setDate(QDate.currentDate().addDays(-7)) + self.date_to.setDate(QDate.currentDate()) + self._range_changed() + + def _update_range_info(self): + txt = f"{self.date_from.date().toString('dd/MM')} → {self.date_to.date().toString('dd/MM')}" + self.range_info.setText(txt) + self.range_btn.setText(f"📅 {txt}") + + def _apply_filters_and_update(self): + self._update_all(self._filter_by_dates(self._raw)) + + def _filter_by_dates(self, data: list[dict]) -> list[dict]: + """Inclusive date-only filter using QDate edits.""" + d_from, d_to = self.date_from.date().toPyDate(), self.date_to.date().toPyDate() + out = [] + for r in data: + ts = r.get("ts") + if ts: + d = _ts_to_date(ts) + if d and not (d_from <= d <= d_to): + continue + out.append(r) + print(f"[KPI-DBG] filter_by_dates: kept={len(out)} of {len(data)}") + return out + + # ───────── Update UI ───────── + + def _update_all(self, filtered: list[dict]) -> None: + self._update_kpis(filtered) + self._update_devices(filtered) + self._update_diseases(filtered) + if self.device_card.isVisible() and hasattr(self, "_sel_dev"): + self._render_device(self._sel_dev, filtered) + + def _update_kpis(self, data: list[dict]) -> None: + total = len(data) + if total == 0: + self.kpi_healthy.findChild(QLabel, "value").setText("0%") + self.kpi_images.findChild(QLabel, "value").setText("0") + self.kpi_devices.findChild(QLabel, "value").setText("0") + return + sick = sum(1 for r in data if _is_sick(r.get("sick"))) + healthy_pct = (total - sick) / total * 100 + devices = len({str(r.get("device_id")) for r in data if r.get("device_id")}) + self.kpi_healthy.findChild(QLabel, "value").setText(f"{healthy_pct:.1f}%") + self.kpi_images.findChild(QLabel, "value").setText(f"{total:,}") + self.kpi_devices.findChild(QLabel, "value").setText(str(devices)) + + # ── Devices: top-5 ── + + def _update_devices(self, data: list[dict]) -> None: + _clear_layout(self.devices_layout) + if not data: + return + stats = defaultdict(lambda: {"total": 0, "sick": 0}) + for r in data: + dev = r.get("device_id") + if not dev: + continue + s = stats[str(dev)] + s["total"] += 1 + if _is_sick(r.get("sick")): + s["sick"] += 1 + pairs = [(dev, (s["sick"] / s["total"] * 100.0 if s["total"] else 0.0)) for dev, s in stats.items()] + for dev, pct in sorted(pairs, key=lambda x: x[1], reverse=True)[:5]: + row = self._build_bar_row(title=str(dev), pct=pct, right_text=f"{pct:.1f}%") + row.clicked.connect(self._on_device_clicked) + self.devices_layout.addWidget(row) + + def _on_device_clicked(self, device_id: str) -> None: + """When device is clicked, hide Grafana and show device details""" + self._sel_dev = device_id + self.grafana_card.setVisible(False) # Hide Grafana if open + self._render_device(device_id, self._filter_by_dates(self._raw)) + + def _render_device(self, device_id: str, data: list[dict]) -> None: + """ + Shows diseases for selected device. + IMPORTANT: % = (sick images with this disease) / (ALL images from device) * 100 + """ + _clear_layout(self.device_layout) + + # Get all images from this device + device_data = [r for r in data if str(r.get("device_id", "")) == str(device_id)] + total_device_images = len(device_data) + + if total_device_images == 0: + self.device_card.setVisible(False) + return + + # Count sick images per disease type + disease_sick_counts = defaultdict(int) + for r in device_data: + if _is_sick(r.get("sick")): + did = r.get("leaf_disease_type_id") + if did is not None: + disease_sick_counts[str(did)] += 1 + + self.device_title.setText(f"Device: {device_id} – Disease Breakdown") + + # Calculate % of total device images + rows = [] + for did, sick_count in disease_sick_counts.items(): + pct = (sick_count / total_device_images * 100.0) if total_device_images else 0.0 + rows.append((self._disease_name(did), pct, sick_count, total_device_images)) + + # Sort by percentage and display + for name, pct, sick_cnt, total_imgs in sorted(rows, key=lambda x: x[1], reverse=True): + txt = f"{pct:.1f}% ({sick_cnt}/{total_imgs} images)" + self.device_layout.addWidget(self._build_bar_row(title=name, pct=pct, right_text=txt)) + + self.device_card.setVisible(True) + + # ── Diseases: top-3 ── + + def _update_diseases(self, data: list[dict]) -> None: + _clear_layout(self.diseases_layout) + if not data: + return + counter: Counter[str] = Counter() + for r in data: + if _is_sick(r.get("sick")): + did = r.get("leaf_disease_type_id") + if did is not None: + counter[str(did)] += 1 + total_sick = sum(counter.values()) or 1 + palette = ["#f97316", "#eab308", "#ef4444"] + for i, (did, cnt) in enumerate(counter.most_common(3)): + name = self._disease_name(did) + pct = cnt / total_sick * 100.0 + color = palette[i] if i < len(palette) else "#6366f1" + self.diseases_layout.addWidget( + self._build_tag(name=name, disease_id=str(did), color=color, pct=pct, count=cnt) + ) + + # ── Disease details: Shows embedded Grafana below ── + + def _on_disease_clicked(self, disease_id: str) -> None: + """When disease is clicked, hide device details and show Grafana below""" + disease_name = self._disease_name(disease_id) + print(f"[LeafDiseaseView] Showing Grafana for disease: {disease_name} (ID: {disease_id})") + + # Hide device card if open + self.device_card.setVisible(False) + + # Update Grafana title and load dashboard + self.grafana_title.setText(f"📊 Disease Analysis: {disease_name}") + + # Build Grafana URL with disease_id variable + grafana_url = ( + f"http://host.docker.internal:3000/d/leaf-disease-detail/" + f"leaf-disease-analysis" + f"?orgId=1&refresh=10s&kiosk=tv" + f"&var-disease_id={disease_id}" + ) + + print(f"[LeafDiseaseView] Loading Grafana: {grafana_url}") + self.grafana_status.setText("📊 Loading dashboard...") + self.grafana_web.setUrl(QUrl(grafana_url)) + + # Show Grafana card + self.grafana_card.setVisible(True) + + # ───────── Reusable tiny builders ───────── + def _build_bar_row(self, title: str, pct: float, right_text: str) -> ClickableRow: + """ + Generic row: title | QProgressBar (color by pct) | right text. + Bar length represents the percentage value. + """ + row = ClickableRow(title) + h = QHBoxLayout(row) + h.setSpacing(10) + h.setContentsMargins(0, 0, 0, 0) + + # left title + name = QLabel(title) + name.setStyleSheet("font-weight: 600; min-width: 180px;") + h.addWidget(name) + + # middle: progress bar (VALUE = pct, so bar length represents percentage) + pb = QProgressBar() + pb.setRange(0, 100) + pb.setValue(int(max(0, min(100, pct)))) + pb.setTextVisible(False) + + # color chunk dynamically by severity + chunk_color = _color_for_pct(pct) # green/orange/red + pb.setStyleSheet( + "QProgressBar { background: #0b1220; border: 1px solid #1f2937; " + "border-radius: 999px; min-height: 14px; max-height: 14px; } " + f"QProgressBar::chunk {{ background: {chunk_color}; border-radius: 999px; }}" + ) + h.addWidget(pb, 1) + + # right label + r = QLabel(right_text) + r.setStyleSheet("color: #94a3b8; min-width: 110px;") + r.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) + h.addWidget(r) + + return row + + def _build_tag(self, name: str, disease_id: str, color: str, pct: float, count: int) -> ClickableTag: + row = ClickableTag(disease_id) + h = QHBoxLayout(row) + h.setSpacing(8) + h.setContentsMargins(0, 0, 0, 0) + + tag = QFrame() + tag.setStyleSheet("background: #0b1220; border: 1px solid #1f2937; border-radius: 999px; padding: 6px 10px;") + tag_h = QHBoxLayout(tag) + tag_h.setSpacing(8) + tag_h.setContentsMargins(6, 4, 6, 4) + + dot = QLabel("●") + dot.setStyleSheet(f"color: {color}; font-size: 16px;") + disp = name if len(name) <= 25 else name[:25] + "…" + tag_h.addWidget(dot) + tag_h.addWidget(QLabel(disp)) + + h.addWidget(tag) + h.addStretch() + + pct_lbl = QLabel(f"{pct:.0f}% ({count} reports)") + pct_lbl.setStyleSheet("color: #94a3b8; font-size: 13px;") + h.addWidget(pct_lbl) + + row.clicked.connect(self._on_disease_clicked) + return row + + # ───────── Naming ───────── + + def _disease_name(self, disease_id: str | int | None) -> str: + if disease_id is None: + return "Unknown Disease" + return self._types.get(str(disease_id), f"Disease #{disease_id}") \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/views/notification_view.py b/AgCloud/GUI/src/vast/views/notification_view.py new file mode 100644 index 000000000..a1552fd15 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/notification_view.py @@ -0,0 +1,20 @@ +from PyQt6.QtWidgets import ( + QVBoxLayout, QWidget +) + +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtCore import QUrl + +class NotificationView(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + web_view = QWebEngineView(self) + + notification_api_url = "http://notification_api:5000" + print(f"[NotificationView] Loading URL: {notification_api_url}") + web_view.setUrl(QUrl(notification_api_url)) + + layout.addWidget(web_view) \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/views/security/analytics/analytics_page.py b/AgCloud/GUI/src/vast/views/security/analytics/analytics_page.py new file mode 100644 index 000000000..d4cdc0475 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/analytics/analytics_page.py @@ -0,0 +1,378 @@ +from __future__ import annotations +from datetime import date +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, + QPushButton, QDateEdit, QFrame, QSizePolicy, + QGraphicsDropShadowEffect, QSplitter, QLineEdit, QMessageBox, QApplication +) +from PyQt6.QtCore import Qt, QDate +from PyQt6.QtGui import QColor +from PyQt6 import QtCore + +from orthophoto_canvas.ui.viewer_factory import create_orthophoto_viewer +from src.vast.views.security.analytics.map_layers.region_layer import RegionLayer +from src.vast.views.security.analytics.map_layers.device_layer import DeviceLayer +from src.vast.views.security.analytics.analytics_provider import ( + load_all_devices, load_all_regions, + get_region_analytics, get_device_analytics +) +from src.vast.views.security.analytics.popup_panel import AnalyticsPanel +from src.vast.views.security.analytics import analytics_provider as ap + + +class GeoAnalyticsView(QWidget): + """Geo-Analytics Dashboard with fixed analytics panel and multi-selection.""" + + def __init__(self, parent: QWidget | None = None): + super().__init__(parent) + self.current_mode = "region" + self.start_date: date | None = None + self.end_date: date | None = None + self.selected_regions = set() + self.selected_devices = set() + + # ───────────────────────────── + # GLOBAL STYLE + # ───────────────────────────── + self.setStyleSheet(""" + QWidget { + background-color: #f9fafb; + font-family: 'Segoe UI', 'DejaVu Sans', Arial, sans-serif; + color: #111827; + font-size: 14px; + } + QComboBox, QDateEdit { + background-color: #ffffff; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 4px 10px; + font-size: 13px; + height: 32px; + min-width: 120px; + color: #111827; + } + QComboBox:hover, QDateEdit:hover { + border-color: #9ca3af; + background-color: #f9fafb; + } + QPushButton#apply_btn { + background-color: #10b981; + color: white; + border-radius: 6px; + font-weight: 600; + padding: 6px 12px; + } + QPushButton#apply_btn:hover { background-color: #059669; } + """) + + # ───────────────────────────── + # ROOT LAYOUT + # ───────────────────────────── + root = QVBoxLayout(self) + root.setContentsMargins(16, 16, 16, 16) + root.setSpacing(12) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + # Header + header = QLabel("🗺️ Geo-Analytics Dashboard") + header.setStyleSheet("font-size:22px;font-weight:700;color:#0f172a;") + root.addWidget(header) + + # ───────────────────────────── + # FILTER BAR + # ───────────────────────────── + filter_frame = QFrame() + filter_frame.setStyleSheet(""" + QFrame { + background-color: #ffffff; + border: 1px solid #d1d5db; + border-radius: 12px; + padding: 10px 14px; + } + """) + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(12) + shadow.setOffset(0, 2) + shadow.setColor(QColor(0, 0, 0, 40)) + filter_frame.setGraphicsEffect(shadow) + + filter_bar = QHBoxLayout(filter_frame) + filter_bar.setSpacing(14) + filter_bar.setContentsMargins(10, 6, 10, 6) + + lbl_mode = QLabel("Mode:") + self.mode_combo = QComboBox() + self.mode_combo.addItems(["Region", "Device"]) + self.mode_combo.currentTextChanged.connect(self._on_mode_changed) + + lbl_date = QLabel("Date range:") + self.start_picker = QDateEdit(QDate.currentDate().addMonths(-1)) + self.start_picker.setCalendarPopup(True) + arrow_lbl = QLabel("→") + self.end_picker = QDateEdit(QDate.currentDate()) + self.end_picker.setCalendarPopup(True) + + apply_btn = QPushButton("Apply Filters") + apply_btn.setObjectName("apply_btn") + apply_btn.clicked.connect(self._apply_filters) + + filter_bar.addWidget(lbl_mode) + filter_bar.addWidget(self.mode_combo) + filter_bar.addSpacing(12) + filter_bar.addWidget(lbl_date) + filter_bar.addWidget(self.start_picker) + filter_bar.addWidget(arrow_lbl) + filter_bar.addWidget(self.end_picker) + filter_bar.addSpacing(12) + filter_bar.addWidget(apply_btn) + filter_bar.addStretch() + root.addWidget(filter_frame) + + # ───────────────────────────── + # AI QUERY BAR + # ───────────────────────────── + query_frame = QFrame() + query_frame.setStyleSheet(""" + QFrame { + background-color: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 18px; + padding: 10px 12px; + } + QLineEdit { + background-color: qlineargradient( + x1:0, y1:0, x2:0, y2:1, + stop:0 #ffffff, stop:1 #f9fafb + ); + border: 1px solid #d1d5db; + border-radius: 18px; + padding: 8px 16px; + font-size: 14px; + color: #111827; + selection-background-color: #bae6fd; + } + QLineEdit:focus { + border-color: #10b981; + background-color: #ffffff; + box-shadow: 0 0 0 3px rgba(16,185,129,0.2); + } + QPushButton#send_btn { + background-color: #10b981; + color: white; + font-weight: 600; + border-radius: 18px; + padding: 0 18px; + border: none; + min-width: 80px; + height: 38px; + } + QPushButton#send_btn:hover { + background-color: #059669; + } + """) + + query_layout = QHBoxLayout(query_frame) + query_layout.setSpacing(8) + query_layout.setContentsMargins(10, 6, 10, 6) + + self.query_input = QLineEdit() + self.query_input.setPlaceholderText("Ask anything — e.g. 'Show regions with most intrusions this month'") + self.query_input.setClearButtonEnabled(True) + self.query_input.setMinimumWidth(420) + self.query_input.setFixedHeight(38) + + # ✅ Text-based Send button + self.query_send = QPushButton("Send") + self.query_send.setObjectName("send_btn") + self.query_send.clicked.connect(self._on_query_sent) + + query_layout.addWidget(self.query_input, stretch=1) + query_layout.addWidget(self.query_send) + + # Floating drop shadow + query_shadow = QGraphicsDropShadowEffect() + query_shadow.setBlurRadius(22) + query_shadow.setOffset(0, 3) + query_shadow.setColor(QColor(0, 0, 0, 50)) + query_frame.setGraphicsEffect(query_shadow) + + # ───────────────────────────── + # MAIN SPLITTER (map + analytics) + # ───────────────────────────── + splitter = QSplitter(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + splitter.setHandleWidth(6) + splitter.setStretchFactor(0, 3) + splitter.setStretchFactor(1, 1) + splitter.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + # LEFT SIDE — map + query box + map_container = QWidget() + map_layout = QVBoxLayout(map_container) + map_layout.setContentsMargins(0, 0, 0, 0) + map_layout.setSpacing(8) + + tiles_root = "./src/vast/orthophoto_canvas/data/tiles" + self.viewer = create_orthophoto_viewer(tiles_root, forced_scheme=None, parent=self) + self.viewer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + map_layout.addWidget(self.viewer, stretch=1) + map_layout.addWidget(query_frame, stretch=0) + query_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + splitter.addWidget(map_container) + + # RIGHT SIDE — analytics panel + self.analytics_panel = AnalyticsPanel("All Regions", {},parent=self) + self.analytics_panel.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) + splitter.addWidget(self.analytics_panel) + root.addWidget(splitter, stretch=1) + + # ───────────────────────────── + # MAP LAYERS + # ───────────────────────────── + self.region_layer = RegionLayer(self.viewer, on_select=self._on_region_selected) + self.device_layer = DeviceLayer(self.viewer, on_select=self._on_device_selected) + + # Initial load + self._load_regions() + self._update_analytics_panel() + + # ───────────────────────────── + # FREE TEXT QUERY HANDLER + # ───────────────────────────── + def _on_query_sent(self): + prompt = self.query_input.text().strip() + if not prompt: + QMessageBox.information(self, "Empty query", "Please type a query first.") + return + + # Change button state + self.query_send.setEnabled(False) + self.query_send.setText("...") # show dots while sending + QApplication.processEvents() + + try: + result = ap.select_entities_from_prompt(prompt) + except Exception as e: + QMessageBox.warning(self, "Error", f"Query failed: {e}") + self.query_send.setEnabled(True) + self.query_send.setText("Send") + return + + # Restore state + self.query_send.setEnabled(True) + self.query_send.setText("Send") + + if not result["ids"]: + QMessageBox.information(self, "No results", "No matching regions or devices found.") + return + + if result["target"] == "region": + self.current_mode = "region" + self.mode_combo.setCurrentText("Region") + self._update_layer_visibility() + self.device_layer.clear() + self.selected_regions = set(map(int, result["ids"])) + self.region_layer.clear() + self._load_regions(self.start_picker.date().toPyDate(), + self.end_picker.date().toPyDate()) + + elif result["target"] == "device": + self.current_mode = "device" + self.mode_combo.setCurrentText("Device") + self._update_layer_visibility() + self.region_layer.clear() + self.selected_devices = set(map(str, result["ids"])) + self.device_layer.clear() + self._load_devices( + self.start_picker.date().toPyDate(), + self.end_picker.date().toPyDate(), + selected_ids=self.selected_devices + ) + + self._update_analytics_panel() + + # ───────────────────────────── + # MODE / FILTERS + # ───────────────────────────── + def _on_mode_changed(self, text: str): + self.current_mode = text.lower() + if self.current_mode == "region": + self.device_layer.clear() + else: + self.region_layer.clear() + self._update_layer_visibility() + self._apply_filters() + + def _apply_filters(self): + self.start_date = self.start_picker.date().toPyDate() + self.end_date = self.end_picker.date().toPyDate() + self.region_layer.clear() + self.device_layer.clear() + self.selected_regions.clear() + self.selected_devices.clear() + self._update_layer_visibility() + if self.current_mode == "region": + self._load_regions(self.start_date, self.end_date) + else: + self._load_devices(self.start_date, self.end_date) + self._update_analytics_panel() + + # ───────────────────────────── + # LOADERS + # ───────────────────────────── + def _load_regions(self, start_date=None, end_date=None): + regions = load_all_regions() + self.region_names = {r["id"]: r["name"] for r in regions} + for region in regions: + self.region_layer.add_region(region, start_date, end_date, selected_ids=self.selected_regions) + + def _load_devices(self, start_date=None, end_date=None, selected_ids=None): + devices = load_all_devices() + for device in devices: + device_id = device["device_id"] + selected = selected_ids and device_id in selected_ids + self.device_layer.add_device(device, start_date, end_date, selected=selected) + + # ───────────────────────────── + # SELECTION HANDLERS + # ───────────────────────────── + def _on_region_selected(self, region_id: int, selected: bool): + if selected: + self.selected_regions.add(region_id) + else: + self.selected_regions.discard(region_id) + self._update_analytics_panel() + + def _on_device_selected(self, device_id: str, selected: bool): + if selected: + self.selected_devices.add(device_id) + else: + self.selected_devices.discard(device_id) + self._update_analytics_panel() + + # ───────────────────────────── + # ANALYTICS UPDATE + # ───────────────────────────── + def _update_analytics_panel(self): + if self.current_mode == "region": + region_list = list(self.selected_regions) + data = get_region_analytics(region_list or None, self.start_date, self.end_date) + title = "All Regions" if not region_list else ", ".join( + self.region_names.get(rid, str(rid)) for rid in region_list) + self.analytics_panel.update_data(title, data) + else: + device_list = list(self.selected_devices) + data = get_device_analytics(device_list or None, self.start_date, self.end_date) + title = "All Devices" if not device_list else ", ".join(device_list) + self.analytics_panel.update_data(title, data) + + + + def _update_layer_visibility(self): + if self.current_mode == "region": + self.region_layer.setVisible(True) + self.device_layer.setVisible(False) + else: + self.region_layer.setVisible(False) + self.device_layer.setVisible(True) diff --git a/AgCloud/GUI/src/vast/views/security/analytics/analytics_provider.py b/AgCloud/GUI/src/vast/views/security/analytics/analytics_provider.py new file mode 100644 index 000000000..9c9b9a599 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/analytics/analytics_provider.py @@ -0,0 +1,333 @@ + + +from datetime import date +from sqlalchemy import create_engine, text +from datetime import datetime, timedelta + +# ───────────────────────────────────────────── +# Database Connection +# ───────────────────────────────────────────── +engine = create_engine( + "postgresql+psycopg2://missions_user:pg123@postgres:5432/missions_db", + echo=False +) + +# ───────────────────────────────────────────── +# Load all regions & devices +# ───────────────────────────────────────────── +def load_all_regions() -> list[dict]: + """Return all regions with ID, name, and geometry.""" + query = text(""" + SELECT id, name, ST_AsGeoJSON(geom) AS geom + FROM regions + ORDER BY id; + """) + with engine.connect() as conn: + return [dict(row) for row in conn.execute(query).mappings().all()] + + +def load_all_devices() -> list[dict]: + """Return all devices with ID, model, owner, and coordinates.""" + query = text(""" + SELECT device_id, model, owner, active, + location_lat, location_lon + FROM devices + WHERE owner = 'security' + ORDER BY device_id; + """) + with engine.connect() as conn: + return [dict(row) for row in conn.execute(query).mappings().all()] + +# ───────────────────────────────────────────── +# DEVICE ANALYTICS (supports multiple or all devices) +# ───────────────────────────────────────────── +def get_device_analytics(device_ids=None, start_date=None, end_date=None) -> dict: + """ + Return aggregated alert analytics for one or more devices. + If device_ids is None or empty, aggregates all devices. + """ + if isinstance(device_ids, str): + device_ids = [device_ids] + + # Default to full current year range if missing + if not start_date: + start_date = date.today().replace(day=1, month=1) + if not end_date: + end_date = date.today() + + # Convert to datetime range that includes the entire end day + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date + timedelta(days=1), datetime.min.time()) + + + with engine.connect() as conn: + params = {"device_ids": device_ids or [], + "start_date": start_dt, + "end_date": end_dt} + + + if device_ids: + device_filter = "AND device_id = ANY(:device_ids)" + else: + device_filter = "" # ← no filter means aggregate all devices + params.pop("device_ids", None) + + + # Alerts by Type + q_by_type = text(f""" + SELECT alert_type, COUNT(*) AS count + FROM alerts + WHERE started_at BETWEEN :start_date AND :end_date + {device_filter} + GROUP BY alert_type + ORDER BY count DESC; + """) + by_type = {r["alert_type"] or "Unknown": r["count"] + for r in conn.execute(q_by_type, params).mappings().all()} + + # Alerts per Month + q_month = text(f""" + WITH months AS ( + SELECT TO_CHAR(gs, 'YYYY-MM') AS month + FROM generate_series( + date_trunc('month', CAST(:start_date AS timestamp)), + date_trunc('month', CAST(:end_date AS timestamp)), + interval '1 month' + ) AS gs + ), + alert_counts AS ( + SELECT TO_CHAR(started_at, 'YYYY-MM') AS month, COUNT(*) AS count + FROM alerts + WHERE started_at BETWEEN :start_date AND :end_date + {device_filter} + GROUP BY TO_CHAR(started_at, 'YYYY-MM') + ) + SELECT m.month, COALESCE(ac.count, 0) AS count + FROM months m + LEFT JOIN alert_counts ac ON ac.month = m.month + ORDER BY m.month; + """) + per_month = {r["month"]: r["count"] + for r in conn.execute(q_month, params).mappings().all()} + + # Totals & averages + q_total = text(f""" + SELECT COUNT(*) AS total, + AVG(severity)::numeric(10,2) AS avg_severity, + AVG(confidence)::numeric(10,2) AS avg_confidence + FROM alerts + WHERE started_at BETWEEN :start_date AND :end_date + {device_filter}; + """) + total = conn.execute(q_total, params).mappings().first() or {} + q_avg_duration = text(f""" + SELECT alert_type, + ROUND(AVG(EXTRACT(EPOCH FROM (ended_at - started_at)) / 60.0), 2) AS avg_minutes + FROM alerts + WHERE started_at BETWEEN :start_date AND :end_date + {device_filter} + AND ended_at IS NOT NULL + GROUP BY alert_type + ORDER BY avg_minutes DESC; + """) + + avg_duration = { + r["alert_type"] or "Unknown": float(r["avg_minutes"] or 0) + for r in conn.execute(q_avg_duration, params).mappings().all() + } + + + return { + "alerts_by_type": by_type, + "alerts_per_month": per_month, + "total_alerts": total.get("total", 0), + "avg_severity": float(total.get("avg_severity") or 0), + "avg_confidence": float(total.get("avg_confidence") or 0), + "avg_duration_per_type": avg_duration, # ← NEW + } + +# ───────────────────────────────────────────── +# REGION ANALYTICS (supports multiple or all regions) +# ───────────────────────────────────────────── +def get_region_analytics(region_ids=None, start_date=None, end_date=None) -> dict: + """ + Aggregates analytics for one or more regions (using PostGIS ST_Intersects). + If region_ids is None or empty, aggregates all regions. + """ + if isinstance(region_ids, (str, int)): + region_ids = [region_ids] + + # Default to full current year range if missing + if not start_date: + start_date = date.today().replace(day=1, month=1) + if not end_date: + end_date = date.today() + + # Convert to datetime range that includes the entire end day + start_dt = datetime.combine(start_date, datetime.min.time()) + end_dt = datetime.combine(end_date + timedelta(days=1), datetime.min.time()) + + + with engine.connect() as conn: + params = { + "region_ids": region_ids or [], + "start_date": start_dt, + "end_date": end_dt, +} + + + # Optional region filter + if region_ids: + region_filter = "AND r.id = ANY(:region_ids)" + else: + region_filter = "" # no filter → aggregate all regions + params.pop("region_ids", None) + + # Alerts by Type + q_by_type = text(f""" + SELECT a.alert_type, COUNT(*) AS count + FROM alerts a + JOIN devices d ON a.device_id = d.device_id + JOIN regions r + ON ST_Intersects(ST_Buffer(r.geom, 0.0002), ST_SetSRID(ST_MakePoint(d.location_lon, d.location_lat), 4326)) + WHERE a.started_at BETWEEN :start_date AND :end_date + {region_filter} + GROUP BY a.alert_type + ORDER BY count DESC; + """) + print(q_by_type,region_filter) + by_type = {r["alert_type"] or "Unknown": r["count"] + for r in conn.execute(q_by_type, params).mappings().all()} + + # Alerts per Month + q_month = text(f""" + WITH months AS ( + SELECT TO_CHAR(gs, 'YYYY-MM') AS month + FROM generate_series( + date_trunc('month', CAST(:start_date AS timestamp)), + date_trunc('month', CAST(:end_date AS timestamp)), + interval '1 month' + ) AS gs + ), + alert_counts AS ( + SELECT TO_CHAR(a.started_at, 'YYYY-MM') AS month, COUNT(*) AS count + FROM alerts a + JOIN devices d ON a.device_id = d.device_id + JOIN regions r + ON ST_Intersects(ST_Buffer(r.geom, 0.0002), ST_SetSRID(ST_MakePoint(d.location_lon, d.location_lat), 4326)) + WHERE a.started_at BETWEEN :start_date AND :end_date + {region_filter} + GROUP BY TO_CHAR(a.started_at, 'YYYY-MM') + ) + SELECT m.month, COALESCE(ac.count, 0) AS count + FROM months m + LEFT JOIN alert_counts ac ON ac.month = m.month + ORDER BY m.month; + """) + per_month = {r["month"]: r["count"] + for r in conn.execute(q_month, params).mappings().all()} + + # Totals & averages + q_total = text(f""" + SELECT COUNT(*) AS total, + AVG(a.severity)::numeric(10,2) AS avg_severity, + AVG(a.confidence)::numeric(10,2) AS avg_confidence + FROM alerts a + JOIN devices d ON a.device_id = d.device_id + JOIN regions r + ON ST_Intersects(ST_Buffer(r.geom, 0.0002), ST_SetSRID(ST_MakePoint(d.location_lon, d.location_lat), 4326)) + WHERE a.started_at BETWEEN :start_date AND :end_date + {region_filter}; + """) + total = conn.execute(q_total, params).mappings().first() or {} + + q_avg_duration = text(f""" + SELECT a.alert_type, + ROUND(AVG(EXTRACT(EPOCH FROM (a.ended_at - a.started_at)) / 60.0), 2) AS avg_minutes + FROM alerts a + JOIN devices d ON a.device_id = d.device_id + JOIN regions r + ON ST_Intersects( + ST_Buffer(r.geom, 0.0002), + ST_SetSRID(ST_MakePoint(d.location_lon, d.location_lat), 4326) + ) + WHERE a.started_at BETWEEN :start_date AND :end_date + {region_filter} + AND a.ended_at IS NOT NULL + GROUP BY a.alert_type + ORDER BY avg_minutes DESC; + """) + avg_duration = { + r["alert_type"] or "Unknown": float(r["avg_minutes"] or 0) + for r in conn.execute(q_avg_duration, params).mappings().all() + } + + + return { + "alerts_by_type": by_type, + "alerts_per_month": per_month, + "total_alerts": total.get("total", 0), + "avg_severity": float(total.get("avg_severity") or 0), + "avg_confidence": float(total.get("avg_confidence") or 0), + "avg_duration_per_type": avg_duration, # ← NEW + } + + +# ───────────────────────────────────────────── +# NATURAL LANGUAGE QUERY SUPPORT +# ───────────────────────────────────────────── +from src.vast.views.security.analytics.sql_generator import generate_sql_from_prompt + + + +def select_entities_from_prompt(prompt: str) -> dict: + """ + Uses the AI SQL generator to convert free-text query → SQL, + executes it, and returns matching entity IDs. + + Returns a dict: + { + "target": "region" | "device" | None, + "ids": [list of IDs] + } + """ + sql, params = generate_sql_from_prompt(prompt) + print(sql,params) + if not sql: + return {"target": None, "ids": []} + + with engine.connect() as conn: + rows = [r[0] for r in conn.execute(text(sql), params)] + + + sql_lower = sql.lower() + if "area" in sql_lower or "region" in sql_lower: + target = "region" + elif "device_id" in sql_lower: + target = "device" + else: + target = "region" if "from regions" in sql_lower else "device" + + import re + + if target == "region": + # Normalize region names to lowercase without spaces/underscores for fuzzy match + def normalize(name: str) -> str: + return re.sub(r'[^a-z0-9]', '', name.lower()) + + region_map = {normalize(r["name"]): r["id"] for r in load_all_regions()} + + mapped = [] + for r in rows: + if isinstance(r, (int, float)): + mapped.append(int(r)) + elif isinstance(r, str): + key = normalize(r) + if key in region_map: + mapped.append(region_map[key]) + rows = mapped + + + return {"target": target, "ids": rows} + + diff --git a/AgCloud/GUI/src/vast/views/security/analytics/map_layers/__init__.py b/AgCloud/GUI/src/vast/views/security/analytics/map_layers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/GUI/src/vast/views/security/analytics/map_layers/device_layer.py b/AgCloud/GUI/src/vast/views/security/analytics/map_layers/device_layer.py new file mode 100644 index 000000000..93939b822 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/analytics/map_layers/device_layer.py @@ -0,0 +1,129 @@ +from PyQt6.QtWidgets import QGraphicsTextItem, QGraphicsDropShadowEffect +from PyQt6.QtGui import QColor, QFont +from PyQt6.QtCore import Qt +from src.vast.orthophoto_canvas.ui.sensors_layer import TILE_SIZE, _latlon_to_xy_at_max_zoom + +from PyQt6.QtWidgets import QGraphicsTextItem, QGraphicsDropShadowEffect, QGraphicsColorizeEffect +from PyQt6.QtGui import QColor, QFont, QFontMetrics +from PyQt6.QtCore import Qt, QPropertyAnimation, QEasingCurve + + +class _DeviceMarker(QGraphicsTextItem): + """A single camera/device emoji marker with selection halo and pulse effect.""" + + def __init__(self, device_id: str, active: bool, on_select=None): + super().__init__("📷") + self.device_id = device_id + self.active = active + self.on_select = on_select + self.selected = False + + # Base style — emoji, color by status + self.normal_color = QColor("#10b981") if active else QColor("#9ca3af") + self.selected_color = QColor("#ffffff") + self.setFont(QFont("Noto Color Emoji", 14)) + self.setDefaultTextColor(self.normal_color) + self.setZValue(1000) + self.setFlag(QGraphicsTextItem.GraphicsItemFlag.ItemIgnoresTransformations, True) + self.setAcceptHoverEvents(True) + + # Halo effect for glow + self.halo = QGraphicsDropShadowEffect() + self.halo.setBlurRadius(40) + self.halo.setOffset(0, 0) + self.halo.setColor(QColor(16, 185, 129, 160)) + self.setGraphicsEffect(None) + + # Pulse animation + self.pulse = QPropertyAnimation(self, b"opacity") + self.pulse.setDuration(1000) + self.pulse.setStartValue(1.0) + self.pulse.setEndValue(0.5) + self.pulse.setEasingCurve(QEasingCurve.Type.InOutQuad) + self.pulse.setLoopCount(-1) + + def mousePressEvent(self, event): + """Toggle selection highlight and trigger callback.""" + self.selected = not self.selected + + if self.selected: + self.setFont(QFont("Noto Color Emoji", 18)) + self.setDefaultTextColor(self.selected_color) + self.halo.setColor(QColor(5, 150, 105, 255)) + self.setGraphicsEffect(self.halo) + self.pulse.start() + else: + self.setFont(QFont("Noto Color Emoji", 14)) + self.setDefaultTextColor(self.normal_color) + self.setGraphicsEffect(None) + self.pulse.stop() + self.setOpacity(1.0) + + if self.on_select: + self.on_select(self.device_id, self.selected) + + super().mousePressEvent(event) + # ───────────────────────────────────────────── + + + + + +# ───────────────────────────────────────────── +# 🗺️ Device Layer +# ───────────────────────────────────────────── +class DeviceLayer: + """Draws device (camera) markers on the orthophoto scene.""" + + def __init__(self, viewer, on_select=None): + self.viewer = viewer + self.scene = viewer.scene + self.devices = {} + self.on_select = on_select + + # Match RegionLayer & AlertLayer → use MAX zoom base tiles + z = viewer.max_zoom_fs + self._x_min_base = viewer.ts.z_ranges[z][0] + self._y_min_base = viewer.ts.z_ranges[z][2] + + def add_device(self, device: dict, start_date=None, end_date=None, selected=False): + + """Add a device marker to the orthophoto scene.""" + lat = device.get("location_lat") + lon = device.get("location_lon") + + # Convert to base XY in max zoom coordinate space + pos = _latlon_to_xy_at_max_zoom(self.viewer, lat, lon) + if not pos: + print(f"[DeviceLayer] ⚠️ Device {device.get('device_id')} outside field bounds") + return + + xb, yb = pos + scene_x = (xb - self._x_min_base) * TILE_SIZE + scene_y = (yb - self._y_min_base) * TILE_SIZE + + marker = _DeviceMarker(device["device_id"], device.get("active", True), self.on_select) + marker.setPos(scene_x, scene_y) + self.scene.addItem(marker) + self.devices[device["device_id"]] = marker + + if selected: + marker.selected = True + marker.setFont(QFont("Noto Color Emoji", 18)) + marker.setDefaultTextColor(marker.selected_color) + marker.setGraphicsEffect(marker.halo) + marker.pulse.start() + + print(f"[DeviceLayer] ✅ Added device '{device['device_id']}' at ({scene_x:.1f}, {scene_y:.1f})") + + def clear(self): + """Remove all device markers.""" + for item in self.devices.values(): + self.scene.removeItem(item) + self.devices.clear() + print("[DeviceLayer] Cleared all devices") + def setVisible(self, visible: bool): + """Show or hide all device markers.""" + for item in self.devices.values(): + item.setVisible(visible) + print(f"[DeviceLayer] Visibility set to {visible}") diff --git a/AgCloud/GUI/src/vast/views/security/analytics/map_layers/region_layer.py b/AgCloud/GUI/src/vast/views/security/analytics/map_layers/region_layer.py new file mode 100644 index 000000000..f62171d38 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/analytics/map_layers/region_layer.py @@ -0,0 +1,153 @@ +from PyQt6.QtWidgets import QGraphicsPolygonItem +from PyQt6.QtGui import QColor, QPen, QPolygonF, QBrush +from PyQt6.QtCore import Qt, QPointF +import json +from shapely.geometry import Polygon, box +from shapely.ops import unary_union +from src.vast.orthophoto_canvas.ui.sensors_layer import ( + TILE_SIZE, + _latlon_to_xy_at_max_zoom, +) + + +class RegionLayer: + """Draws farm regions as interactive polygons positioned by GPS coordinates. + Supports selection by region ID and clips polygons to the orthophoto scene boundaries.""" + + def __init__(self, viewer, on_select=None): + self.viewer = viewer + self.scene = viewer.scene + self.regions = [] + self.on_select = on_select + + # Base tile indices at MAX zoom (same as OrthophotoViewer scene) + z = viewer.max_zoom_fs + self._x_min_base = viewer.ts.z_ranges[z][0] + self._y_min_base = viewer.ts.z_ranges[z][2] + + # Scene boundary in scene coordinates (0,0) → (width,height) + width = (viewer.ts.z_ranges[z][1] - self._x_min_base + 1) * TILE_SIZE + height = (viewer.ts.z_ranges[z][3] - self._y_min_base + 1) * TILE_SIZE + + self.scene_bounds = box(0, 0, width, height) + + # ───────────────────────────────────────────── + def add_region(self, region: dict, start_date=None, end_date=None, selected_ids=None): + """Add a region polygon to the orthophoto map, clipped to scene boundaries.""" + try: + geom_json = json.loads(region["geom"]) + except Exception as e: + print(f"[RegionLayer] ❌ Invalid geometry for {region.get('name')}: {e}") + return + + if not geom_json.get("coordinates"): + print(f"[RegionLayer] ⚠️ Region {region.get('name')} missing coordinates") + return + + outer_ring = geom_json["coordinates"][0] + if len(outer_ring) < 3: + print(f"[RegionLayer] ⚠️ Region {region.get('name')} has too few points") + return + + # ── Project region to scene coordinates + scene_points = [] + for lon, lat in outer_ring: + pos = _latlon_to_xy_at_max_zoom(self.viewer, lat, lon) + if not pos: + continue + xb, yb = pos + sx = (xb - self.viewer._x_min_base) * TILE_SIZE + sy = (yb - self.viewer._y_min_base) * TILE_SIZE + scene_points.append((sx, sy)) + + if len(scene_points) < 3: + print(f"[RegionLayer] ⚠️ Region {region.get('name')} has too few valid projected points") + return + + # ── Clip polygon to field boundaries + poly = Polygon(scene_points) + clipped = poly.intersection(self.scene_bounds) + + if clipped.is_empty: + print(f"[RegionLayer] ⚠️ Region {region.get('name')} lies completely outside field, skipped") + return + + # Merge if multiple fragments (MultiPolygon) + if clipped.geom_type == "MultiPolygon": + clipped = unary_union(clipped) + + # ── Convert back to QPolygonF + def to_qpolygonf(geom): + """Convert Shapely geometry to QPolygonF (skip LineStrings).""" + if geom.is_empty: + return None + + if geom.geom_type == "Polygon": + pts = [QPointF(x, y) for x, y in geom.exterior.coords] + return QPolygonF(pts) + + elif geom.geom_type == "MultiPolygon": + largest = max(geom.geoms, key=lambda g: g.area) + pts = [QPointF(x, y) for x, y in largest.exterior.coords] + return QPolygonF(pts) + + print(f"[RegionLayer] ⚠️ Skipped degenerate geometry ({geom.geom_type}) for region {region['name']}") + return None + + polygon = to_qpolygonf(clipped) + if not polygon: + print(f"[RegionLayer] ⚠️ Region {region['name']} clipped to non-polygon shape → skipped") + return + + # ── Create graphics item + item = QGraphicsPolygonItem(polygon) + item.region_id = region["id"] # ✅ store ID + item.region_name = region["name"] + item.selected = False + item.setZValue(900) + + pen = QPen(QColor("#2563eb")) + pen.setWidthF(1.5) + item.setPen(pen) + + # If this region is among pre-selected IDs, fill it stronger + is_selected = (selected_ids is not None) and (region["id"] in selected_ids) + + base_alpha = 100 if is_selected else 40 + item.setBrush(QColor(37, 99, 235, base_alpha)) + item.selected = bool(is_selected) + + item.setAcceptHoverEvents(True) + item.setFlag(QGraphicsPolygonItem.GraphicsItemFlag.ItemIsSelectable, True) + + # Mouse click toggles selection + item.mousePressEvent = lambda e, it=item: self._toggle_selection(it) + self.scene.addItem(item) + self.regions.append(item) + + print(f"[RegionLayer] ✅ Added region '{item.region_name}' (ID {item.region_id}) " + f"(clipped to {len(clipped.exterior.coords)} vertices)") + + # ───────────────────────────────────────────── + def _toggle_selection(self, item): + """Toggle fill color when selected and trigger callback.""" + item.selected = not item.selected + item.setBrush(QColor(37, 99, 235, 100 if item.selected else 40)) + + if self.on_select: + self.on_select(item.region_id, item.selected) # ✅ send ID instead of name + + # ───────────────────────────────────────────── + def clear(self): + """Remove all region items from the scene.""" + for item in self.regions: + self.scene.removeItem(item) + self.regions.clear() + print("[RegionLayer] Cleared all regions") + # ───────────────────────────────────────────── + def setVisible(self, visible: bool): + """Show or hide all region polygons.""" + for item in self.regions: + item.setVisible(visible) + print(f"[RegionLayer] Visibility set to {visible}") + diff --git a/AgCloud/GUI/src/vast/views/security/analytics/popup_panel.py b/AgCloud/GUI/src/vast/views/security/analytics/popup_panel.py new file mode 100644 index 000000000..f2586e39a --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/analytics/popup_panel.py @@ -0,0 +1,278 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFrame, QGridLayout, QSizePolicy +from PyQt6.QtGui import QColor, QPainter +from PyQt6.QtCore import Qt, QMargins +from PyQt6.QtCharts import ( + QChart, QChartView, QBarSeries, QBarSet, QBarCategoryAxis, + QValueAxis, QLineSeries, QCategoryAxis +) +from PyQt6.QtCharts import QPieSeries # local import to avoid top clutter + +class AnalyticsPanel(QWidget): + """Fixed right-side analytics dashboard panel (2×2 grid layout).""" + + def __init__(self, title: str, data: dict, parent: QWidget | None = None): + super().__init__(parent) + + # ───────────────────────────── + # Palette + # ───────────────────────────── + self.green = "#10b981" + self.black = "#111827" + self.gray = "#6b7280" + self.bg_light = "#f9fafb" + + # ───────────────────────────── + # Layout + # ───────────────────────────── + self.setMinimumWidth(480) + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(12) + + # Title + self.title_label = QLabel(title) + self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.title_label.setStyleSheet(f"font-size:20pt;font-weight:700;color:{self.green};") + layout.addWidget(self.title_label) + + # 2×2 grid + self.grid = QGridLayout() + self.grid.setSpacing(10) + layout.addLayout(self.grid, stretch=1) + + # Populate initial data + self._populate(data) + + # ───────────────────────────── + def update_data(self, title: str, data: dict): + self.title_label.setText(title) + self._populate(data) + + # ───────────────────────────── + def _populate(self, data: dict): + # Clear old widgets + for i in reversed(range(self.grid.count())): + item = self.grid.itemAt(i).widget() + if item: + item.setParent(None) + + # Create 4 panels + # bar_panel = self._section("Alerts by Type", self._make_bar_chart(data.get("alerts_by_type", {}))) + bar_panel = self._section("Alerts by Type", self._make_pie_chart(data.get("alerts_by_type", {}))) + + line_panel = self._section("Alerts per Month", self._make_line_chart(data.get("alerts_per_month", {}))) + summary_panel = self._section("Summary", self._make_summary_panel( + data.get("total_alerts", 0), + data.get("avg_severity", 0), + data.get("avg_confidence", 0) + )) + # details_panel = self._section("Details", self._placeholder("More analytics coming soon...")) + details_panel = self._section( + "Avg Alert Duration (min)", + self._make_bar_chart(data.get("avg_duration_per_type", {})) +) + + # Add to 2×2 grid + self.grid.addWidget(bar_panel, 0, 0) + self.grid.addWidget(line_panel, 0, 1) + self.grid.addWidget(summary_panel, 1, 0) + self.grid.addWidget(details_panel, 1, 1) + + # Equal stretch + self.grid.setRowStretch(0, 1) + self.grid.setRowStretch(1, 1) + self.grid.setColumnStretch(0, 1) + self.grid.setColumnStretch(1, 1) + + # ───────────────────────────── + def _section(self, header_text: str, widget: QWidget): + section = QFrame() + section.setStyleSheet(f"QFrame {{ background:{self.bg_light}; border-radius:10px; }}") + vbox = QVBoxLayout(section) + vbox.setContentsMargins(8, 8, 8, 8) + vbox.setSpacing(6) + header = QLabel(header_text) + header.setStyleSheet(f"color:{self.gray};font-weight:600;font-size:11pt;") + vbox.addWidget(header) + vbox.addWidget(widget) + section.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + return section + + # ───────────────────────────── + # 🔹 Helper: compute nice Y-axis + # ───────────────────────────── + def _setup_nice_y_axis(self, data_values): + axis_y = QValueAxis() + + if not data_values: + axis_y.setRange(0, 3) + axis_y.setTickInterval(1) + return axis_y + + max_val = max(data_values) + upper = max_val * 1.2 + if upper <= 3: + upper = 3.0 + + # Pick a "nice" step: 1, 2, 5, 10, 20, 50, ... + nice_steps = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000] + step = next((s for s in nice_steps if upper / s <= 6), 1000) + upper = (int(upper / step) + 1) * step + + axis_y.setRange(0, upper) + axis_y.setTickInterval(step) + axis_y.setLabelFormat("%.1f" if any(v < 10 for v in data_values) else "%.0f") + # axis_y.setLabelFormat("%.0f") + axis_y.setMinorTickCount(0) + return axis_y + + # ───────────────────────────── + def _make_bar_chart(self, data: dict): + if not data: + return self._placeholder("No data") + + chart = QChart() + chart.legend().setVisible(False) + chart.setBackgroundVisible(False) + chart.setMargins(QMargins(8, 8, 8, 8)) + + # Bar series + bar_set = QBarSet("Alerts") + for v in data.values(): + bar_set.append(float(v)) + series = QBarSeries() + series.append(bar_set) + chart.addSeries(series) + + # X-axis + axis_x = QBarCategoryAxis() + axis_x.append(list(data.keys())) + + # Y-axis (nice scaling) + axis_y = self._setup_nice_y_axis(list(data.values())) + + chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom) + chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft) + series.attachAxis(axis_x) + series.attachAxis(axis_y) + + bar_set.setColor(QColor(self.green)) + + # Chart view + view = QChartView(chart) + view.setRenderHint(QPainter.RenderHint.Antialiasing) + view.setMinimumHeight(200) + view.setStyleSheet("background:transparent;") + return view + # ───────────────────────────── + def _make_pie_chart(self, data: dict): + """Create a pie chart (e.g., Alerts by Type) with distinct green shades.""" + if not data: + return self._placeholder("No data") + + chart = QChart() + chart.legend().setVisible(True) + chart.legend().setAlignment(Qt.AlignmentFlag.AlignBottom) + chart.setBackgroundVisible(False) + chart.setMargins(QMargins(8, 8, 8, 8)) + + # Pie series + series = QPieSeries() + total = sum(data.values()) or 1 + + # Define a clearer set of green shades (light → dark) + green_shades = [ + QColor("#A7F3D0"), # light mint + QColor("#6EE7B7"), # medium mint + QColor("#34D399"), # base green + QColor("#10B981"), # emerald + QColor("#059669"), # dark green + QColor("#047857"), # deeper green + ] + + max_val = max(data.values()) if data else 0 + + for i, (key, val) in enumerate(data.items()): + slice_ = series.append(f"{key} ({val})", float(val)) + slice_.setLabelVisible(True) + + # Pick shade cyclically + color = green_shades[i % len(green_shades)] + slice_.setBrush(color) + slice_.setPen(QColor("#ffffff")) # white borders + slice_.setLabelColor(QColor(self.black)) + + # Slightly explode only the largest slice + # if val == max_val: + # slice_.setExploded(True) + # slice_.setLabelFont(self.font()) + + chart.addSeries(series) + chart.setAnimationOptions(QChart.AnimationOption.AllAnimations) + + # Chart view + view = QChartView(chart) + view.setRenderHint(QPainter.RenderHint.Antialiasing) + view.setMinimumHeight(200) + view.setStyleSheet("background:transparent;") + return view + + + + # ───────────────────────────── + def _make_line_chart(self, data: dict): + if not data: + return self._placeholder("No data") + + sorted_items = sorted(data.items()) + chart = QChart() + chart.legend().setVisible(False) + chart.setBackgroundVisible(False) + chart.setMargins(QMargins(8, 8, 8, 8)) + + # Create line series + series = QLineSeries() + for i, (_, val) in enumerate(sorted_items): + series.append(i, float(val)) + pen = series.pen() + pen.setColor(QColor(self.green)) + pen.setWidth(3) + series.setPen(pen) + chart.addSeries(series) + + # X-axis (months) + axis_x = QCategoryAxis() + for i, (month, _) in enumerate(sorted_items): + axis_x.append(month, i) + + # Y-axis (nice scaling) + axis_y = self._setup_nice_y_axis(list(data.values())) + + chart.addAxis(axis_x, Qt.AlignmentFlag.AlignBottom) + chart.addAxis(axis_y, Qt.AlignmentFlag.AlignLeft) + series.attachAxis(axis_x) + series.attachAxis(axis_y) + + view = QChartView(chart) + view.setRenderHint(QPainter.RenderHint.Antialiasing) + view.setMinimumHeight(200) + view.setStyleSheet("background:transparent;") + return view + + # ───────────────────────────── + def _make_summary_panel(self, total, avg_sev, avg_conf): + label = QLabel( + f"Total Alerts: {total}
" + f"Avg Severity: {avg_sev:.2f}
" + f"Avg Confidence: {avg_conf:.2f}" + ) + label.setAlignment(Qt.AlignmentFlag.AlignCenter) + label.setStyleSheet(f"font-size:12pt;color:{self.black};") + return label + + # ───────────────────────────── + def _placeholder(self, text: str): + lbl = QLabel(text) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl.setStyleSheet("color:#9ca3af;font-size:10pt;font-style:italic;") + return lbl diff --git a/AgCloud/GUI/src/vast/views/security/analytics/sql_generator.py b/AgCloud/GUI/src/vast/views/security/analytics/sql_generator.py new file mode 100644 index 000000000..bf8e5d0a2 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/analytics/sql_generator.py @@ -0,0 +1,243 @@ + + +# #!/usr/bin/env python3 +# # -*- coding: utf-8 -*- + +""" +Free-text → DSL → validated SQL generator for AgGuard analytics dashboard. +Uses your internal DSL schema: +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["..."]}, + {"op": "where", "cond": { ... }}, + {"op": "group_by", "columns": ["..."]}, + {"op": "having", "cond": { ... }}, + {"op": "order_by", "columns": ["..."], "directions": ["ASC"|"DESC"]}, + {"op": "limit", "limit": 50} + ] +} +""" + +import os, json +from openai import OpenAI +from dotenv import load_dotenv +from jsonschema import validate, ValidationError + +# ────────────────────────────────────────────── +# 🔑 Initialize client +# ────────────────────────────────────────────── +load_dotenv() +client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + +# ────────────────────────────────────────────── +# 🧩 DSL schema (the one your DSL actually uses) +# ────────────────────────────────────────────── +QUERY_SCHEMA = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "DSLQueryPlan", + "type": "object", + "properties": { + "source": {"type": "string"}, + "_ops": { + "type": "array", + "items": { + "type": "object", + "properties": { + "op": { + "type": "string", + "enum": [ + "select", "where", "group_by", + "having", "order_by", "limit", "offset" + ] + }, + "columns": { + "type": "array", + "items": { + "type": "string", + + }, + "minItems": 1, + "maxItems": 2, + "uniqueItems": True + }, + "cond": {"type": "object"}, + "directions": { + "type": "array", + "items": {"type": "string", "enum": ["ASC", "DESC"]} + }, + "limit": {"type": "integer", "minimum": 1, "maximum": 500}, + "offset": {"type": "integer", "minimum": 0} + }, + "required": ["op"], + "additionalProperties": True + } + } + }, + "required": ["source", "_ops"], + "additionalProperties": False +} + +# ────────────────────────────────────────────── +# 🧠 DSL-aware system prompt +# ────────────────────────────────────────────── +SYSTEM_PROMPT = """ +You are an expert SQL-to-DSL translator for the AgGuard Analytics Dashboard. +Convert a natural language request into a strict JSON object compatible with the AgGuard DSL. + +────────────────────────────── +📘 TABLE: alerts +────────────────────────────── +CREATE TABLE alerts ( + alert_id TEXT PRIMARY KEY, + alert_type TEXT, + device_id TEXT, + started_at TIMESTAMPTZ, + ended_at TIMESTAMPTZ, + confidence DOUBLE PRECISION, + area TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + severity INT DEFAULT 1, + image_url TEXT, + vod TEXT, + hls TEXT, + ack BOOLEAN DEFAULT FALSE, + meta JSONB, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +Guidelines for using this table: +- Use "alert_id" as the unique identifier. +- Use "started_at" and "ended_at" for time filtering. +- Use "severity" for numerical scoring or comparisons. +- Use "ack" to check whether the alert was acknowledged. +- Use "area", "lat", "lon" for spatial or regional context. +- Use "alert_type" for category filtering. +- Use "device_id" to link to the originating device. +- Avoid creating non-existent columns. + +────────────────────────────── +📘 DSL STRUCTURE +────────────────────────────── +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["..."]}, + {"op": "where", "cond": { ... }}, + {"op": "group_by", "columns": ["..."]}, + {"op": "having", "cond": { ... }}, + {"op": "order_by", "columns": ["..."], "directions": ["ASC"|"DESC"]}, + {"op": "limit", "limit": 50} + ] +} + +────────────────────────────── +📘 CONDITION TREE FORMAT +────────────────────────────── +Conditions use nested AND/OR logic and binary predicates: +- {"all": [ ... ]} → logical AND +- {"any": [ ... ]} → logical OR +- {"op": "", "left": {"col": ""}, "right": {"literal": }} + +Allowed operators: =, !=, <, <=, >, >= + +────────────────────────────── +📘 EXAMPLES +────────────────────────────── +User: "show all alerts with severity >= 4 and not acknowledged" +→ +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["alert_id", "severity", "ack"]}, + {"op": "where", "cond": { + "all": [ + {"op": ">=", "left": {"col": "severity"}, "right": {"literal": 4}}, + {"op": "=", "left": {"col": "ack"}, "right": {"literal": false}} + ] + }} + ] +} + +User: "how many alerts of type fence_hole in the last month" +→ +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["COUNT(*) AS total_alerts"]}, + {"op": "where", "cond": { + "all": [ + {"op": "=", "left": {"col": "alert_type"}, "right": {"literal": "fence_hole"}}, + {"op": ">", "left": {"col": "started_at"}, "right": {"literal": "now() - interval '1 month'"}} + ] + }} + ] +} + +────────────────────────────── +📘 RULES +────────────────────────────── +1. Always use existing alert columns listed above. +2. Never reference tables or aliases like "r." or "d.". +3. Output only valid JSON — never raw SQL. +4. Include aggregates, order, and limit if implied. +5. When selecting entities, the SELECT clause must contain only "device_id" or "area". never include COUNT(*), aggregates, or joins. +6. alert_type is one of the following: masked_person, intruding animal,climbing_fence, fence_hole. +""" + + +# ────────────────────────────────────────────── +# 🧮 Core generator +# ────────────────────────────────────────────── +from src.vast.dsl.ir import Plan +from src.vast.dsl.builder import SQLBuilder +from src.vast.dsl.dialects import PostgresDialect + +def generate_sql_from_prompt(prompt: str) -> tuple[str | None, list]: + """Convert natural language → DSL JSON → validated SQL.""" + response = client.chat.completions.create( + model="gpt-4o-mini", + temperature=0, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": prompt} + ], + response_format={"type": "json_object"} + ) + + obj = json.loads(response.choices[0].message.content) + + try: + validate(instance=obj, schema=QUERY_SCHEMA) + except ValidationError as e: + print("❌ Validation error:", e.message) + return None, [] + + # Compile directly with your DSL + plan = Plan.from_dict(obj) + print(plan) + sql, params = SQLBuilder(PostgresDialect("named")).compile(plan) + print(sql,params) + return sql, params + + +# ────────────────────────────────────────────── +# 🧪 Example usage +# ────────────────────────────────────────────── +if __name__ == "__main__": + user_text = "give me the region that had most alerts last month" + print(f"\n🗣️ User: {user_text}\n") + + sql, params = generate_sql_from_prompt(user_text) + if sql: + print("✅ SQL:\n", sql) + print("🧩 Params:", params) + else: + print("❌ Could not generate SQL.") + + + + + diff --git a/AgCloud/GUI/src/vast/views/security/events_history_page.py b/AgCloud/GUI/src/vast/views/security/events_history_page.py new file mode 100644 index 000000000..b77b46442 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/events_history_page.py @@ -0,0 +1,787 @@ +from PyQt6 import QtWidgets, QtGui, QtCore +import os, sys, vlc +from datetime import datetime +from PyQt6 import sip + + +class EventsHistoryPage(QtWidgets.QWidget): + """AgGuard Security Events History — visual-only severity bar with sorting and fixed filters (with debug prints).""" + + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + self.setContentsMargins(24, 24, 24, 24) + + print("[INIT] EventsHistoryPage initialized") + + # ───────────── GLOBAL STYLE ───────────── + self.setStyleSheet(""" + QWidget { + background-color: #f9fafb; + font-family: 'Segoe UI', 'DejaVu Sans', Arial, sans-serif; + color: #111827; + font-size: 16px; + } + QHeaderView::section { + background-color: #f3f4f6; + color: #111827; + font-weight: 600; + border: none; + padding: 8px; + border-bottom: 1px solid #e5e7eb; + } + QTableWidget { + gridline-color: #e5e7eb; + background-color: #ffffff; + border: 1px solid #d1d5db; + border-radius: 10px; + selection-background-color: #bbf7d0; + selection-color: #065f46; + font-size: 15px; + } + QTableWidget::item { padding: 10px; } + QScrollBar:vertical { + background: transparent; + width: 10px; + margin: 2px; + } + QScrollBar::handle:vertical { + background: #9ca3af; + border-radius: 5px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { background: #6b7280; } + QComboBox, QDateEdit { + background-color: #ffffff; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 4px 10px; + font-size: 14px; + height: 32px; + min-width: 120px; + color: #111827; + } + QComboBox:hover, QDateEdit:hover { + border-color: #9ca3af; + background-color: #f9fafb; + } + QComboBox:focus, QDateEdit:focus { + border: 1px solid #10b981; + background-color: #ffffff; + } + QComboBox QAbstractItemView { + border: none; + background-color: #ffffff; + padding: 6px 4px; + outline: none; + font-size: 16px; + selection-background-color: #10b981; + selection-color: white; + } + QPushButton { + border: none; + border-radius: 6px; + font-weight: 500; + padding: 6px 12px; + } + QPushButton#reload_btn { + background-color: #10b981; + color: white; + font-weight: 600; + } + QPushButton#reload_btn:hover { background-color: #059669; } + QPushButton#clear_btn { + background-color: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; + } + QPushButton#clear_btn:hover { background-color: #e5e7eb; } + QPushButton.view_btn { + background-color: #10b981; + color: white; + padding: 6px 16px; + font-weight: 700; + font-size: 15px; + } + QPushButton.view_btn:hover { background-color: #059669; } + """) + + # ───────────── CONSTANTS ───────────── + self.media_proxy_base = os.getenv("MEDIA_PROXY_BASE", "http://media-proxy:8080").rstrip("/") + self.proxy_local_base = "http://127.0.0.1:19100" + self.all_rows = [] + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setSpacing(18) + + # ───────────── HEADER ───────────── + header = QtWidgets.QHBoxLayout() + title = QtWidgets.QLabel("🧾 Security Events History") + title.setStyleSheet("font-size:22px;font-weight:700;color:#0f172a;") + header.addWidget(title) + header.addStretch(1) + + reload_btn = QtWidgets.QPushButton("Reload") + reload_btn.setObjectName("reload_btn") + reload_btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + reload_btn.clicked.connect(self.load_from_api) + header.addWidget(reload_btn) + main_layout.addLayout(header) + + # ───────────── TOOLBAR ───────────── + toolbar = QtWidgets.QFrame() + toolbar.setStyleSheet(""" + QFrame { + background-color: #ffffff; + border: 1px solid #d1d5db; + border-radius: 14px; + padding: 10px 14px; + } + """) + tl = QtWidgets.QHBoxLayout(toolbar) + tl.setContentsMargins(8, 6, 8, 6) + tl.setSpacing(8) + + self.device_filter = QtWidgets.QComboBox() + self.device_filter.addItem("All Devices") + self.device_filter.currentIndexChanged.connect(self.apply_filters) + + self.anomaly_filter = QtWidgets.QComboBox() + self.anomaly_filter.addItem("All Anomalies") + self.anomaly_filter.currentIndexChanged.connect(self.apply_filters) + + self.severity_slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) + self.severity_slider.setRange(0, 6) + self.severity_slider.setFixedWidth(110) + self._update_slider_style(0) + self.severity_slider.valueChanged.connect(self._update_slider_style) + self.severity_slider.valueChanged.connect(self.apply_filters) + + self.from_date = QtWidgets.QDateEdit(QtCore.QDate.currentDate().addMonths(-1)) + self.from_date.setDisplayFormat("yyyy-MM-dd") + self.from_date.setCalendarPopup(True) + self.from_date.dateChanged.connect(self.apply_filters) + + self.to_date = QtWidgets.QDateEdit(QtCore.QDate.currentDate()) + self.to_date.setDisplayFormat("yyyy-MM-dd") + self.to_date.setCalendarPopup(True) + self.to_date.dateChanged.connect(self.apply_filters) + + self.sort_combo = QtWidgets.QComboBox() + self.sort_combo.addItems([ + "No Sorting", + "Severity (High → Low)", + "Severity (Low → High)", + "Start Time (Newest)", + "Start Time (Oldest)", + "End Time (Newest)", + "End Time (Oldest)", + "Anomaly (A → Z)", + "Anomaly (Z → A)" + ]) + self.sort_combo.currentIndexChanged.connect(self.apply_filters) + + clear_btn = QtWidgets.QPushButton("Clear") + clear_btn.setObjectName("clear_btn") + clear_btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + clear_btn.clicked.connect(self.clear_filters) + + for w in [ + self.device_filter, self.anomaly_filter, + self.severity_slider, self.from_date, self.to_date, + self.sort_combo + ]: + tl.addWidget(w) + tl.addStretch(1) + tl.addWidget(clear_btn) + main_layout.addWidget(toolbar) + + # ───────────── TABLE ───────────── + self.table = QtWidgets.QTableWidget() + self.table.setColumnCount(8) + self.table.setHorizontalHeaderLabels([ + "Device", "Anomaly", "Start Time", "End Time", + "Duration (m)", "Severity", "View", "Feedback" + ]) + + self.table.verticalHeader().setVisible(False) + self.table.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.Stretch) + self.table.verticalHeader().setDefaultSectionSize(56) + main_layout.addWidget(self.table, 1) + # QtCore.QTimer.singleShot(300, self.load_from_api) + self._load_timer = QtCore.QTimer(self) + self._load_timer.setSingleShot(True) + self._load_timer.timeout.connect(self._safe_load) + self._load_timer.start(300) + def _safe_load(self): + if not self.isVisible() or sip.isdeleted(self.table): + print("[SAFE_LOAD] Skipping load: widget closed or deleted.") + return + self.load_from_api() + def showEvent(self, event): + super().showEvent(event) + if not getattr(self, "_loaded_once", False): + self._loaded_once = True + self.load_from_api() + + + def closeEvent(self, event): + print("[CLOSE] EventsHistoryPage closing — stopping load timer.") + if hasattr(self, "_load_timer") and self._load_timer.isActive(): + self._load_timer.stop() + return super().closeEvent(event) + + + + + + + # ───────────── SLIDER STYLE ───────────── + def _update_slider_style(self, value): + percent = value / 6 if value else 0 + self.severity_slider.setStyleSheet(f""" + QSlider::groove:horizontal {{ + border: 1px solid #d1d5db; + height: 6px; + border-radius: 3px; + background: qlineargradient( + x1:0, y1:0, x2:1, y2:0, + stop:0 #10b981, + stop:{percent} #10b981, + stop:{percent} white, + stop:1 white + ); + }} + QSlider::handle:horizontal {{ + width: 16px; + height: 16px; + background: #10b981; + border-radius: 8px; + margin: -5px 0; + border: 1px solid #10b981; + }} + """) + + # ───────────── LOGIC ───────────── + def _safe_int(self, val): + try: + return int(val) + except Exception: + return 0 + + from datetime import datetime + + def _parse_time(self, t): + if not t: + return None + + try: + # Normalize variants: + # "2025-11-14 07:25:04+02:00" + # "2025-11-14T07:25:04+02:00" + # "2025-11-14 07:25:04Z" + nt = t.replace(" ", "T").replace("Z", "+00:00") + dt = datetime.fromisoformat(nt) + + # Convert to naive local time for table filtering + return dt.astimezone().replace(tzinfo=None) + + except Exception: + return None + + + def _fmt_time(self, t): + dt = self._parse_time(t) + if not dt: + return "-" + return dt.strftime("%Y-%m-%d %H:%M:%S") + + + def load_from_api(self): + print("[API] Fetching alerts from:", f"{self.api.base}/api/tables/alerts") + try: + url = f"{self.api.base}/api/tables/alerts?limit=500" + resp = self.api.http.get(url, timeout=8) + resp.raise_for_status() + data = resp.json() + + # Expect structure: {"rows": [...], "count": N} + if isinstance(data, dict) and "rows" in data: + rows = data["rows"] + count = data.get("count", len(rows)) + print(f"[API] Loaded {count} total alerts.") + else: + rows = data if isinstance(data, list) else [] + print(f"[API][WARN] Unexpected format, using raw list of {len(rows)} items.") + + # ───── Filter only relevant alert types ───── + allowed_types = {"climbing_fence", "masked_person", "intruding animal","fence_hole"} + filtered = [r for r in rows if (r.get("alert_type") or "").strip() in allowed_types] + + print(f"[API] Filtered {len(filtered)} / {len(rows)} alerts matching allowed types {allowed_types}.") + + self.all_rows = filtered + + except Exception as e: + print("[API][ERROR]", e) + QtWidgets.QMessageBox.warning(self, "Error", f"Failed to fetch alerts:\n{e}") + return + + # Update the table and filters + self.populate_table(self.all_rows) + self.populate_filters() + + + + def populate_filters(self): + devices = sorted({it.get("device_id") or "-" for it in self.all_rows}) + anomalies = sorted({it.get("alert_type") or "-" for it in self.all_rows}) + + print(f"[FILTERS] Available devices={devices}") + print(f"[FILTERS] Available anomalies={anomalies}") + + # devices + self.device_filter.blockSignals(True) + self.device_filter.clear() + self.device_filter.addItem("All Devices", None) + for d in devices: + self.device_filter.addItem(d, d) + self.device_filter.blockSignals(False) + + # anomalies (friendly display) + self.anomaly_filter.blockSignals(True) + self.anomaly_filter.clear() + self.anomaly_filter.addItem("All Anomalies", None) + for a in anomalies: + label = a.replace("_", " ").title() if a and a != "-" else a + self.anomaly_filter.addItem(label, a) + self.anomaly_filter.blockSignals(False) + + print("[FILTERS] Filters populated.") + + def apply_filters(self): + if not self.all_rows: + print("[FILTER] No rows loaded yet.") + return + + device = self.device_filter.currentData() + anomaly = self.anomaly_filter.currentData() + min_sev = self._safe_int(self.severity_slider.value()) + from_dt = datetime.combine(self.from_date.date().toPyDate(), datetime.min.time()) + to_dt = datetime.combine(self.to_date.date().toPyDate(), datetime.max.time()) + + print(f"\n[FILTER] Applying filters:") + print(f" device={device}, anomaly={anomaly}, min_sev={min_sev},") + print(f" from={from_dt}, to={to_dt}") + print(f" total rows={len(self.all_rows)}") + + filtered = [] + for idx, it in enumerate(self.all_rows): + dev = it.get("device_id") or "-" + anom = it.get("alert_type") or "-" + sev = self._safe_int(it.get("severity")) + started = self._parse_time(it.get("started_at")) + + include = True + reasons = [] + + # Device filter + if device and dev != device: + include = False + reasons.append(f"device mismatch ({dev} ≠ {device})") + + # Anomaly filter + if anomaly and anom != anomaly: + include = False + reasons.append(f"anomaly mismatch ({anom} ≠ {anomaly})") + + # Severity filter + if sev < min_sev: + include = False + reasons.append(f"severity too low ({sev} < {min_sev})") + + # Date filter + if started: + if not (from_dt <= started <= to_dt): + include = False + reasons.append(f"date {started} out of range [{from_dt}, {to_dt}]") + else: + reasons.append("no start date parsed") + + if include: + filtered.append(it) + else: + print(f"[FILTER][X] Row {idx} excluded — {', '.join(reasons)}") + + print(f"[FILTER] {len(filtered)} / {len(self.all_rows)} rows matched filters.\n") + + # Sorting + i = self.sort_combo.currentIndex() + keymap = { + 1: lambda x: self._safe_int(x.get("severity")), + 2: lambda x: self._safe_int(x.get("severity")), + 3: lambda x: self._parse_time(x.get("started_at")) or datetime.min, + 4: lambda x: self._parse_time(x.get("started_at")) or datetime.min, + 5: lambda x: self._parse_time(x.get("ended_at")) or datetime.min, + 6: lambda x: self._parse_time(x.get("ended_at")) or datetime.min, + 7: lambda x: (x.get("alert_type") or "").lower(), + 8: lambda x: (x.get("alert_type") or "").lower(), + } + + if i in keymap: + reverse = i in (1, 3, 5, 8) + print(f"[SORT] Sorting index={i}, reverse={reverse}") + filtered.sort(key=keymap[i], reverse=reverse) + else: + print("[SORT] No sorting applied.") + + self.populate_table(filtered) + + + def clear_filters(self): + print("[FILTER] Clearing filters to defaults.") + self.device_filter.setCurrentIndex(0) + self.anomaly_filter.setCurrentIndex(0) + self.sort_combo.setCurrentIndex(0) + self.severity_slider.setValue(0) + self.from_date.setDate(QtCore.QDate.currentDate().addMonths(-1)) + self.to_date.setDate(QtCore.QDate.currentDate()) + self.apply_filters() + + def _severity_color(self, sev: int) -> str: + """Return green intensity from white (low) to dark green (high).""" + sev = max(1, min(sev, 9)) + # interpolate white (#ffffff) → dark green (#059669) + def lerp_color(c1, c2, t): + c1, c2 = [int(c1[i:i+2], 16) for i in (1, 3, 5)], [int(c2[i:i+2], 16) for i in (1, 3, 5)] + mix = [round(c1[j] + (c2[j]-c1[j])*t) for j in range(3)] + return f"#{mix[0]:02x}{mix[1]:02x}{mix[2]:02x}" + return lerp_color("#ffffff", "#059669", sev / 9) + + def _severity_label(self, sev: int) -> str: + if sev <= 3: + return f"Low ({sev})" + elif sev <= 6: + return f"Medium ({sev})" + else: + return f"Critical ({sev})" + + + + def populate_table(self, rows): + if not hasattr(self, "table") or self.table is None: + print("[TABLE][WARN] Table not available — widget probably closed.") + return + if sip.isdeleted(self.table): + print("[TABLE][WARN] Table was deleted, aborting populate.") + return + print(f"[TABLE] Populating table with {len(rows)} alerts.") + self.table.setRowCount(len(rows)) + + for r, it in enumerate(rows): + # Device + self.table.setItem(r, 0, QtWidgets.QTableWidgetItem(it.get("device_id") or "-")) + + # Anomaly + # Anomaly + alert_type = it.get("alert_type") or "-" + raw_meta = it.get("meta") or {} + meta = {} + + if isinstance(raw_meta, dict): + meta = raw_meta + elif isinstance(raw_meta, str): + try: + meta = json.loads(raw_meta) + except Exception: + try: + import ast + meta = ast.literal_eval(raw_meta) + except Exception: + meta = {} + + subject = meta.get("subject") + + + label = alert_type.replace("_", " ").title() + if alert_type in ("intruding_animal","intruding animal", "climbing_fence") and subject: + label = f"{label} ({subject.title()})" + self.table.setItem(r, 1, QtWidgets.QTableWidgetItem(label)) + + + # Start / End time + self.table.setItem(r, 2, QtWidgets.QTableWidgetItem(self._fmt_time(it.get("started_at")))) + self.table.setItem(r, 3, QtWidgets.QTableWidgetItem(self._fmt_time(it.get("ended_at")))) + + # Duration (minutes) + started = self._parse_time(it.get("started_at")) + ended = self._parse_time(it.get("ended_at")) + duration_m = "-" + if started and ended: + duration_m = f"{(ended - started).total_seconds() / 60:.1f}" + self.table.setItem(r, 4, QtWidgets.QTableWidgetItem(duration_m)) + + + + ## ────── SEVERITY BAR ────── + sev = self._safe_int(it.get("severity")) + sev = max(0, min(sev, 9)) # allow 0–9 + fill = sev / 9.0 # proportional fill + + if sev == 0: + label_text = "None" + color = "#f3f4f6" + elif sev <= 3: + label_text = "Low" + color = "#a7f3d0" + elif sev <= 6: + label_text = "Medium" + color = "#34d399" + else: + label_text = "High" + color = "#059669" + + # Background container + container = QtWidgets.QFrame() + container.setFixedHeight(20) + container.setStyleSheet(""" + QFrame { + background: #e5e7eb; + border: 1px solid #d1d5db; + border-radius: 8px; + } + """) + + layout = QtWidgets.QGridLayout(container) + layout.setContentsMargins(1, 1, 1, 1) + layout.setSpacing(0) + + fill_bar = QtWidgets.QFrame(container) + fill_bar.setStyleSheet(f"background-color: {color}; border-radius: 7px;") + + # ↓ reduce bar width from 90 → 70 for better balance + container_width = 150 + fill_bar.setFixedWidth(int(container_width * fill)) + + layout.addWidget(fill_bar, 0, 0) + layout.setColumnStretch(0, 0) + layout.setColumnStretch(1, 1) + + label = QtWidgets.QLabel(label_text, container) + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + label.setStyleSheet("font-weight:600; color:#064e3b; background:transparent; font-size:12px;") + layout.addWidget(label, 0, 0, 1, 2) + + wrapper = QtWidgets.QWidget() + outer = QtWidgets.QHBoxLayout(wrapper) + outer.setContentsMargins(2, 0, 2, 0) + outer.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + outer.addWidget(container) + self.table.setCellWidget(r, 5, wrapper) + + + + # Centered “View” button for vod + btn = QtWidgets.QPushButton("View") + btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + btn.setFixedHeight(26) + btn.setFixedWidth(65) + btn.setStyleSheet(""" + QPushButton { + background-color: #10b981; + color: white; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + padding: 3px 6px; + } + QPushButton:hover { + background-color: #059669; + } + """) + btn.clicked.connect(lambda _, info=it: self._open_video_player(info)) + + btn_container = QtWidgets.QWidget() + btn_layout = QtWidgets.QHBoxLayout(btn_container) + btn_layout.setContentsMargins(0, 0, 0, 0) + btn_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + btn_layout.addWidget(btn) + self.table.setCellWidget(r, 6, btn_container) + + + # ────── FEEDBACK (visible circular emoji buttons — no custom class) ────── + feedback_widget = QtWidgets.QWidget(self.table) + layout = QtWidgets.QHBoxLayout(feedback_widget) + layout.setContentsMargins(0, 6, 0, 6) + layout.setSpacing(12) + layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + thumb_up = QtWidgets.QPushButton("👍") + thumb_down = QtWidgets.QPushButton("👎") + + for btn in (thumb_up, thumb_down): + btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor) + btn.setCheckable(True) + btn.setFixedSize(48, 48) + btn.setStyleSheet(""" + QPushButton { + background-color: white; + border-radius: 24px; + border: 2px solid #d1d5db; + font-size: 20px; + } + QPushButton:hover { + background-color: #f3f4f6; + border-color: #9ca3af; + } + QPushButton:checked { + background-color: #e5e7eb; + border-color: #4b5563; + } + """) + + feedback_widget.thumb_up = thumb_up + feedback_widget.thumb_down = thumb_down + + # Restore state + meta = it.get("meta") or {} + if isinstance(meta, str): + import json + try: + meta = json.loads(meta) + except Exception: + meta = {} + is_real = meta.get("is_real") + if is_real is True: + thumb_up.setChecked(True) + elif is_real is False: + thumb_down.setChecked(True) + + def handle_feedback_change(checked, alert=it, up_btn=thumb_up, down_btn=thumb_down): + if not checked: + return + is_real_value = up_btn.isChecked() + down_btn.blockSignals(True) + down_btn.setChecked(not is_real_value) + down_btn.blockSignals(False) + self._send_feedback(alert, is_real_value) + + thumb_up.toggled.connect(handle_feedback_change) + thumb_down.toggled.connect(handle_feedback_change) + + layout.addWidget(thumb_up) + layout.addWidget(thumb_down) + feedback_widget.setLayout(layout) + + self.table.setRowHeight(r, 68) + # Wrap feedback inside a centering wrapper + cell_wrapper = QtWidgets.QWidget() + cell_layout = QtWidgets.QHBoxLayout(cell_wrapper) + cell_layout.setContentsMargins(0, 0, 0, 0) + cell_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + cell_layout.addWidget(feedback_widget) + + self.table.setCellWidget(r, 7, cell_wrapper) + + + print("[TABLE] Done populating alerts table.") + + + + def _open_video_player(self, info): + print(f"[VIEW] Opening media for alert={info.get('alert_id')}") + + vod_url = info.get("vod") + image_url = info.get("image_url") + + if vod_url: + print("[VIEW] Found VOD — playing video.") + proxy_url = f"http://127.0.0.1:19100/vod?u={self.media_proxy_base}/vod/{vod_url}" + self._show_vlc_popup(proxy_url) + return + + if image_url: + print("[VIEW] No VOD, found image — showing image popup.") + image_url = f"http://127.0.0.1:19100/vod?u={self.media_proxy_base}/img/{image_url}" + self._show_image_popup(image_url) + return + + QtWidgets.QMessageBox.warning(self, "No Media", "This alert has neither video nor image available.") + + def _show_image_popup(self, url: str): + print(f"[IMAGE] Displaying still image from {url}") + popup = QtWidgets.QDialog(self) + popup.setWindowTitle("Incident Image") + popup.setMinimumSize(640, 480) + layout = QtWidgets.QVBoxLayout(popup) + + label = QtWidgets.QLabel() + label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + layout.addWidget(label, 1) + + # Fetch image via Qt network + from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest + self._manager = QNetworkAccessManager() + + def handle_reply(reply): + data = reply.readAll() + pixmap = QtGui.QPixmap() + if pixmap.loadFromData(data): + label.setPixmap(pixmap.scaled(label.size(), QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation)) + else: + label.setText("❌ Failed to load image.") + self._manager.finished.connect(handle_reply) + + req = QNetworkRequest(QtCore.QUrl(url)) + self._manager.get(req) + popup.exec() + + def _show_vlc_popup(self, url): + print(f"[VIDEO] Playing URL: {url}") + popup = QtWidgets.QDialog(self) + popup.setWindowTitle("Incident Video Playback") + popup.setMinimumSize(640, 400) + vbox = QtWidgets.QVBoxLayout(popup) + player = QtWidgets.QFrame() + player.setStyleSheet("background:black;border-radius:8px;") + vbox.addWidget(player, 1) + inst = vlc.Instance(["--quiet", "--no-video-title-show"]) + mp = inst.media_player_new() + mp.set_media(inst.media_new(url)) + popup.show() + if sys.platform.startswith("win"): + mp.set_hwnd(int(player.winId())) + else: + mp.set_xwindow(int(player.winId())) + mp.play() + print("[VIDEO] Playback started.") + + + def _send_feedback(self, alert: dict, is_real: bool): + """Send user feedback (👍/👎) using API contract.""" + alert_id = alert.get("alert_id") + if not alert_id: + print("[FEEDBACK][WARN] Missing alert_id; cannot update.") + return + + payload = { + "keys": {"alert_id": alert_id}, + "data": {"meta": {**(alert.get("meta") or {}), "is_real": is_real}} + } + + url = f"{self.api.base}/api/tables/alerts" + print(f"[FEEDBACK] PATCH {url} with {payload}") + + try: + resp = self.api.http.patch(url, json=payload, timeout=6) + resp.raise_for_status() + result = resp.json() + print(f"[FEEDBACK] ✅ Updated meta.is_real={is_real}, affected={result.get('affected_rows')}") + except Exception as e: + print("[FEEDBACK][ERROR]", e) + QtWidgets.QMessageBox.warning(self, "Feedback Error", f"Failed to update feedback:\n{e}") + + \ No newline at end of file diff --git a/AgCloud/GUI/src/vast/views/security/incident_player_vlc.py b/AgCloud/GUI/src/vast/views/security/incident_player_vlc.py new file mode 100644 index 000000000..8f4bf1b4b --- /dev/null +++ b/AgCloud/GUI/src/vast/views/security/incident_player_vlc.py @@ -0,0 +1,1878 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +AgGuard Incident Player — PyQt6 + python-vlc with a tiny DVR proxy. + +What’s new in this build: +- Dynamic live lag: on any segment 404/410, the proxy temporarily hides more + tail segments in /live.m3u8 so VLC never requests unavailable parts. + (Decay back to normal once stable.) +- No DVR freeze on resolve (removed items disappear; playback stops/advances). +- No-cache headers on HLS endpoints. +""" + +from __future__ import annotations +import sys, os, asyncio, threading, time, re, json +from dataclasses import dataclass +from typing import Optional, List, Tuple +from urllib.parse import urljoin, urlparse, urlunparse + +from PyQt6 import QtCore, QtWidgets, QtGui +from PyQt6.QtCore import Qt, QUrl, QTimer +from PyQt6.QtWebSockets import QWebSocket +from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest +import vlc # python-vlc +from aiohttp import web, ClientSession +from vast.views.security.events_history_page import EventsHistoryPage + +from src.vast.views.security.analytics.analytics_page import GeoAnalyticsView + + + +# ────────────────────────────────────────────────────────────────────────────── +# Config +# ────────────────────────────────────────────────────────────────────────────── +class Config: + MEDIA_BASE = os.getenv("MEDIA_BASE", "http://media-proxy:8080") + INCIDENT = os.getenv("INCIDENT", "placeholder") + TOKEN = os.getenv("MEDIA_TOKEN", "CHANGE_ME") + BIND = os.getenv("BIND", "127.0.0.1") + PORT = int(os.getenv("PORT", "19100")) + + # Poll upstream playlist ~2–4x per segment (1.0s segments -> 300ms is good) + REFRESH_MS = int(os.getenv("REFRESH_MS", "20000")) + + # Show this many segments in the live window… + LIVE_EDGE_SEGMENTS = int(os.getenv("LIVE_EDGE_SEGMENTS", "3")) + # …but hide the freshest N (stay behind live edge to avoid stalls) + LIVE_LAG_SEGMENTS = int(os.getenv("LIVE_LAG_SEGMENTS", "1")) + + # VLC network caching (ms) + NETWORK_CACHING = int(os.getenv("NETWORK_CACHING", "320")) + + ALERTS_WS = os.getenv("ALERTS_WS", "ws://host.docker.internal:8010/ws/alerts") + ALERTS_SNAPSHOT_HTTP = os.getenv("ALERTS_SNAPSHOT_HTTP", "") + ALLOWED_TYPES = {"climbing_fence", "masked_person","intruding animal"} +# ────────────────────────────────────────────────────────────────────────────── +# Upstream fetcher + DVR state +# ────────────────────────────────────────────────────────────────────────────── +@dataclass +class Segment: + uri: str + duration: float + abs_url: str # absolute URL to fetch + +class DvrState: + def __init__(self, upstream_index_url: str, auth_token: str = "", refresh_ms: int = 800): + self.upstream_index_url = upstream_index_url + self.auth_token = auth_token + self.refresh_ms = refresh_ms + self.init_url: Optional[str] = None + self.target_duration: float = 1.0 + self.version: int = 6 + self.segments: List[Segment] = [] + self._last_playlist_text: Optional[str] = None + self._stop = False + self._ready_evt = threading.Event() + self._lock = threading.Lock() + + @staticmethod + def _absolutize(base: str, maybe_rel: str) -> str: + return urljoin(base, maybe_rel) + + async def _fetch_text(self, session: ClientSession, url: str) -> Tuple[int, str]: + headers = {} + if self.auth_token: + headers["Authorization"] = f"Bearer {self.auth_token}" + async with session.get(url, headers=headers, timeout=10) as resp: + txt = await resp.text() + status = resp.status + if status == 200 and txt.lstrip().startswith("#EXTM3U"): + print(f"[DVR] fetched playlist {status}, {len(txt)} bytes") + else: + print(f"[DVR] upstream status={status}, body[:120]={txt[:120]!r}") + return status, txt + + def stop(self): + self._stop = True + self._ready_evt.set() + + async def run(self): + async with ClientSession() as session: + base = self.upstream_index_url + base_dir = base.rsplit("/", 1)[0] + "/" + while not self._stop: + try: + status, text = await self._fetch_text(session, base) + + # Hard-stop conditions: upstream removed/closed + if status in (404, 410): + print(f"[DVR] upstream gone (HTTP {status}); stop polling") + self.stop() + break + + # Always parse; de-dupe by URL prevents dupes + if text.lstrip().startswith("#EXTM3U"): + self._parse_and_update(text, base_dir) + self._last_playlist_text = text + self._ready_evt.set() + else: + if text != self._last_playlist_text: + self._last_playlist_text = text + print("[DVR] NOTE: got non-HLS body; will retry.") + except Exception as e: + print(f"[DVR] fetch error: {e!r}") + await asyncio.sleep(self.refresh_ms / 1000.0) + + def _parse_and_update(self, playlist_text: str, base_dir: str): + lines = [l.strip() for l in playlist_text.splitlines() if l.strip()] + + target_from_tag: Optional[float] = None + max_seen_extinf = 0.0 + for l in lines: + if l.startswith('#EXT-X-TARGETDURATION:'): + try: + target_from_tag = float(l.split(':', 1)[1]) + except Exception: + pass + elif l.startswith('#EXT-X-VERSION:'): + try: + self.version = int(l.split(':', 1)[1]) + except Exception: + pass + elif l.startswith('#EXT-X-MAP:'): + m = re.search(r'URI="([^"]+)"', l) + if m: + self.init_url = self._absolutize(base_dir, m.group(1)) + elif l.startswith('#EXTINF:'): + try: + d = float(l.split(':', 1)[1].split(',')[0]) + max_seen_extinf = max(max_seen_extinf, d) + except Exception: + pass + + new_segments: List[Segment] = [] + i = 0 + while i < len(lines): + l = lines[i] + if l.startswith('#EXTINF:'): + try: + dur = float(l.split(':', 1)[1].split(',')[0]) + except Exception: + dur = self.target_duration or 1.0 + j = i + 1 + while j < len(lines) and lines[j].startswith('#'): + j += 1 + if j < len(lines): + uri = lines[j] + absu = self._absolutize(base_dir, uri) + new_segments.append(Segment(uri=uri, duration=dur, abs_url=absu)) + i = j + 1 + continue + i += 1 + + if target_from_tag is None or target_from_tag <= 0: + self.target_duration = max(1.0, max_seen_extinf or self.target_duration or 1.0) + else: + self.target_duration = target_from_tag + + added = 0 + with self._lock: + seen_urls = {s.abs_url for s in self.segments} + for s in new_segments: + if s.abs_url not in seen_urls: + self.segments.append(s) + seen_urls.add(s.abs_url) + added += 1 + if added: + print(f"[DVR] +{added} segments (total={len(self.segments)})") + + def render_dvr_vod_playlist(self, *, endlist: bool = False) -> Tuple[str, float]: + with self._lock: + segs = list(self.segments) + init_url = self.init_url + target = int(max(1.0, self.target_duration)) + version = self.version + + total = sum(s.duration for s in segs) + + out: List[str] = [] + out.append('#EXTM3U') + out.append(f'#EXT-X-VERSION:{version}') + out.append('#EXT-X-PLAYLIST-TYPE:EVENT') + out.append('#EXT-X-INDEPENDENT-SEGMENTS') + out.append(f'#EXT-X-TARGETDURATION:{target}') + out.append(f'#EXT-X-MEDIA-SEQUENCE:0') + + if init_url: + out.append(f'#EXT-X-MAP:URI="/seg?u={init_url}"') + + for s in segs: + out.append(f'#EXTINF:{s.duration:.3f},') + out.append(f'/seg?u={s.abs_url}') + + if endlist: + out.append('#EXT-X-ENDLIST') + + return "\n".join(out) + "\n", float(total) + +# ────────────────────────────────────────────────────────────────────────────── +# Aiohttp proxy app +# ────────────────────────────────────────────────────────────────────────────── +import socket + +def is_port_in_use(port=19090, host="127.0.0.1"): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex((host, port)) == 0 + +class ProxyServer: + def __init__(self, media_base: str, camera: Optional[str], incident: Optional[str], + token: str, refresh_ms: int, bind: str, port: int): + self.media_base = media_base.rstrip('/') + self.camera = camera + self.incident = incident + self.token = token + self.refresh_ms = refresh_ms + self.bind = bind + self.port = port + + self.upstream_index: Optional[str] = None + self.dvr: Optional[DvrState] = None + self.resolved: bool = False + + # Dynamic lag control + self._last_seg_404_ts: float = 0.0 # monotonic timestamp of last 404/410 + self._extra_lag_floor: int = 0 # can be bumped to 1–2 and decays over time + + self._app = web.Application() + self._app.router.add_get('/dvr.m3u8', self.handle_dvr) + self._app.router.add_get('/live.m3u8', self.handle_live) + self._app.router.add_get('/seg', self.handle_seg) + self._app.router.add_get('/', self.handle_root) + self._app.router.add_get('/dvr_seek.m3u8', self.handle_dvr_seek) + self._app.router.add_get("/vod", self.handle_vod) + self._app.router.add_get("/img", self.handle_img) + + + + # DEBUG routes + self._app.router.add_get('/debug/upstream', self.handle_debug_upstream) + self._app.router.add_get('/debug/dvr', self.handle_debug_dvr) + self._app.router.add_get('/debug/state', self.handle_debug_state) + + self._runner: Optional[web.AppRunner] = None + self._thread: Optional[threading.Thread] = None + self._loop: Optional[asyncio.AbstractEventLoop] = None + + async def handle_img(self, request): + img_url = request.query.get("u") + if not img_url: + raise web.HTTPBadRequest(text="missing u") + + headers = {} + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + + try: + async with ClientSession() as session: + async with session.get(img_url, headers=headers, timeout=15) as resp: + body = await resp.read() + ctype = resp.headers.get("Content-Type", "image/jpeg") + return web.Response(body=body, content_type=ctype, status=resp.status, headers=self._nocache_headers()) + except Exception as e: + print(f"[HTTP] image fetch error: {e!r}") + return web.Response(text=f"image fetch error: {type(e).__name__}: {e}", content_type="text/plain", status=502) + + + # no-cache headers helper + def _nocache_headers(self) -> dict: + return { + "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", + "Pragma": "no-cache", + "Expires": "0", + } + + # quick helper so UI knows totals + def get_durations_ms(self) -> Tuple[int, int]: + if not self.dvr: + return (0, 0) + with self.dvr._lock: + segs = list(self.dvr.segments) + total_ms = int(sum(s.duration for s in segs) * 1000) + edge = max(1, int(getattr(Config, "LIVE_EDGE_SEGMENTS", 3))) + lag = max(0, int(getattr(Config, "LIVE_LAG_SEGMENTS", 0))) + # Apply dynamic lag here too so UI stays coherent with playlist + lag += self._current_extra_lag() + keep = min(len(segs), max(1, edge + lag)) + last = segs[-keep:] if keep <= len(segs) else segs + live_win_ms = int(sum(s.duration for s in last) * 1000) + return (total_ms, live_win_ms) + async def handle_vod(self, request): + vod_url = request.query.get("u") + if not vod_url: + raise web.HTTPBadRequest(text="missing u") + + if not vod_url.startswith(("http://", "https://")): + vod_url = f"http://{vod_url.lstrip('/')}" + + headers = {} + if self.token: + headers["Authorization"] = f"Bearer {self.token}" + + range_hdr = request.headers.get("Range") + if range_hdr: + headers["Range"] = range_hdr + + try: + async with ClientSession() as session: + async with session.get(vod_url, headers=headers, timeout=None) as resp: + response_headers = { + "Content-Type": resp.headers.get("Content-Type", "video/mp4"), + "Accept-Ranges": resp.headers.get("Accept-Ranges", "bytes"), + **self._nocache_headers(), + } + if "Content-Length" in resp.headers: + response_headers["Content-Length"] = resp.headers["Content-Length"] + if "Content-Range" in resp.headers: + response_headers["Content-Range"] = resp.headers["Content-Range"] + + print(f"[HTTP] vod {resp.status} -> {vod_url} " + f"({resp.headers.get('Content-Length', '?')} bytes, range={range_hdr})") + + proxy_resp = web.StreamResponse(status=resp.status, headers=response_headers) + await proxy_resp.prepare(request) + + try: + async for chunk in resp.content.iter_chunked(8192): + await proxy_resp.write(chunk) + await proxy_resp.write_eof() + except (asyncio.CancelledError, ConnectionResetError, ClientConnectionError, ClientPayloadError) as e: + # ✅ harmless — VLC moved to another range + print(f"[HTTP] client disconnected early ({type(e).__name__}) — OK") + except Exception as e: + print(f"[HTTP] stream write error: {type(e).__name__}: {e}") + finally: + await proxy_resp.write_eof() + + return proxy_resp + + except Exception as e: + print(f"[HTTP] vod fetch error: {e!r} <- {vod_url}") + return web.Response( + text=f"vod fetch error: {type(e).__name__}: {e}", + content_type="text/plain", + status=502, + headers=self._nocache_headers(), + ) + + + # Dynamic lag amount based on recent 404s + def _current_extra_lag(self) -> int: + now = time.monotonic() + extra = 0 + if self._last_seg_404_ts > 0: + dt = now - self._last_seg_404_ts + # immediately after a 404, be conservative with +2; + # after 10s, ease to +1; after 30s, back to +0 + if dt < 10: + extra = 2 + elif dt < 30: + extra = 1 + else: + extra = 0 + # floor in case we had repeated issues and want to hold higher lag briefly + extra = max(extra, self._extra_lag_floor) + # decay the floor gently + if self._extra_lag_floor and (now - self._last_seg_404_ts) > 20: + self._extra_lag_floor = max(0, self._extra_lag_floor - 1) + return extra + + def _bump_extra_lag(self, floor_to: int): + self._last_seg_404_ts = time.monotonic() + self._extra_lag_floor = max(self._extra_lag_floor, floor_to) + print(f"[LIVE] segment 404/410 observed → increasing effective lag (floor={self._extra_lag_floor})") + + # DEBUG HANDLERS + async def handle_debug_upstream(self, _request: web.Request): + if not self.upstream_index: + return web.Response(text="(no upstream_index yet)\n", content_type="text/plain") + headers = {} + if self.token: + headers['Authorization'] = f'Bearer {self.token}' + try: + async with ClientSession() as session: + async with session.get(self.upstream_index, headers=headers, timeout=10) as resp: + body = await resp.text() + out = [ + f"URL: {self.upstream_index}", + f"HTTP {resp.status}", + "", + body + ] + print(f"[HTTP] debug_upstream {resp.status}") + return web.Response(text="\n".join(out), content_type='text/plain', status=resp.status, headers=self._nocache_headers()) + except Exception as e: + return web.Response(text=f"fetch error: {type(e).__name__}: {e}\n", content_type="text/plain", status=500, headers=self._nocache_headers()) + + async def handle_debug_dvr(self, _request: web.Request): + if not self.dvr: + return web.Response(text="(no DVR yet)\n", content_type="text/plain", headers=self._nocache_headers()) + m3u8, total = self.dvr.render_dvr_vod_playlist(endlist=self.resolved) + hdr = f"# segment_count={len(self.dvr.segments)} total_duration_seconds={total:.3f} resolved={self.resolved}\n" + print(f"[HTTP] debug_dvr segments={len(self.dvr.segments)} total_s={total:.3f} endlist={self.resolved}") + return web.Response(text=hdr + m3u8, content_type="text/plain", headers=self._nocache_headers()) + + async def handle_debug_state(self, _request: web.Request): + info = { + "camera": self.camera, + "incident": self.incident, + "upstream_index": self.upstream_index, + "have_dvr": bool(self.dvr), + "segment_count": len(self.dvr.segments) if self.dvr else 0, + "target_duration": getattr(self.dvr, "target_duration", None) if self.dvr else None, + "have_init": bool(getattr(self.dvr, "init_url", None)) if self.dvr else False, + "resolved": self.resolved, + "extra_lag": self._current_extra_lag(), + } + print(f"[HTTP] state: {info}") + return web.json_response(info, headers=self._nocache_headers()) + + # URL helpers + + def _rewrite_to_media_base(self, any_hls_url: str) -> str: + if not any_hls_url: + return any_hls_url + + mb = urlparse(self.media_base) + + # Case 1: bare camera/incident/index.m3u8 → add /hls/ prefix + if not any_hls_url.startswith(("http://", "https://", "/")): + return f"{mb.scheme}://{mb.netloc}/hls/{any_hls_url.lstrip('/')}" + + # Case 2: starts with / but not // → relative path + if any_hls_url.startswith('/') and not any_hls_url.startswith('//'): + # Ensure it passes through /hls/ too + path = any_hls_url.lstrip('/') + if not path.startswith('hls/'): + path = f"hls/{path}" + return f"{mb.scheme}://{mb.netloc}/{path}" + + # Case 3: full URL → normalize its host to media_base + u = urlparse(any_hls_url) + if not u.scheme or not u.netloc: + return f"{mb.scheme}://{mb.netloc}/{any_hls_url.lstrip('/')}" + return urlunparse(u._replace(scheme=mb.scheme, netloc=mb.netloc)) + + def _normalize_live_playlist(self, upstream_text: str, upstream_index_url: str) -> str: + base_dir = upstream_index_url.rsplit("/", 1)[0] + "/" + lines = [l.strip() for l in upstream_text.splitlines() if l.strip()] + + version = 6 + media_seq = 0 + + segments = [] + init_map_abs = None + max_extinf = 1.0 + + i = 0 + while i < len(lines): + l = lines[i] + if l.startswith("#EXT-X-VERSION:"): + try: version = int(l.split(":", 1)[1]) + except: pass + elif l.startswith("#EXT-X-MEDIA-SEQUENCE:"): + try: media_seq = int(l.split(":", 1)[1]) + except: media_seq = 0 + elif l.startswith("#EXT-X-MAP:"): + m = re.search(r'URI="([^"]+)"', l) + if m: + init_map_abs = urljoin(base_dir, m.group(1)) + elif l.startswith("#EXTINF:"): + try: + dur = float(l.split(':', 1)[1].split(',')[0]) + except Exception: + dur = 1.0 + max_extinf = max(max_extinf, dur) + attached = [] + j = i + 1 + while j < len(lines) and lines[j].startswith("#"): + attached.append(lines[j]); j += 1 + if j < len(lines): + uri = lines[j] + segments.append((dur, attached, uri)) + i = j + else: + i = j + i += 1 + continue + i += 1 + + base_edge = max(1, int(getattr(Config, "LIVE_EDGE_SEGMENTS", 3))) + base_lag = max(0, int(getattr(Config, "LIVE_LAG_SEGMENTS", 0))) + # Add dynamic lag derived from recent 404s + effective_lag = base_lag + self._current_extra_lag() + + total = len(segments) + keep = min(total, max(1, base_edge + effective_lag)) + start_index = max(0, total - keep) + end_index = max(0, total - effective_lag) + trimmed = segments[start_index:end_index] + new_media_seq = media_seq + start_index + + out = [ + "#EXTM3U", + f"#EXT-X-VERSION:{version}", + "#EXT-X-PLAYLIST-TYPE:LIVE", + f"#EXT-X-TARGETDURATION:{int(max(1, round(max_extinf + 0.0001)))}", + "#EXT-X-INDEPENDENT-SEGMENTS", + f"#EXT-X-MEDIA-SEQUENCE:{new_media_seq}", + ] + + if init_map_abs: + out.append(f'#EXT-X-MAP:URI="/seg?u={init_map_abs}"') + + for dur, attached_tags, uri in trimmed: + out.append(f"#EXTINF:{dur:.3f},") + for t in attached_tags: + out.append(t) + seg_abs = urljoin(base_dir, uri) + out.append(f'/seg?u={seg_abs}') + + print(f"[LIVE] served {len(trimmed)} segs (edge={base_edge}, lag={effective_lag}, seq={new_media_seq})") + return "\n".join(out) + "\n" + + # Source switching + def switch_source(self, *, camera: Optional[str] = None, + incident: Optional[str] = None, + upstream_hls: Optional[str] = None): + if camera: + self.camera = camera + if incident: + self.incident = incident + + self.resolved = False + self._last_seg_404_ts = 0.0 + self._extra_lag_floor = 0 + + # if upstream_hls: + # self.upstream_index = self._rewrite_to_media_base(upstream_hls) + # else: + # if not (self.camera and self.incident): + # return + # self.upstream_index = f"{self.media_base}/hls/{self.camera}/{self.incident}/index.m3u8" + if upstream_hls: + # Always normalize, even if relative like "CAM-482A/incident-123/index.m3u8" + self.upstream_index = self._rewrite_to_media_base(upstream_hls) + elif self.camera and self.incident: + # Build from camera/incident if no explicit URL + rel_path = f"{self.camera}/{self.incident}/index.m3u8" + self.upstream_index = self._rewrite_to_media_base(rel_path) + else: + return + + + + print(f"[SRC] switch to upstream={self.upstream_index}") + + if self.dvr: + try: + self.dvr.stop() + except Exception: + pass + self.dvr = DvrState(self.upstream_index, auth_token=self.token, refresh_ms=self.refresh_ms) + + if self._loop and self._loop.is_running(): + def _start(): + print("[SRC] starting DVR loop") + self._loop.create_task(self.dvr.run()) + self._loop.call_soon_threadsafe(_start) + + def mark_resolved(self): + if self.resolved: + return + self.resolved = True + if self.dvr: + try: + self.dvr.stop() + except Exception: + pass + self.upstream_index = None + print("[SRC] incident resolved; upstream disabled; no DVR freeze") + + # HTTP handlers + async def handle_root(self, _request: web.Request): + return web.Response(text='OK', content_type='text/plain', headers=self._nocache_headers()) + + async def handle_dvr(self, _request: web.Request): + # No DVR freeze behavior + return web.Response(text="#EXTM3U\n#EXT-X-ENDLIST\n", content_type='application/vnd.apple.mpegurl', status=410, headers=self._nocache_headers()) + + async def handle_live(self, _request: web.Request): + if self.resolved or not self.upstream_index: + return web.Response(text="#EXTM3U\n#EXT-X-ENDLIST\n", content_type='application/vnd.apple.mpegurl', status=410, headers=self._nocache_headers()) + + headers = {} + if self.token: + headers['Authorization'] = f'Bearer {self.token}' + try: + async with ClientSession() as session: + async with session.get(self.upstream_index, headers=headers, timeout=10) as resp: + text = await resp.text() + if resp.status >= 400: + print(f"[HTTP] live.m3u8 upstream {resp.status}") + if resp.status in (404, 410): + self.mark_resolved() + return web.Response(text="#EXTM3U\n#EXT-X-ENDLIST\n", content_type='application/vnd.apple.mpegurl', status=410, headers=self._nocache_headers()) + return web.Response(text=f"# upstream {resp.status}\n{text}", content_type='text/plain', status=resp.status, headers=self._nocache_headers()) + except Exception as e: + print(f"[HTTP] live.m3u8 fetch error: {e!r}") + return web.Response(text=f"# fetch error: {type(e).__name__}: {e}\n", content_type='text/plain', status=502, headers=self._nocache_headers()) + + text = self._normalize_live_playlist(text, self.upstream_index) + return web.Response(text=text, content_type='application/vnd.apple.mpegurl', headers=self._nocache_headers()) + + async def handle_seg(self, request: web.Request): + url = request.query.get('u') + if not url: + raise web.HTTPBadRequest(text='missing u') + headers = {} + if self.token: + headers['Authorization'] = f'Bearer {self.token}' + try: + async with ClientSession() as session: + async with session.get(url, headers=headers, timeout=20) as resp: + body = await resp.read() + ctype = resp.headers.get('Content-Type', 'application/octet-stream') + status = resp.status + print(f"[HTTP] seg {status} {ctype} {len(body)} bytes <- {url}") + # On 404/410, bump lag so subsequent /live.m3u8 hides fresher segs + if status in (404, 410): + self._bump_extra_lag(floor_to=2) + return web.Response(body=body, content_type=ctype, status=status, headers=self._nocache_headers()) + except Exception as e: + print(f"[HTTP] seg fetch error: {e!r} <- {url}") + return web.Response(text=f"segment fetch error: {type(e).__name__}: {e}", content_type="text/plain", status=502, headers=self._nocache_headers()) + + async def handle_dvr_seek(self, request: web.Request): + if self.resolved or not self.dvr: + return web.Response(text="#EXTM3U\n#EXT-X-ENDLIST\n", + content_type='application/vnd.apple.mpegurl', + status=410, + headers=self._nocache_headers()) + + t_ms_str = request.query.get('t', '0') + try: + t_ms = max(0, int(float(t_ms_str))) + except Exception: + t_ms = 0 + + with self.dvr._lock: + segs = list(self.dvr.segments) + init_url = self.dvr.init_url + version = self.dvr.version + target = int(max(1.0, self.dvr.target_duration)) + + # Compute which segment contains t_ms and how far into it we need to start. + acc_ms = 0.0 + start_idx = 0 + intra_ms = 0.0 + for i, s in enumerate(segs): + next_acc = acc_ms + s.duration * 1000.0 + if next_acc > t_ms: + start_idx = i + intra_ms = max(0.0, t_ms - acc_ms) + break + acc_ms = next_acc + else: + # Past the end → start at the last segment, no intra offset + start_idx = max(0, len(segs) - 1) + intra_ms = 0.0 + + trimmed = segs[start_idx:] + media_seq = start_idx + + out = [] + out.append('#EXTM3U') + out.append(f'#EXT-X-VERSION:{version}') + out.append('#EXT-X-PLAYLIST-TYPE:EVENT') + out.append('#EXT-X-INDEPENDENT-SEGMENTS') + out.append(f'#EXT-X-TARGETDURATION:{max(1, target)}') + out.append(f'#EXT-X-MEDIA-SEQUENCE:{media_seq}') + + # PRECISE intra-segment start (many players honor this; helps VLC too) + # Start "intra_ms" seconds *into* the first segment of this playlist. + out.append(f'#EXT-X-START:TIME-OFFSET={intra_ms/1000.0:.3f},PRECISE=YES') + + if init_url: + out.append(f'#EXT-X-MAP:URI="/seg?u={init_url}"') + + for s in trimmed: + out.append(f'#EXTINF:{s.duration:.3f},') + out.append(f'/seg?u={s.abs_url}') + + body = "\n".join(out) + "\n" + print(f"[HTTP] dvr_seek.m3u8 t={t_ms}ms -> start_idx={start_idx} intra={int(intra_ms)}ms segs={len(trimmed)} resolved={self.resolved}") + # Optional debug header — handy to confirm behavior in logs/curl: + headers = self._nocache_headers() | {"X-Start-Offset-Ms": str(int(intra_ms))} + return web.Response(text=body, + content_type='application/vnd.apple.mpegurl', + headers=headers) + + # Lifecycle + def start(self): + def _run_loop(): + loop = asyncio.new_event_loop() + self._loop = loop + asyncio.set_event_loop(loop) + self._runner = web.AppRunner(self._app) + loop.run_until_complete(self._runner.setup()) + site = web.TCPSite(self._runner, self.bind, self.port) + loop.run_until_complete(site.start()) + print(f"[HTTP] proxy listening on http://{self.bind}:{self.port}") + try: + loop.run_forever() + finally: + loop.run_until_complete(self._runner.cleanup()) + loop.stop() + if is_port_in_use(19090): + print("[INFO] DVR proxy already running on port 19090, reusing it.") + else: + self._thread = threading.Thread(target=_run_loop, daemon=True) + self._thread.start() + + def stop(self): + if self.dvr: + self.dvr.stop() + +# ────────────────────────────────────────────────────────────────────────────── +# LEFT PANE + UI — unchanged except: no DVR freeze on resolve +# ────────────────────────────────────────────────────────────────────────────── +class AlertsModel(QtCore.QAbstractListModel): + def __init__(self): + super().__init__() + self._items: list[dict] = [] + + def rowCount(self, parent=None): + return len(self._items) + + def data(self, idx, role): + if not idx.isValid(): + return None + if role == QtCore.Qt.ItemDataRole.DisplayRole: + a = self._items[idx.row()] + status = (a.get("status") or "firing").lower() + return f'[{status}] {a.get("camera")} {a.get("anomaly")} ({a.get("incident_id")})' + return None + + def is_empty(self) -> bool: + return len(self._items) == 0 + + def set_alerts(self, items: list[dict]): + self.beginResetModel() + self._items = list(items or []) + self.endResetModel() + + def add_alerts(self, items): + if not items: + return + start = len(self._items) + self.beginInsertRows(QtCore.QModelIndex(), start, start + len(items) - 1) + self._items.extend(items) + self.endInsertRows() + + def get(self, row: int): + return self._items[row] + + def _key(self, it: dict) -> tuple[str, str]: + return (str(it.get("camera") or ""), str(it.get("incident_id") or "")) + + def as_dict(self) -> dict[tuple[str, str], dict]: + return { self._key(it): it for it in self._items } + + def replace_with(self, merged: dict[tuple[str,str], dict]): + self.set_alerts(list(merged.values())) + + def remove_by_key(self, camera: str, incident_id: str): + k = (str(camera or ""), str(incident_id or "")) + for i, it in enumerate(self._items): + if (str(it.get("camera") or ""), str(it.get("incident_id") or "")) == k: + self.beginRemoveRows(QtCore.QModelIndex(), i, i) + self._items.pop(i) + self.endRemoveRows() + return True + return False + +class AlertItemDelegate(QtWidgets.QStyledItemDelegate): + def paint(self, painter: QtGui.QPainter, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex): + model: AlertsModel = index.model() # type: ignore + a = model.get(index.row()) + r = option.rect + painter.save() + + if option.state & QtWidgets.QStyle.StateFlag.State_Selected: + painter.fillRect(r, QtGui.QColor("#eef8ff")) + elif option.state & QtWidgets.QStyle.StateFlag.State_MouseOver: + painter.fillRect(r, QtGui.QColor("#f6fafc")) + + status = (a.get("status") or "firing").lower() + color = {"firing": "#16a34a", "resolved": "#94a3b8", "warning": "#f59e0b"}.get(status, "#16a34a") + chip = QtCore.QRect(r.left() + 10, r.center().y() - 5, 10, 10) + painter.setBrush(QtGui.QColor(color)) + painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.drawEllipse(chip) + + x = chip.right() + 10 + cam = str(a.get("camera") or "") + anom = str(a.get("anomaly") or "") + subject = str(a.get("subject") or "") + if anom.lower() in ("intruding animal", "intruding_animal", "climbing_fence") and subject: + anom = f"{anom.title()} ({subject.title()})" + else: + anom = anom.title() + + inc = str(a.get("incident_id") or "")[:8] + + title_font = QtGui.QFont(option.font); title_font.setPointSizeF(option.font.pointSizeF() + 1); title_font.setBold(True) + sub_font = QtGui.QFont(option.font); sub_font.setPointSizeF(option.font.pointSizeF() - 1) + + painter.setPen(QtGui.QColor("#111827")) + painter.setFont(title_font) + painter.drawText(QtCore.QRect(x, r.top() + 4, r.width() - 20, 18), + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, + f"{cam} • {anom}") + + painter.setPen(QtGui.QColor("#6b7280")) + painter.setFont(sub_font) + painter.drawText(QtCore.QRect(x, r.top() + 22, r.width() - 20, 16), + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, + f"Incident: {inc}… • Status: {status}") + + painter.restore() + + def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, _index: QtCore.QModelIndex) -> QtCore.QSize: + return QtCore.QSize(220, 42) + +LEFT_LIST_QSS = """ +QListView { + padding: 6px; + background: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 12px; +} +QListView::item { padding: 4px 8px; } +QListView::item:selected { background: #eef8ff; border-radius: 8px; } +QScrollBar:vertical { background: transparent; width: 10px; margin: 8px 2px 8px 2px; border-radius: 5px; } +QScrollBar::handle:vertical { background: #cbd5e1; min-height: 32px; border-radius: 5px; } +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; } +#LeftHeader { color: #6b7280; font-weight: 600; letter-spacing: 0.4px; margin: 0 6px 6px 6px; } +""" + +class SeekSlider(QtWidgets.QSlider): + hovered = QtCore.pyqtSignal(int) + clickedTo = QtCore.pyqtSignal(int) + draggedTo = QtCore.pyqtSignal(int) + + def __init__(self, orientation, parent=None): + super().__init__(orientation, parent) + self._press_x: Optional[float] = None + self._moved: bool = False + self._CLICK_EPS = 4.0 + self._EDGE_SNAP_PX = 8 # ← new: snap zone near the ends + + def mousePressEvent(self, ev: QtGui.QMouseEvent): + if ev.button() == Qt.MouseButton.LeftButton: + self._press_x = float(ev.position().x()) + self._moved = False + self.setSliderDown(True) + ev.accept() + return + super().mousePressEvent(ev) + + def mouseMoveEvent(self, ev: QtGui.QMouseEvent): + x = float(ev.position().x()) + if self._press_x is not None and abs(x - self._press_x) > self._CLICK_EPS: + self._moved = True + val = self._value_for_x(x) + self.hovered.emit(val) + if self._moved: + self.setValue(val) + super().mouseMoveEvent(ev) + + def mouseReleaseEvent(self, ev: QtGui.QMouseEvent): + if ev.button() == Qt.MouseButton.LeftButton and self._press_x is not None: + x = float(ev.position().x()) + val = self._value_for_x(x) + self.setSliderDown(False) + self.setValue(val) + if self._moved: + self.draggedTo.emit(val) + else: + self.clickedTo.emit(val) + self._press_x = None + ev.accept() + return + super().mouseReleaseEvent(ev) + + def _value_for_x(self, x: float) -> int: + opt = QtWidgets.QStyleOptionSlider() + self.initStyleOption(opt) + groove = self.style().subControlRect( + QtWidgets.QStyle.ComplexControl.CC_Slider, + opt, + QtWidgets.QStyle.SubControl.SC_SliderGroove, + self + ) + if groove.width() <= 0: + return self.value() + + # NEW: snap to exact min/max if you're near the ends + if x <= groove.left() + self._EDGE_SNAP_PX: + return self.minimum() + if x >= groove.right() - self._EDGE_SNAP_PX: + return self.maximum() + + ratio = max(0.0, min(1.0, (x - groove.left()) / groove.width())) + return int(self.minimum() + ratio * (self.maximum() - self.minimum())) + + +class VideoSurface(QtWidgets.QStackedWidget): + def __init__(self, vlc_widget: QtWidgets.QWidget, parent=None): + super().__init__(parent) + self.vlcw = vlc_widget + self.loading = QtWidgets.QLabel("Loading…") + self.loading.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.loading.setStyleSheet("color:#b9c0c7; font-size:18px;") + self.addWidget(self.vlcw) + self.addWidget(self.loading) + self.setCurrentIndex(1) + + def show_loading(self, on: bool): + self.setCurrentIndex(1 if on else 0) + +class VlcWidget(QtWidgets.QFrame): + positionChanged = QtCore.pyqtSignal(float) + timeChanged = QtCore.pyqtSignal(int) + + def __init__(self, instance: vlc.Instance, parent=None): + super().__init__(parent) + self.instance = instance + self.mediaplayer = self.instance.media_player_new() + self.setMinimumSize(640, 360) + self._timer = QtCore.QTimer(self) + self._timer.setInterval(200) + self._timer.timeout.connect(self._on_tick) + self._timer.start() + + def _on_tick(self): + if self.mediaplayer: + try: + pos = self.mediaplayer.get_position() + t = self.mediaplayer.get_time() + if pos >= 0: + self.positionChanged.emit(pos) + if t >= 0: + self.timeChanged.emit(t) + except Exception: + pass + + def set_media(self, mrl: str, options: Optional[List[str]] = None): + print(f"[VLC] set_media {mrl} opts={options or []}") + media = self.instance.media_new(mrl) + for opt in (options or []): + media.add_option(opt) + self.mediaplayer.set_media(media) + + def play(self): + if sys.platform.startswith('linux'): + self.mediaplayer.set_xwindow(int(self.winId())) + elif sys.platform.startswith('win'): + self.mediaplayer.set_hwnd(int(self.winId())) + else: + self.mediaplayer.set_nsobject(int(self.winId())) + print("[VLC] play()") + self.mediaplayer.play() + + def pause(self): + print("[VLC] pause()") + self.mediaplayer.pause() + + def set_position(self, pos01: float): + p = max(0.0, min(1.0, float(pos01))) + print(f"[VLC] set_position {p:.3f}") + self.mediaplayer.set_position(p) + + def set_time_ms(self, t_ms: int): + t = int(max(0, t_ms)) + print(f"[VLC] set_time {t}ms") + self.mediaplayer.set_time(t) + +class IncidentPlayerVLC(QtWidgets.QWidget): + def __init__(self, api,alert_service, parent=None): + super().__init__(parent) + self.api = api + self.alert_service = alert_service + self.allow_autoplay = False + self._is_current_page = False + self.cfg = Config() + self.proxy = ProxyServer( + media_base=self.cfg.MEDIA_BASE, + camera=None, + incident=self.cfg.INCIDENT, + token=self.cfg.TOKEN, + refresh_ms=self.cfg.REFRESH_MS, + bind=self.cfg.BIND, + port=self.cfg.PORT, + ) + self.proxy.start() + self.setWindowTitle("AgGuard — Live Incidents") + + self.setMinimumSize(1100, 620) + self.resize(1180, 680) + self.setContentsMargins(6, 6, 6, 6) + + THEME_QSS = """ + QWidget { background:#fafbfc; color:#1f2937; font-size:13px; } + QGroupBox { background:#ffffff; border:1px solid #e5e7eb; border-radius:10px; margin-top:14px; } + QGroupBox::title { subcontrol-origin: margin; left: 12px; top:-6px; padding:0 4px; color:#0f172a; font-weight:600; } + QPushButton { border-radius:10px; padding:7px 12px; background:#10b981; color:white; font-weight:600; border:0; } + QPushButton:hover { background:#0ea371; } + QLabel#timeLabel { color:#6b7280; font-weight:600; } + QLabel#liveBadge { background:#10b981; color:white; padding:3px 8px; border-radius:12px; font-weight:700; } + QLabel#liveBadge.off { background:#9ca3af; } + QSlider::groove:horizontal { height:8px; background:#e7f6ef; border-radius:4px; } + QSlider::handle:horizontal { background:#10b981; width:14px; height:14px; margin:-3px 0; border-radius:7px; } + """ + LEFT_LIST_QSS + self.setStyleSheet(THEME_QSS) + + os.environ.setdefault("VDPAU_DRIVER", "") + os.environ.setdefault("LIBVA_DRIVER_NAME", "") + vlc_opts = [ + f'--network-caching={max(200, int(self.cfg.NETWORK_CACHING))}', + '--live-caching=300', + '--file-caching=300', + '--no-video-title-show', + '--quiet', + '--aout=dummy', + '--avcodec-hw=none', + '--drop-late-frames', + '--skip-frames', + '--clock-jitter=0', + ] + self.vlc_instance = vlc.Instance(*vlc_opts) + self.vlcw = VlcWidget(self.vlc_instance) + self.videoSurface = VideoSurface(self.vlcw) + self.videoSurface.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) + + # Controls + self.btnLive = QtWidgets.QPushButton('Go Live') + self.btnLive.setObjectName("btnLive") + + self.timeLeft = QtWidgets.QLabel('00:00') + self.timeLeft.setObjectName("timeLabel") + self.slider = SeekSlider(QtCore.Qt.Orientation.Horizontal) + self.slider.setRange(0, 0) + self.liveBadge = QtWidgets.QLabel('LIVE') + self.liveBadge.setObjectName("liveBadge") + + # LEFT PANE + leftContainer = QtWidgets.QGroupBox("Alerts") + leftContainer.setSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, + QtWidgets.QSizePolicy.Policy.Expanding) + leftContainer.setMinimumWidth(300) + leftContainer.setMaximumWidth(340) + + leftLayout = QtWidgets.QVBoxLayout(leftContainer) + leftLayout.setContentsMargins(10, 10, 10, 10) + leftLayout.setSpacing(8) + + self.alertList = QtWidgets.QListView() + self.alertList.setMouseTracking(True) + self.alertList.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.SingleSelection) + self.alertList.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems) + self.alertList.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) + self.alertList.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers) + self.alertList.setUniformItemSizes(True) + self.alertList.setSpacing(4) + self.alertList.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + self.alertModel = AlertsModel() + self.alertList.setModel(self.alertModel) + self.alertList.setItemDelegate(AlertItemDelegate(self.alertList)) + leftLayout.addWidget(self.alertList) + + # Details inside player pane + self.detailGroup = QtWidgets.QGroupBox("Details") + self.detailGroup.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Fixed) + grid = QtWidgets.QGridLayout(self.detailGroup) + grid.setContentsMargins(12, 8, 12, 12) + grid.setHorizontalSpacing(24) + grid.setVerticalSpacing(6) + + labels = ["Camera:", "Anomaly:", "Incident ID:", "Status:", "Start Time:"] + self.lblVals = [] + for i, title in enumerate(labels): + k = QtWidgets.QLabel(title) + v = QtWidgets.QLabel("–") + v.setStyleSheet("color:#6b7280;") + grid.addWidget(k, i, 0, 1, 1) + grid.addWidget(v, i, 1, 1, 1) + self.lblVals.append(v) + self.detailGroup.setMaximumHeight(160) + + # Right stack + self.rightStack = QtWidgets.QStackedWidget() + self.rightStack.setContentsMargins(0, 0, 0, 0) + + self.emptyPane = QtWidgets.QWidget() + ep_layout = QtWidgets.QVBoxLayout(self.emptyPane) + ep_layout.setContentsMargins(10, 10, 10, 10) + ep_layout.setSpacing(0) + + noTitle = QtWidgets.QLabel("No alerts") + noTitle.setAlignment(Qt.AlignmentFlag.AlignCenter) + noTitle.setStyleSheet("font-size:22px; font-weight:800; color:#111827;") + + noSub = QtWidgets.QLabel("Alerts will appear here.") + noSub.setAlignment(Qt.AlignmentFlag.AlignCenter) + noSub.setWordWrap(True) + noSub.setStyleSheet("color:#6b7280;") + + ep_layout.addStretch(1) + ep_layout.addWidget(noTitle) + ep_layout.addWidget(noSub) + ep_layout.addStretch(3) + + self.playerPane = QtWidgets.QGroupBox("") + rightLayout = QtWidgets.QVBoxLayout(self.playerPane) + rightLayout.setContentsMargins(10, 10, 10, 10) + rightLayout.setSpacing(10) + + titleRow = QtWidgets.QHBoxLayout() + titleRow.setContentsMargins(0, 0, 0, 0) + titleRow.setSpacing(10) + title = QtWidgets.QLabel("AgGuard — Security Alerts") + title.setStyleSheet("font-size:20px; font-weight:800; color:#111827;") + dotLive = QtWidgets.QLabel("• LIVE") + dotLive.setStyleSheet("color:#10b981; font-weight:700;") + titleRow.addWidget(title) + titleRow.addStretch(1) + titleRow.addWidget(dotLive) + + ctrls = QtWidgets.QHBoxLayout() + ctrls.setContentsMargins(0, 0, 0, 0) + ctrls.setSpacing(10) + ctrls.addWidget(self.btnLive) + ctrls.addSpacing(10) + ctrls.addWidget(self.timeLeft) + ctrls.addSpacing(8) + ctrls.addWidget(self.slider, 1) + ctrls.addSpacing(8) + ctrls.addWidget(self.liveBadge) + + rightLayout.addLayout(titleRow, 0) + rightLayout.addWidget(self.videoSurface, 1) + rightLayout.addLayout(ctrls, 0) + rightLayout.addWidget(self.detailGroup, 0) + + + self.rightStack.addWidget(self.emptyPane) + self.rightStack.addWidget(self.playerPane) + self.rightStack.setCurrentIndex(0) + + splitter = QtWidgets.QSplitter(Qt.Orientation.Horizontal) + splitter.setChildrenCollapsible(False) + splitter.setHandleWidth(6) + splitter.addWidget(leftContainer) + splitter.addWidget(self.rightStack) + splitter.setStretchFactor(0, 0) + splitter.setStretchFactor(1, 1) + splitter.setSizes([320, 900]) + + outer = QtWidgets.QVBoxLayout(self) + outer.setContentsMargins(6, 6, 6, 6) + outer.setSpacing(6) + # outer.addWidget(splitter) + # --- Navigation bar --- + navBar = QtWidgets.QHBoxLayout() + navBar.setContentsMargins(6, 6, 6, 6) + navBar.setSpacing(8) + + btnLiveView = QtWidgets.QPushButton("Live Incidents") + btnLiveView.setCheckable(True) + btnLiveView.setChecked(True) + btnHistory = QtWidgets.QPushButton("Events History") + btnHistory.setCheckable(True) + btnGeo = QtWidgets.QPushButton("Analytics") + btnGeo.setCheckable(True) + + btnStyle = """ + QPushButton { + background:#e5e7eb; border:none; border-radius:8px; + padding:6px 12px; font-weight:600; + } + QPushButton:checked { background:#10b981; color:white; } + """ + btnLiveView.setStyleSheet(btnStyle) + btnHistory.setStyleSheet(btnStyle) + btnGeo.setStyleSheet(btnStyle) + + navBar.addWidget(btnLiveView) + navBar.addWidget(btnHistory) + navBar.addWidget(btnGeo) + navBar.addStretch(1) + + + # --- Main content stack --- + self.stack = QtWidgets.QStackedWidget() + self.livePage = QtWidgets.QWidget() + self.liveLayout = QtWidgets.QVBoxLayout(self.livePage) + self.liveLayout.setContentsMargins(0, 0, 0, 0) + self.liveLayout.addWidget(splitter) + self.historyPage = EventsHistoryPage(api=self.api, parent=self) + self.analyticsPage = GeoAnalyticsView(parent=self) + + self.stack.addWidget(self.livePage) + self.stack.addWidget(self.historyPage) + self.stack.addWidget(self.analyticsPage) + + # --- Combine all together --- + outer.addLayout(navBar) + outer.addWidget(self.stack) + + # --- Navigation logic --- + btnLiveView.clicked.connect(lambda: self._switch_page(0, btnLiveView, btnHistory, btnGeo)) + btnHistory.clicked.connect(lambda: self._switch_page(1, btnHistory, btnLiveView, btnGeo)) + btnGeo.clicked.connect(lambda: self._switch_page(2, btnGeo, btnLiveView, btnHistory)) + + + + # State + self.mode_live = False + self.dvr_duration_ms = 0 + self._dragging = False + self.current_camera: Optional[str] = None + self.current_incident: Optional[str] = None + self.current_status: str = "firing" + + self._last_abs_t_ms: int = 0 + self._playlist_offset_ms: int = 0 + + self._ui_freeze_deadline: float = 0.0 + self._seek_guard_deadline: float = 0.0 + + self._live_sync = QTimer(self) + self._live_sync.setInterval(800) + self._live_sync.timeout.connect(self._maybe_sync_live_timeline) + + #new + self._dvr_growth = QTimer(self) + self._dvr_growth.setInterval(1200) + self._dvr_growth.timeout.connect(self._maybe_grow_dvr_range_only) + + # --- Subscribe to alert service --- + self.alert_service.alertsUpdated.connect(self._on_alerts_updated) + self.alert_service.alertAdded.connect(self._on_alert_added) + self.alert_service.alertRemoved.connect(self._on_alert_removed) + + # Trigger initial load + if not self.alert_service.alerts: + print("[IncidentPlayer] No cached alerts yet — calling load_initial()") + self.alert_service.load_initial() + else: + print("[IncidentPlayer] Using cached alerts:", len(self.alert_service.alerts)) + self._on_alerts_updated(self.alert_service.alerts) + + + # Connections + self.btnLive.clicked.connect(self._go_live) + self.slider.hovered.connect(self._on_slider_hover) + self.slider.clickedTo.connect(self._on_slider_clicked) + self.slider.draggedTo.connect(self._on_slider_drag_released) + self.vlcw.positionChanged.connect(self._on_vlc_pos) + self.vlcw.timeChanged.connect(self._on_vlc_time) + self.alertList.clicked.connect(self._on_pick_alert_from_list) + + self._show_player(False) + self._set_idle() + + + def showEvent(self, event): + super().showEvent(event) + + # Only run when *actually* visible in the QStackedWidget + if not self.isVisible(): + return + + if not self._is_current_page: + self._is_current_page = True + self.allow_autoplay = True + print("[IncidentPlayer] Activated (autoplay enabled)") + + # Restore video surface + self.videoSurface.show() + + # If alerts exist, auto-play the first one + if self.alertModel._items: + self._show_player(True) + self._play_alert(self.alertModel._items[0]) + + + def hideEvent(self, event): + super().hideEvent(event) + + # Only deactivate once + if self._is_current_page: + self._is_current_page = False + self.allow_autoplay = False + print("[IncidentPlayer] Deactivated (autoplay disabled)") + + # Stop VLC safely + try: + self.vlcw.mediaplayer.stop() + except: + pass + + # Hide video surface to stop painting + self.videoSurface.hide() + + self._set_idle() + + + + def _on_alerts_updated(self, alerts: list): + """Called when AlertService emits full list (on initial load).""" + print(f"[AlertService] Full update: {len(alerts)} alerts") + # print("[DEBUG] alerts from AlertService:", alerts) + self._apply_firing_list(alerts) + + def _on_alert_added(self, alert: dict): + """Called when a new alert arrives in real-time.""" + print(f"[AlertService] New alert added: {alert.get('alert_id')}") + self._merge_firing_deltas([alert]) + + def _on_alert_removed(self, alert_id: str): + """Called when an alert is resolved/removed.""" + print(f"[AlertService] Alert removed: {alert_id}") + self.alertModel.set_alerts([ + a for a in self.alertModel._items if a.get("alert_id") != alert_id + ]) + self._update_right_pane_visibility() + + + def _fetch_active_alerts_from_db(self): + """Fetch current active alerts directly from the DB API.""" + try: + print("[DB] Fetching active alerts from dashboard API...") + url = f"{self.api.base}/api/tables/alerts" + resp = self.api.http.get(url, timeout=10) + if resp.status_code != 200: + print(f"[DB] Failed to fetch alerts: {resp.status_code}") + return [] + + data = resp.json() + alerts = data.get("rows", data) if isinstance(data, dict) else data + print(f"[DB] Loaded {len(alerts)} active alerts from DB.") + return alerts + except Exception as e: + print(f"[DB] Error fetching alerts: {e}") + return [] + + + # ───── NO-ALERTS helpers ───── + def _show_player(self, on: bool): + self.rightStack.setCurrentIndex(1 if on else 0) + print(f"[UI] right pane -> {'PLAYER' if on else 'NO-ALERTS'}") + + def _update_right_pane_visibility(self): + have_any = not self.alertModel.is_empty() + print("_update_right_pane_visibility called have any",have_any) + if not have_any: + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + self._set_idle() + self._show_player(False) + else: + self._show_player(True) + + def _switch_page(self, index: int, active_btn: QtWidgets.QPushButton, *others: QtWidgets.QPushButton): + self.stack.setCurrentIndex(index) + active_btn.setChecked(True) + for b in others: + b.setChecked(False) + print(f"[UI] switched to page index={index}") + + + # ───── alerts helpers ───── + def _key(self, it: dict) -> tuple[str, str]: + return (str(it.get('camera') or ''), str(it.get('incident_id') or '')) + + + def _normalize_alert(self, it: dict) -> dict: + meta = it.get("meta") or {} + if isinstance(meta, str): + import json + try: + meta = json.loads(meta) + except Exception: + meta = {} + return { + "camera": it.get("device_id") or it.get("camera"), + "incident_id": it.get("alert_id") or it.get("incident_id"), + "anomaly": it.get("alert_type") or it.get("anomaly"), + "subject": it.get("subject") or meta.get("subject"), # 👈 added line + "hls": it.get("hls"), + "vod": it.get("vod"), + "image_url": it.get("image_url"), + "summary": it.get("summary"), + "severity": it.get("severity"), + "started_at": it.get("started_at") or it.get("startsAt"), + "ended_at": it.get("ended_at") or it.get("endsAt"), + "status": "firing" if not (it.get("ended_at") or it.get("endsAt")) else "resolved", + } + + + + + ##new + def _maybe_grow_dvr_range_only(self): + # Only expand the slider max while paused/seeked (DVR mode). Never move the thumb. + if self.mode_live: + self._dvr_growth.stop() + return + if self.proxy.dvr and not self.proxy.resolved: + _, total = self.proxy.dvr.render_dvr_vod_playlist() + new_max = int(total * 1000) + if new_max > self.dvr_duration_ms: + self.dvr_duration_ms = new_max + self.slider.setRange(0, self.dvr_duration_ms) + + + def _apply_firing_list(self, firing: list[dict]): + firing = [self._normalize_alert(it) for it in (firing or []) if it] + # print("[DEBUG] normalized firing list:", firing) + + # ⬇️ Only keep desired alert types + + firing = [ + it for it in firing + if (it.get("status") or "firing").lower() == "firing" + and (it.get("anomaly") or "").lower() in self.cfg.ALLOWED_TYPES + ] + + sel = self.alertList.selectionModel().currentIndex() if self.alertList.selectionModel() else QtCore.QModelIndex() + selected_inc = selected_cam = None + if sel.isValid(): + try: + cur = self.alertModel.get(sel.row()) + selected_inc = cur.get('incident_id') + selected_cam = cur.get('camera') + except Exception: + pass + + self.alertModel.set_alerts(firing) + self._update_right_pane_visibility() + + if selected_inc is not None: + for row, it in enumerate(firing): + if it.get('incident_id') == selected_inc and it.get('camera') == selected_cam: + idx = self.alertModel.index(row, 0) + self.alertList.selectionModel().select(idx, QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect) + self.alertList.setCurrentIndex(idx) + break + + cur_cam = self.current_camera + cur_inc = self.current_incident or self.cfg.INCIDENT + still_there = any( + it.get('camera') == cur_cam and it.get('incident_id') == cur_inc + for it in firing + ) if (cur_cam and cur_inc) else False + + if (cur_cam and cur_inc) and not still_there: + if self.current_camera and self.current_incident: + self.alertModel.remove_by_key(self.current_camera, self.current_incident) + self.current_status = "resolved" + self.proxy.mark_resolved() + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + + if firing: + self._show_player(True) + self._play_alert(firing[0]) + else: + self._set_idle() + self._show_player(False) + return + + if firing and not still_there: + if self.allow_autoplay: + self._show_player(True) + self._play_alert(firing[0]) + else: + print("[IncidentPlayer] Ignored new alert (page not visible)") + + + + def _merge_firing_deltas(self, deltas: list[dict]): + current = self.alertModel.as_dict() + changed = False + + + + for raw in (deltas or []): + it = self._normalize_alert(raw) + if (it.get("anomaly") or "").lower() not in self.cfg.ALLOWED_TYPES: + continue # ⬅️ skip other alert types + k = self._key(it) + + if it.get('status') == 'firing': + if current.get(k) != it: + current[k] = it + changed = True + else: + if k in current: + current.pop(k, None) + changed = True + + if (self.current_camera, self.current_incident) == k and it.get('status') != 'firing': + self.current_status = "resolved" + self.proxy.mark_resolved() + if self.current_camera and self.current_incident: + self.alertModel.remove_by_key(self.current_camera, self.current_incident) + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + + if not changed: + return + + sel = self.alertList.selectionModel().currentIndex() if self.alertList.selectionModel() else QtCore.QModelIndex() + selected_key = None + if sel.isValid(): + try: + cur = self.alertModel.get(sel.row()) + selected_key = self._key(cur) + except Exception: + pass + + self.alertModel.replace_with(current) + self._update_right_pane_visibility() + + if selected_key: + items = list(current.values()) + for row, it in enumerate(items): + if self._key(it) == selected_key: + idx = self.alertModel.index(row, 0) + self.alertList.selectionModel().select(idx, QtCore.QItemSelectionModel.SelectionFlag.ClearAndSelect) + self.alertList.setCurrentIndex(idx) + break + + cur_cam = self.current_camera + cur_inc = self.current_incident or self.cfg.INCIDENT + has_current = (cur_cam and cur_inc and (cur_cam, cur_inc) in current) + if not has_current: + items = list(current.values()) + if items: + if self.allow_autoplay: + self._show_player(True) + self._play_alert(items[0]) + else: + print("[IncidentPlayer] Ignored alert (page not visible)") + else: + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + self._set_idle() + self._show_player(False) + + # if items: + # self._show_player(True) + # self._play_alert(items[0]) + # else: + # try: self.vlcw.mediaplayer.stop() + # except Exception: pass + # self._set_idle() + # self._show_player(False) + + # ───── helpers ───── + def _freeze_ui(self, seconds: float = 0.8): + self._ui_freeze_deadline = time.monotonic() + max(0.1, seconds) + + def _maybe_sync_live_timeline(self): + if not self.mode_live: + self._live_sync.stop() + return + + total_ms, live_win_ms = self.proxy.get_durations_ms() + if total_ms <= 0 or live_win_ms <= 0: + return + + target_offset = max(0, total_ms - live_win_ms) + t_rel = self.vlcw.mediaplayer.get_time() + if t_rel < 0: + t_rel = 0 + abs_t = min(target_offset + t_rel, total_ms) + + changed = (self.dvr_duration_ms != total_ms) or (abs(self._playlist_offset_ms - target_offset) > 250) + if changed: + self.dvr_duration_ms = total_ms + self._playlist_offset_ms = target_offset + + self.slider.setRange(0, total_ms) + self.slider.blockSignals(True) + self.slider.setValue(abs_t) + self.slider.blockSignals(False) + + self._last_abs_t_ms = abs_t + self._update_time_label(abs_t) + + # List click → play + def _on_pick_alert_from_list(self, idx: QtCore.QModelIndex): + if not idx.isValid(): + return + it = self.alertModel.get(idx.row()) + print(f"[UI] picked alert: {it}") + self._show_player(True) + self._play_alert(it) + + def _play_alert(self, it: dict): + # Normalize to ensure keys like 'camera', 'anomaly', 'incident_id', 'started_at' exist + it = self._normalize_alert(it) + + cam = it.get('camera') + inc = it.get('incident_id') or self.cfg.INCIDENT + hls_url = it.get('hls') or None + + self.current_camera = cam + self.current_incident = inc + self.current_status = (it.get('status') or 'firing').lower() + + self.proxy.switch_source(camera=cam, incident=inc, upstream_hls=hls_url) + self.setWindowTitle("AgGuard — Live Incidents") + self._update_details(it) + self.videoSurface.show_loading(True) + + QtCore.QTimer.singleShot(150, self._go_live) + + + def _update_details(self, it: dict): + subject = it.get("subject") + anom = it.get("anomaly") or "–" + if anom.lower() in ("intruding animal", "intruding_animal","climbing_fence") and subject: + anom = f"{anom.title()} ({subject.title()})" + + vals = [ + it.get('camera') or '–', + anom, + it.get('incident_id') or '–', + (it.get('status') or self.current_status or '–'), + it.get('started_at') or '–', + ] + for lbl, v in zip(self.lblVals, vals): + lbl.setText(v) + + + # ───── slider / playback helpers ───── + def _fmt(self, ms: int) -> str: + s = max(0, ms // 1000) + h, s = divmod(s, 3600) + m, s = divmod(s, 60) + if h: + return f"{h:d}:{m:02d}:{s:02d}" + return f"{m:d}:{s:02d}" + + def _set_live_badge(self, live: bool): + if live: + self.liveBadge.setText("LIVE") + self.liveBadge.setStyleSheet("background:#10b981; color:white; padding:3px 8px; border-radius:12px; font-weight:700;") + else: + self.liveBadge.setText("DVR") + self.liveBadge.setStyleSheet("background:#9ca3af; color:white; padding:3px 8px; border-radius:12px; font-weight:700;") + + def _set_idle(self): + self.mode_live = False + self._set_live_badge(False) + self.dvr_duration_ms = 0 + self._last_abs_t_ms = 0 + self._playlist_offset_ms = 0 + self.timeLeft.setText("00:00") + self.videoSurface.show_loading(True) + for v in self.lblVals: + v.setText("–") + print("[MODE] IDLE") + + def _go_live(self): + # resume live sync; stop DVR growth + if not self._live_sync.isActive(): + self._live_sync.start() + self._dvr_growth.stop() + + if self.current_status == "resolved": + print("[MODE] resolved; not going live") + self._set_idle() + return + + if not self.proxy.upstream_index: + return + + total_ms, live_win_ms = self.proxy.get_durations_ms() + self.dvr_duration_ms = max(self.dvr_duration_ms, total_ms) + self._playlist_offset_ms = max(0, total_ms - live_win_ms) + + self.mode_live = True + self._set_live_badge(True) + self.videoSurface.show_loading(True) + + self._freeze_ui(1.0) + self._update_time_label(self.dvr_duration_ms) + + live_url = f"http://{self.cfg.BIND}:{self.cfg.PORT}/live.m3u8" + + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + + live_edge_total = max(2, int(self.cfg.LIVE_EDGE_SEGMENTS) + int(self.cfg.LIVE_LAG_SEGMENTS)) + self.vlcw.set_media( + live_url, + options=[ + "--demux=hls", + ":no-audio", + ":http-reconnect=true", + ":hls-keep-live-session", + f":hls-live-edge={min(3, max(2, live_edge_total))}", + ":hls-segment-threads=2", + f":network-caching={max(200, int(self.cfg.NETWORK_CACHING))}", + ], + ) + self.vlcw.play() + self._live_sync.start() + + self.slider.setEnabled(True) + self.slider.setRange(0, self.dvr_duration_ms) + self.slider.blockSignals(True) + self.slider.setValue(self.dvr_duration_ms) + self.slider.blockSignals(False) + + QtCore.QTimer.singleShot(300, lambda: self.videoSurface.show_loading(False)) + print(f"[MODE] LIVE offset={self._playlist_offset_ms}ms total={self.dvr_duration_ms}ms") + + def _load_dvr(self): + print("[DVR] _load_dvr called but DVR freeze is disabled.") + self._set_idle() + + def _on_slider_clicked(self, value: int): + print(f"[SEEK] click -> {value}ms (mode_live={self.mode_live})") + self._freeze_ui(0.8) + if self.mode_live: + self.mode_live = False + self._set_live_badge(False) + self._seek_via_playlist(value) + + def _on_slider_drag_released(self, value: int): + print(f"[SEEK] drag-release -> {value}ms (mode_live={self.mode_live})") + self._freeze_ui(0.8) + if self.mode_live: + self.mode_live = False + self._set_live_badge(False) + self._seek_via_playlist(value) + + def _seek_via_playlist(self, t_ms: int): + # kill live sync right away so it cannot pull the thumb toward live + self._live_sync.stop() + if not self._dvr_growth.isActive(): + self._dvr_growth.start() + + if self.current_status == "resolved" or not self.proxy.dvr: + print("[SEEK] ignored (no DVR while resolved)") + return + + t_ms = max(0, min(int(t_ms), max(0, self.dvr_duration_ms))) + seek_url = f"http://{self.cfg.BIND}:{self.cfg.PORT}/dvr_seek.m3u8?t={t_ms}" + print(f"[SEEK] switching media to seek playlist: {seek_url}") + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + + self._playlist_offset_ms = t_ms + + if self.slider.maximum() < max(self.dvr_duration_ms, t_ms): + self.slider.setRange(0, max(self.dvr_duration_ms, t_ms)) + + self.vlcw.set_media(seek_url, options=["--demux=hls", ":no-audio"]) + self.vlcw.play() + self.mode_live = False + self._set_live_badge(False) + + self._update_time_label(t_ms) + self.slider.blockSignals(True) + self.slider.setValue(t_ms) + self.slider.blockSignals(False) + + self._last_abs_t_ms = t_ms + self._seek_guard_deadline = time.monotonic() + 2.0 + + QTimer.singleShot(700, lambda: setattr(self, "_ui_freeze_deadline", 0.0)) + + def _on_slider_hover(self, value: int): + if self.dvr_duration_ms > 0: + self._update_time_label(int(value)) + + def _on_vlc_pos(self, _pos01: float): + pass + + def _on_vlc_time(self, t_ms: int): + if t_ms < 0: + return + if time.monotonic() < self._ui_freeze_deadline: + return + + absolute_ms = self._playlist_offset_ms + t_ms + + now = time.monotonic() + if absolute_ms < self._last_abs_t_ms: + if now < self._seek_guard_deadline: + self._last_abs_t_ms = absolute_ms + else: + absolute_ms = self._last_abs_t_ms + else: + self._last_abs_t_ms = absolute_ms + + self._update_time_label(absolute_ms) + + if self.dvr_duration_ms > 0: + self.slider.blockSignals(True) + self.slider.setValue(min(absolute_ms, self.dvr_duration_ms)) + self.slider.blockSignals(False) + + if self.proxy.dvr and not self.proxy.resolved and (int(time.time()) % 2 == 0): + _, total = self.proxy.dvr.render_dvr_vod_playlist() + new_dur = int(total * 1000) + if new_dur > self.dvr_duration_ms: + self.dvr_duration_ms = new_dur + self.slider.setRange(0, self.dvr_duration_ms) + + def _update_time_label(self, t_ms: int): + s = max(0, t_ms // 1000) + h, s = divmod(s, 3600) + m, s = divmod(s, 60) + txt = f"{h:d}:{m:02d}:{s:02d}" if h else f"{m:d}:{s:02d}" + self.timeLeft.setText(txt) + + def closeEvent(self, event: QtGui.QCloseEvent): + try: + self.proxy.stop() + except Exception: + pass + super().closeEvent(event) + + diff --git a/AgCloud/GUI/src/vast/views/sensorDetailsTab.py b/AgCloud/GUI/src/vast/views/sensorDetailsTab.py new file mode 100644 index 000000000..9fb041576 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/sensorDetailsTab.py @@ -0,0 +1,277 @@ +import traceback +import plotly.graph_objects as go +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QComboBox +) +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtCore import QTimer + + +class SensorDetailsTab(QWidget): + """Sensor Details Tab – compact, clean, and fully in English.""" + + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + self.sensor_id = None + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(6) + + # --- Sensor selection area --- + self.input_layout = QHBoxLayout() + self.label = QLabel("Select sensor:") + self.label.setStyleSheet("font-weight:600;font-size:12px;") + + self.sensor_dropdown = QComboBox() + self.sensor_dropdown.setStyleSheet(""" + QComboBox { + padding:4px 8px; + border:1px solid #cbd5e1; + border-radius:4px; + font-size:12px; + background:white; + min-width:180px; + } + QComboBox:hover { border:1px solid #2563eb; } + """) + + self.load_button = QPushButton("Show Data") + self.load_button.setStyleSheet(""" + QPushButton { + background:#2563eb; + color:white; + border:none; + border-radius:4px; + padding:4px 10px; + font-size:12px; + font-weight:600; + } + QPushButton:hover { background:#1d4ed8; } + """) + self.load_button.clicked.connect(self._on_load_clicked) + + self.input_layout.addWidget(self.label) + self.input_layout.addWidget(self.sensor_dropdown) + self.add_button = self.load_button + self.input_layout.addWidget(self.add_button) + main_layout.addLayout(self.input_layout) + + # --- Web view area --- + self.web = QWebEngineView() + main_layout.addWidget(self.web) + + # --- Auto-refresh timer --- + self.timer = QTimer(self) + self.timer.timeout.connect(self.refresh_data) + self.timer.start(15000) + + self._load_sensor_list() + + self.web.setHtml( + "

Please select a sensor to view details

" + ) + + # -------------------------------------------------------- + def _load_sensor_list(self): + """ + Load only sensors that have data: + - event_logs_sensors OR + - sensors_anomalies_modal OR + - sensor_anomalies + """ + try: + r = self.api.http.get(f"{self.api.base}/api/tables/sensors") + sensors = r.json().get("rows", []) + + self.sensor_dropdown.clear() + self.sensor_dropdown.addItem("-- Select Sensor --", None) + + for s in sensors: + sid = s.get("sensor_id") + sname = s.get("sensor_name", "") + + # ---------- check if this sensor has real data ---------- + has_data = False + + # check modal anomalies + r_modal = self.api.http.get( + f"{self.api.base}/api/tables/sensors_anomalies_modal?sensor_id={sid}&limit=1" + ).json().get("rows", []) + if r_modal: + has_data = True + + # check sensor anomalies table + if not has_data: + r_anoms = self.api.http.get( + f"{self.api.base}/api/tables/sensor_anomalies?sensor={sid}&limit=1" + ).json().get("rows", []) + if r_anoms: + has_data = True + + # check logs + if not has_data: + r_logs = self.api.http.get( + f"{self.api.base}/api/tables/event_logs_sensors?device_id={sid}&limit=1" + ).json().get("rows", []) + if r_logs: + has_data = True + + # add only if data exists + if has_data: + display = f"{sid} – {sname}" + self.sensor_dropdown.addItem(display, sid) + + except Exception as e: + print(f"[SensorDetailsTab] Failed to load sensors list: {e}") + + # -------------------------------------------------------- + def _on_load_clicked(self): + selected_id = self.sensor_dropdown.currentData() + if not selected_id: + self.web.setHtml("

Please select a sensor

") + return + self.load_sensor(str(selected_id)) + + # -------------------------------------------------------- + def load_sensor(self, sensor_id: str): + self.sensor_id = sensor_id + self.refresh_data() + + # -------------------------------------------------------- + def refresh_data(self): + if not self.sensor_id: + return + try: + # Sensors + r_sensor = self.api.http.get(f"{self.api.base}/api/tables/sensors?sensor_id={self.sensor_id}") + sensors = r_sensor.json().get("rows", []) + sensor_data = sensors[0] if sensors else {} + + # Logs + r_logs = self.api.http.get( + f"{self.api.base}/api/tables/event_logs_sensors?device_id={self.sensor_id}&order_by=start_ts&order_dir=desc" + ).json().get("rows", []) + + # Modal anomalies + r_modal = self.api.http.get( + f"{self.api.base}/api/tables/sensors_anomalies_modal?sensor_id={self.sensor_id}&order_by=ts&order_dir=desc" + ).json().get("rows", []) + + # Sensor anomalies + r_anoms = self.api.http.get( + f"{self.api.base}/api/tables/sensor_anomalies?sensor={self.sensor_id}&limit=50&order_by=ts&order_dir=desc" + ).json().get("rows", []) + + # Active alert + active_alert = next((a for a in r_logs if a.get("end_ts") is None), None) + + chart_html = self._build_plot(r_anoms) + page_html = self._build_html(sensor_data, r_logs, r_modal, active_alert, chart_html) + + self.web.setHtml(page_html) + + except Exception as e: + traceback.print_exc() + self.web.setHtml(f"

Error: {e}

") + + # -------------------------------------------------------- + def _build_plot(self, anoms): + if not anoms: + return "
No data available for this sensor
" + + timestamps = [a.get("ts") for a in anoms] + values = [a.get("value") for a in anoms] + + fig = go.Figure() + fig.add_trace(go.Scatter( + x=timestamps, y=values, mode="lines+markers", + line=dict(color="#2563eb", width=2), + marker=dict(size=4) + )) + + fig.update_layout( + template="plotly_white", + height=240, + margin=dict(l=20, r=20, t=20, b=20), + xaxis_title="Timestamp", + yaxis_title="Value", + font=dict(family="Inter,Segoe UI,sans-serif", size=10) + ) + + return fig.to_html(include_plotlyjs="cdn", full_html=False) + + # -------------------------------------------------------- + def _build_html(self, sensor_data, logs, modal, active_alert, chart_html): + sensor_name = sensor_data.get("sensor_name", self.sensor_id) + + active_html = "" + if active_alert: + sev = active_alert.get("severity", "warn").capitalize() + issue = active_alert.get("issue_type", "Unknown") + started = active_alert.get("start_ts", "")[:19] + active_html = f""" +
+ Active Alert: {issue} | Severity: {sev} | Started: {started} +
+ """ + + combined = [] + for l in logs: + combined.append({ + "time": l.get("start_ts"), + "issue": l.get("issue_type"), + "severity": l.get("severity"), + "source": "event_logs_sensors" + }) + + for m in modal: + is_anomaly = m.get("anomaly") not in (0, "0", False, "false", None) + combined.append({ + "time": m.get("ts"), + "issue": "Model anomaly detected" if is_anomaly else "Model normal", + "severity": "critical" if is_anomaly else "info", + "source": "sensors_anomalies_modal" + }) + + combined.sort(key=lambda x: x.get("time") or "", reverse=True) + + rows = "".join([ + f"{r['time'][:19]}{r['issue']}" + f"{r['severity'].capitalize()}{r['source']}" + for r in combined + ]) or "No alerts found" + + return f""" + + + + + +

Sensor: {sensor_name}

+{active_html} +

Sensor Readings

{chart_html}
+

Alerts History

+ +{rows}
TimeIssueSeveritySource
+ +""" + + diff --git a/AgCloud/GUI/src/vast/views/sensorsMainView.py b/AgCloud/GUI/src/vast/views/sensorsMainView.py new file mode 100644 index 000000000..1c8176d7e --- /dev/null +++ b/AgCloud/GUI/src/vast/views/sensorsMainView.py @@ -0,0 +1,84 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTabWidget +from PyQt6.QtCore import Qt +from views.sensorsMapView import SensorsMapView +from views.sensorDetailsTab import SensorDetailsTab + + +class SensorsMainView(QWidget): + """ + Main container for the sensors module. + Contains two tabs: + 1. Map view (SensorsMapView) + 2. Sensor details (SensorDetailsTab) + """ + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + self.setWindowTitle("🌾 Sensors Dashboard") + self.setMinimumSize(1100, 750) + + # --- Layout --- # + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(10) + + # --- Header --- # + title = QLabel("📡 Sensors Dashboard") + title.setStyleSheet(""" + font-size:22px; + font-weight:800; + color:#0f172a; + margin-bottom:4px; + """) + layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignLeft) + + # --- Tabs --- # + self.tabs = QTabWidget() + self.tabs.setTabPosition(QTabWidget.TabPosition.North) + self.tabs.setStyleSheet(""" + QTabWidget::pane { + border: 1px solid #cbd5e1; + border-radius: 10px; + background: #f8fafc; + } + QTabBar::tab { + padding: 8px 16px; + margin-right: 2px; + background: #e2e8f0; + border-radius: 6px 6px 0 0; + font-weight: 600; + color: #0f172a; + } + QTabBar::tab:selected { + background: #2563eb; + color: white; + } + """) + + # --- Map tab --- # + self.map_tab = SensorsMapView(api, self) + self.tabs.addTab(self.map_tab, "🗺️ Map") + + # --- Details tab --- # + self.details_tab = SensorDetailsTab(api, self) + self.tabs.addTab(self.details_tab, "📊 Sensor Details") + + # Add tabs to layout + layout.addWidget(self.tabs) + + # ========================================================== + # === Navigation between tabs + # ========================================================== + def show_sensor_details(self, sensor_id: str): + """ + Called by the map (via JS bridge) when user clicks 'view details' on a sensor. + Loads the details tab and switches to it. + """ + print(f"[SensorsMainView] Showing details for sensor: {sensor_id}") + self.details_tab.load_sensor(sensor_id) + self.tabs.setCurrentIndex(1) + + def back_to_map(self): + """Switch back to the map tab.""" + print("[SensorsMainView] Returning to map tab") + self.tabs.setCurrentIndex(0) diff --git a/AgCloud/GUI/src/vast/views/sensorsMapView.py b/AgCloud/GUI/src/vast/views/sensorsMapView.py new file mode 100644 index 000000000..9c5c515e3 --- /dev/null +++ b/AgCloud/GUI/src/vast/views/sensorsMapView.py @@ -0,0 +1,142 @@ +import os, json +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTableWidget, QPushButton, QTableWidgetItem, QSizePolicy, QFrame +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtCore import QUrl, QTimer, Qt, pyqtSlot, QObject +from PyQt6.QtWebChannel import QWebChannel +from pathlib import Path +from dashboard_api import DashboardApi + +# Disable GPU (useful in Docker) +os.environ["QTWEBENGINE_DISABLE_GPU"] = "1" +os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-webgl" + +class JsBridge(QObject): + def __init__(self, parent): + super().__init__() + self.parent = parent + + @pyqtSlot(str) + def openSensorDetail(self, sensor_id): + """Called from JS when clicking 'view details'.""" + print(f"[JsBridge] openSensorDetail({sensor_id})") + # נסרוק עד לחלון העליון (MainWindow) בצורה בטוחה + try: + main_window = self.parent.window() + except Exception: + main_window = None + + if main_window and hasattr(main_window, "show_sensor_details"): + main_window.show_sensor_details(sensor_id) + +class SensorsMapView(QWidget): + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + self._map_ready = False + self._visible = False + self._closing = False + + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + title = QLabel("🗺️ Sensor Map") + title.setStyleSheet("font-size:20px;font-weight:700;color:#0f172a;") + layout.addWidget(title) + + # Map frame + map_frame = QFrame() + map_layout = QVBoxLayout(map_frame) + map_layout.setContentsMargins(0, 0, 0, 0) + self.web = QWebEngineView() + map_layout.addWidget(self.web) + layout.addWidget(map_frame, stretch=2) + + # Load map HTML + html_path = Path(__file__).resolve().parent / "assets" / "sensors_map.html" + self.web.setUrl(QUrl.fromLocalFile(str(html_path))) + + # Stats table + self.table = QTableWidget() + self.table.setAlternatingRowColors(True) + layout.addWidget(self.table, stretch=1) + + btn = QPushButton("⟳ Load Zone Stats") + btn.setStyleSheet("background:#2563eb;color:white;font-weight:700;padding:8px 14px;border-radius:8px;") + btn.clicked.connect(self.load_zone_stats) + layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignRight) + + # JS bridge + self.channel = QWebChannel() + self.bridge = JsBridge(self) + self.channel.registerObject("pyObj", self.bridge) + self.web.page().setWebChannel(self.channel) + self.web.loadFinished.connect(self._on_map_ready) + + self.timer = QTimer(self) + self.timer.timeout.connect(self.refresh_all) + + def _on_map_ready(self): + self._map_ready = True + print("[SensorsMapView] Map ready") + QTimer.singleShot(1000, self._inject_data) + + def _inject_data(self): + if not self._map_ready: + return + try: + r = self.api.http.get(f"{self.api.base}/api/tables/sensor_anomalies") + data = r.json() + js_data = json.dumps(data.get("rows", data)) + js = f"window.SENSOR_DATA={js_data};if(typeof renderSensors==='function')renderSensors(window.SENSOR_DATA);" + self.web.page().runJavaScript(js) + except Exception as e: + print("[SensorsMapView] Error:", e) + + def load_zone_stats(self): + try: + r = self.api.http.get(f"{self.api.base}/api/tables/sensor_zone_stats?limit=10&order_by=inserted_at&order_dir=desc") + rows = r.json().get("rows", []) + except Exception as e: + print("[SensorsMapView] API error:", e) + return + + self.table.clear() + if not rows: + self.table.setRowCount(0) + self.table.setColumnCount(1) + self.table.setHorizontalHeaderLabels(["No data"]) + return + + + exclude_keys = {"max", "min", "std", "median","mean"} + + keys = [k for k in rows[0].keys() if k not in exclude_keys] + + self.table.setColumnCount(len(keys)) + self.table.setHorizontalHeaderLabels(keys) + self.table.setRowCount(len(rows)) + + for i, row in enumerate(rows): + for j, key in enumerate(keys): + item = QTableWidgetItem(str(row.get(key, ""))) + item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.table.setItem(i, j, item) + + self.table.resizeColumnsToContents() + self.table.horizontalHeader().setStretchLastSection(True) + + def refresh_all(self): + if self._closing or not self._visible: + return + self.load_zone_stats() + self._inject_data() + + def showEvent(self, event): + super().showEvent(event) + self._visible = True + QTimer.singleShot(1000, self._inject_data) + + def hideEvent(self, event): + self._visible = False + super().hideEvent(event) diff --git a/AgCloud/GUI/src/vast/views/sensors_status_summary.py b/AgCloud/GUI/src/vast/views/sensors_status_summary.py new file mode 100644 index 000000000..79d20a46f --- /dev/null +++ b/AgCloud/GUI/src/vast/views/sensors_status_summary.py @@ -0,0 +1,521 @@ +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QTableWidget, + QTableWidgetItem, QHeaderView, QPushButton +) +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QColor +from datetime import datetime, timedelta + + +class SensorsStatusSummary(QWidget): + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + + # Cache for performance optimization + self._last_sensors_fetch = None + self._sensors_cache = [] + self._last_events_check = None + self._events_cache = [] + self._last_event_id = 0 # Track last processed event ID + + # Cache duration (5 minutes for sensors, 1 minute for events) + self._sensors_cache_duration = timedelta(minutes=5) + self._events_cache_duration = timedelta(minutes=1) + + self._build_ui() + self.load_data() + + # Auto-refresh timer for events only (every 30 seconds) + self._refresh_timer = QTimer() + self._refresh_timer.timeout.connect(self._refresh_events_only) + self._refresh_timer.start(30000) # 30 seconds + + def _build_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(30, 30, 30, 30) + main_layout.setSpacing(25) + + # -------- MODERN HEADER -------- + header_layout = QVBoxLayout() + + title = QLabel("🌾 Sensors Status Dashboard") + title.setStyleSheet(""" + QLabel { + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-size: 32px; + font-weight: 800; + color: #1a1a1a; + margin-bottom: 8px; + letter-spacing: -0.5px; + } + """) + + subtitle = QLabel("Real-time monitoring of agricultural sensors") + subtitle.setStyleSheet(""" + QLabel { + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-size: 16px; + font-weight: 400; + color: #6B7280; + margin-bottom: 15px; + } + """) + + header_layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignLeft) + header_layout.addWidget(subtitle, alignment=Qt.AlignmentFlag.AlignLeft) + main_layout.addLayout(header_layout) + + # -------- MODERN STATUS CARDS -------- + cards_row = QHBoxLayout() + cards_row.setSpacing(20) + self.active_card = self._create_status_card("Active Sensors", "●", "#10B981", "#F0FDF4") + self.inactive_card = self._create_status_card("Inactive Sensors", "●", "#EF4444", "#FEF2F2") + cards_row.addWidget(self.active_card) + cards_row.addWidget(self.inactive_card) + main_layout.addLayout(cards_row) + + # -------- MODERN TABLE -------- + self.table = QTableWidget(0, 5) + self.table.setHorizontalHeaderLabels(["ID", "Sensor Type", "Plant", "Plant ID", "Status"]) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.table.verticalHeader().setVisible(False) + self.table.setAlternatingRowColors(True) + self.table.setStyleSheet(""" + QTableWidget { + background-color: #ffffff; + alternate-background-color: #F9FAFB; + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-size: 14px; + border: 2px solid #E5E7EB; + border-radius: 12px; + gridline-color: #F3F4F6; + selection-background-color: #EEF2FF; + } + QHeaderView::section { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #F8FAFC, stop: 1 #F1F5F9); + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-weight: 700; + font-size: 15px; + color: #1F2937; + border: none; + border-bottom: 2px solid #E5E7EB; + padding: 12px 8px; + text-align: left; + } + QTableWidget::item { + padding: 12px 8px; + border-bottom: 1px solid #F3F4F6; + } + QTableWidget::item:selected { + background-color: #EEF2FF; + color: #1E40AF; + } + QTableWidget::item:hover { + background-color: #F8FAFC; + } + """) + main_layout.addWidget(self.table) + + # -------- MODERN REFRESH BUTTON -------- + button_layout = QHBoxLayout() + + refresh_btn = QPushButton("↻ Refresh Data") + refresh_btn.setFixedWidth(150) + refresh_btn.setFixedHeight(45) + refresh_btn.clicked.connect(self.refresh_all) + refresh_btn.setStyleSheet(""" + QPushButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #3B82F6, stop: 1 #1D4ED8); + color: white; + border-radius: 12px; + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-size: 15px; + font-weight: 600; + padding: 0px 16px; + border: none; + letter-spacing: 0.3px; + } + QPushButton:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #2563EB, stop: 1 #1E40AF); + } + QPushButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #1D4ED8, stop: 1 #1E3A8A); + } + """) + + button_layout.addStretch() + button_layout.addWidget(refresh_btn) + main_layout.addLayout(button_layout) + + self.setLayout(main_layout) + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #F8FAFC, stop: 1 #F1F5F9); + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + } + """) + + # -------- MODERN CARD CREATOR -------- + def _create_status_card(self, title_text, icon, accent_color, bg_color): + frame = QFrame() + frame.setFixedHeight(120) + frame.setStyleSheet(f""" + QFrame {{ + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 {bg_color}, stop: 1 #ffffff); + border-radius: 16px; + border: none; + padding: 0px; + }} + """) + + layout = QHBoxLayout(frame) + layout.setContentsMargins(24, 20, 24, 20) + layout.setSpacing(18) + + # Icon section + icon_label = QLabel(icon) + icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + icon_label.setFixedSize(50, 50) + icon_label.setStyleSheet(f""" + QLabel {{ + color: {accent_color}; + font-size: 36px; + font-weight: 900; + background: transparent; + border-radius: 25px; + font-family: 'Segoe UI Symbol', 'Arial'; + }} + """) + layout.addWidget(icon_label) + + # Text section + text_layout = QVBoxLayout() + text_layout.setSpacing(5) + + title = QLabel(title_text) + title.setStyleSheet(f""" + QLabel {{ + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-size: 16px; + font-weight: 600; + color: #374151; + letter-spacing: 0.2px; + }} + """) + + count = QLabel("0") + count.setObjectName(title_text.lower().replace(" ", "_")) + count.setStyleSheet(f""" + QLabel {{ + font-family: 'Segoe UI', 'Roboto', 'Inter', sans-serif; + font-size: 32px; + font-weight: 800; + color: {accent_color}; + letter-spacing: -1px; + }} + """) + + text_layout.addWidget(title) + text_layout.addWidget(count) + layout.addLayout(text_layout) + + return frame + + # -------- LOAD DATA (OPTIMIZED) -------- + def load_data(self, force_sensors_refresh=False): + """Load sensors and events data with caching optimization.""" + try: + # Load sensors (with caching) + sensors = self._get_sensors_cached(force_sensors_refresh) + + # Load recent keepalive events only (last 2 hours for performance) + events = self._get_recent_keepalive_events() + + except Exception as e: + print("[SensorsStatusSummary] Error loading data:", e) + return + + # identify inactive sensors by looking at the LATEST record for each device + inactive_ids = set() + + # Group events by device_id and issue_type + device_latest = {} + + for e in events: + if (e.get("issue_type") in ["missing_keepalive", "prolonged_silence"] + and str(e.get("device_id", "")).isdigit()): + device_id = str(e["device_id"]) + issue_type = e.get("issue_type") + key = f"{device_id}_{issue_type}" + + # Keep only the latest record for each device+issue_type combination + if key not in device_latest: + device_latest[key] = e + else: + # Compare by ID (higher ID = more recent) since start_ts can be identical + current_id = device_latest[key].get("id", 0) + new_id = e.get("id", 0) + if new_id > current_id: + device_latest[key] = e + + # Now check which devices have open issues based on latest records + for key, latest_event in device_latest.items(): + if latest_event.get("end_ts") is None: # Latest record is still open + device_id = str(latest_event["device_id"]) + inactive_ids.add(device_id) + + print(f"[SensorsStatusSummary] Found {len(inactive_ids)} sensors with active keepalive issues (based on latest records)") + + active = [s for s in sensors if s["id"] not in inactive_ids] + inactive = [s for s in sensors if s["id"] in inactive_ids] + + print(f"[SensorsStatusSummary] Status: {len(active)} active, {len(inactive)} inactive sensors") + print(f"[SensorsStatusSummary] DEBUG: inactive_ids = {sorted(inactive_ids)}") + print(f"[SensorsStatusSummary] DEBUG: sensor IDs = {[s['id'] for s in sensors]}") + print(f"[SensorsStatusSummary] DEBUG: active sensor IDs = {[s['id'] for s in active]}") + print(f"[SensorsStatusSummary] DEBUG: inactive sensor IDs = {[s['id'] for s in inactive]}") + + # Debug: show which sensors are inactive + if inactive_ids: + print(f"[SensorsStatusSummary] Inactive sensor IDs: {sorted(inactive_ids)}") + + self._update_cards(active, inactive) + self._update_table(active, inactive) + + def _get_sensors_cached(self, force_refresh=False): + """Get sensors list with caching (sensors don't change often).""" + now = datetime.now() + + if (not force_refresh and + self._last_sensors_fetch and + self._sensors_cache and + (now - self._last_sensors_fetch) < self._sensors_cache_duration): + return self._sensors_cache + + # Fetch fresh sensors data + res_sensors = self.api.http.get(f"{self.api.base}/api/tables/devices_sensor") + self._sensors_cache = res_sensors.json().get("rows", []) + self._last_sensors_fetch = now + print(f"[SensorsStatusSummary] Refreshed sensors cache: {len(self._sensors_cache)} sensors") + return self._sensors_cache + + def _get_recent_keepalive_events(self): + """Get events with smart caching - only fetch new events since last check.""" + now = datetime.now() + + # Check if cache is still valid + if (self._last_events_check and + self._events_cache and + (now - self._last_events_check) < self._events_cache_duration): + return self._events_cache + + try: + # Strategy: Get only NEW events since last check (incremental loading) + if self._last_event_id > 0: + # Get only events with ID > last processed ID + url = f"{self.api.base}/api/tables/event_logs_sensors?limit=200&order_by=id&order_dir=desc" + res_events = self.api.http.get(url) + new_events = res_events.json().get("rows", []) + + # Filter only truly new events + really_new = [e for e in new_events if e.get("id", 0) > self._last_event_id] + + if really_new: + print(f"[SensorsStatusSummary] Found {len(really_new)} new events") + # Update cache with new events + self._update_events_cache(really_new) + else: + print("[SensorsStatusSummary] No new events since last check") + else: + # First load - get recent events + print("[SensorsStatusSummary] First load - fetching initial events") + url = f"{self.api.base}/api/tables/event_logs_sensors?limit=500&order_by=id&order_dir=desc" + res_events = self.api.http.get(url) + all_events = res_events.json().get("rows", []) + self._initialize_events_cache(all_events) + + self._last_events_check = now + return self._events_cache + + except Exception as e: + print(f"[SensorsStatusSummary] Error loading events: {e}") + return self._events_cache or [] + + def _initialize_events_cache(self, all_events): + """Initialize cache on first load.""" + # Filter relevant events and store in cache + two_hours_ago = datetime.now() - timedelta(hours=2) + + filtered_events = [] + max_id = 0 + + for event in all_events: + event_id = event.get("id", 0) + if event_id > max_id: + max_id = event_id + + # Check issue type + if event.get("issue_type") not in ["missing_keepalive", "prolonged_silence"]: + continue + + # Keep both open events AND recently closed events (for cache invalidation) + is_open = event.get("end_ts") is None + is_recently_closed = False + + if not is_open: + # Check if closed recently (last 5 minutes) + end_ts_str = event.get("end_ts") + if end_ts_str: + try: + end_ts = datetime.fromisoformat(end_ts_str.replace('Z', '+00:00')) + five_min_ago = datetime.now() - timedelta(minutes=5) + is_recently_closed = end_ts.replace(tzinfo=None) >= five_min_ago + except (ValueError, AttributeError): + pass + + # Keep only open events or recently closed ones + if not (is_open or is_recently_closed): + continue + + # Check if recent (within last 2 hours) + start_ts_str = event.get("start_ts") + if start_ts_str: + try: + start_ts = datetime.fromisoformat(start_ts_str.replace('Z', '+00:00')) + if start_ts.replace(tzinfo=None) < two_hours_ago: + continue # Too old + except (ValueError, AttributeError): + continue # Invalid timestamp + + filtered_events.append(event) + + self._events_cache = filtered_events + self._last_event_id = max_id + print(f"[SensorsStatusSummary] Initialized cache with {len(filtered_events)} relevant events") + + def _update_events_cache(self, new_events): + """Update cache with new events (incremental).""" + two_hours_ago = datetime.now() - timedelta(hours=2) + + # Process new events + new_relevant = [] + max_id = self._last_event_id + + for event in new_events: + event_id = event.get("id", 0) + if event_id > max_id: + max_id = event_id + + # Apply same filtering (include recent closures) + if event.get("issue_type") in ["missing_keepalive", "prolonged_silence"]: + is_open = event.get("end_ts") is None + is_recently_closed = False + + if not is_open: + end_ts_str = event.get("end_ts") + if end_ts_str: + try: + end_ts = datetime.fromisoformat(end_ts_str.replace('Z', '+00:00')) + five_min_ago = datetime.now() - timedelta(minutes=5) + is_recently_closed = end_ts.replace(tzinfo=None) >= five_min_ago + except (ValueError, AttributeError): + pass + + if is_open or is_recently_closed: + # Check if recent + start_ts_str = event.get("start_ts") + if start_ts_str: + try: + start_ts = datetime.fromisoformat(start_ts_str.replace('Z', '+00:00')) + if start_ts.replace(tzinfo=None) >= two_hours_ago: + new_relevant.append(event) + except (ValueError, AttributeError): + pass + + # Update cache: add new events and remove old ones + self._events_cache.extend(new_relevant) + + # Clean old events from cache (older than 2 hours) + self._events_cache = [ + e for e in self._events_cache + if self._is_event_recent(e, two_hours_ago) + ] + + self._last_event_id = max_id + print(f"[SensorsStatusSummary] Added {len(new_relevant)} new events, cache now has {len(self._events_cache)} events") + + def _is_event_recent(self, event, threshold): + """Check if event is recent enough to keep in cache.""" + start_ts_str = event.get("start_ts") + if not start_ts_str: + return True # Keep if no timestamp + + try: + start_ts = datetime.fromisoformat(start_ts_str.replace('Z', '+00:00')) + return start_ts.replace(tzinfo=None) >= threshold + except (ValueError, AttributeError): + return True # Keep if invalid timestamp + + def _refresh_events_only(self): + """Auto-refresh only events data (called by timer).""" + try: + self.load_data(force_sensors_refresh=False) + except Exception as e: + print(f"[SensorsStatusSummary] Auto-refresh error: {e}") + + def refresh_all(self): + """Force refresh all data (sensors + events) - clear all caches.""" + # Clear all caches + self._events_cache = [] + self._last_event_id = 0 + self._last_events_check = None + self._sensors_cache = [] + self._last_sensors_fetch = None + + print("[SensorsStatusSummary] Cleared all caches - doing full refresh") + self.load_data(force_sensors_refresh=True) + + def _update_cards(self, active, inactive): + self.active_card.findChild(QLabel, "active_sensors").setText(str(len(active))) + self.inactive_card.findChild(QLabel, "inactive_sensors").setText(str(len(inactive))) + + def _update_table(self, active, inactive): + all_data = [(s, "Active") for s in active] + [(s, "Inactive") for s in inactive] + self.table.setRowCount(len(all_data)) + + for r, (sensor, status) in enumerate(all_data): + sid = QTableWidgetItem(str(sensor.get("id", ""))) + typ = QTableWidgetItem(sensor.get("sensor_type", "")) + # devices_sensor table doesn't have owner_name, use plant_id instead + plant_id = QTableWidgetItem(f"Plant {sensor.get('plant_id', '—')}") + # devices_sensor table doesn't have location, show plant_id info instead + location = QTableWidgetItem(f"Plant ID: {sensor.get('plant_id', 'N/A')}") + # Modern status with colored badges + if status == "Active": + stat = QTableWidgetItem("● ONLINE") + stat.setForeground(Qt.GlobalColor.darkGreen) + else: + stat = QTableWidgetItem("● OFFLINE") + stat.setForeground(Qt.GlobalColor.darkRed) + + # Style inactive rows with subtle background + if status == "Inactive": + gray_bg = QColor(248, 250, 252) + gray_text = QColor(107, 114, 128) + + for item in (sid, typ, plant_id, location): + item.setBackground(gray_bg) + item.setForeground(gray_text) + + self.table.setItem(r, 0, sid) + self.table.setItem(r, 1, typ) + self.table.setItem(r, 2, plant_id) + self.table.setItem(r, 3, location) + self.table.setItem(r, 4, stat) diff --git a/AgCloud/GUI/src/vast/views/sensors_view.py b/AgCloud/GUI/src/vast/views/sensors_view.py new file mode 100644 index 000000000..5a6dc952c --- /dev/null +++ b/AgCloud/GUI/src/vast/views/sensors_view.py @@ -0,0 +1,339 @@ +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QScrollArea, QGridLayout, QFrame, QDialog, QDialogButtonBox, + QFormLayout, QComboBox +) +from PyQt6.QtCore import Qt, QTimer +import traceback + + +SEVERITY_RANK = { + "info": 0, + "ok": 0, + "normal": 0, + "warn": 1, + "warning": 1, + "error": 2, + "critical": 3, +} + + +# ============================================================ +# SENSOR CARD +# ============================================================ +class SensorCard(QFrame): + def __init__(self, sensor_data: dict, on_click): + super().__init__() + self.data = sensor_data + self.on_click = on_click + self.setObjectName("card") + self._build_ui() + self.mousePressEvent = self._on_click + + def _on_click(self, event): + self.on_click(self.data) + + def _build_ui(self): + layout = QVBoxLayout(self) + layout.setSpacing(5) + layout.setContentsMargins(12, 12, 12, 10) + + title = QLabel(self.data.get("sensor_name", "Unknown Sensor")) + title.setStyleSheet("font-weight:600; font-size:15px; color:#111;") + layout.addWidget(title) + + # ← NEW: show sensor_id + sensor_id = self.data.get("sensor_id", "N/A") + id_lbl = QLabel(f"ID: {sensor_id}") + id_lbl.setStyleSheet("color:#777; font-size:12px;") + layout.addWidget(id_lbl) + + stype = QLabel(f"Type: {self.data.get('sensor_type', 'N/A')}") + stype.setStyleSheet("color:#555; font-size:12px;") + layout.addWidget(stype) + + issue = QLabel(f"Issue: {self.data.get('Issue', 'No alerts')}") + issue.setStyleSheet("font-size:12px; color:#444;") + layout.addWidget(issue) + + layout.addStretch() + + sev = self.data.get("Severity", "info").lower() + sev_label = QLabel(sev.capitalize()) + sev_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + sev_label.setFixedHeight(20) + + if sev == "info": + sev_label.setStyleSheet("background-color:#D9FAD3; color:#1B5E20; border-radius:6px; font-weight:600;") + elif sev in ("warn", "warning"): + sev_label.setStyleSheet("background-color:#FFF5BA; color:#8B8000; border-radius:6px; font-weight:600;") + elif sev in ("error", "critical"): + sev_label.setStyleSheet("background-color:#FFD5D5; color:#B71C1C; border-radius:6px; font-weight:600;") + else: + sev_label.setStyleSheet("background-color:#EEE; color:#333; border-radius:6px; font-weight:600;") + + layout.addWidget(sev_label) + + self.setFixedSize(230, 120) + + +# ============================================================ +# ALERT DETAILS DIALOG +# ============================================================ +class AlertDialog(QDialog): + def __init__(self, sensor): + super().__init__() + self.setWindowTitle(f"Sensor Details – {sensor.get('sensor_name')}") + self.setMinimumSize(520, 450) + + layout = QVBoxLayout(self) + + # ← NEW: richer sensor details + sensor_details = f""" + Name: {sensor.get('sensor_name')}
+ ID: {sensor.get('sensor_id')}
+ Type: {sensor.get('sensor_type')}
+ Status: {sensor.get('status', 'Unknown')}
+ Latitude: {sensor.get('lat', 'N/A')}
+ Longitude: {sensor.get('lon', 'N/A')}
+ """ + header = QLabel(sensor_details) + header.setWordWrap(True) + layout.addWidget(header) + + alerts = sensor.get("All Alerts", []) + layout.addSpacing(10) + + body = QWidget() + body_layout = QVBoxLayout(body) + body_layout.setSpacing(8) + + if alerts: + for a in sorted(alerts, key=lambda x: x.get("start_ts", ""), reverse=True): + card = QFrame() + severity = a.get("severity", "info") + border_color = { + "critical": "#FF4444", + "error": "#FF8800", + "warn": "#FFCC00", + }.get(severity, "#44AA44") + + card.setStyleSheet(f""" + QFrame {{ + border-radius: 8px; + border: 2px solid {border_color}; + background-color: #FFF; + padding: 6px; + margin: 2px; + }} + """) + + form = QFormLayout(card) + start = a.get("start_ts", "")[:19] + end = a.get("end_ts", "") + form.addRow("Start:", QLabel(start)) + form.addRow("End:", QLabel(end if end else "[ACTIVE]")) + form.addRow("Issue:", QLabel(a.get("issue_type", ""))) + form.addRow("Severity:", QLabel(a.get("severity", ""))) + + details = a.get("details", {}) + for k, v in details.items(): + form.addRow(f"{k.title()}:", QLabel(str(v))) + + body_layout.addWidget(card) + else: + body_layout.addWidget(QLabel("No alerts.")) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setWidget(body) + layout.addWidget(scroll) + + btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok) + btns.accepted.connect(self.accept) + layout.addWidget(btns) + + +# ============================================================ +# MAIN VIEW +# ============================================================ +class SensorsView(QWidget): + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + self.all_sensors = [] + + self._build_ui() + self.load_sensors() + + self.timer = QTimer(self) + self.timer.timeout.connect(self.load_sensors) + self.timer.start(30000) + + def _build_ui(self): + main_layout = QVBoxLayout(self) + header = QHBoxLayout() + + title = QLabel("🌡️ Unified Sensor Alerts Dashboard") + title.setStyleSheet("font-size:22px; font-weight:700; color:#111;") + header.addWidget(title) + header.addStretch() + + self.filter_box = QComboBox() + self.filter_box.addItems(["All", "Info", "Warning", "Error", "Critical"]) + self.filter_box.currentTextChanged.connect(self._apply_filters) + header.addWidget(self.filter_box) + + self.search_box = QLineEdit() + self.search_box.setPlaceholderText("Search sensors...") + self.search_box.textChanged.connect(self._apply_filters) + self.search_box.setFixedWidth(220) + header.addWidget(self.search_box) + + # ← REMOVED refresh button completely + + main_layout.addLayout(header) + + self.scroll = QScrollArea() + self.scroll.setWidgetResizable(True) + self.container = QWidget() + self.grid = QGridLayout(self.container) + self.grid.setSpacing(12) + self.scroll.setWidget(self.container) + main_layout.addWidget(self.scroll) + + self.setLayout(main_layout) + + # ------------------------------------------------------------ + def load_sensors(self): + try: + res_sensors = self.api.http.get(f"{self.api.base}/api/tables/sensors").json() + res_anoms = self.api.http.get(f"{self.api.base}/api/tables/sensors_anomalies_modal").json() + res_logs = self.api.http.get(f"{self.api.base}/api/tables/event_logs_sensors").json() + + sensors = res_sensors.get("rows", []) + anomalies = res_anoms.get("rows", []) + alerts = res_logs.get("rows", []) + + except: + traceback.print_exc() + return + + # --------------- map anomalies --------------- + anomaly_latest = {} + for a in anomalies: + sid = a.get("sensor_id") + if not sid: + continue + prev = anomaly_latest.get(sid) + if prev is None or a.get("ts", "") > prev.get("ts", ""): + anomaly_latest[sid] = a + + # --------------- map alerts --------------- + alerts_by_sensor = {} + for alert in alerts: + dev = alert.get("device_id") + if not dev: + continue + if alert.get("end_ts"): + continue + alerts_by_sensor.setdefault(dev, []).append(alert) + + # --------------- merge --------------- + merged = [] + for s in sensors: + sensor_id = s.get("sensor_id") + name = s.get("sensor_name") + s_type = s.get("sensor_type") + + alerts_for_s = alerts_by_sensor.get(name, []) + active = [a for a in alerts_for_s if not a.get("end_ts")] + + if active: + latest = sorted(active, key=lambda x: x.get("start_ts", ""), reverse=True)[0] + sev_alert = latest.get("severity", "info").lower() + issue_alert = latest.get("issue_type", "") + else: + sev_alert = "info" + issue_alert = None + + anom = anomaly_latest.get(sensor_id) + if anom and anom.get("anomaly", 0) > 0: + sev_anom = "error" + issue_anom = "Anomaly detected" + else: + sev_anom = "info" + issue_anom = None + + sev_final = sev_alert + issue_final = issue_alert or "No active alerts" + if SEVERITY_RANK[sev_anom] > SEVERITY_RANK[sev_alert]: + sev_final = sev_anom + issue_final = issue_anom + + all_alerts = alerts_for_s.copy() + if anom: + all_alerts.append({ + "start_ts": anom.get("ts"), + "severity": sev_anom, + "issue_type": "anomaly_modal", + "details": {"anomaly": anom.get("anomaly")} + }) + + merged.append({ + "sensor_id": sensor_id, + "sensor_name": name, + "sensor_type": s_type, + "Issue": issue_final, + "Severity": sev_final, + "All Alerts": all_alerts, + "status": s.get("status"), + "lat": s.get("lat"), + "lon": s.get("lon"), + }) + + self.all_sensors = merged + self._apply_filters() + + # ------------------------------------------------------------ + def _render_cards(self, sensors): + for i in reversed(range(self.grid.count())): + w = self.grid.itemAt(i).widget() + if w: + w.setParent(None) + + if not sensors: + lbl = QLabel("No sensors found") + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.grid.addWidget(lbl, 0, 0) + return + + cols = 3 + for i, s in enumerate(sensors): + card = SensorCard(s, self._show_alert_history) + r, c = divmod(i, cols) + self.grid.addWidget(card, r, c) + + # ------------------------------------------------------------ + def _apply_filters(self): + text = self.search_box.text().lower().strip() + filt = self.filter_box.currentText().lower() + + filtered = [] + for s in self.all_sensors: + name = str(s.get("sensor_name", "")).lower() + t = str(s.get("sensor_type", "")).lower() + sev = s.get("Severity", "").lower() + + if text and text not in name and text not in t: + continue + if filt != "all" and filt not in sev: + continue + filtered.append(s) + + self._render_cards(filtered) + + # ------------------------------------------------------------ + def _show_alert_history(self, sensor): + dlg = AlertDialog(sensor) + dlg.exec() diff --git a/AgCloud/GUI/src/vast/views/sound/map_background.png b/AgCloud/GUI/src/vast/views/sound/map_background.png new file mode 100644 index 000000000..2f71da42e Binary files /dev/null and b/AgCloud/GUI/src/vast/views/sound/map_background.png differ diff --git a/AgCloud/GUI/src/vast/views/sound/sound_view.py b/AgCloud/GUI/src/vast/views/sound/sound_view.py new file mode 100644 index 000000000..0d3d59d2d --- /dev/null +++ b/AgCloud/GUI/src/vast/views/sound/sound_view.py @@ -0,0 +1,2030 @@ +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, + QComboBox, QPushButton, QLineEdit, QDateEdit, + QFrame, QMessageBox, QTabWidget, QTableWidget, + QTableWidgetItem, QHeaderView, QAbstractItemView, + QStackedWidget, QSizePolicy, QCheckBox, QToolBar, QScrollArea +) +from PyQt6.QtCore import Qt, QDate, QUrl, QTimer, pyqtSignal, QSize +from PyQt6.QtGui import QPixmap, QColor, QCursor, QPainter, QFont +from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput +from dashboard_api import DashboardApi +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import numpy as np +from datetime import datetime, timedelta +from vast.dashboard_api import DashboardApi +import requests +import os +import math + + +# ========================================================== +# Audio Waveform Visualizer +# ========================================================== +class AudioWaveform(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setMinimumHeight(180) + self.setMaximumHeight(220) + self.bars = [] + self.animation_offset = 0 + self.is_playing = False + + self.timer = QTimer(self) + self.timer.timeout.connect(self.animate) + + self.setStyleSheet(""" + background: qlineargradient(x1:0, y1:0, x2:1, y2:1, + stop:0 #0f0c29, stop:0.5 #302b63, stop:1 #24243e); + border-radius: 12px; + border: 3px solid qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #667eea, stop:1 #764ba2); + """) + + self.default_text = "🎵 Press Play to Visualize Audio 🎵" + + def start_animation(self): + self.is_playing = True + self.bars = [0.2 + (i % 5) * 0.15 for i in range(100)] + self.timer.start(40) + + def stop_animation(self): + self.is_playing = False + self.timer.stop() + self.bars = [] + self.update() + + def animate(self): + if not self.is_playing: + return + + self.animation_offset = (self.animation_offset + 2) % 360 + + for i in range(len(self.bars)): + wave1 = math.sin((self.animation_offset + i * 8) * math.pi / 180) + wave2 = math.cos((self.animation_offset + i * 12) * math.pi / 180) + self.bars[i] = 0.25 + abs(wave1 * 0.35) + abs(wave2 * 0.25) + + self.update() + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + width = self.width() + height = self.height() + + if not self.bars: + painter.setPen(QColor(150, 180, 230, 200)) + font = painter.font() + font.setPointSize(14) + font.setBold(True) + painter.setFont(font) + painter.drawText(0, 0, width, height, + Qt.AlignmentFlag.AlignCenter, + self.default_text) + return + + bar_width = width / len(self.bars) + center_y = height / 2 + + gradient_colors = [ + (QColor(102, 126, 234), QColor(118, 75, 162)), + (QColor(67, 233, 123), QColor(56, 249, 215)), + (QColor(251, 200, 212), QColor(151, 149, 240)), + (QColor(250, 208, 196), QColor(255, 209, 255)) + ] + + for i, amplitude in enumerate(self.bars): + x = i * bar_width + bar_height = amplitude * (height - 30) + y_top = center_y - bar_height / 2 + y_bottom = center_y + bar_height / 2 + + gradient_idx = (i // 25) % len(gradient_colors) + color1, color2 = gradient_colors[gradient_idx] + + from PyQt6.QtGui import QLinearGradient + gradient = QLinearGradient(x, y_top, x, y_bottom) + gradient.setColorAt(0, color1) + gradient.setColorAt(1, color2) + + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(gradient) + + bar_rect_width = max(2, bar_width - 1.5) + painter.drawRoundedRect( + int(x + 0.75), int(y_top), + int(bar_rect_width), int(bar_height), + 3, 3 + ) + + glow = QColor(255, 255, 255, 30) + painter.setBrush(glow) + painter.drawRoundedRect( + int(x + bar_width * 0.2), int(y_top + 2), + int(bar_rect_width * 0.3), int(bar_height * 0.4), + 2, 2 + ) + + +# ========================================================== +# Microphone Button Widget +# ========================================================== +class MicrophoneButton(QPushButton): + def __init__(self, mic_id: str, mic_name: str, mic_type: str, parent=None): + super().__init__(parent) + self.mic_id = mic_id + self.mic_name = mic_name + self.mic_type = mic_type + self.is_selected = False + + if mic_type == "audio": + self.setFixedSize(70, 70) + self.shape_style = "border-radius: 35px;" + else: # ultrasound + self.setFixedSize(70, 70) + self.shape_style = "border-radius: 8px;" + + self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + + self.base_color = "#000000" + self.selected_color = "#0078d4" + self.hover_color = "#222222" + self.disabled_color = "#888888" + + self.update_style() + + self.setText(f"{mic_id.upper()}") + self.setToolTip(f"{mic_name}
Type: {mic_type}
Click to select") + + def update_style(self): + if not self.isEnabled(): + color = self.disabled_color + border = "#555" + elif self.is_selected: + color = self.selected_color + border = "#1e90ff" + else: + color = self.base_color + border = "white" + + self.setStyleSheet(f""" + QPushButton {{ + background-color: {color}; + color: white; + border: 3px solid {border}; + {self.shape_style} + font-size: 15px; + font-weight: bold; + padding: 4px; + }} + QPushButton:hover {{ + background-color: {self.hover_color}; + }} + """) + + def set_selected(self, selected: bool): + self.is_selected = selected + self.update_style() + + def set_disabled_state(self, disabled: bool): + self.setEnabled(not disabled) + self.update_style() + + +# ========================================================== +# Interactive Map with Image +# ========================================================== +class ImageMapView(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(15) + + self.selected_mics = [] + self.selected_type = None + self.mic_buttons = {} + + self.stacked_widget = QStackedWidget() + + # Map view page + self.map_page = QWidget() + layout = QVBoxLayout(self.map_page) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(15) + + # Control panel + control_panel = QFrame() + control_panel.setStyleSheet(""" + QFrame { + background-color: white; + border: 2px solid #d1d5da; + border-radius: 8px; + padding: 10px; + } + """) + control_layout = QHBoxLayout(control_panel) + + self.selection_label = QLabel("Select microphones to view recordings") + self.selection_label.setStyleSheet("font-weight: bold; color: #333;") + + self.view_button = QPushButton("View Selected Recordings") + self.view_button.setEnabled(False) + self.view_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + self.view_button.setStyleSheet(""" + QPushButton { + background-color: #28a745; + color: white; + border-radius: 6px; + padding: 10px 20px; + font-weight: bold; + } + QPushButton:hover:enabled { + background-color: #218838; + } + QPushButton:disabled { + background-color: #CCCCCC; + } + """) + self.view_button.clicked.connect(self.view_selected_recordings) + + self.clear_button = QPushButton("✕ Clear Selection") + self.clear_button.setEnabled(False) + self.clear_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + self.clear_button.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border-radius: 6px; + padding: 10px 20px; + font-weight: bold; + } + QPushButton:hover:enabled { + background-color: #c82333; + } + QPushButton:disabled { + background-color: #CCCCCC; + } + """) + self.clear_button.clicked.connect(self.clear_selection) + + control_layout.addWidget(self.selection_label) + control_layout.addStretch() + control_layout.addWidget(self.clear_button) + control_layout.addWidget(self.view_button) + + subtitle = QLabel("Click on microphones to select them (same type only)") + subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) + subtitle.setStyleSheet("font-size: 14px; color: #666; padding: 5px;") + + self.map_container = QWidget() + self.map_container.setMinimumSize(800, 600) + self.map_container.setStyleSheet(""" + background-color: #e8f4f8; + border: 3px solid #4A90E2; + border-radius: 15px; + """) + + self.background_label = QLabel(self.map_container) + self.background_label.setGeometry(0, 0, 800, 600) + self.background_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Load map image + self._load_map_image() + + # Microphone definitions + # Map to actual device_ids from docker-compose: + # MIC-01 = environment sounds (sounds/) + # MIC-02 = ultrasound plants (plants/) + self.microphones = [ + {"id": "MIC-01", "name": "Environment Mic", "type": "audio", "position": (200, 150)}, + {"id": "MIC-02", "name": "Plant Ultrasound", "type": "ultrasound", "position": (500, 300)}, + {"id": "MIC-03", "name": "Environment Mic", "type": "audio", "position": (350, 450)} + ] + + for mic in self.microphones: + btn = MicrophoneButton(mic["id"], mic["name"], mic["type"], self.map_container) + btn.move(mic["position"][0], mic["position"][1]) + btn.clicked.connect(lambda checked, m=mic, b=btn: self.on_microphone_clicked(m, b)) + self.mic_buttons[mic["id"]] = btn + + legend = QLabel("🎤 circle = Audio Sensor • 🔊 square = Ultrasound Sensor") + legend.setAlignment(Qt.AlignmentFlag.AlignCenter) + legend.setStyleSheet(""" + font-size: 14px; + font-weight: 500; + padding: 10px; + background-color: white; + border: 2px solid #ddd; + border-radius: 8px; + color: #333; + """) + + layout.addWidget(control_panel) + layout.addWidget(subtitle) + layout.addWidget(self.map_container, 1) + layout.addWidget(legend) + + self.stacked_widget.addWidget(self.map_page) + self.recordings_page = None + + self.main_layout.addWidget(self.stacked_widget) + + def _load_map_image(self): + """Load the map background image""" + possible_paths = [ + "map_background.png", + "./map_background.png", + "../map_background.png", + os.path.join(os.getcwd(), "map_background.png"), + os.path.join(os.path.dirname(__file__), "map_background.png") + ] + + image_loaded = False + for path in possible_paths: + if os.path.exists(path): + pixmap = QPixmap(path) + if not pixmap.isNull(): + scaled = pixmap.scaled(800, 600, Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation) + self.background_label.setPixmap(scaled) + image_loaded = True + break + + if not image_loaded: + self.background_label.setStyleSheet("font-size: 16px; color: #888; background-color: #d0e8f2;") + self.background_label.setText("Sensor Locations\n\n(Map image not found)") + + def on_microphone_clicked(self, mic_data, button): + mic_id = mic_data["id"] + mic_type = mic_data["type"] + + if mic_id in self.selected_mics: + self.selected_mics.remove(mic_id) + button.set_selected(False) + + if not self.selected_mics: + self.selected_type = None + self.enable_all_buttons() + + self.update_selection_display() + return + + if self.selected_type is None: + self.selected_type = mic_type + self.disable_other_type_buttons(mic_type) + + if mic_type == self.selected_type: + self.selected_mics.append(mic_id) + button.set_selected(True) + self.update_selection_display() + + def disable_other_type_buttons(self, allowed_type): + for mic in self.microphones: + if mic["type"] != allowed_type: + self.mic_buttons[mic["id"]].set_disabled_state(True) + + def enable_all_buttons(self): + for btn in self.mic_buttons.values(): + btn.set_disabled_state(False) + + def clear_selection(self): + self.selected_mics = [] + self.selected_type = None + for btn in self.mic_buttons.values(): + btn.set_selected(False) + btn.set_disabled_state(False) + self.update_selection_display() + + def update_selection_display(self): + count = len(self.selected_mics) + if count == 0: + self.selection_label.setText("Select microphones to view recordings") + self.view_button.setEnabled(False) + self.clear_button.setEnabled(False) + else: + type_text = "Audio" if self.selected_type == "audio" else "Ultrasound" + self.selection_label.setText( + f"Selected {count} {type_text} microphone(s): {', '.join([m.upper() for m in self.selected_mics])}" + ) + self.view_button.setEnabled(True) + self.clear_button.setEnabled(True) + + def view_selected_recordings(self): + if not self.selected_mics: + return + + selected_mic_data = [mic for mic in self.microphones if mic["id"] in self.selected_mics] + + if self.recordings_page: + self.stacked_widget.removeWidget(self.recordings_page) + self.recordings_page.deleteLater() + + self.recordings_page = QWidget() + recordings_layout = QVBoxLayout(self.recordings_page) + recordings_layout.setContentsMargins(0, 0, 0, 0) + recordings_layout.setSpacing(0) + + # Header + header_container = QWidget() + color = "#4A90E2" if self.selected_type == "audio" else "#50C878" + header_container.setStyleSheet(f"background-color: {color};") + header_layout = QHBoxLayout(header_container) + header_layout.setContentsMargins(10, 10, 10, 10) + + back_button = QPushButton("← Back to Map") + back_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + back_button.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 255, 255, 0.2); + color: white; + border: 2px solid white; + border-radius: 6px; + padding: 8px 16px; + font-weight: bold; + } + QPushButton:hover { background-color: rgba(255, 255, 255, 0.3); } + """) + back_button.clicked.connect(self.show_map) + + mic_names = ", ".join([m["name"] for m in selected_mic_data]) + header = QLabel(f"Recordings: {mic_names}") + header.setAlignment(Qt.AlignmentFlag.AlignCenter) + header.setStyleSheet("font-size: 20px; font-weight: bold; color: white;") + + header_layout.addWidget(back_button) + header_layout.addWidget(header, 1) + header_layout.addStretch() + + type_text = "AUDIO" if self.selected_type == "audio" else "ULTRASOUND" + mic_ids = ", ".join([m["id"].upper() for m in selected_mic_data]) + subtitle = QLabel(f"Type: {type_text} • Microphones: {mic_ids}") + subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter) + subtitle.setStyleSheet(""" + font-size: 13px; color: white; padding: 5px; + background-color: rgba(0, 0, 0, 0.2); + """) + + recordings_layout.addWidget(header_container) + recordings_layout.addWidget(subtitle) + + mic_ids_list = [m["id"] for m in selected_mic_data] + sound_tab = RecordingsTab( + mic_ids=mic_ids_list, + recording_type=self.selected_type, + parent=self + ) + recordings_layout.addWidget(sound_tab) + + self.stacked_widget.addWidget(self.recordings_page) + self.stacked_widget.setCurrentWidget(self.recordings_page) + + def show_map(self): + self.stacked_widget.setCurrentWidget(self.map_page) + + +# ========================================================== +# Recordings Tab +# ========================================================== + +class RecordingsTab(QWidget): + def __init__(self, mic_ids=None, recording_type="audio", parent=None, api=None): + super().__init__(parent) + self.mic_ids = mic_ids if mic_ids else [] + self.recording_type = recording_type + self.api = api + + + # API endpoints + if recording_type == "ultrasound": + self.api_url = "http://db_api_service:8001/api/files/plant-predictions/" + else: + self.api_url = "http://db_api_service:8001/api/files/audio-aggregates/" + + layout = QVBoxLayout(self) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(15) + + # Filters Frame + filter_frame = self._create_filter_frame() + + list_label = QLabel("Available Recordings") + list_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #333; padding: 5px;") + + # Table setup + self.file_table = self._create_table() + + # Waveform + waveform_container = self._create_waveform_container() + + # Status + self.status_label = QLabel("Ready") + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.status_label.setStyleSheet(""" + font-size: 13px; color: #666; padding: 8px; + background-color: #f6f8fa; border-radius: 6px; border: 1px solid #d1d5da; + """) + + # Media player + self.player = QMediaPlayer() + self.audio_output = QAudioOutput() + self.player.setAudioOutput(self.audio_output) + self.player.playbackStateChanged.connect(self.on_playback_state_changed) + + layout.addWidget(filter_frame) + layout.addWidget(list_label) + layout.addWidget(self.file_table, 1) + layout.addWidget(waveform_container) + layout.addWidget(self.status_label) + + self.refresh_button.clicked.connect(self.update_list) + self.update_list() + + def _create_filter_frame(self): + filter_frame = QFrame() + filter_frame.setStyleSheet(""" + QFrame { background-color: #ffffff; border: 2px solid #e1e4e8; + border-radius: 12px; padding: 15px; } + """) + filters_layout = QVBoxLayout(filter_frame) + filters_layout.setSpacing(12) + + # Row 1 + row1 = QHBoxLayout() + row1.setSpacing(10) + + type_label = QLabel("Filter by Type:") + type_label.setStyleSheet("font-weight: bold; color: #333;") + + self.noise_filter = QComboBox() + self.noise_filter.setStyleSheet(""" + QComboBox { padding: 8px; border: 2px solid #d1d5da; border-radius: 6px; + background: white; min-width: 150px; } + QComboBox:hover { border: 2px solid #4A90E2; } + """) + + if self.recording_type == "ultrasound": + self.noise_filter.addItems([ + "All signals", "Empty Pot", "Greenhouse Noises", + "Tobacco Cut", "Tobacco Dry", "Tomato Cut", "Tomato Dry" + ]) + else: + self.noise_filter.addItems([ + "All types", "predatory_animals", "non_predatory_animals", + "birds", "fire", "footsteps", "insects", + "screaming", "shotgun", "stormy_weather", + "streaming_water", "vehicle", "Other" + ]) + + date_label = QLabel("Date Range:") + date_label.setStyleSheet("font-weight: bold; color: #333;") + + self.date_from = QDateEdit() + self.date_from.setCalendarPopup(True) + self.date_from.setDate(QDate.currentDate().addDays(-7)) + self.date_from.setStyleSheet(""" + QDateEdit { padding: 8px; border: 2px solid #d1d5da; + border-radius: 6px; background: white; } + """) + + self.date_to = QDateEdit() + self.date_to.setCalendarPopup(True) + self.date_to.setDate(QDate.currentDate()) + self.date_to.setStyleSheet(self.date_from.styleSheet()) + + row1.addWidget(type_label) + row1.addWidget(self.noise_filter) + row1.addWidget(date_label) + row1.addWidget(self.date_from) + row1.addWidget(QLabel("→")) + row1.addWidget(self.date_to) + + # Row 2 + row2 = QHBoxLayout() + row2.setSpacing(10) + + sort_label = QLabel("Sort by:") + sort_label.setStyleSheet("font-weight: bold; color: #333;") + + self.sort_by = QComboBox() + self.sort_by.addItems(["Date (Newest)", "Date (Oldest)", "Length", "Device"]) + self.sort_by.setStyleSheet(self.noise_filter.styleSheet()) + + self.search_box = QLineEdit() + self.search_box.setPlaceholderText("Search by filename...") + self.search_box.setStyleSheet(""" + QLineEdit { padding: 8px; border: 2px solid #d1d5da; border-radius: 6px; + background: white; font-size: 14px; } + QLineEdit:focus { border: 2px solid #4A90E2; } + """) + + self.refresh_button = QPushButton("🔄 Refresh") + self.refresh_button.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + self.refresh_button.setStyleSheet(""" + QPushButton { background-color: #4A90E2; color: white; border-radius: 8px; + padding: 10px 20px; font-weight: bold; font-size: 14px; } + QPushButton:hover { background-color: #357ABD; } + """) + + row2.addWidget(sort_label) + row2.addWidget(self.sort_by) + row2.addWidget(self.search_box, 2) + row2.addWidget(self.refresh_button) + + filters_layout.addLayout(row1) + filters_layout.addLayout(row2) + + return filter_frame + + def _create_table(self): + table = QTableWidget() + + if self.recording_type == "ultrasound": + table.setColumnCount(6) + table.setHorizontalHeaderLabels([ + "File", "Device", "Predicted Class", "Confidence", "Watering Status", "Actions" + ]) + else: + table.setColumnCount(6) + table.setHorizontalHeaderLabels([ + "Filename", "Device", "Predicted Label", "Probability", "Format", "Actions" + ]) + + header = table.horizontalHeader() + header.setStretchLastSection(False) + for i in range(table.columnCount()): + header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch) + + table.setStyleSheet(""" + QTableWidget { + background: #ffffff; + border: 2px solid #d1d5da; + border-radius: 10px; + gridline-color: #e1e4e8; + font-size: 14px; + } + QTableWidget::item { padding: 8px; } + QTableWidget::item:hover { background-color: #f6f8fa; } + QTableWidget::item:selected { background-color: #d6eaff; color: #0366d6; } + QHeaderView::section { + background-color: #f6f8fa; + padding: 10px; + border: 1px solid #d1d5da; + font-weight: bold; + color: #24292e; + } + """) + + table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + table.verticalHeader().setVisible(False) + + return table + + def _create_waveform_container(self): + waveform_container = QFrame() + waveform_container.setStyleSheet(""" + QFrame { background-color: #ffffff; border: 2px solid #4A90E2; + border-radius: 10px; padding: 10px; } + """) + waveform_layout = QVBoxLayout(waveform_container) + waveform_layout.setSpacing(5) + + waveform_label = QLabel("🎵 Audio Visualizer") + waveform_label.setStyleSheet(""" + font-size: 16px; font-weight: bold; color: #4A90E2; padding: 5px; + """) + waveform_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.waveform = AudioWaveform() + self.waveform.setMinimumHeight(150) + + waveform_layout.addWidget(waveform_label) + waveform_layout.addWidget(self.waveform) + + return waveform_container + + def on_playback_state_changed(self, state): + print("[DEBUG] Playback state:", state) + if state == QMediaPlayer.PlaybackState.PlayingState: + self.waveform.start_animation() + elif state == QMediaPlayer.PlaybackState.StoppedState: + self.waveform.stop_animation() + if self.status_label.text().startswith("Playing:"): + self.status_label.setText("Finished") + + def update_list(self): + self.file_table.setRowCount(0) + self.file_table.verticalHeader().setDefaultSectionSize(60) + self.status_label.setText("Loading...") + + params = { + "date_from": self.date_from.date().toString("yyyy-MM-dd"), + "date_to": self.date_to.date().toString("yyyy-MM-dd"), + "search": self.search_box.text().strip(), + "sort_by": self.sort_by.currentText(), + "limit": 100 + } + + # Add type filter + filter_value = self.noise_filter.currentText() + if self.recording_type == "ultrasound": + if filter_value not in ("All signals", "All types"): + params["predicted_class"] = filter_value + else: + if filter_value not in ("All types", "All signals"): + params["type"] = filter_value + + # Add device IDs if provided + if self.mic_ids: + params["device_ids"] = ",".join(self.mic_ids) + + try: + # response = requests.get(self.api_url, params=params, timeout=10) + response = self.api.http.get(self.api_url, params=params, timeout=10) + response.raise_for_status() + data = response.json() + + print(f"[DEBUG] Fetched {len(data)} records from {self.api_url}") + + # Populate table + for f in data: + row = self.file_table.rowCount() + self.file_table.insertRow(row) + self.file_table.setRowHeight(row, 60) + + # Check if compressed + is_compressed = f.get("is_compressed", False) + + # Set text color based on compression + text_color = QColor("#888888") if is_compressed else QColor("#000000") + + if self.recording_type == "ultrasound": + # Ultrasonic plant predictions + filename = f.get("file", "N/A") + device_id = f.get("device_id", "N/A") + pred_class = f.get("predicted_class", "Unknown") + confidence = f.get("confidence", 0) + watering_status = f.get("watering_status", "N/A") + url = f.get("url", "") + + item0 = QTableWidgetItem(filename) + item0.setForeground(text_color) + self.file_table.setItem(row, 0, item0) + + item1 = QTableWidgetItem(device_id) + item1.setForeground(text_color) + self.file_table.setItem(row, 1, item1) + + item2 = QTableWidgetItem(pred_class) + item2.setForeground(text_color) + self.file_table.setItem(row, 2, item2) + + item3 = QTableWidgetItem(f"{confidence:.2%}") + item3.setForeground(text_color) + self.file_table.setItem(row, 3, item3) + + item4 = QTableWidgetItem(watering_status) + item4.setForeground(text_color) + self.file_table.setItem(row, 4, item4) + else: + # Audio aggregates + filename = f.get("filename", "N/A") + device_id = f.get("device_id", "N/A") + label = f.get("predicted_label", "Unknown") + prob = f.get("probability", 0) + url = f.get("url", "") + format_str = "OPUS (Compressed)" if is_compressed else "WAV (Original)" + + item0 = QTableWidgetItem(filename) + item0.setForeground(text_color) + self.file_table.setItem(row, 0, item0) + + item1 = QTableWidgetItem(device_id) + item1.setForeground(text_color) + self.file_table.setItem(row, 1, item1) + + item2 = QTableWidgetItem(label) + item2.setForeground(text_color) + self.file_table.setItem(row, 2, item2) + + item3 = QTableWidgetItem(f"{prob:.2%}") + item3.setForeground(text_color) + self.file_table.setItem(row, 3, item3) + + item4 = QTableWidgetItem(format_str) + item4.setForeground(text_color) + self.file_table.setItem(row, 4, item4) + + # Actions column with Play/Stop buttons + control_widget = QWidget() + control_layout = QHBoxLayout(control_widget) + control_layout.setContentsMargins(2, 2, 2, 2) + control_layout.setSpacing(6) + + play_btn = QPushButton("▶") + play_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + play_btn.setFixedSize(35, 30) + + # Adjust button style for compressed files + if is_compressed: + play_btn.setStyleSheet(""" + QPushButton { + background-color: #888888; + color: white; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover:enabled { background-color: #666666; } + QPushButton:disabled { background-color: #cccccc; color: #888888; } + """) + play_btn.setToolTip("Compressed OPUS file - may have compatibility issues") + else: + play_btn.setStyleSheet(""" + QPushButton { + background-color: #0078d4; + color: white; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover:enabled { background-color: #005fa3; } + QPushButton:disabled { background-color: #cccccc; color: #888888; } + """) + + stop_btn = QPushButton("⏹") + stop_btn.setCursor(QCursor(Qt.CursorShape.PointingHandCursor)) + stop_btn.setFixedSize(35, 30) + stop_btn.setEnabled(False) + stop_btn.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border-radius: 4px; + font-weight: bold; + } + QPushButton:disabled { background-color: #b0b0b0; } + QPushButton:hover:enabled { background-color: #c82333; } + """) + + # Store current row buttons for state management + play_btn.setProperty("row", row) + stop_btn.setProperty("row", row) + + play_btn.clicked.connect( + lambda checked=False, u=url, fname=filename, pb=play_btn, sb=stop_btn, compressed=is_compressed: + self.play_row_audio(u, fname, pb, sb, compressed) + ) + stop_btn.clicked.connect( + lambda checked=False, pb=play_btn, sb=stop_btn: + self.stop_row_audio(pb, sb) + ) + + control_layout.addWidget(play_btn) + control_layout.addWidget(stop_btn) + + # Set correct column index for Actions (column 5 for both types now) + self.file_table.setCellWidget(row, 5, control_widget) + + if self.file_table.rowCount() == 0: + self.file_table.insertRow(0) + empty_item = QTableWidgetItem("No recordings found") + empty_item.setForeground(QColor("#999")) + self.file_table.setItem(0, 0, empty_item) + self.file_table.setSpan(0, 0, 1, 6) + + self.status_label.setText(f"✓ Loaded {len(data)} recordings") + + except requests.exceptions.Timeout: + self.status_label.setText("⚠ Request timeout") + QMessageBox.warning(self, "Timeout", "Request timed out. Please try again.") + except requests.exceptions.ConnectionError: + self.status_label.setText("⚠ Connection error") + QMessageBox.warning(self, "Connection Error", + "Could not connect to server. Check your connection.") + except Exception as e: + self.status_label.setText("⚠ Error loading data") + QMessageBox.warning(self, "Error", f"Failed to load recordings:\n{str(e)}") + + def play_row_audio(self, url, filename, play_btn, stop_btn, is_compressed=False): + """Play audio from MinIO URL""" + if not url: + QMessageBox.warning(self, "No URL", "Audio file URL not available") + return + + # Stop any currently playing audio first + self.player.stop() + + print(f"[DEBUG] Attempting to play: {url}") + + # Warn about compressed files + if is_compressed: + reply = QMessageBox.question( + self, + "Compressed File", + "This is a compressed OPUS file. Playback may not work properly.\n\nContinue anyway?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) + if reply == QMessageBox.StandardButton.No: + return + + # Test MinIO connectivity + try: + print(f"[DEBUG] Testing URL accessibility: {url}") + check = requests.head(url, timeout=3) + print(f"[DEBUG] MinIO response: {check.status_code}") + + if check.status_code == 403: + QMessageBox.warning(self, "Access Denied", + f"MinIO returned 403 Forbidden.\n\nThe bucket may not be public.\n\nURL: {url}") + return + elif check.status_code == 404: + QMessageBox.warning(self, "File Not Found", + f"MinIO returned 404 Not Found.\n\nThe file may have been deleted.\n\nURL: {url}") + return + elif check.status_code != 200: + QMessageBox.warning(self, "MinIO Error", + f"MinIO returned status {check.status_code}\n\nURL: {url}") + return + + except requests.exceptions.ConnectionError as e: + QMessageBox.warning(self, "Connection Error", + f"Cannot connect to MinIO server.\n\nMake sure MinIO is running.\n\nError: {str(e)[:200]}") + return + except requests.exceptions.Timeout: + QMessageBox.warning(self, "Timeout", + "MinIO request timed out.\n\nThe server may be slow or unreachable.") + return + except Exception as e: + QMessageBox.warning(self, "Network Error", + f"Failed to reach MinIO:\n\n{str(e)[:200]}") + return + + # Update UI + self.waveform.start_animation() + status_text = f"▶ Playing: {filename}" + if is_compressed: + status_text += " (Compressed)" + self.status_label.setText(status_text) + + # Disable all play buttons, enable this stop button + self._disable_all_play_buttons() + play_btn.setEnabled(False) + stop_btn.setEnabled(True) + stop_btn.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { background-color: #c82333; } + """) + + try: + # Set audio source and play + qurl = QUrl(url) + print(f"[DEBUG] QUrl created: {qurl.toString()}") + self.player.setSource(qurl) + self.player.play() + print(f"[DEBUG] Player state after play(): {self.player.playbackState()}") + + except Exception as e: + print(f"[ERROR] Playback failed: {e}") + self.waveform.stop_animation() + self.status_label.setText("⚠ Playback failed") + QMessageBox.warning(self, "Playback Error", f"Failed to play audio:\n{str(e)}") + self._enable_all_play_buttons() + stop_btn.setEnabled(False) + + def stop_row_audio(self, play_btn, stop_btn): + """Stop currently playing audio""" + self.player.stop() + self.waveform.stop_animation() + self.status_label.setText("⏹ Stopped") + + # Re-enable all play buttons + self._enable_all_play_buttons() + + def _disable_all_play_buttons(self): + """Disable all play buttons in the table""" + for row in range(self.file_table.rowCount()): + widget = self.file_table.cellWidget(row, 6) + if widget: + layout = widget.layout() + if layout and layout.count() >= 1: + play_btn = layout.itemAt(0).widget() + if play_btn: + play_btn.setEnabled(False) + + def _enable_all_play_buttons(self): + """Enable all play buttons and disable all stop buttons""" + for row in range(self.file_table.rowCount()): + widget = self.file_table.cellWidget(row, 5) + if widget: + layout = widget.layout() + if layout and layout.count() >= 2: + play_btn = layout.itemAt(0).widget() + stop_btn = layout.itemAt(1).widget() + if play_btn: + play_btn.setEnabled(True) + if stop_btn: + stop_btn.setEnabled(False) + stop_btn.setStyleSheet(""" + QPushButton { + background-color: #6c757d; + color: white; + border-radius: 4px; + font-weight: bold; + } + QPushButton:disabled { background-color: #b0b0b0; } + """) + +# ========================================================== +# Analytics Dashboard Tab +# ========================================================== +class AnalyticsDashboard(QWidget): + """Sound detection dashboard with filtering by time range and sound type""" + + SOUND_TYPES = [ + "non_predatory_animals", + "predatory_animals", + "birds", + "fire", + "footsteps", + "insects", + "screaming", + "shotgun", + "stormy_weather", + "streaming_water", + "vehicle" + ] + + # 11 shades of green palette + GREEN_PALETTE = [ + '#004d00', # Dark green + '#006600', + '#008000', # Green + '#1a9900', + '#33b300', + '#4dcc00', + '#66e600', + '#80ff00', + '#99ff33', + '#b3ff66', + '#ccff99' # Light green + ] + + LIGHT_THEME = { + 'bg': '#f8f9fa', + 'card': '#ffffff', + 'text': '#333333', + 'border': '#e0e0e0', + 'primary': '#1976D2', + 'accent': '#4dcc00' + } + + DARK_THEME = { + 'bg': '#1e1e1e', + 'card': '#2d2d2d', + 'text': '#e0e0e0', + 'border': '#444444', + 'primary': '#64B5F6', + 'accent': '#66e600' + } + + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + self.current_time_range = 'day' + self.current_sound_types = [] # Multi-select list + self.is_dark_theme = False + self.current_theme = self.LIGHT_THEME.copy() + + self.setMinimumSize(QSize(1350, 1000)) + + # Main layout + main_layout = QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # Toolbar + toolbar = self._create_control_panel() + main_layout.addWidget(toolbar) + + # Content frame + content_frame = QFrame() + content_layout = QVBoxLayout() + content_layout.setContentsMargins(12, 12, 12, 12) + content_layout.setSpacing(12) + + # Filter frame + filter_frame = QFrame() + filter_frame.setStyleSheet(""" + QFrame { + background-color: white; + border: 1px solid #e8e8e8; + border-radius: 8px; + } + """) + filter_frame.setMaximumHeight(450) + filter_layout = QVBoxLayout() + filter_layout.setContentsMargins(12, 10, 12, 10) + filter_layout.setSpacing(15) + + # Time range row + time_row = QHBoxLayout() + time_label = QLabel("Time Range:") + time_label.setFont(QFont("Arial", 10, QFont.Weight.Bold)) + time_row.addWidget(time_label) + self.time_filter = QComboBox() + self.time_filter.addItems(['1 Day', '1 Week', '1 Month']) + self.time_filter.setCurrentText('1 Day') + self.time_filter.currentTextChanged.connect(self._on_filter_changed) + self.time_filter.setMinimumWidth(140) + time_row.addWidget(self.time_filter) + time_row.addStretch() + filter_layout.addLayout(time_row) + + # Sound types header + sound_header_row = QHBoxLayout() + sound_label = QLabel("Sound Types (select multiple):") + sound_label.setFont(QFont("Arial", 10, QFont.Weight.Bold)) + sound_header_row.addWidget(sound_label) + + self.selection_label = QLabel("All sounds selected") + self.selection_label.setStyleSheet("color: #1976D2; font-weight: bold;") + sound_header_row.addWidget(self.selection_label) + sound_header_row.addStretch() + + clear_btn = QPushButton("Clear All") + clear_btn.setMaximumWidth(100) + clear_btn.clicked.connect(self._clear_sound_selection) + sound_header_row.addWidget(clear_btn) + + apply_btn = QPushButton("Apply Filter") + apply_btn.setMaximumWidth(100) + apply_btn.setStyleSheet(""" + QPushButton { + background-color: #1976D2; + color: white; + font-weight: bold; + } + QPushButton:hover { + background-color: #1565C0; + } + """) + apply_btn.clicked.connect(self._refresh_data) + sound_header_row.addWidget(apply_btn) + filter_layout.addLayout(sound_header_row) + + # Checkbox container + checkbox_container = QFrame() + checkbox_container.setObjectName("checkboxContainer") + checkbox_container.setStyleSheet(""" + QFrame#checkboxContainer { + background-color: white; + border: 2px solid #1976D2; + border-radius: 6px; + max-height: 350px; + } + """) + checkbox_layout = QGridLayout() + checkbox_layout.setSpacing(5) + checkbox_layout.setContentsMargins(10, 10, 10, 10) + + self.sound_checkboxes = {} + for idx, sound_name in enumerate(self.SOUND_TYPES): + checkbox = QCheckBox(sound_name) + checkbox.stateChanged.connect(self._on_sound_checkbox_changed) + self.sound_checkboxes[sound_name] = checkbox + row = idx // 3 + col = idx % 3 + checkbox_layout.addWidget(checkbox, row, col) + + checkbox_container.setLayout(checkbox_layout) + filter_layout.addWidget(checkbox_container) + filter_frame.setLayout(filter_layout) + content_layout.addWidget(filter_frame) + + # Activity calendar + calendar_frame = self._create_activity_calendar() + content_layout.addWidget(calendar_frame) + + # Chart grid + grid = QGridLayout() + grid.setSpacing(12) + grid.setRowStretch(0, 1) + grid.setRowStretch(1, 1) + grid.setRowStretch(2, 1) + grid.setColumnStretch(0, 1) + grid.setColumnStretch(1, 1) + + # Helper function for uniform frames + def make_chart_frame(title, canvas): + frame = self._create_chart_frame(title, canvas) + frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + frame.setMinimumHeight(320) + frame.setMaximumHeight(320) + return frame + + # Row 1 + self.dist_canvas = self._create_canvas(figsize=(6, 5)) + grid.addWidget(make_chart_frame("Sound Distribution (Count)", self.dist_canvas), 0, 0) + + self.timeline_canvas = self._create_canvas(figsize=(6, 5)) + grid.addWidget(make_chart_frame("Detection Timeline", self.timeline_canvas), 0, 1) + + # Row 2 + self.heatmap_canvas = self._create_canvas(figsize=(6, 5)) + grid.addWidget(make_chart_frame("Sound Heatmap - Activity Patterns", self.heatmap_canvas), 1, 0) + + self.correlation_canvas = self._create_canvas(figsize=(6, 5)) + grid.addWidget(make_chart_frame("Correlation Explorer", self.correlation_canvas), 1, 1) + + # Row 3 + self.confidence_canvas = self._create_canvas(figsize=(6, 5)) + grid.addWidget(make_chart_frame("Model Health Monitor", self.confidence_canvas), 2, 0) + + stats_frame = self._create_stats_frame() + stats_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + stats_frame.setMinimumHeight(320) + stats_frame.setMaximumHeight(320) + grid.addWidget(stats_frame, 2, 1) + + # Add grid to content + content_layout.addLayout(grid, stretch=10) + content_frame.setLayout(content_layout) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll_area.setWidget(content_frame) + + main_layout.addWidget(scroll_area) + self.setLayout(main_layout) + + # Timer + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self._refresh_data) + self.refresh_timer.start(30000) # 30 seconds + + # Initial load + self._refresh_data() + + def _create_control_panel(self) -> QToolBar: + """Create toolbar with control buttons: Refresh, Export CSV, Toggle Theme""" + toolbar = QToolBar("Control Panel") + toolbar.setMovable(False) + toolbar.setIconSize(QSize(16, 16)) + toolbar.setStyleSheet(""" + QToolBar { + background-color: #f0f0f0; + border-bottom: 1px solid #ddd; + spacing: 10px; + padding: 8px; + } + QToolButton { + padding: 6px 12px; + font-weight: bold; + } + """) + + toolbar.addAction("🔄 Refresh", self._refresh_data) + toolbar.addSeparator() + toolbar.addAction("💾 Export CSV", self._export_csv) + toolbar.addSeparator() + toolbar.addAction("🌓 Toggle Theme", self._toggle_theme) + + return toolbar + + def _create_activity_calendar(self) -> QFrame: + """Create a calendar showing detection activity per day""" + frame = QFrame() + frame.setStyleSheet(""" + QFrame { + background-color: white; + border: 1px solid #e8e8e8; + border-radius: 8px; + } + """) + frame.setMaximumHeight(120) + + layout = QVBoxLayout() + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(8) + + title = QLabel("Activity Calendar (Last 30 Days)") + title.setFont(QFont("Arial", 10, QFont.Weight.Bold)) + layout.addWidget(title) + + # Create grid for calendar + calendar_grid = QHBoxLayout() + calendar_grid.setSpacing(2) + + today = datetime.now().date() + for i in range(30): + date = today - timedelta(days=29-i) + day_box = QFrame() + day_box.setMinimumSize(QSize(20, 20)) + day_box.setMaximumSize(QSize(20, 20)) + + # Random intensity for demo (replace with real data from API) + intensity = np.random.rand() + color = self._get_intensity_color(intensity) + + day_box.setStyleSheet(f""" + QFrame {{ + background-color: {color}; + border: 1px solid #ddd; + border-radius: 2px; + }} + """) + + day_label = QLabel(str(date.day)) + day_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + day_label.setFont(QFont("Arial", 6)) + day_layout = QVBoxLayout() + day_layout.setContentsMargins(0, 0, 0, 0) + day_layout.addWidget(day_label) + day_box.setLayout(day_layout) + + calendar_grid.addWidget(day_box) + + calendar_grid.addStretch() + layout.addLayout(calendar_grid) + frame.setLayout(layout) + return frame + + def _get_intensity_color(self, intensity: float) -> str: + """Map intensity (0-1) to green color from palette""" + if intensity < 0.2: + return self.GREEN_PALETTE[0] + elif intensity < 0.4: + return self.GREEN_PALETTE[2] + elif intensity < 0.6: + return self.GREEN_PALETTE[4] + elif intensity < 0.8: + return self.GREEN_PALETTE[7] + else: + return self.GREEN_PALETTE[10] + + def _toggle_theme(self): + """Toggle between light and dark theme""" + self.is_dark_theme = not self.is_dark_theme + self.current_theme = self.DARK_THEME.copy() if self.is_dark_theme else self.LIGHT_THEME.copy() + self._apply_theme() + self._refresh_data() + + def _apply_theme(self): + """Apply current theme to all widgets""" + theme = self.current_theme + + self.setStyleSheet(f""" + AnalyticsDashboard {{ + background-color: {theme['bg']}; + }} + QComboBox {{ + padding: 8px 12px; + border: 2px solid {theme['border']}; + border-radius: 6px; + background-color: {theme['card']}; + color: {theme['text']}; + min-width: 200px; + font-size: 10pt; + font-weight: 500; + }} + QComboBox:hover {{ + border: 2px solid {theme['primary']}; + }} + QCheckBox {{ + padding: 6px 12px; + font-size: 10pt; + color: {theme['text']}; + }} + QCheckBox:hover {{ + background-color: {theme['primary']}; + }} + QLabel {{ + color: {theme['text']}; + }} + QPushButton {{ + padding: 6px 12px; + background-color: {theme['card']}; + border: 1px solid {theme['border']}; + border-radius: 4px; + font-weight: bold; + color: {theme['text']}; + }} + QPushButton:hover {{ + background-color: {theme['primary']}; + color: white; + }} + QFrame {{ + background-color: {theme['card']}; + border: 1px solid {theme['border']}; + border-radius: 8px; + }} + QToolBar {{ + background-color: {theme['card']}; + border-bottom: 1px solid {theme['border']}; + }} + """) + + def _export_csv(self): + """Export current data to CSV""" + try: + import csv + + filename = f"sound_detection_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + sound_filter = self.current_sound_types if self.current_sound_types else None + data = self.api.get_audio_distribution( + self.current_time_range, + limit=100, + sound_types=sound_filter + ) + + with open(filename, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=['sound_type', 'count']) + writer.writeheader() + for row in data: + writer.writerow({ + 'sound_type': row.get('head_pred_label'), + 'count': row.get('count') + }) + + QMessageBox.information(self, "Success", f"Data exported to {filename}") + print(f"[AnalyticsDashboard] Data exported to {filename}", flush=True) + except Exception as e: + QMessageBox.warning(self, "Error", f"Export failed: {str(e)}") + print(f"[AnalyticsDashboard] Export error: {e}", flush=True) + + def _create_canvas(self, figsize=(5.5, 4.5)): + """Create matplotlib canvas for chart""" + canvas = FigureCanvas(Figure(figsize=figsize, dpi=90)) + canvas.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + return canvas + + def _create_chart_frame(self, title: str, widget: QWidget) -> QFrame: + """Create a framed chart container""" + frame = QFrame() + frame.setStyleSheet(""" + QFrame { + background-color: white; + border: 1px solid #e8e8e8; + border-radius: 8px; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(8) + + title_label = QLabel(title) + title_label.setFont(QFont("Arial", 11, QFont.Weight.Bold)) + title_label.setStyleSheet("color: #1976D2; margin-bottom: 4px;") + layout.addWidget(title_label) + + layout.addWidget(widget, 1) + frame.setLayout(layout) + return frame + + def _create_stats_frame(self) -> QFrame: + """Create frame with 4 stat boxes in 2x2 grid""" + frame = QFrame() + frame.setStyleSheet(""" + QFrame { + background-color: white; + border: 1px solid #e8e8e8; + border-radius: 8px; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(10) + + title_label = QLabel("Statistics") + title_label.setFont(QFont("Arial", 11, QFont.Weight.Bold)) + title_label.setStyleSheet("color: #1976D2; margin-bottom: 6px;") + layout.addWidget(title_label) + + stats_grid = QGridLayout() + stats_grid.setSpacing(10) + stats_grid.setRowStretch(0, 1) + stats_grid.setRowStretch(1, 1) + stats_grid.setColumnStretch(0, 1) + stats_grid.setColumnStretch(1, 1) + + # Store references for updating + self.stat_boxes = {} + + stat_names = [ + ("Total Files", "total_files"), + ("Unknown Type", "unknown_count"), + ("Avg Confidence", "avg_confidence"), + ("Avg Processing", "avg_processing_ms") + ] + + for idx, (label, key) in enumerate(stat_names): + row = idx // 2 + col = idx % 2 + box = self._create_stat_box(label) + self.stat_boxes[key] = box + stats_grid.addWidget(box, row, col) + + layout.addLayout(stats_grid, 1) + frame.setLayout(layout) + return frame + + def _create_stat_box(self, label: str) -> QFrame: + """Create a single statistic box""" + box = QFrame() + box.setStyleSheet(""" + QFrame { + background-color: #fafafa; + border: 2px solid #e8e8e8; + border-radius: 8px; + } + """) + + layout = QVBoxLayout() + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(8) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + label_widget = QLabel(label) + label_widget.setFont(QFont("Arial", 9, QFont.Weight.Bold)) + label_widget.setStyleSheet("color: #666;") + label_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(label_widget) + + value_widget = QLabel("--") + value_widget.setFont(QFont("Arial", 22, QFont.Weight.Bold)) + value_widget.setStyleSheet("color: #1976D2;") + value_widget.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(value_widget) + + box.setLayout(layout) + box._label = label_widget + box._value = value_widget + return box + + def _on_sound_checkbox_changed(self): + """Update selection label when checkboxes change""" + selected = [] + for sound_name, checkbox in self.sound_checkboxes.items(): + if checkbox.isChecked(): + selected.append(sound_name) + + self.current_sound_types = selected + + # Update label + if not selected: + self.selection_label.setText("All sounds selected") + elif len(selected) == 1: + self.selection_label.setText(f"1 sound type selected: {selected[0]}") + else: + self.selection_label.setText(f"{len(selected)} sound types selected") + + print(f"[AnalyticsDashboard] Selection changed: {self.current_sound_types or 'All'}", flush=True) + + def _clear_sound_selection(self): + """Clear all sound type selections""" + for checkbox in self.sound_checkboxes.values(): + checkbox.setChecked(False) + + self.current_sound_types = [] + self.selection_label.setText("All sounds selected") + self.time_filter.setCurrentText('1 Day') + self.current_time_range = 'day' + + print(f"[AnalyticsDashboard] Filters cleared", flush=True) + self._refresh_data() + + def _on_filter_changed(self): + """Handle time filter changes""" + time_map = {'1 Day': 'day', '1 Week': 'week', '1 Month': 'month'} + self.current_time_range = time_map.get(self.time_filter.currentText(), 'day') + + print(f"[AnalyticsDashboard] Time range changed to: {self.current_time_range}", flush=True) + self._refresh_data() + + def _refresh_data(self): + """Refresh all charts with current filters""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + print(f"[AnalyticsDashboard] Refreshing - Time: {self.current_time_range}, Sounds: {sound_filter or 'All'}", flush=True) + + self._clear_canvas(self.dist_canvas) + self._clear_canvas(self.timeline_canvas) + self._clear_canvas(self.confidence_canvas) + + self._update_distribution_chart() + self._update_timeline_chart() + self._update_heatmap_chart() + self._update_correlation_chart() + self._update_confidence_chart() + self._update_stats_boxes() + except Exception as e: + print(f"[AnalyticsDashboard] Refresh error: {e}", flush=True) + import traceback + traceback.print_exc() + + def _update_distribution_chart(self): + """Update bar chart - distribution of sound types by COUNT""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + data = self.api.get_audio_distribution( + self.current_time_range, + limit=15, + sound_types=sound_filter + ) + + if not data: + self._show_no_data(self.dist_canvas) + return + + labels = [d['head_pred_label'] for d in data] + counts = [d['count'] for d in data] + + ax = self.dist_canvas.figure.add_subplot(111) + + # Use green palette + colors = [self.GREEN_PALETTE[i % len(self.GREEN_PALETTE)] for i in range(len(labels))] + bars = ax.bar(range(len(labels)), counts, color=colors, edgecolor='black', linewidth=0.5) + + ax.set_xticks(range(len(labels))) + ax.set_xticklabels(labels, rotation=45, ha='right', fontsize=8) + ax.set_ylabel('Count', fontsize=9, fontweight='bold') + ax.grid(True, alpha=0.3, linestyle='--', axis='y') + + # Add value labels on bars + for bar in bars: + height = bar.get_height() + ax.text(bar.get_x() + bar.get_width()/2., height, + f'{int(height)}', + ha='center', va='bottom', fontsize=8, fontweight='bold') + + self.dist_canvas.figure.tight_layout() + self.dist_canvas.draw() + except Exception as e: + print(f"[AnalyticsDashboard] Distribution chart error: {e}", flush=True) + self._show_no_data(self.dist_canvas) + + def _update_timeline_chart(self): + """Update line chart - timeline of detections""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + data = self.api.get_audio_timeline( + self.current_time_range, + sound_types=sound_filter + ) + + if not data: + self._show_no_data(self.timeline_canvas) + return + + # Group by time_bucket and sum counts + timeline_dict = {} + for row in data: + time_bucket = row['time_bucket'] + count = row['count'] + if time_bucket not in timeline_dict: + timeline_dict[time_bucket] = 0 + timeline_dict[time_bucket] += count + + sorted_times = sorted(timeline_dict.keys()) + times = [str(t)[:16] for t in sorted_times] + counts = [timeline_dict[t] for t in sorted_times] + + ax = self.timeline_canvas.figure.add_subplot(111) + + ax.plot(times, counts, marker='o', linewidth=2, markersize=5, color='#1a9900') + ax.fill_between(range(len(times)), counts, alpha=0.2, color='#4dcc00') + ax.set_xlabel('Time', fontsize=9, fontweight='bold') + ax.set_ylabel('Detections', fontsize=9, fontweight='bold') + ax.grid(True, alpha=0.3, linestyle='--') + ax.tick_params(labelsize=8) + + self.timeline_canvas.figure.autofmt_xdate(rotation=45, ha='right') + self.timeline_canvas.figure.tight_layout() + self.timeline_canvas.draw() + except Exception as e: + print(f"[AnalyticsDashboard] Timeline chart error: {e}", flush=True) + self._show_no_data(self.timeline_canvas) + + def _update_confidence_chart(self): + """Update Model Health Monitor chart - Avg Confidence % & Processing Time""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + data = self.api.get_model_health_metrics( + self.current_time_range, + sound_types=sound_filter + ) + + if not data: + self._show_no_data(self.confidence_canvas) + return + + times = [str(d["time_bucket"])[:16] for d in data] + avg_conf = [d["avg_confidence"] * 100 for d in data] + avg_proc = [d["avg_processing_ms"] for d in data] + + fig = self.confidence_canvas.figure + fig.clear() + + ax1 = fig.add_subplot(111) + ax1.set_title("Model Performance Trends", fontsize=10, fontweight="bold", color="#1976D2") + ax1.plot(times, avg_conf, color="#33b300", marker="o", linewidth=2, label="Avg Confidence %") + ax1.fill_between(range(len(avg_conf)), avg_conf, alpha=0.15, color="#66e600") + ax1.set_ylabel("Confidence (%)", fontsize=9, fontweight="bold") + ax1.set_ylim(0, 100) + ax1.tick_params(axis='x', rotation=45, labelsize=8) + ax1.grid(True, alpha=0.3, linestyle="--") + + # Processing time on secondary y-axis + ax2 = ax1.twinx() + ax2.plot(times, avg_proc, color="#99ff33", marker="^", linestyle="--", linewidth=2, label="Avg Processing (ms)") + ax2.set_ylabel("Processing Time (ms)", fontsize=9, fontweight="bold", color="#66e600") + ax2.tick_params(axis='y', labelcolor="#66e600") + + # Combined legend + lines, labels = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines + lines2, labels + labels2, loc="upper left", fontsize=8) + + fig.tight_layout() + self.confidence_canvas.draw() + + except Exception as e: + print(f"[AnalyticsDashboard] Model Health Monitor chart error: {e}", flush=True) + self._show_no_data(self.confidence_canvas) + + def _update_stats_boxes(self): + """Update statistics boxes""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + stats = self.api.get_audio_stats( + self.current_time_range, + sound_types=sound_filter + ) + + print(f"[AnalyticsDashboard] Stats received: {stats}", flush=True) + + if stats: + total = stats.get('total_files', 0) or 0 + self.stat_boxes['total_files']._value.setText(str(total)) + + unknown = stats.get('unknown_count', 0) or 0 + self.stat_boxes['unknown_count']._value.setText(str(unknown)) + + avg_conf = stats.get('avg_confidence') + if avg_conf is not None and avg_conf > 0: + self.stat_boxes['avg_confidence']._value.setText(f"{avg_conf:.1%}") + else: + self.stat_boxes['avg_confidence']._value.setText("--") + + avg_proc = stats.get('avg_processing_ms') + if avg_proc is not None and avg_proc > 0: + self.stat_boxes['avg_processing_ms']._value.setText(f"{avg_proc:.0f}ms") + else: + self.stat_boxes['avg_processing_ms']._value.setText("--") + else: + for key in self.stat_boxes: + self.stat_boxes[key]._value.setText("--") + except Exception as e: + print(f"[AnalyticsDashboard] Stats update error: {e}", flush=True) + import traceback + traceback.print_exc() + + def _clear_canvas(self, canvas): + """Clear a matplotlib canvas completely""" + canvas.figure.clear() + + def _show_no_data(self, canvas): + """Show 'No Data Available' message on canvas""" + ax = canvas.figure.add_subplot(111) + ax.text(0.5, 0.5, 'No Data Available', + ha='center', va='center', fontsize=14, fontweight='bold', + transform=ax.transAxes, color='#999') + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.axis('off') + canvas.draw() + + def closeEvent(self, event): + """Clean up timer when closing""" + self.refresh_timer.stop() + super().closeEvent(event) + + def _update_heatmap_chart(self): + """Update heatmap chart - activity by hour of day and day of week""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + data = self.api.get_audio_heatmap( + self.current_time_range, + sound_types=sound_filter + ) + + if not data: + self._show_no_data(self.heatmap_canvas) + return + + # Create 24 hours x 7 days matrix + heatmap_data = np.zeros((24, 7)) + + # Fill matrix with counts + for row in data: + hour = int(row['hour_of_day']) + day = int(row['day_of_week']) + count = row['count'] + heatmap_data[hour, day] += count + + ax = self.heatmap_canvas.figure.add_subplot(111) + + # Create heatmap using imshow with green colormap + im = ax.imshow(heatmap_data, cmap='Greens', aspect='auto', interpolation='nearest') + + # Set ticks + ax.set_xticks(range(7)) + ax.set_xticklabels(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], fontsize=8) + ax.set_yticks(range(0, 24, 2)) + ax.set_yticklabels([f'{h:02d}:00' for h in range(0, 24, 2)], fontsize=7) + + ax.set_xlabel('Day of Week', fontsize=9, fontweight='bold') + ax.set_ylabel('Hour of Day', fontsize=9, fontweight='bold') + + # Add colorbar + cbar = self.heatmap_canvas.figure.colorbar(im, ax=ax, pad=0.02) + cbar.set_label('Detections', fontsize=8) + cbar.ax.tick_params(labelsize=7) + + # Add text annotations for non-zero values + for i in range(24): + for j in range(7): + if heatmap_data[i, j] > 0: + text_color = 'white' if heatmap_data[i, j] > heatmap_data.max() * 0.5 else 'black' + ax.text(j, i, int(heatmap_data[i, j]), + ha="center", va="center", color=text_color, fontsize=6, fontweight='bold') + + self.heatmap_canvas.figure.tight_layout() + self.heatmap_canvas.draw() + except Exception as e: + print(f"[AnalyticsDashboard] Heatmap chart error: {e}", flush=True) + import traceback + traceback.print_exc() + self._show_no_data(self.heatmap_canvas) + + def _update_correlation_chart(self): + """Update correlation heatmap - shows relationships between sound types""" + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + data = self.api.get_audio_correlations( + self.current_time_range, + sound_types=sound_filter + ) + + print(f"[AnalyticsDashboard] Correlation data received: {len(data) if data else 0} rows", flush=True) + if data and len(data) > 0: + print(f"[AnalyticsDashboard] Sample data: {data[:3]}", flush=True) + + if not data or len(data) < 1: + print("[AnalyticsDashboard] No correlation data - showing 'No Data'", flush=True) + self._show_no_data(self.correlation_canvas) + return + + # Build data structure without pandas + # Find all time buckets and sound types + time_buckets = sorted(list(set(row['time_bucket'] for row in data))) + sound_types = sorted(list(set(row['sound_type'] for row in data))) + + print(f"[AnalyticsDashboard] Found {len(time_buckets)} time buckets, {len(sound_types)} sound types", flush=True) + print(f"[AnalyticsDashboard] Sound types: {sound_types}", flush=True) + + if len(time_buckets) < 2 or len(sound_types) < 2: + print(f"[AnalyticsDashboard] Not enough data for correlation: {len(time_buckets)} buckets, {len(sound_types)} types", flush=True) + self._show_no_data(self.correlation_canvas) + return + + # Create matrix: rows=time_buckets, cols=sound_types + n_times = len(time_buckets) + n_sounds = len(sound_types) + data_matrix = np.zeros((n_times, n_sounds)) + + # Fill the matrix + time_idx = {t: i for i, t in enumerate(time_buckets)} + sound_idx = {s: i for i, s in enumerate(sound_types)} + + for row in data: + t_idx = time_idx[row['time_bucket']] + s_idx = sound_idx[row['sound_type']] + data_matrix[t_idx, s_idx] = row['detection_count'] + + print(f"[AnalyticsDashboard] Data matrix shape: {data_matrix.shape}", flush=True) + + # Calculate correlation matrix using numpy + corr_matrix = np.corrcoef(data_matrix.T) + + # Handle NaN values (if columns have zero standard deviation) + corr_matrix = np.nan_to_num(corr_matrix, nan=0.0) + + print(f"[AnalyticsDashboard] Correlation matrix shape: {corr_matrix.shape}", flush=True) + + # Create the plot + ax = self.correlation_canvas.figure.add_subplot(111) + + # Create heatmap with green colormap only + im = ax.imshow(corr_matrix, cmap='Greens', aspect='auto', vmin=-1, vmax=1) + + # Set labels + ax.set_xticks(range(len(sound_types))) + ax.set_yticks(range(len(sound_types))) + ax.set_xticklabels(sound_types, rotation=45, ha='right', fontsize=7) + ax.set_yticklabels(sound_types, fontsize=7) + + # Add correlation values inside cells + for i in range(len(sound_types)): + for j in range(len(sound_types)): + value = corr_matrix[i, j] + # Use white text for dark backgrounds (high correlation) + text_color = 'white' if value > 0.5 else 'black' + ax.text(j, i, f'{value:.2f}', + ha='center', va='center', + color=text_color, fontsize=6, fontweight='bold') + + # Add colorbar + cbar = self.correlation_canvas.figure.colorbar(im, ax=ax, fraction=0.046, pad=0.04) + cbar.set_label('Correlation Strength', rotation=270, labelpad=15, fontsize=8) + + ax.set_title('Sound Type Correlations\nDarker = Stronger Co-occurrence', + fontsize=9, fontweight='bold', pad=10) + + self.correlation_canvas.figure.tight_layout() + self.correlation_canvas.draw() + + print("[AnalyticsDashboard] Correlation chart updated successfully", flush=True) + + except Exception as e: + print(f"[AnalyticsDashboard] Correlation chart error: {e}", flush=True) + import traceback + traceback.print_exc() + self._show_no_data(self.correlation_canvas) + + +# ========================================================== +# Main Sound View with Tabs +# ========================================================== +class SoundView(QWidget): + def __init__(self, api=None, parent=None): + super().__init__(parent) + self.api = api + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + self.tabs = QTabWidget() + self.tabs.setStyleSheet(""" + QTabWidget::pane { + border: 2px solid #d1d5da; + border-radius: 10px; + background: white; + } + QTabBar::tab { + background: #f6f8fa; + padding: 12px 24px; + border-radius: 8px 8px 0 0; + margin-right: 4px; + font-size: 14px; + font-weight: 500; + color: #586069; + } + QTabBar::tab:selected { + background-color: #4A90E2; + color: white; + } + QTabBar::tab:hover { background: #e1e4e8; } + """) + + self.map_tab = ImageMapView() + self.env_tab = RecordingsTab(recording_type="audio", api=self.api) + self.plant_tab = RecordingsTab(recording_type="ultrasound", api=self.api) + + + # Add Analytics Dashboard tab if API is provided + if self.api: + self.analytics_tab = AnalyticsDashboard(self.api) + self.tabs.addTab(self.analytics_tab, "📊 Analytics Dashboard") + + self.tabs.addTab(self.map_tab, "🗺️ Interactive Map") + self.tabs.addTab(self.env_tab, "🎵 Environment Sounds") + self.tabs.addTab(self.plant_tab, "🌿 Plant Ultrasounds") + + layout.addWidget(self.tabs) \ No newline at end of file diff --git a/AgCloud/RelDB/Dockerfile b/AgCloud/RelDB/Dockerfile new file mode 100644 index 000000000..4c2f5bdba --- /dev/null +++ b/AgCloud/RelDB/Dockerfile @@ -0,0 +1,60 @@ +# syntax=docker/dockerfile:1 +FROM postgis/postgis:16-3.4 + +# ==== 1) Parameters ==== + +# ==== 2) Install tools: pg_cron + Python + cron + pgvector ==== +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-16-cron \ + postgresql-16-pgvector \ + python3-minimal python3-pip \ + cron \ + && rm -rf /var/lib/apt/lists/* + +# ==== 3) Directories for WAL and backups ==== +RUN mkdir -p /var/lib/postgresql/wal_archive /var/lib/postgresql/backups && \ + chown -R postgres:postgres /var/lib/postgresql/wal_archive /var/lib/postgresql/backups + +# ==== 4) PostgreSQL WAL + pg_cron config ==== +RUN echo "wal_level = 'replica'" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "archive_mode = on" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "max_wal_senders = 5" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "archive_timeout = '30s'" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "archive_command = 'cp %p /var/lib/postgresql/wal_archive/%f'" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "log_checkpoints = on" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "shared_preload_libraries = 'pg_cron,vector'" >> /usr/share/postgresql/postgresql.conf.sample && \ + echo "cron.database_name = 'missions_db'" >> /usr/share/postgresql/postgresql.conf.sample + +# ==== 5) Copy Python PITR scripts ==== +COPY pitr/backup.py /usr/local/bin/backup.py +COPY pitr/cron_backup.py /usr/local/bin/cron_backup.py +COPY pitr/recover.py /usr/local/bin/recover.py +RUN chmod +x /usr/local/bin/*.py + + +# ==== 6) Init files (run once if PGDATA empty) ==== +COPY build_tables/schema.sql /docker-entrypoint-initdb.d/01_schema.sql +COPY build_tables/02_event_logs_partition.sql /docker-entrypoint-initdb.d/02_event_logs_partition.sql +COPY initdb/03_partitions.sql /docker-entrypoint-initdb.d/03_partitions.sql +COPY build_tables/loader.sql /docker-entrypoint-initdb.d/04_loader.sql +COPY initdb/04_cron.sql /docker-entrypoint-initdb.d/05_cron.sql +COPY build_tables/05_agcloud_audio_init.sql /docker-entrypoint-initdb.d/06_agcloud_audio_init.sql + +# ==== 7) Replication grants ==== +RUN echo "ALTER ROLE missions_user WITH REPLICATION;" > /docker-entrypoint-initdb.d/00_grant_replication.sql + +# ==== 8) Cron job (nightly backup 03:00) ==== +RUN echo "0 3 * * * postgres python3 /usr/local/bin/cron_backup.py" >> /etc/crontab + +EXPOSE 5432 + +# ==== 9) Healthcheck ==== +HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=5 \ + CMD pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" || exit 1 + +# ==== 10) Final CMD: start cron + Postgres + initial backup ==== +CMD ["bash", "-c", "service cron start && \ +chown -R postgres:postgres /var/lib/postgresql/wal_archive /var/lib/postgresql/backups && \ +docker-entrypoint.sh postgres & \ +until pg_isready -U \"$POSTGRES_USER\" -d \"$POSTGRES_DB\"; do sleep 2; done; \ +su - postgres -c 'python3 /usr/local/bin/backup.py' && wait -n"] diff --git a/AgCloud/RelDB/README.md b/AgCloud/RelDB/README.md new file mode 100644 index 000000000..a8623f534 --- /dev/null +++ b/AgCloud/RelDB/README.md @@ -0,0 +1,153 @@ +# 🚀 Quick Start – PostgreSQL + Monitoring Stack + +This project sets up PostgreSQL (via Bitnami Helm on Minikube-in-Docker) together with Prometheus, Grafana, Alertmanager, and postgres_exporter. + +## 1. Run Everything + +```bash +docker compose up -d --build +``` + + + + +## 2. Open the UIs + +- Prometheus → [http://localhost:9090](http://localhost:9090) +- Grafana → [http://localhost:3000](http://localhost:3000) (login: `admin` / `admin`) + +-- In Grafana you have to enter into dashboard and press new, and then import and choose the json file in this folder.(there is simple and multi you can choos) + +That’s it — dashboards will already be provisioned and connected. + +--- + +## Verify in Prometheus / Grafana + +In Prometheus UI run: +``` +rate(pg_wal_stats_wal_bytes[5m]) +``` + +In Grafana dashboard, check WAL throughput / BRIN / replication lag panels. + +--- + +## 🔎 PostgreSQL with pgvector + +The PostgreSQL image used in this project is extended from `bitnami/postgresql:16` and includes the [`pgvector`](https://github.com/pgvector/pgvector) extension. + +### Build the image +From inside the `RelDB` directory: +```bash +docker build -t agcloud/postgresql-pgvector:16 -f pgvector.Dockerfile . +``` + +### Run the container +```bash +docker run --name agcloud-db \ + -e POSTGRESQL_POSTGRES_PASSWORD=SuperSecret123 \ + -e POSTGRESQL_USERNAME=agcloud \ + -e POSTGRESQL_PASSWORD=agcloud \ + -e POSTGRESQL_DATABASE=agcloud \ + -p 5432:5432 \ + -v pgdata:/bitnami/postgresql \ + -d agcloud/postgresql-pgvector:16 +``` + +### Default credentials +- Superuser: `postgres` / `SuperSecret123` +- User: `agcloud` / `agcloud` + +### Verify pgvector +Check installed extensions: +```bash +docker exec -it agcloud-db psql -U postgres -d agcloud -c "\dx" +``` + +If `vector` is not listed (e.g., on an existing volume), enable it manually: +```bash +docker exec -it agcloud-db psql -U postgres -d agcloud -c "CREATE EXTENSION IF NOT EXISTS vector;" +``` + +### Example usage +```sql +CREATE TABLE items (id serial PRIMARY KEY, embedding vector(3)); +INSERT INTO items (embedding) VALUES ('[1,2,3]'), ('[2,0,0]'); +SELECT id, embedding +FROM items +ORDER BY embedding <=> '[1,2,3]' +LIMIT 1; +``` + + +## Notes + +- Default credentials: + - postgres / `PgAdmin!ChangeMe123` + - missions_user / `Missions!ChangeMe123` +- Configuration files: + - `prometheus.yml`, `prometheus-recording.rules.yml`, `postgres-alerts.yml` + - `grafana-datasource.yml`, `grafana-dashboards.yml`, `grafana-dashboard.json` +- All SQL init scripts under `/work/initdb` are automatically applied in order. + +Enjoy your graphs! 🎉 + + +# pitr +1. option to run munual bakcup: +``` +docker exec -u postgres -it db python3 /usr/local/bin/backup.py +``` + +2. check database: +``` +docker exec db psql -U missions_user -d missions_db -c "SELECT COUNT(*) AS row_count FROM anomalies;" + +docker exec db psql -U missions_user -d missions_db -c "DELETE FROM anomalies WHERE anomaly_id = 123;" +``` + +## 🔄 Recovery (PITR – Point in Time Recovery) + +We use the `recover.py` helper script to restore the database from base backups and WAL archives. + +### 3. Recovery modes + +- **Latest** → restore up to the latest available WAL: +```bash + docker exec -u postgres -it db python3 /usr/local/bin/recover.py latest +``` + +- **Minutes ago** → restore to a point N minutes in the past: +```bash + docker exec -u postgres -it db python3 /usr/local/bin/recover.py minutes 2 +``` + +- **Exact time** → restore to a specific timestamp: +```bash + docker exec -u postgres -it db python3 /usr/local/bin/recover.py time "2025-09-07T11:15:00+03:00" +``` + +# Restart the container + +- After preparing the recovery files, the script will print: + +[RECOVERY] Recovery setup complete ✅ +[RECOVERY] Please restart the container to apply recovery: + docker restart db + +Run: +```bash +docker restart db +``` + +check not in recovery: +```bash +docker exec -it db psql -U missions_user -d missions_db -c "SELECT pg_is_in_recovery();" +``` + +wait for f, and run: +```bash +docker exec -u postgres -it db python3 /usr/local/bin/backup.py + +``` diff --git a/AgCloud/RelDB/build_tables/02_event_logs_partition.sql b/AgCloud/RelDB/build_tables/02_event_logs_partition.sql new file mode 100644 index 000000000..abb00ff63 --- /dev/null +++ b/AgCloud/RelDB/build_tables/02_event_logs_partition.sql @@ -0,0 +1,2 @@ +-- Ensures inserts into partitioned event_logs succeed without creating date partitions. +CREATE TABLE IF NOT EXISTS event_logs_default PARTITION OF event_logs DEFAULT; diff --git a/AgCloud/RelDB/build_tables/05_agcloud_audio_init.sql b/AgCloud/RelDB/build_tables/05_agcloud_audio_init.sql new file mode 100644 index 000000000..d2b3afc83 --- /dev/null +++ b/AgCloud/RelDB/build_tables/05_agcloud_audio_init.sql @@ -0,0 +1,98 @@ +-- agcloud_audio initialization (schema + tables + indexes + 11-class view) + +BEGIN; + +-- 1) Schema +CREATE SCHEMA IF NOT EXISTS agcloud_audio; + +-- Use schema for subsequent CREATEs +SET search_path TO agcloud_audio, public; + +-- 2) runs: per-run metadata +CREATE TABLE IF NOT EXISTS runs ( + run_id TEXT PRIMARY KEY, + started_at TIMESTAMPTZ NOT NULL DEFAULT now(), + finished_at TIMESTAMPTZ, + + model_name TEXT, + checkpoint TEXT, + head_path TEXT, + labels_csv TEXT, + + window_sec DOUBLE PRECISION NOT NULL CHECK (window_sec > 0), + hop_sec DOUBLE PRECISION NOT NULL CHECK (hop_sec > 0), + pad_last BOOLEAN NOT NULL, + agg TEXT NOT NULL CHECK (agg IN ('mean','max')), + topk INTEGER NOT NULL CHECK (topk >= 1), + device TEXT NOT NULL, + + code_version TEXT, + notes TEXT +); + +-- 3) file_aggregates: final per-file outputs; references public.sounds_new_sounds_connections(id) +CREATE TABLE IF NOT EXISTS file_aggregates ( + run_id TEXT NOT NULL, + file_id BIGINT NOT NULL, + + -- Flexible multi-class head probabilities (label -> prob) + head_probs_json JSONB, + + -- Final decision with "unknown" fallback + head_pred_label TEXT, + head_pred_prob DOUBLE PRECISION CHECK (head_pred_prob IS NULL OR (head_pred_prob BETWEEN 0 AND 1)), + head_unknown_threshold DOUBLE PRECISION, + head_is_another BOOLEAN, + + -- Aggregation context + num_windows INTEGER CHECK (num_windows IS NULL OR num_windows >= 0), + agg_mode TEXT, + + -- Performance metric (ms) + processing_ms INTEGER CHECK (processing_ms IS NULL OR processing_ms >= 0), + + PRIMARY KEY (run_id, file_id), + FOREIGN KEY (run_id) REFERENCES runs(run_id) ON DELETE CASCADE, + FOREIGN KEY (file_id) REFERENCES public.sound_new_sounds_connections(id) ON DELETE CASCADE +); + +-- Helpful indexes +CREATE INDEX ix_agcloud_file_agg_run + ON file_aggregates(run_id); + +CREATE INDEX ix_agcloud_file_agg_pred_label + ON file_aggregates(head_pred_label); + +-- JSONB GIN index to enable key/containment queries on probabilities map +CREATE INDEX ix_agcloud_file_agg_probs_gin + ON file_aggregates USING GIN (head_probs_json); + +-- 4) Views +DROP VIEW IF EXISTS v_file_aggregates_probs10; +DROP VIEW IF EXISTS v_file_aggregates_probs11; + +-- 11-class columns view (current head taxonomy) +CREATE VIEW v_file_aggregates_probs11 AS +SELECT + fa.run_id, + fa.file_id, + (fa.head_probs_json->>'predatory_animals')::double precision AS head_p_predatory_animals, + (fa.head_probs_json->>'non_predatory_animals')::double precision AS head_p_non_predatory_animals, + (fa.head_probs_json->>'birds')::double precision AS head_p_birds, + (fa.head_probs_json->>'fire')::double precision AS head_p_fire, + (fa.head_probs_json->>'footsteps')::double precision AS head_p_footsteps, + (fa.head_probs_json->>'insects')::double precision AS head_p_insects, + (fa.head_probs_json->>'screaming')::double precision AS head_p_screaming, + (fa.head_probs_json->>'shotgun')::double precision AS head_p_shotgun, + (fa.head_probs_json->>'stormy_weather')::double precision AS head_p_stormy_weather, + (fa.head_probs_json->>'streaming_water')::double precision AS head_p_streaming_water, + (fa.head_probs_json->>'vehicle')::double precision AS head_p_vehicle, + fa.head_pred_label, + fa.head_pred_prob, + fa.head_unknown_threshold, + fa.head_is_another, + fa.num_windows, + fa.agg_mode +FROM file_aggregates fa; + +COMMIT; diff --git a/AgCloud/RelDB/build_tables/loader.sql b/AgCloud/RelDB/build_tables/loader.sql new file mode 100644 index 000000000..4eaab5914 --- /dev/null +++ b/AgCloud/RelDB/build_tables/loader.sql @@ -0,0 +1,206 @@ +-- Extended synthetic data loader for schema_extended_v2.sql + +-- Insert devices +INSERT INTO devices (device_id, model, owner, active) VALUES + ('dev-a','drone-x','TeamA',true), + ('dev-b','drone-x','TeamA',true), + ('dev-c','rover-y','TeamB',true), + ('dev-d','rover-y','TeamB',true), + ('dev-e','sensor-z','TeamC',true), + ('dev-f','sensor-z','TeamC',true), + ('dev-g','ground-l','TeamD',true,NULL,NULL), + ('dev-h','ground-l','TeamD',true,NULL,NULL), + ('dev-i','ground-l','TeamD',true,NULL,NULL), + ('dev-j','ground-l','TeamD',true,NULL,NULL), + ('dev-k','ground-l','TeamD',true,NULL,NULL) + ('mic-1','sound-a','TeamD',true), + ('mic-2','sound-a','TeamD',true), + ('mic-33','sound-a','TeamD',true), + ('mic-u-2','sound-ul','TeamD',true) +ON CONFLICT DO NOTHING; + + +-- Insert some regions +INSERT INTO regions (name, geom) +VALUES + ('North Field', ST_GeomFromText('POLYGON((34.75 32.00,34.90 32.00,34.90 32.10,34.75 32.10,34.75 32.00))',4326)), + ('South Field', ST_GeomFromText('POLYGON((34.90 31.95,35.05 31.95,35.05 32.05,34.90 32.05,34.90 31.95))',4326)) +ON CONFLICT DO NOTHING; + +-- Insert anomaly types +INSERT INTO anomaly_types (code, description) +VALUES + ('ALT_LOW','Altitude too low'), + ('ALT_HIGH','Altitude too high'), + ('SENSOR_FAIL','Sensor failure'), + ('COMM_LOSS','Communication lost') +ON CONFLICT DO NOTHING; + +-- Seed leaf disease types +INSERT INTO leaf_disease_types (name) +VALUES + ('pepper__bacterial_spot'), + ('pepper__healthy'), + ('potato__early_blight'), + ('potato__healthy'), + ('potato__late_blight'), + ('tomato__bacterial_spot'), + ('tomato__early_blight'), + ('tomato__healthy'), + ('tomato__late_blight'), + ('tomato__leaf_mold'), + ('tomato__mosaic_virus'), + ('tomato__septoria_leaf_spot'), + ('tomato__spider_mites'), + ('tomato__target_spot'), + ('tomato__yellowleaf_curl_virus') +ON CONFLICT DO NOTHING; + +-- Insert 5 missions +WITH params AS ( + SELECT 34.75::double precision AS min_lon, 35.05 AS max_lon, + 31.95::double precision AS min_lat, 32.20 AS max_lat +) +INSERT INTO missions (start_time, end_time, area_geom) +SELECT + now() - (i || ' hours')::interval, + now() - ((i-1) || ' hours')::interval, + ST_MakePolygon(ST_GeomFromText( + format('LINESTRING(%1$s %3$s,%2$s %3$s,%2$s %4$s,%1$s %4$s,%1$s %3$s)', + min_lon, max_lon, min_lat, max_lat), 4326)) +FROM params, generate_series(5,1,-1) AS s(i); + +-- Insert telemetry (~60k rows for demo; adjust up for perf test) +WITH params AS ( + SELECT 34.75::double precision AS min_lon, 35.05 AS max_lon, + 31.95::double precision AS min_lat, 32.20 AS max_lat +), +devices AS ( + SELECT device_id FROM devices +), +ins AS ( + INSERT INTO telemetry (mission_id, device_id, ts, geom, altitude) + SELECT + (SELECT mission_id FROM missions ORDER BY random() LIMIT 1), + d.device_id, + now() - ((g % 10000) || ' seconds')::interval, + ST_SetSRID(ST_MakePoint( + (p.min_lon + random()*(p.max_lon - p.min_lon)), + (p.min_lat + random()*(p.max_lat - p.min_lat)) + ),4326), + 50 + (random()*150)::int + FROM params p, devices d, generate_series(1,10000) g + RETURNING 1 +) +SELECT count(*) AS rows_inserted FROM ins; + +-- Insert tile_stats (small demo set) +WITH params AS ( + SELECT 34.75::double precision AS min_lon, 35.05 AS max_lon, + 31.95::double precision AS min_lat, 32.20 AS max_lat +), +T AS ( + SELECT + (SELECT mission_id FROM missions ORDER BY random() LIMIT 1) AS mission_id, + 'tile-' || gs AS tile_id, + (random()*10)::real AS anomaly_score, + ST_Buffer( + ST_SetSRID(ST_MakePoint( + (min_lon + random()*(max_lon - min_lon)), + (min_lat + random()*(max_lat - min_lat)) + ),4326)::geography,50,'quad_segs=2' + )::geometry(Polygon,4326) AS geom + FROM params, generate_series(1,1000) AS gs +) +INSERT INTO tile_stats (mission_id, tile_id, anomaly_score, geom) +SELECT mission_id, tile_id, anomaly_score, geom FROM T; + +-- Insert anomalies +INSERT INTO anomalies (mission_id, device_id, ts, anomaly_type_id, severity, details, geom) +SELECT + (SELECT mission_id FROM missions ORDER BY random() LIMIT 1), + (SELECT device_id FROM devices ORDER BY random() LIMIT 1), + now() - ((g % 5000) || ' seconds')::interval, + (SELECT anomaly_type_id FROM anomaly_types ORDER BY random() LIMIT 1), + (random()*10)::real, + jsonb_build_object('note','synthetic anomaly'), + ST_SetSRID(ST_MakePoint(34.8+random()*0.2,31.95+random()*0.25),4326) +FROM generate_series(1,200) g; + +-- Insert files metadata (synthetic) +INSERT INTO files (bucket, object_key, content_type, size_bytes, etag, mission_id, device_id, metadata) +SELECT + 'mission-data', + 'images/img_'||g||'.jpg', + 'image/jpeg', + (100000+random()*200000)::bigint, + md5(random()::text), + (SELECT mission_id FROM missions ORDER BY random() LIMIT 1), + (SELECT device_id FROM devices ORDER BY random() LIMIT 1), + jsonb_build_object('note','synthetic file') +FROM generate_series(1,20) g; + +-- Insert event logs (small demo set) +INSERT INTO event_logs (ts, level, source, message, details, trace_id, user_id) +SELECT + now() - ((g % 1000) || ' seconds')::interval, + (ARRAY['INFO','WARN','ERROR'])[1+floor(random()*3)::int], + (ARRAY['ingestor','api','flink-job'])[1+floor(random()*3)::int], + 'Synthetic log message #'||g, + jsonb_build_object('note','synthetic log'), + md5(g::text), + CASE WHEN random()<0.3 THEN (100+g) ELSE -1 END +FROM generate_series(1,100) g; + +-- Insert 1000 random embeddings +INSERT INTO embeddings (vec) +SELECT ARRAY( + SELECT random() + FROM generate_series(1, 784) +) +FROM generate_series(1, 1000) +ON CONFLICT DO NOTHING; + +-- === Seed בסיסי לטבלת task_thresholds === +INSERT INTO task_thresholds (task, label, threshold, updated_by) +VALUES + (CAST('ripeness' AS task_type_enum), '', 0.75, 'seed'), + (CAST('disease' AS task_type_enum), '', 0.60, 'seed'), + (CAST('size' AS task_type_enum), '', 0.55, 'seed'), + (CAST('color' AS task_type_enum), '', 0.65, 'seed'), + (CAST('quality' AS task_type_enum), '', 0.80, 'seed') +ON CONFLICT (task, label) +DO UPDATE SET + threshold = EXCLUDED.threshold, + updated_by = EXCLUDED.updated_by, + updated_at = NOW(); + +-- Seed sample leaf reports with random data +DO $$ +DECLARE + devices_arr text[] := ARRAY['dev-g', 'dev-h', 'dev-i', 'dev-j', 'dev-k']; + disease_ids int[]; + d text; + t int; + start_date timestamptz := now() - interval '30 days'; + rand_ts timestamptz; + conf double precision; + sick_val boolean; +BEGIN + -- Get all disease type IDs + SELECT array_agg(id) INTO disease_ids FROM leaf_disease_types; + + -- Insert 300 random reports + FOR i IN 1..300 LOOP + d := devices_arr[ceil(random() * array_length(devices_arr,1))]; + t := disease_ids[ceil(random() * array_length(disease_ids,1))]; + rand_ts := start_date + (random() * interval '30 days'); + conf := round(random()::numeric, 2); + sick_val := (conf > 0.4); + + INSERT INTO leaf_reports(device_id, leaf_disease_type_id, ts, confidence, sick) + VALUES (d, t, rand_ts, conf, sick_val); + END LOOP; +END $$; + + diff --git a/AgCloud/RelDB/build_tables/schema.sql b/AgCloud/RelDB/build_tables/schema.sql new file mode 100644 index 000000000..68a2194b3 --- /dev/null +++ b/AgCloud/RelDB/build_tables/schema.sql @@ -0,0 +1,964 @@ +-- Extended schema v2: adds devices, anomaly catalog, logs, files, and regions. +-- Order matters: referenced tables first. + +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS vector; + +-- === Catalogs / reference tables === + +-- Devices catalog +CREATE TABLE IF NOT EXISTS devices ( + device_id text PRIMARY KEY, + model text, + owner text, + active boolean DEFAULT true, + location_lat DOUBLE PRECISION, + location_lon DOUBLE PRECISION +); + +-- Predefined regions (optional: for missions crossing multiple regions) +CREATE TABLE IF NOT EXISTS regions ( + id bigserial PRIMARY KEY, + name text NOT NULL, + geom geometry(Polygon, 4326) NOT NULL +); + +-- Types of anomalies +CREATE TABLE IF NOT EXISTS anomaly_types ( + anomaly_type_id serial PRIMARY KEY, + code text UNIQUE NOT NULL, + description text NOT NULL +); + +--Types of leaf diseases +CREATE TABLE IF NOT EXISTS leaf_disease_types ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL +); +-- === Core entities === + +CREATE TABLE IF NOT EXISTS leaf_reports ( + id BIGSERIAL PRIMARY KEY, + device_id TEXT NOT NULL REFERENCES devices(device_id), + leaf_disease_type_id INT NOT NULL REFERENCES leaf_disease_types(id), + ts TIMESTAMPTZ NOT NULL, + confidence DOUBLE PRECISION CHECK (confidence >= 0 AND confidence <= 1), + sick BOOLEAN NOT NULL +); + + +-- Missions table +CREATE TABLE IF NOT EXISTS missions ( + mission_id BIGSERIAL PRIMARY KEY, + start_time timestamptz NOT NULL, + end_time timestamptz, + area_geom geometry(Polygon, 4326) NOT NULL, + CHECK (end_time IS NULL OR end_time > start_time) +); + +-- Optional link table if you want explicit mission↔region mapping +CREATE TABLE IF NOT EXISTS mission_regions ( + mission_id bigint NOT NULL REFERENCES missions(mission_id) ON DELETE CASCADE, + region_id bigint NOT NULL REFERENCES regions(id) ON DELETE CASCADE, + PRIMARY KEY (mission_id, region_id) +); + +-- Telemetry points (raw stream) +CREATE TABLE IF NOT EXISTS telemetry ( + id BIGSERIAL PRIMARY KEY, + mission_id BIGINT NOT NULL REFERENCES missions(mission_id) ON DELETE CASCADE, + device_id text NOT NULL REFERENCES devices(device_id), + ts timestamptz NOT NULL, + geom geometry(Point, 4326) NOT NULL, + altitude real CHECK (altitude >= 0) +); + +-- Per-tile aggregated stats (for heatmaps etc.) +CREATE TABLE IF NOT EXISTS tile_stats ( + id BIGSERIAL PRIMARY KEY, + mission_id BIGINT NOT NULL REFERENCES missions(mission_id) ON DELETE CASCADE, + tile_id text NOT NULL, + anomaly_score real, + geom geometry(Polygon, 4326) NOT NULL, + UNIQUE (mission_id, tile_id) +); + +-- Individual anomaly events (point-level) +CREATE TABLE IF NOT EXISTS anomalies ( + anomaly_id bigserial PRIMARY KEY, + mission_id bigint NOT NULL REFERENCES missions(mission_id) ON DELETE CASCADE, + device_id text NOT NULL REFERENCES devices(device_id), + ts timestamptz NOT NULL, + anomaly_type_id int NOT NULL REFERENCES anomaly_types(anomaly_type_id), + severity real CHECK (severity >= 0), + details jsonb, + geom geometry(Point,4326) +); + +-- Files stored in MinIO (S3-compatible) and referenced here +CREATE TABLE IF NOT EXISTS files ( + file_id bigserial PRIMARY KEY, + bucket text NOT NULL, -- MinIO bucket name + object_key text NOT NULL, -- path/key inside the bucket + content_type text, -- MIME type (image/jpeg, application/geo+json, ...) + size_bytes bigint CHECK (size_bytes >= 0), + etag text, -- checksum returned by S3/MinIO (MD5/Etag) + created_at timestamptz NOT NULL DEFAULT now(), + mission_id bigint REFERENCES missions(mission_id) ON DELETE SET NULL, + device_id text REFERENCES devices(device_id) ON DELETE SET NULL, + tile_id text, -- optional link to a tile identifier + footprint geometry(Polygon,4326), -- spatial footprint if known + metadata jsonb, -- arbitrary extra metadata + UNIQUE (bucket, object_key) +); + +-- System / application logs (partitioned by time) +CREATE TABLE IF NOT EXISTS event_logs ( + log_id bigserial, + ts timestamptz NOT NULL, + level text NOT NULL CHECK (level IN ('DEBUG','INFO','WARN','ERROR','FATAL')), + source text NOT NULL, + message text NOT NULL, + details jsonb, + trace_id text, + user_id bigint NOT NULL DEFAULT -1, -- -1 = not triggered by a user + PRIMARY KEY (log_id, ts) +) PARTITION BY RANGE (ts); + + +-- === Partitioned parent for telemetry (daily range) === +CREATE TABLE IF NOT EXISTS telemetry_new ( + mission_id BIGINT NOT NULL REFERENCES missions(mission_id) ON DELETE CASCADE, + ts timestamptz NOT NULL, + device_id text NOT NULL REFERENCES devices(device_id), + geom geometry(Point,4326) NOT NULL, + altitude real, + PRIMARY KEY (mission_id, ts) +) PARTITION BY RANGE (ts); + +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(150) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS clients ( + schedule_id BIGSERIAL PRIMARY KEY, + client_id BIGINT NOT NULL, + team VARCHAR(150), + cron_expr TEXT, + active_days TEXT, + time_window TEXT, + last_updated TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- CREATE TABLE IF NOT EXISTS ultrasonic_plant_predictions ( +-- id UUID PRIMARY KEY DEFAULT gen_random_uuid(), +-- predicted_class TEXT NOT NULL, +-- confidence FLOAT NOT NULL, +-- -- status TEXT NOT NULL, +-- prediction_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +-- ); +CREATE TABLE IF NOT EXISTS ultrasonic_plant_predictions ( + id BIGSERIAL PRIMARY KEY, + file TEXT, + predicted_class TEXT, + confidence DOUBLE PRECISION, + watering_status TEXT, + status TEXT, + prediction_time TIMESTAMPTZ DEFAULT now() +); + +-- service_accounts +CREATE TABLE IF NOT EXISTS public.service_accounts ( + id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + name varchar(150) NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + token_hash text NOT NULL +); + + +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token TEXT UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + + + +--- === Embeddings table for vector data (e.g. image embeddings) === +CREATE TABLE IF NOT EXISTS embeddings ( + id BIGSERIAL PRIMARY KEY, + vec vector(784) +); +CREATE TABLE IF NOT EXISTS training_runs ( + id BIGSERIAL PRIMARY KEY, + run_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + backbone TEXT NOT NULL, + image_size INT NOT NULL, + num_epochs INT NOT NULL, + train_split NUMERIC(4,3) NOT NULL, + top1_acc NUMERIC(5,4) NOT NULL, + best_top1_acc NUMERIC(5,4) NOT NULL, + artifacts_bucket TEXT NOT NULL, + artifacts_prefix TEXT NOT NULL, + labels_object TEXT NOT NULL, + best_ckpt_object TEXT NOT NULL, + metrics_object TEXT NOT NULL, + cm_object TEXT NOT NULL, + seed INT NOT NULL +); + +-- Inferenceevent_logs_sensors, instead of Inference logs: +CREATE TABLE IF NOT EXISTS inference_logs ( + id BIGSERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), + model_backbone TEXT NOT NULL, + image_size INT NOT NULL, + fruit_type TEXT NOT NULL, + score NUMERIC(5,4) NOT NULL, + latency_ms NUMERIC(8,3) NOT NULL, + client_ip TEXT, + error TEXT, + image_url TEXT +); + +-- Ripeness predictions table +CREATE TABLE IF NOT EXISTS ripeness_predictions ( + id BIGSERIAL PRIMARY KEY, + inference_log_id BIGINT NOT NULL REFERENCES inference_logs(id) ON DELETE CASCADE, + ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), + ripeness_label TEXT NOT NULL CHECK (ripeness_label IN ('ripe', 'unripe', 'overripe')), + ripeness_score DOUBLE PRECISION NOT NULL, + model_name TEXT NOT NULL, + run_id UUID NOT NULL, + device_id TEXT REFERENCES devices(device_id), + UNIQUE (inference_log_id) +); + +-- Create indexes for ripeness_predictions +CREATE INDEX IF NOT EXISTS ix_ripeness_inflog ON ripeness_predictions(inference_log_id); +CREATE INDEX IF NOT EXISTS ix_ripeness_ts ON ripeness_predictions(ts); +CREATE INDEX IF NOT EXISTS ix_ripeness_device ON ripeness_predictions(device_id); +CREATE INDEX IF NOT EXISTS ix_ripeness_run ON ripeness_predictions(run_id); +CREATE INDEX IF NOT EXISTS ix_leaf_reports_ts_brin ON leaf_reports USING BRIN (ts); +CREATE INDEX IF NOT EXISTS ix_leaf_reports_device_ts ON leaf_reports (device_id, ts); +CREATE INDEX IF NOT EXISTS ix_leaf_reports_type_ts ON leaf_reports (leaf_disease_type_id, ts); + +-- Weekly ripeness rollups table +CREATE TABLE IF NOT EXISTS ripeness_weekly_rollups_ts ( + id BIGSERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + fruit_type TEXT NOT NULL, + device_id TEXT REFERENCES devices(device_id), + run_id UUID NOT NULL, + cnt_total INTEGER NOT NULL, + cnt_ripe INTEGER NOT NULL, + cnt_unripe INTEGER NOT NULL, + cnt_overripe INTEGER NOT NULL, + pct_ripe DOUBLE PRECISION NOT NULL +); + +-- Create indexes for ripeness_weekly_rollups_ts +CREATE INDEX IF NOT EXISTS ix_rwrt_ts ON ripeness_weekly_rollups_ts(ts); +CREATE INDEX IF NOT EXISTS ix_rwrt_fruit_ts ON ripeness_weekly_rollups_ts(fruit_type, ts); +CREATE INDEX IF NOT EXISTS ix_rwrt_device ON ripeness_weekly_rollups_ts(device_id); +CREATE INDEX IF NOT EXISTS ix_rwrt_run ON ripeness_weekly_rollups_ts(run_id); + +-- Sensor event logs table. +CREATE TABLE IF NOT EXISTS devices_sensor ( + id TEXT UNIQUE NOT NULL, + plant_id INT, + sensor_type TEXT, + last_seen TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (id) +); +-- Sensor event logs table. +CREATE TABLE IF NOT EXISTS event_logs_sensors( + id bigserial PRIMARY KEY, + device_id TEXT NOT NULL REFERENCES devices_sensor(id), + issue_type text NOT NULL, + severity text NOT NULL CHECK (severity IN ('info','warn','error','critical')), + start_ts timestamptz NOT NULL DEFAULT now(), + end_ts timestamptz NULL, + details jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT event_logs_sensors_end_after_start + CHECK (end_ts IS NULL OR end_ts >= start_ts) +); + + + +CREATE TABLE IF NOT EXISTS public.sensor_anomalies ( + id BIGSERIAL PRIMARY KEY, + idSensor INT NOT NULL, + plant_id INT NOT NULL, + sensor VARCHAR(64) NOT NULL, + ts TIMESTAMPTZ NOT NULL, + value DOUBLE PRECISION, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + zone VARCHAR(128), + result JSONB NOT NULL, + inserted_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + + + +CREATE TABLE IF NOT EXISTS public.sensor_zone_stats ( + id BIGSERIAL PRIMARY KEY, + zone VARCHAR(128) NOT NULL, + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + count INT NOT NULL, + mean DOUBLE PRECISION, + median DOUBLE PRECISION, + min DOUBLE PRECISION, + max DOUBLE PRECISION, + std DOUBLE PRECISION, + anomalies INT, + inserted_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + + + +--- Alerts_leaves table + +CREATE TABLE IF NOT EXISTS public.alerts_leaves ( + id bigserial PRIMARY KEY, + entity_id text NOT NULL, + rule text NOT NULL, + window_start timestamptz NOT NULL, + window_end timestamptz NOT NULL, + score double precision NOT NULL, + first_seen timestamptz NOT NULL, + last_seen timestamptz NOT NULL, + status text NOT NULL CHECK (status IN ('OPEN','ACK','RESOLVED')), + meta_json jsonb +); + +CREATE INDEX IF NOT EXISTS ix_alerts_leaves_entity_rule ON public.alerts_leaves(entity_id, rule); +CREATE INDEX IF NOT EXISTS ix_alerts_leaves_status ON public.alerts_leaves(status); + + +--- === Soil moisture irrigation tables === + +CREATE TABLE IF NOT EXISTS soil_moisture_events ( + id SERIAL PRIMARY KEY, + device_id TEXT NOT NULL REFERENCES devices(device_id), + ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), + dry_ratio REAL NOT NULL, + decision TEXT NOT NULL, + confidence REAL NOT NULL, + patch_count INT NOT NULL, + idempotency_key TEXT NOT NULL, + extra JSONB DEFAULT '{}'::jsonb +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_events_idem ON soil_moisture_events (idempotency_key); + +CREATE TABLE IF NOT EXISTS irrigation_schedule ( + device_id TEXT PRIMARY KEY REFERENCES devices(device_id), + + next_run_at TIMESTAMPTZ NOT NULL, + duration_min INT NOT NULL, + updated_by TEXT NOT NULL, + update_reason TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS irrigation_schedule_audit ( + id SERIAL PRIMARY KEY, + device_id TEXT NOT NULL, + prev_next_run_at TIMESTAMPTZ, + prev_duration_min INT, + next_run_at TIMESTAMPTZ NOT NULL, + duration_min INT NOT NULL, + updated_by TEXT NOT NULL, + update_reason TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE irrigation_policies ( + device_id TEXT NOT NULL, + prev_state TEXT, + dry_ratio_high REAL, + dry_ratio_low REAL, + min_patches INT, + duration_min INT, + updated_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (device_id), + CONSTRAINT fk_device + FOREIGN KEY (device_id) REFERENCES devices(device_id) + ON DELETE CASCADE +); + + +CREATE TABLE IF NOT EXISTS alerts ( + + -- Required fields + alert_id TEXT PRIMARY KEY, + alert_type TEXT, + device_id TEXT, + started_at TIMESTAMPTZ, + + -- Optional / dynamic fields + ended_at TIMESTAMPTZ, + confidence DOUBLE PRECISION, + area TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + severity INT DEFAULT 1, + image_url TEXT, + vod TEXT, + hls TEXT, + + -- Acknowledgment field + ack BOOLEAN DEFAULT FALSE, -- TRUE when the alert was acknowledged + + -- Flexible metadata for anything else + meta JSONB, + + -- System fields + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() +); + +-- === Task thresholds (enum + table) === +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'task_type_enum') THEN + CREATE TYPE task_type_enum AS ENUM ( + 'ripeness', + 'disease', + 'size', + 'color', + 'quality' + ); + END IF; +END$$; + +CREATE TABLE IF NOT EXISTS task_thresholds ( + threshold_id SERIAL PRIMARY KEY, + task task_type_enum NOT NULL, + label TEXT NOT NULL DEFAULT '', + threshold NUMERIC(6,4) NOT NULL CHECK (threshold >= 0 AND threshold <= 1), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_by TEXT, + CONSTRAINT ux_task_thresholds_task_label UNIQUE (task, label) +); + +CREATE TABLE public.image_new_aerial_connections ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255), + key TEXT, + linked_time TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS public.aerial_images_metadata ( + id SERIAL PRIMARY KEY, + + -- File and drone metadata + file_name TEXT NOT NULL, + drone_id TEXT NOT NULL, + capture_time TIMESTAMP WITH TIME ZONE NOT NULL, + + -- Raw JSON as received (latitude/longitude) + gis_origin JSONB NOT NULL, + + -- Geometry point auto-generated from JSON + geom_point geometry(Point, 4326) + GENERATED ALWAYS AS ( + ST_SetSRID( + ST_MakePoint( + (gis_origin->>'longitude')::double precision, + (gis_origin->>'latitude')::double precision + ), + 4326 + ) + ) STORED, + + -- Flight attributes + altitude_m DOUBLE PRECISION, + done BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS ix_aerial_geom_point_gist +ON public.aerial_images_metadata USING GIST (geom_point); + + +CREATE TABLE IF NOT EXISTS public.aerial_image_object_detections ( + id SERIAL PRIMARY KEY, + img_key TEXT NOT NULL, + label TEXT NOT NULL, + confidence DOUBLE PRECISION NOT NULL, + bbox_x1 DOUBLE PRECISION NOT NULL, + bbox_y1 DOUBLE PRECISION NOT NULL, + bbox_x2 DOUBLE PRECISION NOT NULL, + bbox_y2 DOUBLE PRECISION NOT NULL, + detected_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_image_object_detections_key + ON public.aerial_image_object_detections (img_key); + + +CREATE TABLE IF NOT EXISTS public.aerial_image_anomaly_detections ( + id SERIAL PRIMARY KEY, + img_key TEXT NOT NULL, + label TEXT NOT NULL, + confidence DOUBLE PRECISION NOT NULL, + bbox_x1 DOUBLE PRECISION NOT NULL, + bbox_y1 DOUBLE PRECISION NOT NULL, + bbox_x2 DOUBLE PRECISION NOT NULL, + bbox_y2 DOUBLE PRECISION NOT NULL, + detected_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_image_anomaly_detections_key + ON public.aerial_image_anomaly_detections (img_key); + + +CREATE TABLE IF NOT EXISTS public.aerial_images_complete_metadata ( + id SERIAL PRIMARY KEY, + file_name TEXT NOT NULL, + device_id TEXT NOT NULL, + gis_origin JSONB, + gis geometry(Point, 4326) + GENERATED ALWAYS AS ( + ST_SetSRID( + ST_MakePoint( + (gis_origin->>'longitude')::double precision, + (gis_origin->>'latitude')::double precision + ), + 4326 + ) + ) STORED, + img_key TEXT NOT NULL UNIQUE, + timestamp_utc TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_aerial_metadata_device_id + ON public.aerial_images_complete_metadata (device_id); + +CREATE INDEX IF NOT EXISTS idx_aerial_metadata_timestamp + ON public.aerial_images_complete_metadata (timestamp_utc); + +CREATE INDEX IF NOT EXISTS idx_aerial_metadata_gis + ON public.aerial_images_complete_metadata USING GIST (gis); + + +CREATE TABLE IF NOT EXISTS public.field_polygons ( + id SERIAL PRIMARY KEY, + gis geometry(Point, 4326) NOT NULL, + boundary geometry(Polygon, 4326) NOT NULL, + area_sq_m DOUBLE PRECISION GENERATED ALWAYS AS ( + ST_Area(geography(boundary)) + ) STORED, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_field_polygons_gis + ON public.field_polygons USING GIST (gis); + + +CREATE TABLE IF NOT EXISTS public.aerial_image_segmentation ( + id SERIAL PRIMARY KEY, + img_key TEXT NOT NULL, + mask_path TEXT, + other FLOAT DEFAULT 0, + bareland FLOAT DEFAULT 0, + rangeland FLOAT DEFAULT 0, + developed_space FLOAT DEFAULT 0, + road FLOAT DEFAULT 0, + tree FLOAT DEFAULT 0, + water FLOAT DEFAULT 0, + agriculture FLOAT DEFAULT 0, + building FLOAT DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_segmentation_img_key + ON public.aerial_image_segmentation (img_key); + + +CREATE TABLE public.sound_new_sounds_connections ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255), + key TEXT, + linked_time TIMESTAMPTZ +); + +CREATE TABLE public.sound_new_plants_connections ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255), + key TEXT, + linked_time TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS ix_task_thresholds_task ON task_thresholds (task); +CREATE INDEX IF NOT EXISTS ix_task_thresholds_updated_at ON task_thresholds (updated_at); + + +CREATE TABLE IF NOT EXISTS public.sounds_metadata ( + id BIGSERIAL PRIMARY KEY, + file_name TEXT NOT NULL, + device_id TEXT NOT NULL REFERENCES public.devices(device_id), + capture_time TIMESTAMPTZ NOT NULL, + duration_sec DOUBLE PRECISION CHECK (duration_sec >= 0), + done BOOLEAN NOT NULL DEFAULT FALSE, + sample_rate_hz INTEGER CHECK (sample_rate_hz > 0), + channels SMALLINT CHECK (channels > 0), + content_type TEXT, + + gis_origin JSONB NOT NULL, + + geom_point geometry(Point, 4326) + GENERATED ALWAYS AS ( + ST_SetSRID( + ST_MakePoint( + (gis_origin->>'longitude')::double precision, + (gis_origin->>'latitude')::double precision + ), + 4326 + ) + ) STORED, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT ux_sounds_dev_time UNIQUE (device_id, capture_time) +); + +CREATE INDEX IF NOT EXISTS ix_sounds_meta_ts_brin + ON public.sounds_metadata USING BRIN (capture_time); +CREATE INDEX IF NOT EXISTS ix_sounds_meta_device_time + ON public.sounds_metadata (device_id, capture_time); +CREATE INDEX IF NOT EXISTS ix_sounds_meta_geom_point_gist + ON public.sounds_metadata USING GIST (geom_point); +CREATE INDEX IF NOT EXISTS ix_sounds_meta_file_name + ON public.sounds_metadata (file_name); +CREATE INDEX IF NOT EXISTS ix_sounds_meta_created_brin + ON public.sounds_metadata USING BRIN (created_at); + + +CREATE TABLE IF NOT EXISTS public.sounds_ultra_metadata ( + id BIGSERIAL PRIMARY KEY, + file_name TEXT NOT NULL, + device_id TEXT NOT NULL REFERENCES public.devices(device_id), + capture_time TIMESTAMPTZ NOT NULL, + duration_sec DOUBLE PRECISION CHECK (duration_sec >= 0), + done BOOLEAN NOT NULL DEFAULT FALSE, + sample_rate_hz INTEGER CHECK (sample_rate_hz > 0), + channels SMALLINT CHECK (channels > 0), + content_type TEXT, + + gis_origin JSONB NOT NULL, + + geom_point geometry(Point, 4326) + GENERATED ALWAYS AS ( + ST_SetSRID( + ST_MakePoint( + (gis_origin->>'longitude')::double precision, + (gis_origin->>'latitude')::double precision + ), + 4326 + ) + ) STORED, + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT ux_ultra_sounds_dev_time UNIQUE (device_id, capture_time) +); + + +CREATE INDEX IF NOT EXISTS ix_ultra_sounds_meta_ts_brin + ON public.sounds_ultra_metadata USING BRIN (capture_time); +CREATE INDEX IF NOT EXISTS ix_ultra_sounds_meta_device_time + ON public.sounds_ultra_metadata (device_id, capture_time); +CREATE INDEX IF NOT EXISTS ix_ultra_sounds_meta_geom_point_gist + ON public.sounds_ultra_metadata USING GIST (geom_point); +CREATE INDEX IF NOT EXISTS ix_ultra_sounds_meta_file_name + ON public.sounds_ultra_metadata (file_name); +CREATE INDEX IF NOT EXISTS ix_ultra_sounds_meta_created_brin + ON public.sounds_ultra_metadata USING BRIN (created_at); + + +CREATE TABLE public.image_new_security_connections ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255), + key TEXT, + linked_time TIMESTAMPTZ +); + + +-- === Indexes for performance optimization === + + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_ts_brin + ON public.sensor_anomalies USING BRIN (ts); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_zone + ON public.sensor_anomalies (zone); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_sensor + ON public.sensor_anomalies (sensor); + + +CREATE INDEX IF NOT EXISTS ix_sensor_zone_stats_zone_window + ON public.sensor_zone_stats (zone, window_start, window_end); + +CREATE INDEX IF NOT EXISTS ix_sensor_zone_stats_anomalies + ON public.sensor_zone_stats (anomalies); +CREATE INDEX IF NOT EXISTS ix_sensors_name ON sensors (sensor_name); +CREATE INDEX IF NOT EXISTS ix_sensors_type ON sensors (sensor_type); +CREATE INDEX IF NOT EXISTS ix_sensors_status ON sensors (status); +CREATE INDEX IF NOT EXISTS ix_sensors_location ON sensors (location_lat, location_lon); + +-- Spatial +CREATE INDEX IF NOT EXISTS ix_missions_area_geom_gist ON missions USING GIST (area_geom); +CREATE INDEX IF NOT EXISTS ix_telemetry_geom_gist ON telemetry USING GIST (geom); +CREATE INDEX IF NOT EXISTS ix_tile_stats_geom_gist ON tile_stats USING GIST (geom); +CREATE INDEX IF NOT EXISTS ix_files_footprint_gist ON files USING GIST (footprint); + +-- Time-series +CREATE INDEX IF NOT EXISTS ix_telemetry_ts_brin ON telemetry USING BRIN (ts); +CREATE INDEX IF NOT EXISTS ix_anomalies_ts_brin ON anomalies USING BRIN (ts); + +-- Lookup / filtering +CREATE INDEX IF NOT EXISTS ix_telemetry_mission_ts ON telemetry (mission_id, ts); +CREATE INDEX IF NOT EXISTS ix_anomalies_mission_ts ON anomalies (mission_id, ts); +CREATE INDEX IF NOT EXISTS ix_files_mission_created ON files (mission_id, created_at); + +-- JSONB for flexible search +CREATE INDEX IF NOT EXISTS ix_anomalies_details_gin ON anomalies USING GIN (details); +CREATE INDEX IF NOT EXISTS ix_files_metadata_gin ON files USING GIN (metadata); + +-- Regions spatial index +CREATE INDEX IF NOT EXISTS ix_regions_geom_gist ON regions USING GIST (geom); + + +-- Vector index for embeddings (using HNSW) +CREATE INDEX IF NOT EXISTS idx_embeddings_vec_hnsw ON embeddings USING hnsw (vec vector_l2_ops) WITH (m=4, ef_construction=10); + +CREATE INDEX IF NOT EXISTS ix_users_username ON users (username); +CREATE INDEX IF NOT EXISTS ix_refresh_tokens_user_id ON refresh_tokens (user_id); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_service_accounts_name ON public.service_accounts (name); +CREATE INDEX IF NOT EXISTS ix_service_accounts_id ON public.service_accounts (id); + +CREATE INDEX IF NOT EXISTS idx_infer_ts ON inference_logs (ts); +CREATE INDEX IF NOT EXISTS idx_infer_fruit ON inference_logs (fruit_type); + +-- Sensors logs +CREATE INDEX IF NOT EXISTS ix_event_logs_sensors_device_start ON event_logs_sensors (device_id, start_ts); +CREATE INDEX IF NOT EXISTS ix_event_logs_sensors_start_brin ON event_logs_sensors USING BRIN (start_ts); +CREATE INDEX IF NOT EXISTS ix_event_logs_sensors_details_gin ON event_logs_sensors USING GIN (details jsonb_path_ops); + + + + + +-- CREATE INDEX IF NOT EXISTS ix_alerts_entity_rule ON public.alerts(entity_id, rule); +-- CREATE INDEX IF NOT EXISTS ix_alerts_status ON public.alerts(status); + +-- ============================================ +-- 🔹 MISSING TABLES AND INDEXES FROM FIRST SCHEMA +-- ============================================ + + +-- Zones table (for linking sensors to geographic areas) +CREATE TABLE IF NOT EXISTS public.zones ( + id SERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL, + geom geometry(POLYGON, 4326) NOT NULL +); + +-- Extended sensors table with all environmental metrics +DROP TABLE IF EXISTS public.sensors CASCADE; +CREATE TABLE IF NOT EXISTS public.sensors ( + id SERIAL PRIMARY KEY, + sensor_name TEXT UNIQUE NOT NULL, + sensor_type TEXT NOT NULL, + owner_name TEXT, + location_lat DOUBLE PRECISION, + location_lon DOUBLE PRECISION, + install_date TIMESTAMP DEFAULT NOW(), + status TEXT DEFAULT 'active', + description TEXT, + last_maintenance TIMESTAMP, + value DOUBLE PRECISION, + humidity DOUBLE PRECISION, + temperature DOUBLE PRECISION, + ph DOUBLE PRECISION, + rainfall DOUBLE PRECISION, + soil_moisture DOUBLE PRECISION, + co2_concentration DOUBLE PRECISION, + n DOUBLE PRECISION, + p DOUBLE PRECISION, + k DOUBLE PRECISION, + label TEXT, + timestamp TIMESTAMPTZ NOT NULL, + msg_type TEXT, + plant_id INT, + soil_type INT, + sunlight_exposure DOUBLE PRECISION, + wind_speed DOUBLE PRECISION, + organic_matter DOUBLE PRECISION, + irrigation_frequency DOUBLE PRECISION, + crop_density DOUBLE PRECISION, + pest_pressure DOUBLE PRECISION, + fertilizer_usage DOUBLE PRECISION, + growth_stage INT, + urban_area_proximity DOUBLE PRECISION, + water_source_type INT, + frost_risk DOUBLE PRECISION, + water_usage_efficiency DOUBLE PRECISION +); + +-- Sensor anomalies table with full structure and JSONB result +DROP TABLE IF EXISTS public.sensor_anomalies CASCADE; +CREATE TABLE IF NOT EXISTS public.sensor_anomalies ( + id BIGSERIAL PRIMARY KEY, + idSensor INT NOT NULL, + plant_id INT NOT NULL, + sensor VARCHAR(64) NOT NULL, + ts TIMESTAMPTZ NOT NULL, + value DOUBLE PRECISION, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + zone VARCHAR(128), + result JSONB NOT NULL, + inserted_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS public.sensors ( + sensor_id INT PRIMARY KEY, + sid TEXT, + sensor_name TEXT NOT NULL, + sensor_type TEXT NOT NULL, + owner_name TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + install_date TIMESTAMP DEFAULT NOW(), + status TEXT DEFAULT 'active', + description TEXT, + last_maintenance TIMESTAMP, + value DOUBLE PRECISION, + humidity DOUBLE PRECISION, + temperature DOUBLE PRECISION, + ph DOUBLE PRECISION, + rainfall DOUBLE PRECISION, + soil_moisture DOUBLE PRECISION, + co2_concentration DOUBLE PRECISION, + n DOUBLE PRECISION, + p DOUBLE PRECISION, + k DOUBLE PRECISION, + label TEXT, + timestamp TIMESTAMPTZ NOT NULL, + msg_type TEXT, + plant_id INT, + soil_type INT, + sunlight_exposure DOUBLE PRECISION, + wind_speed DOUBLE PRECISION, + organic_matter DOUBLE PRECISION, + irrigation_frequency DOUBLE PRECISION, + crop_density DOUBLE PRECISION, + pest_pressure DOUBLE PRECISION, + fertilizer_usage DOUBLE PRECISION, + growth_stage INT, + urban_area_proximity DOUBLE PRECISION, + water_source_type INT, + frost_risk DOUBLE PRECISION, + water_usage_efficiency DOUBLE PRECISION +); +CREATE TABLE IF NOT EXISTS public.sensors_anomalies_modal ( + id BIGSERIAL PRIMARY KEY, + sensor_id INT NOT NULL REFERENCES sensors(sensor_id) ON DELETE CASCADE, + ts TIMESTAMPTZ NOT NULL, + anomaly REAL NOT NULL CHECK (anomaly >= 0), + inserted_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + + + +-- Updated event_logs_sensors referencing devices_sensor +DROP TABLE IF EXISTS event_logs_sensors CASCADE; +CREATE TABLE IF NOT EXISTS event_logs_sensors( + id bigserial PRIMARY KEY, + device_id TEXT NOT NULL REFERENCES devices_sensor(id), + issue_type text NOT NULL, + severity text NOT NULL CHECK (severity IN ('info','warn','error','critical')), + start_ts timestamptz NOT NULL DEFAULT now(), + end_ts timestamptz NULL, + details jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT event_logs_sensors_end_after_start + CHECK (end_ts IS NULL OR end_ts >= start_ts) +); + +-- Sensor zone statistics (for per-region summaries) +CREATE TABLE IF NOT EXISTS public.sensor_zone_stats ( + id BIGSERIAL PRIMARY KEY, + zone VARCHAR(128) NOT NULL, + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + count INT NOT NULL, + mean DOUBLE PRECISION, + median DOUBLE PRECISION, + min DOUBLE PRECISION, + max DOUBLE PRECISION, + std DOUBLE PRECISION, + anomalies INT, + inserted_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- ============================================ +-- 🔹 INDEXES FOR SENSOR TABLES +-- ============================================ + +CREATE INDEX IF NOT EXISTS ix_sensors_anomalies_modal_sensor_ts + ON sensors_anomalies_modal (sensor_id, ts); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_ts_brin + ON public.sensor_anomalies USING BRIN (ts); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_zone + ON public.sensor_anomalies (zone); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_sensor + ON public.sensor_anomalies (sensor); + +CREATE INDEX IF NOT EXISTS ix_sensor_zone_stats_zone_window + ON public.sensor_zone_stats (zone, window_start, window_end); + +CREATE INDEX IF NOT EXISTS ix_sensor_zone_stats_anomalies + ON public.sensor_zone_stats (anomalies); + +CREATE INDEX IF NOT EXISTS ix_sensors_name ON sensors (sensor_name); +CREATE INDEX IF NOT EXISTS ix_sensors_type ON sensors (sensor_type); +CREATE INDEX IF NOT EXISTS ix_sensors_status ON sensors (status); +CREATE INDEX IF NOT EXISTS ix_sensors_location ON sensors (location_lat, location_lon); + + + + + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_ts_brin + ON public.sensor_anomalies USING BRIN (ts); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_zone + ON public.sensor_anomalies (zone); + +CREATE INDEX IF NOT EXISTS ix_sensor_anomalies_sensor + ON public.sensor_anomalies (sensor); + + +CREATE INDEX IF NOT EXISTS ix_sensor_zone_stats_zone_window + ON public.sensor_zone_stats (zone, window_start, window_end); + +CREATE INDEX IF NOT EXISTS ix_sensor_zone_stats_anomalies + ON public.sensor_zone_stats (anomalies); diff --git a/AgCloud/RelDB/docker-compose.yml b/AgCloud/RelDB/docker-compose.yml new file mode 100644 index 000000000..e8ac85ae0 --- /dev/null +++ b/AgCloud/RelDB/docker-compose.yml @@ -0,0 +1,119 @@ +version: "3.9" + +services: + db: + build: . + container_name: db + environment: + POSTGRES_USER: missions_user + POSTGRES_PASSWORD: pg123 + POSTGRES_DB: missions_db + + # --- ENV for scripts --- + PGHOST: 127.0.0.1 + PGPORT: 5432 + PGDATA: /var/lib/postgresql/data + WAL_DIR: /var/lib/postgresql/wal_archive + BACKUP_DIR: /var/lib/postgresql/backups + RETENTION: 7 + TZ: Asia/Jerusalem + + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - wal_archive:/var/lib/postgresql/wal_archive + - backups:/var/lib/postgresql/backups + healthcheck: + test: ["CMD", "pg_isready", "-U", "missions_user", "-d", "missions_db"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - airnet + restart: unless-stopped + + rover_ingest: + build: + context: .. + dockerfile: services/rover_ingest/Dockerfile + command: ["python","-m","services.rover_ingest.app"] + environment: + MINIO_ENDPOINT: "host.docker.internal:9000" + MINIO_BUCKET: "rover-images" + MINIO_ACCESS_KEY: "minioadmin" + MINIO_SECRET_KEY: "minioadmin" + PG_DSN: "dbname=missions_db user=missions_user host=db port=5432 password=pg123" + + depends_on: + db: + condition: service_healthy + + extra_hosts: + - "host.docker.internal:host-gateway" + + ports: + - "9109:9109" # Prometheus metrics + restart: unless-stopped + networks: + - airnet + + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:9109/metrics').read()"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 20s + + + postgres_exporter: + image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0 + environment: + DATA_SOURCE_NAME: "postgresql://missions_user:pg123@db:5432/missions_db?sslmode=disable" + command: + - "--extend.query-path=/etc/postgres-queries.yml" + volumes: + - ./graphs/postgres-queries.yml:/etc/postgres-queries.yml + depends_on: + - db + ports: + - "9187:9187" + networks: + - airnet + + prometheus: + image: prom/prometheus:latest + volumes: + - ./graphs/prometheus.yml:/etc/prometheus/prometheus.yml + - ./graphs/prometheus-recording.rules.yml:/etc/prometheus/recording.rules.yml + # - ./graphs/postgres-alerts.yml:/etc/prometheus/alerts.yml + depends_on: + - postgres_exporter + ports: + - "9090:9090" + networks: + - airnet + + grafana: + image: grafana/grafana-oss:latest + volumes: + - ./graphs/grafana-dashboard.json:/etc/grafana/provisioning/dashboards/postgres-dashboard.json + - ./graphs/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/dashboards.yml + - ./graphs/grafana-datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml + depends_on: + - prometheus + ports: + - "3000:3000" + networks: + - airnet + + +networks: + airnet: + driver: bridge + +volumes: + postgres_data: + wal_archive: + backups: \ No newline at end of file diff --git a/AgCloud/RelDB/explain.md b/AgCloud/RelDB/explain.md new file mode 100644 index 000000000..627c7255c --- /dev/null +++ b/AgCloud/RelDB/explain.md @@ -0,0 +1,90 @@ +# PostgreSQL Monitoring Stack + +This project sets up a full observability stack for PostgreSQL (with PostGIS), including: + +- **Metric collection** via `postgres_exporter` +- **Time-series storage and rules** via `Prometheus` +- **Alerting pipeline** via `Alertmanager` +- **Visualization** via `Grafana` + +## 🎯 Goals + +- Track WAL activity (`wal_bytes`), replication lag, and BRIN index efficiency +- Detect anomalies and raise alerts with **MTTD < 10 min** +- Visualize and analyze performance over time + +--- + +## 🔧 Services + +| Service | Description | +|---------------------|-------------| +| `db` | PostgreSQL + PostGIS (the monitored DB) | +| `postgres_exporter`| Exposes metrics from PostgreSQL | +| `prometheus` | Scrapes and processes metrics, evaluates rules | +| `alertmanager` | Receives alerts and routes them | +| `grafana` | Visualizes all metrics and alerts | + +--- + +## 📁 Project Files + +| File | Purpose | +|--------------------------------|---------| +| `docker-compose.yml` | Deploys the stack | +| `postgres-queries.yml` | Custom metrics exposed by postgres_exporter | +| `prometheus.yml` | Prometheus main config | +| `prometheus-recording.rules.yml` | Precomputed rule expressions | +| `postgres-alerts.yml` | Alert conditions | +| `alertmanager.yml` | Alert routing (dummy endpoint for now) | +| `grafana-dashboard.json` | Ready-to-import Grafana dashboard | + +--- + +## ⚙ How it works + +- `postgres_exporter` exposes raw and extended metrics from Postgres. +- Prometheus scrapes these metrics every 30s. +- `recording.rules` calculate derivative data like WAL throughput and cache hit ratios. +- `alerts.yml` defines warning/critical thresholds (e.g., WAL rate, lag > 120s). +- Alerts are sent to Alertmanager and shown in Prometheus UI. +- Grafana queries Prometheus and presents rich dashboards. + +--- + +## 🚀 Getting Started + +1. Place all config files in the same folder. +2. Run: `docker compose up -d` +3. Access services: + - Prometheus: http://localhost:9090 + - Grafana: http://localhost:3000 (default login: `admin` / `admin`) + - Exporter: http://localhost:9187 + +4. Import the `grafana-dashboard.json` into Grafana. +5. At the command line, type: `rate(pg_wal_stats_wal_bytes[5m])` + +This shows WAL write throughput (bytes per second). + + +--- + + +## 📊 Recommended Dashboards + +- **WAL throughput** (rate in bytes/sec) +- **Replication lag** (seconds and bytes) +- **BRIN index hit ratio** (per index) + +### Example +

+ Grafana Dashboard Example +

+ + + +## 📌 Notes + +- Assumes database is running on service `db` at port 5432. +- Requires Postgres user with `pg_monitor` role. +- Easily extendable for CPU, memory, connection tracking, etc. diff --git a/AgCloud/RelDB/graphs/grafana-dashboard-multi.json b/AgCloud/RelDB/graphs/grafana-dashboard-multi.json new file mode 100644 index 000000000..b871c38b4 --- /dev/null +++ b/AgCloud/RelDB/graphs/grafana-dashboard-multi.json @@ -0,0 +1,58 @@ +{ + "id": null, + "title": "Postgres – Selected Metrics", + "tags": ["postgres", "prometheus"], + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "10s", + "panels": [ + { + "type": "timeseries", + "title": "WAL Throughput (bytes/s)", + "datasource": "Prometheus", + "targets": [ + { "expr": "rate(pg_wal_stats_wal_bytes[30m])", "legendFormat": "WAL bytes/s" } + ], + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "BRIN Hit Ratio by Index", + "datasource": "Prometheus", + "targets": [ + { "expr": "pg_brin_index_io_brin_hit_ratio", "legendFormat": "{{schemaname}}.{{index_name}}" } + ], + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "Transactions per Second (TPS)", + "datasource": "Prometheus", + "targets": [ + { "expr": "rate(pg_stat_database_xact_commit[1m]) + rate(pg_stat_database_xact_rollback[1m])", "legendFormat": "TPS" } + ], + "gridPos": { "x": 0, "y": 8, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "Cache Hit Ratio", + "datasource": "Prometheus", + "targets": [ + { "expr": "pg_stat_database_blks_hit / (pg_stat_database_blks_hit + pg_stat_database_blks_read)", "legendFormat": "Hit Ratio" } + ], + "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "Active Connections", + "datasource": "Prometheus", + "targets": [ + { "expr": "pg_stat_activity_count{state=\"active\"}", "legendFormat": "Active" }, + { "expr": "pg_stat_activity_count{state=\"idle\"}", "legendFormat": "Idle" } + ], + "gridPos": { "x": 0, "y": 16, "w": 24, "h": 8 } + } + ] + } + \ No newline at end of file diff --git a/AgCloud/RelDB/graphs/grafana-dashboard-simple.json b/AgCloud/RelDB/graphs/grafana-dashboard-simple.json new file mode 100644 index 000000000..8dc444f1c --- /dev/null +++ b/AgCloud/RelDB/graphs/grafana-dashboard-simple.json @@ -0,0 +1,56 @@ +{ + "id": null, + "title": "Postgres – WAL, Replication, BRIN", + "tags": ["postgres", "prometheus"], + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "5s", + "panels": [ + { + "type": "graph", + "title": "WAL Throughput (bytes/s)", + "targets": [ + { + "expr": "rate(pg_wal_stats_wal_bytes[30s])", + "legendFormat": "WAL bytes/s" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "Replication Lag (bytes)", + "targets": [ + { + "expr": "pg_replication_lag_bytes_primary", + "legendFormat": "{{application_name}}" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "Replication Lag (seconds)", + "targets": [ + { + "expr": "pg_replication_replay_lag_seconds_standby", + "legendFormat": "standby lag" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "BRIN Hit Ratio by Index", + "targets": [ + { + "expr": "pg_brin_index_io_brin_hit_ratio", + "legendFormat": "{{schemaname}}.{{index_name}}" + } + ], + "datasource": "Prometheus" + } + ] + } + \ No newline at end of file diff --git a/AgCloud/RelDB/graphs/grafana-dashboards.yml b/AgCloud/RelDB/graphs/grafana-dashboards.yml new file mode 100644 index 000000000..f85e3bd21 --- /dev/null +++ b/AgCloud/RelDB/graphs/grafana-dashboards.yml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/AgCloud/RelDB/graphs/grafana-datasource.yml b/AgCloud/RelDB/graphs/grafana-datasource.yml new file mode 100644 index 000000000..0eddf2629 --- /dev/null +++ b/AgCloud/RelDB/graphs/grafana-datasource.yml @@ -0,0 +1,7 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/AgCloud/RelDB/graphs/image.png b/AgCloud/RelDB/graphs/image.png new file mode 100644 index 000000000..de3f1e2bc Binary files /dev/null and b/AgCloud/RelDB/graphs/image.png differ diff --git a/AgCloud/RelDB/graphs/postgres-alerts.yml b/AgCloud/RelDB/graphs/postgres-alerts.yml new file mode 100644 index 000000000..adf9fa86a --- /dev/null +++ b/AgCloud/RelDB/graphs/postgres-alerts.yml @@ -0,0 +1,63 @@ +groups: + - name: postgres_alerts + interval: 30s + + rules: + # 1) Very high WAL rate (abnormal traffic) + - alert: HighWalThroughput + expr: pg:wal_bytes:rate_per_s > 5e6 # > ~5MB/s for 5 minutes + for: 5m + labels: + severity: warning + annotations: + summary: "High WAL throughput" + description: "WAL rate {{ $value | humanize }} B/s above threshold for 5 minutes" + + # 2) Almost no WAL (risk: system 'frozen' or no traffic) + - alert: LowWalThroughput + expr: pg:wal_bytes:rate_per_s < 1000 # < 1KB/s + for: 10m + labels: + severity: info + annotations: + summary: "Very low WAL throughput" + description: "Almost no WAL writes for 10 minutes" + + # 3) Replication delay (if replicas exist) + - alert: ReplicationLagSecondsHigh + expr: pg:replication_lag_seconds > 120 + for: 5m + labels: + severity: critical + annotations: + summary: "Replication lag high (seconds)" + description: "Replication lag higher than 120 seconds for 5 minutes" + + - alert: ReplicationLagBytesHigh + expr: pg:replication_lag_bytes > 50e6 # > ~50MB + for: 5m + labels: + severity: warning + annotations: + summary: "Replication lag high (bytes)" + description: "Replication lag greater than 50MB for 5 minutes" + + # 4) Low BRIN efficiency + - alert: BrinHitRatioLow + expr: pg:brin_hit_ratio < 0.6 + for: 10m + labels: + severity: warning + annotations: + summary: "BRIN hit ratio low" + description: "BRIN hit ratio dropped below 60% for 10 minutes" + + # 5) Exporter down + - alert: PostgresExporterDown + expr: up{job="postgres_exporter"} == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "postgres_exporter down" + description: "Exporter not available for 2 minutes" diff --git a/AgCloud/RelDB/graphs/postgres-queries.yml b/AgCloud/RelDB/graphs/postgres-queries.yml new file mode 100644 index 000000000..4d3b58440 --- /dev/null +++ b/AgCloud/RelDB/graphs/postgres-queries.yml @@ -0,0 +1,283 @@ +# Save as postgres-queries.yml +# Custom queries for postgres_exporter to expose WAL, replication lag, and BRIN stats. + +pg_wal_stats: + query: | + SELECT wal_records, wal_fpi, wal_bytes + FROM pg_stat_wal + metrics: + - wal_records: + usage: COUNTER + description: "Number of WAL records since startup" + - wal_fpi: + usage: COUNTER + description: "Number of WAL full page images since startup" + - wal_bytes: + usage: COUNTER + description: "Total WAL bytes generated since startup" + +# On PRIMARY: lag in bytes per replica +pg_replication_lag_bytes_primary: + query: | + SELECT + application_name, + GREATEST(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn), 0) AS bytes_lag + FROM pg_stat_replication + metrics: + - application_name: + usage: LABEL + description: "Replica application_name as seen from primary" + - bytes_lag: + usage: GAUGE + description: "Replication lag in bytes (primary view)" + +# On STANDBY: lag in seconds (returns a row only on standby) +pg_replication_replay_lag_seconds_standby: + query: | + SELECT + EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp())) AS replay_lag_seconds + WHERE pg_is_in_recovery() + metrics: + - replay_lag_seconds: + usage: GAUGE + description: "Replication replay lag in seconds (standby only)" + +# BRIN index I/O and hit ratio per BRIN index +pg_brin_index_io: + query: | + SELECT + s.schemaname, + s.relname AS table_name, + s.indexrelname AS index_name, + s.idx_blks_read, + s.idx_blks_hit, + CASE + WHEN (s.idx_blks_read + s.idx_blks_hit) > 0 + THEN (s.idx_blks_hit::float / (s.idx_blks_read + s.idx_blks_hit)) + ELSE NULL + END AS brin_hit_ratio + FROM pg_statio_user_indexes s + JOIN pg_class i ON i.oid = s.indexrelid + JOIN pg_am am ON am.oid = i.relam + WHERE am.amname = 'brin' + metrics: + - schemaname: + usage: LABEL + description: "Schema" + - table_name: + usage: LABEL + description: "Table" + - index_name: + usage: LABEL + description: "BRIN index name" + - idx_blks_read: + usage: COUNTER + description: "Index blocks read from disk for this BRIN index" + - idx_blks_hit: + usage: COUNTER + description: "Index blocks found in cache for this BRIN index" + - brin_hit_ratio: + usage: GAUGE + description: "Cache hit ratio for this BRIN index (0..1)" + + +# Active connections +pg_active_connections: + query: | + SELECT state, COUNT(*) as count + FROM pg_stat_activity + GROUP BY state + metrics: + - state: + usage: LABEL + description: "Connection state (active, idle, etc.)" + - count: + usage: GAUGE + description: "Number of connections per state" + +# Cache hit ratio per table +pg_table_cache_hit_ratio: + query: | + SELECT + relname as table, + heap_blks_read, + heap_blks_hit, + CASE + WHEN (heap_blks_read + heap_blks_hit) > 0 + THEN (heap_blks_hit::float / (heap_blks_read + heap_blks_hit)) + ELSE NULL + END as cache_hit_ratio + FROM pg_statio_user_tables + metrics: + - table: + usage: LABEL + description: "Table name" + - heap_blks_read: + usage: COUNTER + description: "Disk blocks read" + - heap_blks_hit: + usage: COUNTER + description: "Cache hits" + - cache_hit_ratio: + usage: GAUGE + description: "Cache hit ratio (0..1)" + +# Database size +pg_database_size: + query: | + SELECT datname, pg_database_size(datname) as size_bytes + FROM pg_database + metrics: + - datname: + usage: LABEL + description: "Database name" + - size_bytes: + usage: GAUGE + description: "Database size in bytes" + +# ============================================ +# LEAF DISEASES METRICS +# ============================================ + +# Total leaf reports +leaf_reports_total: + query: "SELECT COUNT(*)::float as total FROM public.leaf_reports" + master: true + metrics: + - total: + usage: "GAUGE" + description: "Total number of leaf disease reports" + +# Reports by disease type +leaf_reports_by_disease: + query: | + SELECT + COALESCE(ldt.name, 'Unknown') as disease_name, + lr.leaf_disease_type_id::text as disease_id, + COUNT(*)::float as count + FROM public.leaf_reports lr + LEFT JOIN public.leaf_disease_types ldt ON lr.leaf_disease_type_id = ldt.id + WHERE lr.sick = true + GROUP BY lr.leaf_disease_type_id, ldt.name + master: true + metrics: + - disease_name: + usage: "LABEL" + description: "Disease name" + - disease_id: + usage: "LABEL" + description: "Disease type ID" + - count: + usage: "GAUGE" + description: "Number of reports per disease" + +# Reports by device +leaf_reports_by_device: + query: | + SELECT + device_id::text as device_id, + COUNT(*)::float as total_reports, + SUM(CASE WHEN sick THEN 1 ELSE 0 END)::float as sick_reports + FROM public.leaf_reports + GROUP BY device_id + master: true + metrics: + - device_id: + usage: "LABEL" + description: "Device ID" + - total_reports: + usage: "GAUGE" + description: "Total reports per device" + - sick_reports: + usage: "GAUGE" + description: "Sick reports per device" + +# Daily disease progression (time series) +leaf_disease_daily_progression: + query: | + SELECT + COALESCE(ldt.name, 'Unknown') as disease_name, + lr.leaf_disease_type_id::text as disease_id, + TO_CHAR(DATE_TRUNC('day', lr.ts), 'YYYY-MM-DD') as report_date, + EXTRACT(EPOCH FROM DATE_TRUNC('day', lr.ts))::float as date_timestamp, + COUNT(*)::float as sick_count + FROM public.leaf_reports lr + LEFT JOIN public.leaf_disease_types ldt ON lr.leaf_disease_type_id = ldt.id + WHERE lr.sick = true + AND lr.ts > NOW() - INTERVAL '90 days' + GROUP BY lr.leaf_disease_type_id, ldt.name, DATE_TRUNC('day', lr.ts) + ORDER BY date_timestamp DESC + master: true + metrics: + - disease_name: + usage: "LABEL" + description: "Disease name" + - disease_id: + usage: "LABEL" + description: "Disease type ID" + - report_date: + usage: "LABEL" + description: "Report date (YYYY-MM-DD)" + - date_timestamp: + usage: "GAUGE" + description: "Date timestamp for X axis" + - sick_count: + usage: "GAUGE" + description: "Number of sick reports per day" + +# Hourly disease detection (last 7 days) +leaf_disease_hourly_detection: + query: | + SELECT + COALESCE(ldt.name, 'Unknown') as disease_name, + lr.leaf_disease_type_id::text as disease_id, + EXTRACT(EPOCH FROM DATE_TRUNC('hour', lr.ts))::float as hour_timestamp, + COUNT(*)::float as count + FROM public.leaf_reports lr + LEFT JOIN public.leaf_disease_types ldt ON lr.leaf_disease_type_id = ldt.id + WHERE lr.sick = true + AND lr.ts > NOW() - INTERVAL '7 days' + GROUP BY lr.leaf_disease_type_id, ldt.name, DATE_TRUNC('hour', lr.ts) + ORDER BY hour_timestamp DESC + master: true + metrics: + - disease_name: + usage: "LABEL" + description: "Disease name" + - disease_id: + usage: "LABEL" + description: "Disease type ID" + - hour_timestamp: + usage: "GAUGE" + description: "Hour timestamp" + - count: + usage: "GAUGE" + description: "Detections per hour" + +# Disease severity by device (percentage) +leaf_disease_severity_by_device: + query: | + SELECT + COALESCE(ldt.name, 'Unknown') as disease_name, + lr.leaf_disease_type_id::text as disease_id, + lr.device_id::text as device_id, + (SUM(CASE WHEN lr.sick THEN 1 ELSE 0 END)::float / COUNT(*)::float * 100) as sick_percentage + FROM public.leaf_reports lr + LEFT JOIN public.leaf_disease_types ldt ON lr.leaf_disease_type_id = ldt.id + WHERE lr.ts > NOW() - INTERVAL '30 days' + GROUP BY lr.leaf_disease_type_id, ldt.name, lr.device_id + HAVING COUNT(*) > 5 + master: true + metrics: + - disease_name: + usage: "LABEL" + description: "Disease name" + - disease_id: + usage: "LABEL" + description: "Disease type ID" + - device_id: + usage: "LABEL" + description: "Device ID" + - sick_percentage: + usage: "GAUGE" + description: "Percentage of sick reports (0-100)" diff --git a/AgCloud/RelDB/graphs/prometheus-recording.rules.yml b/AgCloud/RelDB/graphs/prometheus-recording.rules.yml new file mode 100644 index 000000000..dab962a23 --- /dev/null +++ b/AgCloud/RelDB/graphs/prometheus-recording.rules.yml @@ -0,0 +1,25 @@ +# Save as prometheus-recording.rules.yml +groups: +- name: postgres_recording + interval: 30s + rules: + # WAL throughput (bytes per second) + - record: pg:wal_bytes:rate_per_s + expr: rate(pg_wal_stats_wal_bytes[5m]) + + # Replication lag in seconds (standbys) + - record: pg:replication_lag_seconds + expr: max(replay_lag_seconds) + + # Replication lag in bytes (primary perspective per replica) + - record: pg:replication_lag_bytes + expr: max(bytes_lag) by (application_name) + + # BRIN hit ratio over time by index (avoid divide-by-zero) + - record: pg:brin_hit_ratio + expr: | + sum by (index_name, schemaname, table_name)(rate(idx_blks_hit[10m])) + / clamp_min( + sum by (index_name, schemaname, table_name)(rate(idx_blks_hit[10m]) + rate(idx_blks_read[10m])), + 1 + ) diff --git a/AgCloud/RelDB/graphs/prometheus.yml b/AgCloud/RelDB/graphs/prometheus.yml new file mode 100644 index 000000000..7ebb44c3c --- /dev/null +++ b/AgCloud/RelDB/graphs/prometheus.yml @@ -0,0 +1,21 @@ +global: + scrape_interval: 30s + +rule_files: + - /etc/prometheus/prometheus-recording.rules.yml + - /etc/prometheus/postgres-alerts.yml + +alerting: + alertmanagers: + - static_configs: + - targets: ['alertmanager:9093'] + +scrape_configs: + - job_name: 'postgres_exporter' + static_configs: + - targets: ['postgres_exporter:9187'] + + - job_name: 'rover_ingest' + scrape_interval: 10s + static_configs: + - targets: ['rover_ingest:9109'] diff --git a/AgCloud/RelDB/initdb/01-enable-pgvector.sql b/AgCloud/RelDB/initdb/01-enable-pgvector.sql new file mode 100644 index 000000000..842be4fc3 --- /dev/null +++ b/AgCloud/RelDB/initdb/01-enable-pgvector.sql @@ -0,0 +1,2 @@ +-- Create pgvector extension if it does not already exist +CREATE EXTENSION IF NOT EXISTS vector; diff --git a/AgCloud/RelDB/initdb/03_partitions.sql b/AgCloud/RelDB/initdb/03_partitions.sql new file mode 100644 index 000000000..f00b8fcff --- /dev/null +++ b/AgCloud/RelDB/initdb/03_partitions.sql @@ -0,0 +1,156 @@ +-- partition_maintenance.sql +CREATE SCHEMA IF NOT EXISTS admin; + +-- Helper: create [from,to) +CREATE OR REPLACE FUNCTION admin.create_range_partition( + parent_table text, + part_name text, + from_ts timestamptz, + to_ts timestamptz +) RETURNS void +LANGUAGE plpgsql AS $func$ +BEGIN + EXECUTE format( + 'CREATE TABLE IF NOT EXISTS %I PARTITION OF %s FOR VALUES FROM (%L) TO (%L)', + part_name, parent_table, from_ts, to_ts + ); +END; +$func$; + +-- Per-partition indexes (telemetry_new) +CREATE OR REPLACE FUNCTION admin.ensure_telemetry_partition_indexes(part_name text) +RETURNS void LANGUAGE plpgsql AS $func$ +BEGIN + EXECUTE format('CREATE INDEX IF NOT EXISTS %I_ts_brin ON %I USING BRIN (ts);', part_name, part_name); + EXECUTE format('CREATE INDEX IF NOT EXISTS %I_mission_ts ON %I (mission_id, ts);', part_name, part_name); + EXECUTE format('CREATE INDEX IF NOT EXISTS %I_geom_gist ON %I USING GIST (geom);', part_name, part_name); +END; +$func$; + +-- Per-partition indexes (event_logs) +CREATE OR REPLACE FUNCTION admin.ensure_event_logs_partition_indexes(part_name text) +RETURNS void LANGUAGE plpgsql AS $func$ +BEGIN + EXECUTE format('CREATE INDEX IF NOT EXISTS %I_ts_brin ON %I USING BRIN (ts);', part_name, part_name); + EXECUTE format('CREATE INDEX IF NOT EXISTS %I_level ON %I (level);', part_name, part_name); + EXECUTE format('CREATE INDEX IF NOT EXISTS %I_source ON %I (source);', part_name, part_name); +END; +$func$; + +-- Daily partitions (telemetry_new) +CREATE OR REPLACE FUNCTION admin.make_daily_partitions_telemetry( + start_day date, + num_days integer +) RETURNS void +LANGUAGE plpgsql AS $func$ +DECLARE + i int; d_from timestamptz; d_to timestamptz; pname text; +BEGIN + FOR i IN 0..num_days-1 LOOP + d_from := (start_day + i)::timestamptz; + d_to := (start_day + i + 1)::timestamptz; + pname := 'telemetry_new_p' || to_char(d_from,'YYYYMMDD'); + PERFORM admin.create_range_partition('public.telemetry_new', pname, d_from, d_to); + PERFORM admin.ensure_telemetry_partition_indexes(pname); + END LOOP; +END; +$func$; + +-- Weekly partitions (event_logs) +CREATE OR REPLACE FUNCTION admin.make_weekly_partitions_event_logs( + week_start date, + num_weeks integer +) RETURNS void +LANGUAGE plpgsql AS $func$ +DECLARE + i int; w_from timestamptz; w_to timestamptz; pname text; aligned date; +BEGIN + aligned := date_trunc('week', week_start::timestamptz)::date; + FOR i IN 0..num_weeks-1 LOOP + w_from := (aligned + (i*7))::timestamptz; + w_to := (aligned + (i*7+7))::timestamptz; + pname := 'event_logs_p' || to_char(w_from,'IYYYIW'); + PERFORM admin.create_range_partition('public.event_logs', pname, w_from, w_to); + PERFORM admin.ensure_event_logs_partition_indexes(pname); + END LOOP; +END; +$func$; + +-- Driver: next week +CREATE OR REPLACE FUNCTION admin.ensure_next_week_partitions() RETURNS void +LANGUAGE plpgsql AS $func$ +DECLARE + next_monday date := date_trunc('week', now())::date + 7; +BEGIN + PERFORM admin.make_daily_partitions_telemetry(next_monday, 7); + PERFORM admin.make_weekly_partitions_event_logs(next_monday, 1); +END; +$func$; + +-- Retention: drop partitions whose UPPER bound < now()-keep +CREATE OR REPLACE FUNCTION admin.drop_old_partitions( + parent_table regclass, + keep_interval interval +) RETURNS int +LANGUAGE plpgsql AS $func$ +DECLARE + r record; + to_ts timestamptz; + dropped int := 0; + bound_text text; +BEGIN + FOR r IN + SELECT c.oid, + format('%s.%I', n.nspname, c.relname) AS partname, + pg_get_expr(c.relpartbound, c.oid, true) AS bound + FROM pg_inherits i + JOIN pg_class c ON c.oid = i.inhrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE i.inhparent = parent_table + LOOP + bound_text := r.bound; + + BEGIN + -- Example bound text: FOR VALUES FROM ('2025-08-01 ...') TO ('2025-08-02 ...') + SELECT substring(bound_text from 'TO \(''([^'']+)''\)')::timestamptz INTO to_ts; + + IF to_ts IS NOT NULL AND to_ts < (now() AT TIME ZONE 'UTC') - keep_interval THEN + EXECUTE format('DROP TABLE IF EXISTS %s;', r.partname); + dropped := dropped + 1; + END IF; + EXCEPTION WHEN others THEN + CONTINUE; -- skip DEFAULT/unexpected + END; + END LOOP; + + RETURN dropped; +END; +$func$; + +-- One-year retention wrapper +CREATE OR REPLACE FUNCTION admin.apply_yearly_retention() +RETURNS void LANGUAGE plpgsql AS $func$ +BEGIN + PERFORM admin.drop_old_partitions('public.telemetry_new', interval '1 year'); + PERFORM admin.drop_old_partitions('public.event_logs', interval '1 year'); +END; +$func$; + +-- ========================= +-- pg_cron scheduling (optional but recommended) +-- ========================= +CREATE EXTENSION IF NOT EXISTS pg_cron; + +-- Every Sunday 03:00 – create next week's partitions (+ next 7 days) +SELECT cron.schedule( + 'partitions-next-week', + '0 3 * * 0', + $$SELECT admin.ensure_next_week_partitions();$$ +); + +-- 1st of every month 03:10 – retention (keep 1 year) +SELECT cron.schedule( + 'partitions-monthly-retention', + '10 3 1 * *', + $$SELECT admin.apply_yearly_retention();$$ +); diff --git a/AgCloud/RelDB/initdb/04_cron.sql b/AgCloud/RelDB/initdb/04_cron.sql new file mode 100644 index 000000000..3b01b916c --- /dev/null +++ b/AgCloud/RelDB/initdb/04_cron.sql @@ -0,0 +1,31 @@ +-- 04_cron.sql +-- Scheduling automatic tasks with pg_cron +-- Runs after 03_partitions.sql (so that all functions already exist) +CREATE EXTENSION IF NOT EXISTS pg_cron; + +-- 1) Every Sunday at 03:00 – create partitions for the coming week +SELECT cron.schedule( + 'partitions-next-week', + '0 3 * * 0', + $$SELECT admin.ensure_next_week_partitions();$$ +); + +-- 2) On the 1st of every month at 03:10 – retention for the last year +SELECT cron.schedule( + 'partitions-monthly-retention', + '10 3 1 * *', + $$SELECT admin.apply_yearly_retention();$$ +); + +-- 3) Every 10 minutes — re-homing from the default partition (DEFAULT) to the correct partitions +SELECT cron.schedule( + 'rehoming-telemetry-default', + '*/10 * * * *', + $$SELECT admin.rehome_telemetry_default();$$ +); + +SELECT cron.schedule( + 'rehoming-event-logs-default', + '*/10 * * * *', + $$SELECT admin.rehome_event_logs_default();$$ +); \ No newline at end of file diff --git a/AgCloud/RelDB/initdb/05_rover_images.sql b/AgCloud/RelDB/initdb/05_rover_images.sql new file mode 100644 index 000000000..93ca57da9 --- /dev/null +++ b/AgCloud/RelDB/initdb/05_rover_images.sql @@ -0,0 +1,39 @@ +-- Schema + table for rover still-image indexing + +CREATE SCHEMA IF NOT EXISTS rover AUTHORIZATION missions_user; + +CREATE TABLE IF NOT EXISTS rover.images ( + image_id text PRIMARY KEY, -- unique image identifier + device_id text NOT NULL, -- rover/camera id + captured_at timestamptz NOT NULL, -- UTC capture time + + lat double precision NOT NULL CHECK (lat >= -90 AND lat <= 90), + lon double precision NOT NULL CHECK (lon >= -180 AND lon <= 180), + + heading_deg double precision, -- [0,360) + pitch_deg double precision, + roll_deg double precision, + alt_m double precision, -- camera height (meters) + fov_deg double precision, + gps_accuracy_m double precision, + temp_c double precision, -- optional ambient temp + + s3_key text NOT NULL, -- object key in MinIO/S3 + mime_type text, + size_bytes bigint, + sha256 text, + exif_present boolean, + firmware text, + capture_seq bigint, + + meta_src text NOT NULL, -- 'manifest' | 'telemetry' | 'exif_fallback' + schema_ver int NOT NULL DEFAULT 1, + source_ts timestamptz, + trace_id text, + + ingested_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_images_device_time ON rover.images (device_id, captured_at); +CREATE INDEX IF NOT EXISTS idx_images_captured_at ON rover.images (captured_at); +CREATE UNIQUE INDEX IF NOT EXISTS uq_images_sha256 ON rover.images (sha256) WHERE sha256 IS NOT NULL; diff --git a/AgCloud/RelDB/pgvector.Dockerfile b/AgCloud/RelDB/pgvector.Dockerfile new file mode 100644 index 000000000..6123c2b54 --- /dev/null +++ b/AgCloud/RelDB/pgvector.Dockerfile @@ -0,0 +1,23 @@ +# Extend Bitnami PostgreSQL 16 image with pgvector extension (built from source) +FROM bitnami/postgresql:16 + +# Switch to root for build tools installation +USER root + +# Ensure pg_config points to Bitnami's PostgreSQL and is on PATH +ENV PG_CONFIG=/opt/bitnami/postgresql/bin/pg_config +ENV PATH="/opt/bitnami/postgresql/bin:${PATH}" + +# Build pgvector from source against Bitnami PostgreSQL +RUN apt-get update \ + && apt-get install -y --no-install-recommends build-essential git ca-certificates \ + && git clone --depth 1 https://github.com/pgvector/pgvector.git /tmp/pgvector \ + && make -C /tmp/pgvector \ + && make -C /tmp/pgvector install \ + && rm -rf /var/lib/apt/lists/* /tmp/pgvector + +# Back to non-root user as expected by Bitnami +USER 1001 + +# Init script: create pgvector extension on first DB initialization (new data dir) +COPY initdb/01-enable-pgvector.sql /docker-entrypoint-initdb.d/01-enable-pgvector.sql diff --git a/AgCloud/RelDB/pitr/backup.py b/AgCloud/RelDB/pitr/backup.py new file mode 100644 index 000000000..fa052cb82 --- /dev/null +++ b/AgCloud/RelDB/pitr/backup.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +""" +backup.py — PostgreSQL base backup with retention. +Waits until Postgres is ready and out of recovery, then runs pg_basebackup. +""" + +import os, subprocess, time +from datetime import datetime + +BACKUP_DIR = os.getenv("BACKUP_DIR", "/var/lib/postgresql/backups") +RETENTION = int(os.getenv("RETENTION", "7")) +PGUSER = os.getenv("POSTGRES_USER", "missions_user") +PGPASSWORD = os.getenv("POSTGRES_PASSWORD", "pg123") +PGHOST = os.getenv("PGHOST", "127.0.0.1") +PGPORT = os.getenv("PGPORT", "5432") +PGDATABASE = os.getenv("POSTGRES_DB", "missions_db") +os.environ["PGPASSWORD"] = PGPASSWORD + +def wait_for_postgres(): + while True: + try: + subprocess.check_call( + ["pg_isready", "-h", PGHOST, "-p", PGPORT, "-U", PGUSER, "-d", PGDATABASE], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + print("[BACKUP] PostgreSQL is ready ✅") + break + except subprocess.CalledProcessError: + print("[BACKUP] Waiting for PostgreSQL...") + time.sleep(2) + +def wait_until_not_in_recovery(): + while True: + try: + result = subprocess.check_output([ + "psql", "-h", PGHOST, "-p", PGPORT, "-U", PGUSER, "-d", PGDATABASE, + "-t", "-c", "SELECT pg_is_in_recovery();" + ], text=True).strip() + if result == "f": + print("[BACKUP] Database is out of recovery ✅") + break + else: + print("[BACKUP] Still in recovery, waiting...") + except subprocess.CalledProcessError: + print("[BACKUP] Waiting for PostgreSQL to accept queries...") + time.sleep(3) + +def run_backup(): + wait_for_postgres() + wait_until_not_in_recovery() + + os.makedirs(BACKUP_DIR, exist_ok=True) + backup_name = "base_" + datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = os.path.join(BACKUP_DIR, backup_name) + + print(f"[BACKUP] Starting base backup → {backup_path}") + subprocess.check_call([ + "pg_basebackup", "-h", PGHOST, "-p", PGPORT, "-U", PGUSER, + "-D", backup_path, "-Fp", "-Xs", "-P", "-R", "-v" + ]) + + # Retention + backups = sorted([b for b in os.listdir(BACKUP_DIR) if b.startswith("base_")], reverse=True) + for i, b in enumerate(backups): + if i >= RETENTION: + subprocess.call(["rm", "-rf", os.path.join(BACKUP_DIR, b)]) + print(f"[BACKUP] Removed old backup {b}") + +if __name__ == "__main__": + run_backup() diff --git a/AgCloud/RelDB/pitr/cron_backup.py b/AgCloud/RelDB/pitr/cron_backup.py new file mode 100644 index 000000000..ba419ba54 --- /dev/null +++ b/AgCloud/RelDB/pitr/cron_backup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +""" +cron_backup.py — Runs backup via cron, logs to BACKUP_DIR/cron.log +""" + +import os, subprocess, datetime + +BACKUP_DIR = os.getenv("BACKUP_DIR", "/var/lib/postgresql/backups") +LOGFILE = os.path.join(BACKUP_DIR, "cron.log") + +with open(LOGFILE, "a") as f: + f.write(f"[CRON-BACKUP] Starting backup at {datetime.datetime.now()}\n") + +try: + subprocess.check_call(["python3", "/usr/local/bin/backup.py"], + stdout=open(LOGFILE, "a"), + stderr=open(LOGFILE, "a")) + with open(LOGFILE, "a") as f: + f.write(f"[CRON-BACKUP] Finished at {datetime.datetime.now()}\n") +except Exception as e: + with open(LOGFILE, "a") as f: + f.write(f"[CRON-BACKUP] ERROR: {e}\n") diff --git a/AgCloud/RelDB/pitr/recover.py b/AgCloud/RelDB/pitr/recover.py new file mode 100644 index 000000000..928c3faaf --- /dev/null +++ b/AgCloud/RelDB/pitr/recover.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +""" +recover.py — PostgreSQL PITR recovery (Safe Mode) +Always prepares recovery without crashing, even if WAL or backups are missing. +Does NOT start Postgres automatically — restart container manually. + +Usage: + recover.py latest + recover.py minutes + recover.py time "YYYY-MM-DDTHH:MM:SS+03:00" +""" + +import os, sys, subprocess +from datetime import datetime, timedelta + +# ==== ENV variables (provided by docker-compose) ==== +PGDATA = os.getenv("PGDATA", "/var/lib/postgresql/data") +BACKUPS_DIR = os.getenv("BACKUP_DIR", "/var/lib/postgresql/backups") +WAL_DIR = os.getenv("WAL_DIR", "/var/lib/postgresql/wal_archive") + +def run(cmd, check=True): + """Run a shell command and print it.""" + print(f"[RECOVERY] $ {' '.join(cmd)}") + return subprocess.run(cmd, shell=False, check=check) + +def parse_backup_ts(name): + """Extract datetime from backup folder name base_YYYYMMDD_HHMMSS""" + try: + return datetime.strptime(name.replace("base_", ""), "%Y%m%d_%H%M%S") + except ValueError: + return None + +def choose_backup_for_time(target_time): + """ + Choose the latest base backup that is <= target_time. + If none exist, fallback to the latest available. + """ + candidates = [] + for b in os.listdir(BACKUPS_DIR): + if b.startswith("base_"): + ts = parse_backup_ts(b) + if ts and ts <= target_time: + candidates.append((ts, b)) + if not candidates: + print(f"[RECOVERY] WARNING: No backup found before {target_time}, using latest available.") + backups = sorted([b for b in os.listdir(BACKUPS_DIR) if b.startswith("base_")]) + if not backups: + return None + return os.path.join(BACKUPS_DIR, backups[-1]) + chosen = max(candidates, key=lambda x: x[0])[1] + print(f"[RECOVERY] Chosen base backup: {chosen}") + return os.path.join(BACKUPS_DIR, chosen) + +def write_recovery_conf(recovery_target=None): + """ + Write recovery parameters into postgresql.auto.conf + Adds recovery.signal file to trigger PITR mode. + """ + conf_file = os.path.join(PGDATA, "postgresql.auto.conf") + with open(conf_file, "a") as f: + # Allow recovery to continue even if a WAL file is missing + f.write(f"\nrestore_command = 'cp {WAL_DIR}/%f %p || true'\n") + f.write("recovery_target_action = 'promote'\n") + if recovery_target: + f.write(f"recovery_target_time = '{recovery_target}'\n") + # Create recovery.signal + open(os.path.join(PGDATA, "recovery.signal"), "w").close() + +def recover(mode, arg=None): + """ + Main recovery logic: + - Pick correct backup depending on mode (latest, minutes, time). + - Restore base backup into PGDATA. + - Configure recovery. + - Print warnings if WAL is incomplete. + """ + # 1. Pick backup + if mode == "time": + try: + target_time = datetime.fromisoformat(arg) + except Exception: + print("[RECOVERY] ERROR: Invalid time format. Use YYYY-MM-DDTHH:MM:SS+TZ") + sys.exit(1) + backup = choose_backup_for_time(target_time) + recovery_target = arg + elif mode == "minutes": + t = datetime.now() - timedelta(minutes=int(arg)) + backup = choose_backup_for_time(t) + recovery_target = t.isoformat() + else: # latest + backups = sorted([b for b in os.listdir(BACKUPS_DIR) if b.startswith("base_")]) + if not backups: + print("[RECOVERY] WARNING: No backups at all! DB may start empty.") + backup = None + else: + backup = os.path.join(BACKUPS_DIR, backups[-1]) + recovery_target = None + + # 2. Restore base backup + if backup: + print(f"[RECOVERY] Using backup {backup}") + run(["rm", "-rf", f"{PGDATA}/*"], check=False) + run(["cp", "-R", f"{backup}/.", PGDATA]) + else: + print("[RECOVERY] No backup restored (DB will try to start with existing PGDATA)") + + # 3. Write recovery config + write_recovery_conf(recovery_target) + + # 4. Validate WAL presence + wals = [f for f in os.listdir(WAL_DIR) if f.endswith(".history") or f.isalnum()] + if not wals: + print("[RECOVERY] WARNING: No WAL files found in archive — recovery may promote immediately.") + + # 5. Finish + print("\n[RECOVERY] ✅ Recovery setup complete (Safe Mode)") + print("Restart the container:") + print(" docker restart db\n") + print("Check recovery status:") + print(" docker exec -it db psql -U missions_user -d missions_db -c \"SELECT pg_is_in_recovery();\"") + print("When finished, run manual backup:") + print(" docker exec -u postgres -it db python3 /usr/local/bin/backup.py\n") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: recover.py latest|time|minutes [value]") + sys.exit(1) + mode = sys.argv[1] + if mode == "latest": + recover("latest") + elif mode == "time": + if len(sys.argv) < 3: + print("Usage: recover.py time 'YYYY-MM-DDTHH:MM:SS+TZ'") + sys.exit(1) + recover("time", sys.argv[2]) + elif mode == "minutes": + if len(sys.argv) < 3: + print("Usage: recover.py minutes ") + sys.exit(1) + recover("minutes", sys.argv[2]) + else: + print("Usage: recover.py latest|time|minutes [value]") + sys.exit(1) diff --git a/AgCloud/RelDB/values.yaml b/AgCloud/RelDB/values.yaml new file mode 100644 index 000000000..7fec2b5ff --- /dev/null +++ b/AgCloud/RelDB/values.yaml @@ -0,0 +1,9 @@ +image: + repository: agcloud/postgresql-pgvector + tag: "16" + +auth: + existingSecret: pg-auth + username: "missions_user" + database: "missions_db" + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/.gitattributes b/AgCloud/airflow_bundle/leaf-pipeline/.gitattributes new file mode 100644 index 000000000..ada57005e --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/.gitattributes @@ -0,0 +1,5 @@ +projects/leaf-counting/weights/*.pt filter=lfs diff=lfs merge=lfs -text +projects/leaf-counting/weights/*.pth filter=lfs diff=lfs merge=lfs -text +projects/leaf-counting/weights/*.safetensors filter=lfs diff=lfs merge=lfs -text + +projects/Detection_Jobs/**/models/* filter=lfs diff=lfs merge=lfs -text diff --git a/AgCloud/airflow_bundle/leaf-pipeline/.gitignore b/AgCloud/airflow_bundle/leaf-pipeline/.gitignore new file mode 100644 index 000000000..b5c271165 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/.gitignore @@ -0,0 +1,90 @@ +# === OS / Editors === +.DS_Store +*.swp +*.swo +.idea/ +.vscode/ +*.code-workspace + +# === Python === +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.egg-info/ +.eggs/ +.build/ +build/ +dist/ +.mypy_cache/ +.pytest_cache/ +.coverage +.coverage.* +.cache/ +.ipynb_checkpoints/ + +# Virtual envs +.venv/ +venv/ +env/ + +# === Docker / Compose === +docker.env +.env +.env.* +.env.local +.envrc + +# === Airflow === +airflow/logs/ +airflow/airflow.db +airflow/airflow.db-journal +airflow/*.pid +airflow/*webserver*.log +airflow/*webserver*.out +airflow/*webserver*.err +airflow/*scheduler*.log +airflow/*scheduler*.out +airflow/*scheduler*.err +airflow/dags/*.bak.* +airflow/staging/ + +# === Projects: leaf-counting === +projects/leaf-counting/out_detect/ +projects/leaf-counting/out_crops/ +projects/leaf-counting/out_pwb/ +projects/leaf-counting/runs_local/ +projects/leaf-counting/weights/ +projects/leaf-counting/.venv/ +projects/leaf-counting/staging/ + +# === Projects: Detection_Jobs / disease-monitor === +projects/Detection_Jobs/**/__pycache__/ +projects/Detection_Jobs/**/.mypy_cache/ +projects/Detection_Jobs/**/.pytest_cache/ +projects/disease-monitor/**/__pycache__/ +projects/disease-monitor/**/.mypy_cache/ +projects/disease-monitor/**/.pytest_cache/ + +# === Secrets / Certs === +*.key +*.pem +*.crt +*.p12 +*credentials*.json +*service_account*.json +*.secrets.* +.secrets/ +secrets/ +projects/Detection_Jobs/.git.backup-*.tar.gz +airflow/dags/leaf-counting/runs_local/ +airflow/dags/leaf-counting/demo_images/ +airflow/dags/leaf-counting/out_*/ +projects/Detection_Jobs/**/models/ +projects/disease-monitor/disease-monitor/alerts.db +projects/**/.git.backup-*.tar.gz +.gitignore.bak* +airflow/dags/leaf-counting/ + + +dags_leaf_counting_backup.tgz \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/Dockerfile b/AgCloud/airflow_bundle/leaf-pipeline/Dockerfile new file mode 100644 index 000000000..53af135cf --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/Dockerfile @@ -0,0 +1,42 @@ + +FROM mcr.microsoft.com/devcontainers/python:1-3.10-bullseye +# RUN apt-get update && apt-get install -y --no-install-recommends \ +# libgl1 libglib2.0-0 ffmpeg curl ca-certificates && \ +# rm -rf /var/lib/apt/lists/* +# RUN apt-get update && apt-get install -y --no-install-recommends \ +# libgl1 libglib2.0-0 ffmpeg curl ca-certificates \ +# util-linux procps \ +# && rm -rf /var/lib/apt/lists/* + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 libglib2.0-0 ffmpeg curl ca-certificates \ + util-linux procps \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m pip install --no-cache-dir --upgrade pip wheel setuptools + +ENV AIRFLOW_VERSION=2.9.3 +ENV PYTHON_VERSION=3.10 +ENV CONSTRAINT_URL=https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt +RUN pip install --no-cache-dir "apache-airflow==${AIRFLOW_VERSION}" --constraint "${CONSTRAINT_URL}" + +RUN pip install --no-cache-dir \ + "apache-airflow-providers-docker" \ + --constraint "${CONSTRAINT_URL}" + + +# === PyTorch CPU wheels === +RUN pip install --no-cache-dir \ + --extra-index-url https://download.pytorch.org/whl/cpu \ + torch==2.3.1+cpu torchvision==0.18.1+cpu torchaudio==2.3.1+cpu + +# === YOLO=== +RUN pip install --no-cache-dir \ + numpy==1.26.4 opencv-python-headless==4.9.0.80 ultralytics==8.2.10 \ + onnx==1.16.1 onnxruntime==1.18.1 \ + boto3 minio awscli requests tqdm + +#root +RUN useradd -ms /bin/bash airflow +USER airflow +WORKDIR /opt/airflow diff --git a/AgCloud/airflow_bundle/leaf-pipeline/README b/AgCloud/airflow_bundle/leaf-pipeline/README new file mode 100644 index 000000000..2d7d28b57 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/README @@ -0,0 +1,8 @@ +docker compose --profile images up -d --build + + +projects/leaf-counting/weights/ +https://drive.google.com/drive/folders/1RIp71J7kSLdyQ7UtCdS_piiPYG5hvKOD?usp=sharing + +projects/Detection_Jobs/Detection_Jobs/models/ +https://drive.google.com/drive/folders/1u3S8nryoGgjUC_5LKTdc-qvXZmOyJT7X?usp=sharing \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/airflow/airflow.cfg b/AgCloud/airflow_bundle/leaf-pipeline/airflow/airflow.cfg new file mode 100644 index 000000000..8f1408c24 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/airflow/airflow.cfg @@ -0,0 +1,2420 @@ +[core] +# The folder where your airflow pipelines live, most likely a +# subfolder in a code repository. This path must be absolute. +# +# Variable: AIRFLOW__CORE__DAGS_FOLDER +# +dags_folder = /opt/airflow/dags + +# Hostname by providing a path to a callable, which will resolve the hostname. +# The format is "package.function". +# +# For example, default value ``airflow.utils.net.getfqdn`` means that result from patched +# version of `socket.getfqdn() `__, +# see related `CPython Issue `__. +# +# No argument should be required in the function specified. +# If using IP address as hostname is preferred, use value ``airflow.utils.net.get_host_ip_address`` +# +# Variable: AIRFLOW__CORE__HOSTNAME_CALLABLE +# +hostname_callable = airflow.utils.net.getfqdn + +# A callable to check if a python file has airflow dags defined or not and should +# return ``True`` if it has dags otherwise ``False``. +# If this is not provided, Airflow uses its own heuristic rules. +# +# The function should have the following signature +# +# .. code-block:: python +# +# def func_name(file_path: str, zip_file: zipfile.ZipFile | None = None) -> bool: ... +# +# Variable: AIRFLOW__CORE__MIGHT_CONTAIN_DAG_CALLABLE +# +might_contain_dag_callable = airflow.utils.file.might_contain_dag_via_default_heuristic + +# Default timezone in case supplied date times are naive +# can be `UTC` (default), `system`, or any `IANA ` +# timezone string (e.g. Europe/Amsterdam) +# +# Variable: AIRFLOW__CORE__DEFAULT_TIMEZONE +# +default_timezone = utc + +# The executor class that airflow should use. Choices include +# ``SequentialExecutor``, ``LocalExecutor``, ``CeleryExecutor``, +# ``KubernetesExecutor``, ``CeleryKubernetesExecutor``, ``LocalKubernetesExecutor`` or the +# full import path to the class when using a custom executor. +# +# Variable: AIRFLOW__CORE__EXECUTOR +# +executor = SequentialExecutor + +# The auth manager class that airflow should use. Full import path to the auth manager class. +# +# Variable: AIRFLOW__CORE__AUTH_MANAGER +# +auth_manager = airflow.providers.fab.auth_manager.fab_auth_manager.FabAuthManager + +# This defines the maximum number of task instances that can run concurrently per scheduler in +# Airflow, regardless of the worker count. Generally this value, multiplied by the number of +# schedulers in your cluster, is the maximum number of task instances with the running +# state in the metadata database. +# +# Variable: AIRFLOW__CORE__PARALLELISM +# +parallelism = 32 + +# The maximum number of task instances allowed to run concurrently in each DAG. To calculate +# the number of tasks that is running concurrently for a DAG, add up the number of running +# tasks for all DAG runs of the DAG. This is configurable at the DAG level with ``max_active_tasks``, +# which is defaulted as ``[core] max_active_tasks_per_dag``. +# +# An example scenario when this would be useful is when you want to stop a new dag with an early +# start date from stealing all the executor slots in a cluster. +# +# Variable: AIRFLOW__CORE__MAX_ACTIVE_TASKS_PER_DAG +# +max_active_tasks_per_dag = 16 + +# Are DAGs paused by default at creation +# +# Variable: AIRFLOW__CORE__DAGS_ARE_PAUSED_AT_CREATION +# +dags_are_paused_at_creation = True + +# The maximum number of active DAG runs per DAG. The scheduler will not create more DAG runs +# if it reaches the limit. This is configurable at the DAG level with ``max_active_runs``, +# which is defaulted as ``[core] max_active_runs_per_dag``. +# +# Variable: AIRFLOW__CORE__MAX_ACTIVE_RUNS_PER_DAG +# +max_active_runs_per_dag = 16 + +# (experimental) The maximum number of consecutive DAG failures before DAG is automatically paused. +# This is also configurable per DAG level with ``max_consecutive_failed_dag_runs``, +# which is defaulted as ``[core] max_consecutive_failed_dag_runs_per_dag``. +# If not specified, then the value is considered as 0, +# meaning that the dags are never paused out by default. +# +# Variable: AIRFLOW__CORE__MAX_CONSECUTIVE_FAILED_DAG_RUNS_PER_DAG +# +max_consecutive_failed_dag_runs_per_dag = 0 + +# The name of the method used in order to start Python processes via the multiprocessing module. +# This corresponds directly with the options available in the Python docs: +# `multiprocessing.set_start_method +# `__ +# must be one of the values returned by `multiprocessing.get_all_start_methods() +# `__. +# +# Example: mp_start_method = fork +# +# Variable: AIRFLOW__CORE__MP_START_METHOD +# +# mp_start_method = + +# Whether to load the DAG examples that ship with Airflow. It's good to +# get started, but you probably want to set this to ``False`` in a production +# environment +# +# Variable: AIRFLOW__CORE__LOAD_EXAMPLES +# +load_examples = True + +# Path to the folder containing Airflow plugins +# +# Variable: AIRFLOW__CORE__PLUGINS_FOLDER +# +plugins_folder = /opt/airflow/plugins + +# Should tasks be executed via forking of the parent process +# +# * ``False``: Execute via forking of the parent process +# * ``True``: Spawning a new python process, slower than fork, but means plugin changes picked +# up by tasks straight away +# +# Variable: AIRFLOW__CORE__EXECUTE_TASKS_NEW_PYTHON_INTERPRETER +# +execute_tasks_new_python_interpreter = False + +# Secret key to save connection passwords in the db +# +# Variable: AIRFLOW__CORE__FERNET_KEY +# +fernet_key = + +# Whether to disable pickling dags +# +# Variable: AIRFLOW__CORE__DONOT_PICKLE +# +donot_pickle = True + +# How long before timing out a python file import +# +# Variable: AIRFLOW__CORE__DAGBAG_IMPORT_TIMEOUT +# +dagbag_import_timeout = 30.0 + +# Should a traceback be shown in the UI for dagbag import errors, +# instead of just the exception message +# +# Variable: AIRFLOW__CORE__DAGBAG_IMPORT_ERROR_TRACEBACKS +# +dagbag_import_error_tracebacks = True + +# If tracebacks are shown, how many entries from the traceback should be shown +# +# Variable: AIRFLOW__CORE__DAGBAG_IMPORT_ERROR_TRACEBACK_DEPTH +# +dagbag_import_error_traceback_depth = 2 + +# How long before timing out a DagFileProcessor, which processes a dag file +# +# Variable: AIRFLOW__CORE__DAG_FILE_PROCESSOR_TIMEOUT +# +dag_file_processor_timeout = 50 + +# The class to use for running task instances in a subprocess. +# Choices include StandardTaskRunner, CgroupTaskRunner or the full import path to the class +# when using a custom task runner. +# +# Variable: AIRFLOW__CORE__TASK_RUNNER +# +task_runner = StandardTaskRunner + +# If set, tasks without a ``run_as_user`` argument will be run with this user +# Can be used to de-elevate a sudo user running Airflow when executing tasks +# +# Variable: AIRFLOW__CORE__DEFAULT_IMPERSONATION +# +default_impersonation = + +# What security module to use (for example kerberos) +# +# Variable: AIRFLOW__CORE__SECURITY +# +security = + +# Turn unit test mode on (overwrites many configuration options with test +# values at runtime) +# +# Variable: AIRFLOW__CORE__UNIT_TEST_MODE +# +unit_test_mode = False + +# Whether to enable pickling for xcom (note that this is insecure and allows for +# RCE exploits). +# +# Variable: AIRFLOW__CORE__ENABLE_XCOM_PICKLING +# +enable_xcom_pickling = False + +# What classes can be imported during deserialization. This is a multi line value. +# The individual items will be parsed as a pattern to a glob function. +# Python built-in classes (like dict) are always allowed. +# +# Variable: AIRFLOW__CORE__ALLOWED_DESERIALIZATION_CLASSES +# +allowed_deserialization_classes = airflow.* + +# What classes can be imported during deserialization. This is a multi line value. +# The individual items will be parsed as regexp patterns. +# This is a secondary option to ``[core] allowed_deserialization_classes``. +# +# Variable: AIRFLOW__CORE__ALLOWED_DESERIALIZATION_CLASSES_REGEXP +# +allowed_deserialization_classes_regexp = + +# When a task is killed forcefully, this is the amount of time in seconds that +# it has to cleanup after it is sent a SIGTERM, before it is SIGKILLED +# +# Variable: AIRFLOW__CORE__KILLED_TASK_CLEANUP_TIME +# +killed_task_cleanup_time = 60 + +# Whether to override params with dag_run.conf. If you pass some key-value pairs +# through ``airflow dags backfill -c`` or +# ``airflow dags trigger -c``, the key-value pairs will override the existing ones in params. +# +# Variable: AIRFLOW__CORE__DAG_RUN_CONF_OVERRIDES_PARAMS +# +dag_run_conf_overrides_params = True + +# If enabled, Airflow will only scan files containing both ``DAG`` and ``airflow`` (case-insensitive). +# +# Variable: AIRFLOW__CORE__DAG_DISCOVERY_SAFE_MODE +# +dag_discovery_safe_mode = True + +# The pattern syntax used in the +# `.airflowignore +# `__ +# files in the DAG directories. Valid values are ``regexp`` or ``glob``. +# +# Variable: AIRFLOW__CORE__DAG_IGNORE_FILE_SYNTAX +# +dag_ignore_file_syntax = regexp + +# The number of retries each task is going to have by default. Can be overridden at dag or task level. +# +# Variable: AIRFLOW__CORE__DEFAULT_TASK_RETRIES +# +default_task_retries = 0 + +# The number of seconds each task is going to wait by default between retries. Can be overridden at +# dag or task level. +# +# Variable: AIRFLOW__CORE__DEFAULT_TASK_RETRY_DELAY +# +default_task_retry_delay = 300 + +# The maximum delay (in seconds) each task is going to wait by default between retries. +# This is a global setting and cannot be overridden at task or DAG level. +# +# Variable: AIRFLOW__CORE__MAX_TASK_RETRY_DELAY +# +max_task_retry_delay = 86400 + +# The weighting method used for the effective total priority weight of the task +# +# Variable: AIRFLOW__CORE__DEFAULT_TASK_WEIGHT_RULE +# +default_task_weight_rule = downstream + +# The default task execution_timeout value for the operators. Expected an integer value to +# be passed into timedelta as seconds. If not specified, then the value is considered as None, +# meaning that the operators are never timed out by default. +# +# Variable: AIRFLOW__CORE__DEFAULT_TASK_EXECUTION_TIMEOUT +# +default_task_execution_timeout = + +# Updating serialized DAG can not be faster than a minimum interval to reduce database write rate. +# +# Variable: AIRFLOW__CORE__MIN_SERIALIZED_DAG_UPDATE_INTERVAL +# +min_serialized_dag_update_interval = 30 + +# If ``True``, serialized DAGs are compressed before writing to DB. +# +# .. note:: +# +# This will disable the DAG dependencies view +# +# Variable: AIRFLOW__CORE__COMPRESS_SERIALIZED_DAGS +# +compress_serialized_dags = False + +# Fetching serialized DAG can not be faster than a minimum interval to reduce database +# read rate. This config controls when your DAGs are updated in the Webserver +# +# Variable: AIRFLOW__CORE__MIN_SERIALIZED_DAG_FETCH_INTERVAL +# +min_serialized_dag_fetch_interval = 10 + +# Maximum number of Rendered Task Instance Fields (Template Fields) per task to store +# in the Database. +# All the template_fields for each of Task Instance are stored in the Database. +# Keeping this number small may cause an error when you try to view ``Rendered`` tab in +# TaskInstance view for older tasks. +# +# Variable: AIRFLOW__CORE__MAX_NUM_RENDERED_TI_FIELDS_PER_TASK +# +max_num_rendered_ti_fields_per_task = 30 + +# On each dagrun check against defined SLAs +# +# Variable: AIRFLOW__CORE__CHECK_SLAS +# +check_slas = True + +# Path to custom XCom class that will be used to store and resolve operators results +# +# Example: xcom_backend = path.to.CustomXCom +# +# Variable: AIRFLOW__CORE__XCOM_BACKEND +# +xcom_backend = airflow.models.xcom.BaseXCom + +# By default Airflow plugins are lazily-loaded (only loaded when required). Set it to ``False``, +# if you want to load plugins whenever 'airflow' is invoked via cli or loaded from module. +# +# Variable: AIRFLOW__CORE__LAZY_LOAD_PLUGINS +# +lazy_load_plugins = True + +# By default Airflow providers are lazily-discovered (discovery and imports happen only when required). +# Set it to ``False``, if you want to discover providers whenever 'airflow' is invoked via cli or +# loaded from module. +# +# Variable: AIRFLOW__CORE__LAZY_DISCOVER_PROVIDERS +# +lazy_discover_providers = True + +# Hide sensitive **Variables** or **Connection extra json keys** from UI +# and task logs when set to ``True`` +# +# .. note:: +# +# Connection passwords are always hidden in logs +# +# Variable: AIRFLOW__CORE__HIDE_SENSITIVE_VAR_CONN_FIELDS +# +hide_sensitive_var_conn_fields = True + +# A comma-separated list of extra sensitive keywords to look for in variables names or connection's +# extra JSON. +# +# Variable: AIRFLOW__CORE__SENSITIVE_VAR_CONN_NAMES +# +sensitive_var_conn_names = + +# Task Slot counts for ``default_pool``. This setting would not have any effect in an existing +# deployment where the ``default_pool`` is already created. For existing deployments, users can +# change the number of slots using Webserver, API or the CLI +# +# Variable: AIRFLOW__CORE__DEFAULT_POOL_TASK_SLOT_COUNT +# +default_pool_task_slot_count = 128 + +# The maximum list/dict length an XCom can push to trigger task mapping. If the pushed list/dict has a +# length exceeding this value, the task pushing the XCom will be failed automatically to prevent the +# mapped tasks from clogging the scheduler. +# +# Variable: AIRFLOW__CORE__MAX_MAP_LENGTH +# +max_map_length = 1024 + +# The default umask to use for process when run in daemon mode (scheduler, worker, etc.) +# +# This controls the file-creation mode mask which determines the initial value of file permission bits +# for newly created files. +# +# This value is treated as an octal-integer. +# +# Variable: AIRFLOW__CORE__DAEMON_UMASK +# +daemon_umask = 0o077 + +# Class to use as dataset manager. +# +# Example: dataset_manager_class = airflow.datasets.manager.DatasetManager +# +# Variable: AIRFLOW__CORE__DATASET_MANAGER_CLASS +# +# dataset_manager_class = + +# Kwargs to supply to dataset manager. +# +# Example: dataset_manager_kwargs = {"some_param": "some_value"} +# +# Variable: AIRFLOW__CORE__DATASET_MANAGER_KWARGS +# +# dataset_manager_kwargs = + +# Dataset URI validation should raise an exception if it is not compliant with AIP-60. +# By default this configuration is false, meaning that Airflow 2.x only warns the user. +# In Airflow 3, this configuration will be enabled by default. +# +# Variable: AIRFLOW__CORE__STRICT_DATASET_URI_VALIDATION +# +strict_dataset_uri_validation = False + +# (experimental) Whether components should use Airflow Internal API for DB connectivity. +# +# Variable: AIRFLOW__CORE__DATABASE_ACCESS_ISOLATION +# +database_access_isolation = False + +# (experimental) Airflow Internal API url. +# Only used if ``[core] database_access_isolation`` is ``True``. +# +# Example: internal_api_url = http://localhost:8080 +# +# Variable: AIRFLOW__CORE__INTERNAL_API_URL +# +# internal_api_url = + +# The ability to allow testing connections across Airflow UI, API and CLI. +# Supported options: ``Disabled``, ``Enabled``, ``Hidden``. Default: Disabled +# Disabled - Disables the test connection functionality and disables the Test Connection button in UI. +# Enabled - Enables the test connection functionality and shows the Test Connection button in UI. +# Hidden - Disables the test connection functionality and hides the Test Connection button in UI. +# Before setting this to Enabled, make sure that you review the users who are able to add/edit +# connections and ensure they are trusted. Connection testing can be done maliciously leading to +# undesired and insecure outcomes. +# See `Airflow Security Model: Capabilities of authenticated UI users +# `__ +# for more details. +# +# Variable: AIRFLOW__CORE__TEST_CONNECTION +# +test_connection = Disabled + +# The maximum length of the rendered template field. If the value to be stored in the +# rendered template field exceeds this size, it's redacted. +# +# Variable: AIRFLOW__CORE__MAX_TEMPLATED_FIELD_LENGTH +# +max_templated_field_length = 4096 + +[database] +# Path to the ``alembic.ini`` file. You can either provide the file path relative +# to the Airflow home directory or the absolute path if it is located elsewhere. +# +# Variable: AIRFLOW__DATABASE__ALEMBIC_INI_FILE_PATH +# +alembic_ini_file_path = alembic.ini + +# The SQLAlchemy connection string to the metadata database. +# SQLAlchemy supports many different database engines. +# See: `Set up a Database Backend: Database URI +# `__ +# for more details. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_CONN +# +sql_alchemy_conn = sqlite:////opt/airflow/airflow.db + +# Extra engine specific keyword args passed to SQLAlchemy's create_engine, as a JSON-encoded value +# +# Example: sql_alchemy_engine_args = {"arg1": true} +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_ENGINE_ARGS +# +# sql_alchemy_engine_args = + +# The encoding for the databases +# +# Variable: AIRFLOW__DATABASE__SQL_ENGINE_ENCODING +# +sql_engine_encoding = utf-8 + +# Collation for ``dag_id``, ``task_id``, ``key``, ``external_executor_id`` columns +# in case they have different encoding. +# By default this collation is the same as the database collation, however for ``mysql`` and ``mariadb`` +# the default is ``utf8mb3_bin`` so that the index sizes of our index keys will not exceed +# the maximum size of allowed index when collation is set to ``utf8mb4`` variant, see +# `GitHub Issue Comment `__ +# for more details. +# +# Variable: AIRFLOW__DATABASE__SQL_ENGINE_COLLATION_FOR_IDS +# +# sql_engine_collation_for_ids = + +# If SQLAlchemy should pool database connections. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_ENABLED +# +sql_alchemy_pool_enabled = True + +# The SQLAlchemy pool size is the maximum number of database connections +# in the pool. 0 indicates no limit. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_SIZE +# +sql_alchemy_pool_size = 5 + +# The maximum overflow size of the pool. +# When the number of checked-out connections reaches the size set in pool_size, +# additional connections will be returned up to this limit. +# When those additional connections are returned to the pool, they are disconnected and discarded. +# It follows then that the total number of simultaneous connections the pool will allow +# is **pool_size** + **max_overflow**, +# and the total number of "sleeping" connections the pool will allow is pool_size. +# max_overflow can be set to ``-1`` to indicate no overflow limit; +# no limit will be placed on the total number of concurrent connections. Defaults to ``10``. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_MAX_OVERFLOW +# +sql_alchemy_max_overflow = 10 + +# The SQLAlchemy pool recycle is the number of seconds a connection +# can be idle in the pool before it is invalidated. This config does +# not apply to sqlite. If the number of DB connections is ever exceeded, +# a lower config value will allow the system to recover faster. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_RECYCLE +# +sql_alchemy_pool_recycle = 1800 + +# Check connection at the start of each connection pool checkout. +# Typically, this is a simple statement like "SELECT 1". +# See `SQLAlchemy Pooling: Disconnect Handling - Pessimistic +# `__ +# for more details. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_POOL_PRE_PING +# +sql_alchemy_pool_pre_ping = True + +# The schema to use for the metadata database. +# SQLAlchemy supports databases with the concept of multiple schemas. +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_SCHEMA +# +sql_alchemy_schema = + +# Import path for connect args in SQLAlchemy. Defaults to an empty dict. +# This is useful when you want to configure db engine args that SQLAlchemy won't parse +# in connection string. This can be set by passing a dictionary containing the create engine parameters. +# For more details about passing create engine parameters (keepalives variables, timeout etc) +# in Postgres DB Backend see `Setting up a PostgreSQL Database +# `__ +# e.g ``connect_args={"timeout":30}`` can be defined in ``airflow_local_settings.py`` and +# can be imported as shown below +# +# Example: sql_alchemy_connect_args = airflow_local_settings.connect_args +# +# Variable: AIRFLOW__DATABASE__SQL_ALCHEMY_CONNECT_ARGS +# +# sql_alchemy_connect_args = + +# Whether to load the default connections that ship with Airflow when ``airflow db init`` is called. +# It's good to get started, but you probably want to set this to ``False`` in a production environment. +# +# Variable: AIRFLOW__DATABASE__LOAD_DEFAULT_CONNECTIONS +# +load_default_connections = True + +# Number of times the code should be retried in case of DB Operational Errors. +# Not all transactions will be retried as it can cause undesired state. +# Currently it is only used in ``DagFileProcessor.process_file`` to retry ``dagbag.sync_to_db``. +# +# Variable: AIRFLOW__DATABASE__MAX_DB_RETRIES +# +max_db_retries = 3 + +# Whether to run alembic migrations during Airflow start up. Sometimes this operation can be expensive, +# and the users can assert the correct version through other means (e.g. through a Helm chart). +# Accepts ``True`` or ``False``. +# +# Variable: AIRFLOW__DATABASE__CHECK_MIGRATIONS +# +check_migrations = True + +[logging] +# The folder where airflow should store its log files. +# This path must be absolute. +# There are a few existing configurations that assume this is set to the default. +# If you choose to override this you may need to update the +# ``[logging] dag_processor_manager_log_location`` and +# ``[logging] child_process_log_directory settings`` as well. +# +# Variable: AIRFLOW__LOGGING__BASE_LOG_FOLDER +# +base_log_folder = /opt/airflow/logs +processor_log_folder = /opt/airflow/logs/scheduler + +# Airflow can store logs remotely in AWS S3, Google Cloud Storage or Elastic Search. +# Set this to ``True`` if you want to enable remote logging. +# +# Variable: AIRFLOW__LOGGING__REMOTE_LOGGING +# +remote_logging = False + +# Users must supply an Airflow connection id that provides access to the storage +# location. Depending on your remote logging service, this may only be used for +# reading logs, not writing them. +# +# Variable: AIRFLOW__LOGGING__REMOTE_LOG_CONN_ID +# +remote_log_conn_id = + +# Whether the local log files for GCS, S3, WASB and OSS remote logging should be deleted after +# they are uploaded to the remote location. +# +# Variable: AIRFLOW__LOGGING__DELETE_LOCAL_LOGS +# +delete_local_logs = False + +# Path to Google Credential JSON file. If omitted, authorization based on `the Application Default +# Credentials +# `__ will +# be used. +# +# Variable: AIRFLOW__LOGGING__GOOGLE_KEY_PATH +# +google_key_path = + +# Storage bucket URL for remote logging +# S3 buckets should start with **s3://** +# Cloudwatch log groups should start with **cloudwatch://** +# GCS buckets should start with **gs://** +# WASB buckets should start with **wasb** just to help Airflow select correct handler +# Stackdriver logs should start with **stackdriver://** +# +# Variable: AIRFLOW__LOGGING__REMOTE_BASE_LOG_FOLDER +# +remote_base_log_folder = + +# The remote_task_handler_kwargs param is loaded into a dictionary and passed to the ``__init__`` +# of remote task handler and it overrides the values provided by Airflow config. For example if you set +# ``delete_local_logs=False`` and you provide ``{"delete_local_copy": true}``, then the local +# log files will be deleted after they are uploaded to remote location. +# +# Example: remote_task_handler_kwargs = {"delete_local_copy": true} +# +# Variable: AIRFLOW__LOGGING__REMOTE_TASK_HANDLER_KWARGS +# +remote_task_handler_kwargs = + +# Use server-side encryption for logs stored in S3 +# +# Variable: AIRFLOW__LOGGING__ENCRYPT_S3_LOGS +# +encrypt_s3_logs = False + +# Logging level. +# +# Supported values: ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, ``DEBUG``. +# +# Variable: AIRFLOW__LOGGING__LOGGING_LEVEL +# +logging_level = INFO + +# Logging level for celery. If not set, it uses the value of logging_level +# +# Supported values: ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, ``DEBUG``. +# +# Variable: AIRFLOW__LOGGING__CELERY_LOGGING_LEVEL +# +celery_logging_level = + +# Logging level for Flask-appbuilder UI. +# +# Supported values: ``CRITICAL``, ``ERROR``, ``WARNING``, ``INFO``, ``DEBUG``. +# +# Variable: AIRFLOW__LOGGING__FAB_LOGGING_LEVEL +# +fab_logging_level = WARNING + +# Logging class +# Specify the class that will specify the logging configuration +# This class has to be on the python classpath +# +# Example: logging_config_class = my.path.default_local_settings.LOGGING_CONFIG +# +# Variable: AIRFLOW__LOGGING__LOGGING_CONFIG_CLASS +# +logging_config_class = + +# Flag to enable/disable Colored logs in Console +# Colour the logs when the controlling terminal is a TTY. +# +# Variable: AIRFLOW__LOGGING__COLORED_CONSOLE_LOG +# +colored_console_log = True + +# Log format for when Colored logs is enabled +# +# Variable: AIRFLOW__LOGGING__COLORED_LOG_FORMAT +# +colored_log_format = [%%(blue)s%%(asctime)s%%(reset)s] {%%(blue)s%%(filename)s:%%(reset)s%%(lineno)d} %%(log_color)s%%(levelname)s%%(reset)s - %%(log_color)s%%(message)s%%(reset)s + +# Specifies the class utilized by Airflow to implement colored logging +# +# Variable: AIRFLOW__LOGGING__COLORED_FORMATTER_CLASS +# +colored_formatter_class = airflow.utils.log.colored_log.CustomTTYColoredFormatter + +# Format of Log line +# +# Variable: AIRFLOW__LOGGING__LOG_FORMAT +# +log_format = [%%(asctime)s] {%%(filename)s:%%(lineno)d} %%(levelname)s - %%(message)s + +# Defines the format of log messages for simple logging configuration +# +# Variable: AIRFLOW__LOGGING__SIMPLE_LOG_FORMAT +# +simple_log_format = %%(asctime)s %%(levelname)s - %%(message)s + +# Where to send dag parser logs. If "file", logs are sent to log files defined by child_process_log_directory. +# +# Variable: AIRFLOW__LOGGING__DAG_PROCESSOR_LOG_TARGET +# +dag_processor_log_target = file + +# Format of Dag Processor Log line +# +# Variable: AIRFLOW__LOGGING__DAG_PROCESSOR_LOG_FORMAT +# +dag_processor_log_format = [%%(asctime)s] [SOURCE:DAG_PROCESSOR] {%%(filename)s:%%(lineno)d} %%(levelname)s - %%(message)s + +# Determines the formatter class used by Airflow for structuring its log messages +# The default formatter class is timezone-aware, which means that timestamps attached to log entries +# will be adjusted to reflect the local timezone of the Airflow instance +# +# Variable: AIRFLOW__LOGGING__LOG_FORMATTER_CLASS +# +log_formatter_class = airflow.utils.log.timezone_aware.TimezoneAware + +# An import path to a function to add adaptations of each secret added with +# ``airflow.utils.log.secrets_masker.mask_secret`` to be masked in log messages. The given function +# is expected to require a single parameter: the secret to be adapted. It may return a +# single adaptation of the secret or an iterable of adaptations to each be masked as secrets. +# The original secret will be masked as well as any adaptations returned. +# +# Example: secret_mask_adapter = urllib.parse.quote +# +# Variable: AIRFLOW__LOGGING__SECRET_MASK_ADAPTER +# +secret_mask_adapter = + +# Specify prefix pattern like mentioned below with stream handler ``TaskHandlerWithCustomFormatter`` +# +# Example: task_log_prefix_template = {{ti.dag_id}}-{{ti.task_id}}-{{execution_date}}-{{ti.try_number}} +# +# Variable: AIRFLOW__LOGGING__TASK_LOG_PREFIX_TEMPLATE +# +task_log_prefix_template = + +# Formatting for how airflow generates file names/paths for each task run. +# +# Variable: AIRFLOW__LOGGING__LOG_FILENAME_TEMPLATE +# +log_filename_template = dag_id={{ ti.dag_id }}/run_id={{ ti.run_id }}/task_id={{ ti.task_id }}/{%% if ti.map_index >= 0 %%}map_index={{ ti.map_index }}/{%% endif %%}attempt={{ try_number }}.log + +# Formatting for how airflow generates file names for log +# +# Variable: AIRFLOW__LOGGING__LOG_PROCESSOR_FILENAME_TEMPLATE +# +log_processor_filename_template = {{ filename }}.log + +# Full path of dag_processor_manager logfile. +# +# Variable: AIRFLOW__LOGGING__DAG_PROCESSOR_MANAGER_LOG_LOCATION +# +dag_processor_manager_log_location = /opt/airflow/logs/dag_processor_manager/dag_processor_manager.log + +# Whether DAG processor manager will write logs to stdout +# +# Variable: AIRFLOW__LOGGING__DAG_PROCESSOR_MANAGER_LOG_STDOUT +# +dag_processor_manager_log_stdout = False + +# Name of handler to read task instance logs. +# Defaults to use ``task`` handler. +# +# Variable: AIRFLOW__LOGGING__TASK_LOG_READER +# +task_log_reader = task + +# A comma\-separated list of third-party logger names that will be configured to print messages to +# consoles\. +# +# Example: extra_logger_names = connexion,sqlalchemy +# +# Variable: AIRFLOW__LOGGING__EXTRA_LOGGER_NAMES +# +extra_logger_names = + +# When you start an Airflow worker, Airflow starts a tiny web server +# subprocess to serve the workers local log files to the airflow main +# web server, who then builds pages and sends them to users. This defines +# the port on which the logs are served. It needs to be unused, and open +# visible from the main web server to connect into the workers. +# +# Variable: AIRFLOW__LOGGING__WORKER_LOG_SERVER_PORT +# +worker_log_server_port = 8793 + +# Port to serve logs from for triggerer. +# See ``[logging] worker_log_server_port`` description for more info. +# +# Variable: AIRFLOW__LOGGING__TRIGGER_LOG_SERVER_PORT +# +trigger_log_server_port = 8794 + +# We must parse timestamps to interleave logs between trigger and task. To do so, +# we need to parse timestamps in log files. In case your log format is non-standard, +# you may provide import path to callable which takes a string log line and returns +# the timestamp (datetime.datetime compatible). +# +# Example: interleave_timestamp_parser = path.to.my_func +# +# Variable: AIRFLOW__LOGGING__INTERLEAVE_TIMESTAMP_PARSER +# +# interleave_timestamp_parser = + +# Permissions in the form or of octal string as understood by chmod. The permissions are important +# when you use impersonation, when logs are written by a different user than airflow. The most secure +# way of configuring it in this case is to add both users to the same group and make it the default +# group of both users. Group-writeable logs are default in airflow, but you might decide that you are +# OK with having the logs other-writeable, in which case you should set it to ``0o777``. You might +# decide to add more security if you do not use impersonation and change it to ``0o755`` to make it +# only owner-writeable. You can also make it just readable only for owner by changing it to ``0o700`` +# if all the access (read/write) for your logs happens from the same user. +# +# Example: file_task_handler_new_folder_permissions = 0o775 +# +# Variable: AIRFLOW__LOGGING__FILE_TASK_HANDLER_NEW_FOLDER_PERMISSIONS +# +file_task_handler_new_folder_permissions = 0o775 + +# Permissions in the form or of octal string as understood by chmod. The permissions are important +# when you use impersonation, when logs are written by a different user than airflow. The most secure +# way of configuring it in this case is to add both users to the same group and make it the default +# group of both users. Group-writeable logs are default in airflow, but you might decide that you are +# OK with having the logs other-writeable, in which case you should set it to ``0o666``. You might +# decide to add more security if you do not use impersonation and change it to ``0o644`` to make it +# only owner-writeable. You can also make it just readable only for owner by changing it to ``0o600`` +# if all the access (read/write) for your logs happens from the same user. +# +# Example: file_task_handler_new_file_permissions = 0o664 +# +# Variable: AIRFLOW__LOGGING__FILE_TASK_HANDLER_NEW_FILE_PERMISSIONS +# +file_task_handler_new_file_permissions = 0o664 + +# By default Celery sends all logs into stderr. +# If enabled any previous logging handlers will get *removed*. +# With this option AirFlow will create new handlers +# and send low level logs like INFO and WARNING to stdout, +# while sending higher severity logs to stderr. +# +# Variable: AIRFLOW__LOGGING__CELERY_STDOUT_STDERR_SEPARATION +# +celery_stdout_stderr_separation = False + +# If enabled, Airflow may ship messages to task logs from outside the task run context, e.g. from +# the scheduler, executor, or callback execution context. This can help in circumstances such as +# when there's something blocking the execution of the task and ordinarily there may be no task +# logs at all. +# This is set to ``True`` by default. If you encounter issues with this feature +# (e.g. scheduler performance issues) it can be disabled. +# +# Variable: AIRFLOW__LOGGING__ENABLE_TASK_CONTEXT_LOGGER +# +enable_task_context_logger = True + +[metrics] +# `StatsD `__ integration settings. + +# If true, ``[metrics] metrics_allow_list`` and ``[metrics] metrics_block_list`` will use +# regex pattern matching anywhere within the metric name instead of only prefix matching +# at the start of the name. +# +# Variable: AIRFLOW__METRICS__METRICS_USE_PATTERN_MATCH +# +metrics_use_pattern_match = False + +# Configure an allow list (comma separated string) to send only certain metrics. +# If ``[metrics] metrics_use_pattern_match`` is ``false``, match only the exact metric name prefix. +# If ``[metrics] metrics_use_pattern_match`` is ``true``, provide regex patterns to match. +# +# Example: metrics_allow_list = "scheduler,executor,dagrun,pool,triggerer,celery" or "^scheduler,^executor,heartbeat|timeout" +# +# Variable: AIRFLOW__METRICS__METRICS_ALLOW_LIST +# +metrics_allow_list = + +# Configure a block list (comma separated string) to block certain metrics from being emitted. +# If ``[metrics] metrics_allow_list`` and ``[metrics] metrics_block_list`` are both configured, +# ``[metrics] metrics_block_list`` is ignored. +# +# If ``[metrics] metrics_use_pattern_match`` is ``false``, match only the exact metric name prefix. +# +# If ``[metrics] metrics_use_pattern_match`` is ``true``, provide regex patterns to match. +# +# Example: metrics_block_list = "scheduler,executor,dagrun,pool,triggerer,celery" or "^scheduler,^executor,heartbeat|timeout" +# +# Variable: AIRFLOW__METRICS__METRICS_BLOCK_LIST +# +metrics_block_list = + +# Enables sending metrics to StatsD. +# +# Variable: AIRFLOW__METRICS__STATSD_ON +# +statsd_on = False + +# Specifies the host address where the StatsD daemon (or server) is running +# +# Variable: AIRFLOW__METRICS__STATSD_HOST +# +statsd_host = localhost + +# Specifies the port on which the StatsD daemon (or server) is listening to +# +# Variable: AIRFLOW__METRICS__STATSD_PORT +# +statsd_port = 8125 + +# Defines the namespace for all metrics sent from Airflow to StatsD +# +# Variable: AIRFLOW__METRICS__STATSD_PREFIX +# +statsd_prefix = airflow + +# A function that validate the StatsD stat name, apply changes to the stat name if necessary and return +# the transformed stat name. +# +# The function should have the following signature +# +# .. code-block:: python +# +# def func_name(stat_name: str) -> str: ... +# +# Variable: AIRFLOW__METRICS__STAT_NAME_HANDLER +# +stat_name_handler = + +# To enable datadog integration to send airflow metrics. +# +# Variable: AIRFLOW__METRICS__STATSD_DATADOG_ENABLED +# +statsd_datadog_enabled = False + +# List of datadog tags attached to all metrics(e.g: ``key1:value1,key2:value2``) +# +# Variable: AIRFLOW__METRICS__STATSD_DATADOG_TAGS +# +statsd_datadog_tags = + +# Set to ``False`` to disable metadata tags for some of the emitted metrics +# +# Variable: AIRFLOW__METRICS__STATSD_DATADOG_METRICS_TAGS +# +statsd_datadog_metrics_tags = True + +# If you want to utilise your own custom StatsD client set the relevant +# module path below. +# Note: The module path must exist on your +# `PYTHONPATH ` +# for Airflow to pick it up +# +# Variable: AIRFLOW__METRICS__STATSD_CUSTOM_CLIENT_PATH +# +# statsd_custom_client_path = + +# If you want to avoid sending all the available metrics tags to StatsD, +# you can configure a block list of prefixes (comma separated) to filter out metric tags +# that start with the elements of the list (e.g: ``job_id,run_id``) +# +# Example: statsd_disabled_tags = job_id,run_id,dag_id,task_id +# +# Variable: AIRFLOW__METRICS__STATSD_DISABLED_TAGS +# +statsd_disabled_tags = job_id,run_id + +# To enable sending Airflow metrics with StatsD-Influxdb tagging convention. +# +# Variable: AIRFLOW__METRICS__STATSD_INFLUXDB_ENABLED +# +statsd_influxdb_enabled = False + +# Enables sending metrics to OpenTelemetry. +# +# Variable: AIRFLOW__METRICS__OTEL_ON +# +otel_on = False + +# Specifies the hostname or IP address of the OpenTelemetry Collector to which Airflow sends +# metrics and traces. +# +# Variable: AIRFLOW__METRICS__OTEL_HOST +# +otel_host = localhost + +# Specifies the port of the OpenTelemetry Collector that is listening to. +# +# Variable: AIRFLOW__METRICS__OTEL_PORT +# +otel_port = 8889 + +# The prefix for the Airflow metrics. +# +# Variable: AIRFLOW__METRICS__OTEL_PREFIX +# +otel_prefix = airflow + +# Defines the interval, in milliseconds, at which Airflow sends batches of metrics and traces +# to the configured OpenTelemetry Collector. +# +# Variable: AIRFLOW__METRICS__OTEL_INTERVAL_MILLISECONDS +# +otel_interval_milliseconds = 60000 + +# If ``True``, all metrics are also emitted to the console. Defaults to ``False``. +# +# Variable: AIRFLOW__METRICS__OTEL_DEBUGGING_ON +# +otel_debugging_on = False + +# If ``True``, SSL will be enabled. Defaults to ``False``. +# To establish an HTTPS connection to the OpenTelemetry collector, +# you need to configure the SSL certificate and key within the OpenTelemetry collector's +# ``config.yml`` file. +# +# Variable: AIRFLOW__METRICS__OTEL_SSL_ACTIVE +# +otel_ssl_active = False + +[secrets] +# Full class name of secrets backend to enable (will precede env vars and metastore in search path) +# +# Example: backend = airflow.providers.amazon.aws.secrets.systems_manager.SystemsManagerParameterStoreBackend +# +# Variable: AIRFLOW__SECRETS__BACKEND +# +backend = + +# The backend_kwargs param is loaded into a dictionary and passed to ``__init__`` +# of secrets backend class. See documentation for the secrets backend you are using. +# JSON is expected. +# +# Example for AWS Systems Manager ParameterStore: +# ``{"connections_prefix": "/airflow/connections", "profile_name": "default"}`` +# +# Variable: AIRFLOW__SECRETS__BACKEND_KWARGS +# +backend_kwargs = + +# .. note:: |experimental| +# +# Enables local caching of Variables, when parsing DAGs only. +# Using this option can make dag parsing faster if Variables are used in top level code, at the expense +# of longer propagation time for changes. +# Please note that this cache concerns only the DAG parsing step. There is no caching in place when DAG +# tasks are run. +# +# Variable: AIRFLOW__SECRETS__USE_CACHE +# +use_cache = False + +# .. note:: |experimental| +# +# When the cache is enabled, this is the duration for which we consider an entry in the cache to be +# valid. Entries are refreshed if they are older than this many seconds. +# It means that when the cache is enabled, this is the maximum amount of time you need to wait to see a +# Variable change take effect. +# +# Variable: AIRFLOW__SECRETS__CACHE_TTL_SECONDS +# +cache_ttl_seconds = 900 + +[cli] +# In what way should the cli access the API. The LocalClient will use the +# database directly, while the json_client will use the api running on the +# webserver +# +# Variable: AIRFLOW__CLI__API_CLIENT +# +api_client = airflow.api.client.local_client + +# If you set web_server_url_prefix, do NOT forget to append it here, ex: +# ``endpoint_url = http://localhost:8080/myroot`` +# So api will look like: ``http://localhost:8080/myroot/api/experimental/...`` +# +# Variable: AIRFLOW__CLI__ENDPOINT_URL +# +endpoint_url = http://localhost:8080 + +[debug] +# Used only with ``DebugExecutor``. If set to ``True`` DAG will fail with first +# failed task. Helpful for debugging purposes. +# +# Variable: AIRFLOW__DEBUG__FAIL_FAST +# +fail_fast = False + +[api] +# Enables the deprecated experimental API. Please note that these API endpoints do not have +# access control. An authenticated user has full access. +# +# .. warning:: +# +# This `Experimental REST API +# `__ is +# deprecated since version 2.0. Please consider using +# `the Stable REST API +# `__. +# For more information on migration, see +# `RELEASE_NOTES.rst `_ +# +# Variable: AIRFLOW__API__ENABLE_EXPERIMENTAL_API +# +enable_experimental_api = False + +# Comma separated list of auth backends to authenticate users of the API. See +# `Security: API +# `__ for possible values. +# ("airflow.api.auth.backend.default" allows all requests for historic reasons) +# +# Variable: AIRFLOW__API__AUTH_BACKENDS +# +auth_backends = airflow.api.auth.backend.session + +# Used to set the maximum page limit for API requests. If limit passed as param +# is greater than maximum page limit, it will be ignored and maximum page limit value +# will be set as the limit +# +# Variable: AIRFLOW__API__MAXIMUM_PAGE_LIMIT +# +maximum_page_limit = 100 + +# Used to set the default page limit when limit param is zero or not provided in API +# requests. Otherwise if positive integer is passed in the API requests as limit, the +# smallest number of user given limit or maximum page limit is taken as limit. +# +# Variable: AIRFLOW__API__FALLBACK_PAGE_LIMIT +# +fallback_page_limit = 100 + +# The intended audience for JWT token credentials used for authorization. This value must match on the client and server sides. If empty, audience will not be tested. +# +# Example: google_oauth2_audience = project-id-random-value.apps.googleusercontent.com +# +# Variable: AIRFLOW__API__GOOGLE_OAUTH2_AUDIENCE +# +google_oauth2_audience = + +# Path to Google Cloud Service Account key file (JSON). If omitted, authorization based on +# `the Application Default Credentials +# `__ will +# be used. +# +# Example: google_key_path = /files/service-account-json +# +# Variable: AIRFLOW__API__GOOGLE_KEY_PATH +# +google_key_path = + +# Used in response to a preflight request to indicate which HTTP +# headers can be used when making the actual request. This header is +# the server side response to the browser's +# Access-Control-Request-Headers header. +# +# Variable: AIRFLOW__API__ACCESS_CONTROL_ALLOW_HEADERS +# +access_control_allow_headers = + +# Specifies the method or methods allowed when accessing the resource. +# +# Variable: AIRFLOW__API__ACCESS_CONTROL_ALLOW_METHODS +# +access_control_allow_methods = + +# Indicates whether the response can be shared with requesting code from the given origins. +# Separate URLs with space. +# +# Variable: AIRFLOW__API__ACCESS_CONTROL_ALLOW_ORIGINS +# +access_control_allow_origins = + +# Indicates whether the **xcomEntries** endpoint supports the **deserialize** +# flag. If set to ``False``, setting this flag in a request would result in a +# 400 Bad Request error. +# +# Variable: AIRFLOW__API__ENABLE_XCOM_DESERIALIZE_SUPPORT +# +enable_xcom_deserialize_support = False + +[lineage] +# what lineage backend to use +# +# Variable: AIRFLOW__LINEAGE__BACKEND +# +backend = + +[operators] +# The default owner assigned to each new operator, unless +# provided explicitly or passed via ``default_args`` +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_OWNER +# +default_owner = airflow + +# The default value of attribute "deferrable" in operators and sensors. +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_DEFERRABLE +# +default_deferrable = false + +# Indicates the default number of CPU units allocated to each operator when no specific CPU request +# is specified in the operator's configuration +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_CPUS +# +default_cpus = 1 + +# Indicates the default number of RAM allocated to each operator when no specific RAM request +# is specified in the operator's configuration +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_RAM +# +default_ram = 512 + +# Indicates the default number of disk storage allocated to each operator when no specific disk request +# is specified in the operator's configuration +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_DISK +# +default_disk = 512 + +# Indicates the default number of GPUs allocated to each operator when no specific GPUs request +# is specified in the operator's configuration +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_GPUS +# +default_gpus = 0 + +# Default queue that tasks get assigned to and that worker listen on. +# +# Variable: AIRFLOW__OPERATORS__DEFAULT_QUEUE +# +default_queue = default + +# Is allowed to pass additional/unused arguments (args, kwargs) to the BaseOperator operator. +# If set to ``False``, an exception will be thrown, +# otherwise only the console message will be displayed. +# +# Variable: AIRFLOW__OPERATORS__ALLOW_ILLEGAL_ARGUMENTS +# +allow_illegal_arguments = False + +[webserver] +# The message displayed when a user attempts to execute actions beyond their authorised privileges. +# +# Variable: AIRFLOW__WEBSERVER__ACCESS_DENIED_MESSAGE +# +access_denied_message = Access is Denied + +# Path of webserver config file used for configuring the webserver parameters +# +# Variable: AIRFLOW__WEBSERVER__CONFIG_FILE +# +config_file = /opt/airflow/webserver_config.py + +# The base url of your website: Airflow cannot guess what domain or CNAME you are using. +# This is used to create links in the Log Url column in the Browse - Task Instances menu, +# as well as in any automated emails sent by Airflow that contain links to your webserver. +# +# Variable: AIRFLOW__WEBSERVER__BASE_URL +# +base_url = http://localhost:8080 + +# Default timezone to display all dates in the UI, can be UTC, system, or +# any IANA timezone string (e.g. **Europe/Amsterdam**). If left empty the +# default value of core/default_timezone will be used +# +# Example: default_ui_timezone = America/New_York +# +# Variable: AIRFLOW__WEBSERVER__DEFAULT_UI_TIMEZONE +# +default_ui_timezone = UTC + +# The ip specified when starting the web server +# +# Variable: AIRFLOW__WEBSERVER__WEB_SERVER_HOST +# +web_server_host = 0.0.0.0 + +# The port on which to run the web server +# +# Variable: AIRFLOW__WEBSERVER__WEB_SERVER_PORT +# +web_server_port = 8080 + +# Paths to the SSL certificate and key for the web server. When both are +# provided SSL will be enabled. This does not change the web server port. +# +# Variable: AIRFLOW__WEBSERVER__WEB_SERVER_SSL_CERT +# +web_server_ssl_cert = + +# Paths to the SSL certificate and key for the web server. When both are +# provided SSL will be enabled. This does not change the web server port. +# +# Variable: AIRFLOW__WEBSERVER__WEB_SERVER_SSL_KEY +# +web_server_ssl_key = + +# The type of backend used to store web session data, can be ``database`` or ``securecookie``. For the +# ``database`` backend, sessions are store in the database and they can be +# managed there (for example when you reset password of the user, all sessions for that user are +# deleted). For the ``securecookie`` backend, sessions are stored in encrypted cookies on the client +# side. The ``securecookie`` mechanism is 'lighter' than database backend, but sessions are not deleted +# when you reset password of the user, which means that other than waiting for expiry time, the only +# way to invalidate all sessions for a user is to change secret_key and restart webserver (which +# also invalidates and logs out all other user's sessions). +# +# When you are using ``database`` backend, make sure to keep your database session table small +# by periodically running ``airflow db clean --table session`` command, especially if you have +# automated API calls that will create a new session for each call rather than reuse the sessions +# stored in browser cookies. +# +# Example: session_backend = securecookie +# +# Variable: AIRFLOW__WEBSERVER__SESSION_BACKEND +# +session_backend = database + +# Number of seconds the webserver waits before killing gunicorn master that doesn't respond +# +# Variable: AIRFLOW__WEBSERVER__WEB_SERVER_MASTER_TIMEOUT +# +web_server_master_timeout = 120 + +# Number of seconds the gunicorn webserver waits before timing out on a worker +# +# Variable: AIRFLOW__WEBSERVER__WEB_SERVER_WORKER_TIMEOUT +# +web_server_worker_timeout = 120 + +# Number of workers to refresh at a time. When set to 0, worker refresh is +# disabled. When nonzero, airflow periodically refreshes webserver workers by +# bringing up new ones and killing old ones. +# +# Variable: AIRFLOW__WEBSERVER__WORKER_REFRESH_BATCH_SIZE +# +worker_refresh_batch_size = 1 + +# Number of seconds to wait before refreshing a batch of workers. +# +# Variable: AIRFLOW__WEBSERVER__WORKER_REFRESH_INTERVAL +# +worker_refresh_interval = 6000 + +# If set to ``True``, Airflow will track files in plugins_folder directory. When it detects changes, +# then reload the gunicorn. If set to ``True``, gunicorn starts without preloading, which is slower, +# uses more memory, and may cause race conditions. Avoid setting this to ``True`` in production. +# +# Variable: AIRFLOW__WEBSERVER__RELOAD_ON_PLUGIN_CHANGE +# +reload_on_plugin_change = False + +# Secret key used to run your flask app. It should be as random as possible. However, when running +# more than 1 instances of webserver, make sure all of them use the same ``secret_key`` otherwise +# one of them will error with "CSRF session token is missing". +# The webserver key is also used to authorize requests to Celery workers when logs are retrieved. +# The token generated using the secret key has a short expiry time though - make sure that time on +# ALL the machines that you run airflow components on is synchronized (for example using ntpd) +# otherwise you might get "forbidden" errors when the logs are accessed. +# +# Variable: AIRFLOW__WEBSERVER__SECRET_KEY +# +secret_key = xIcs2bnO5KyoXMwIyf88+g== + +# Number of workers to run the Gunicorn web server +# +# Variable: AIRFLOW__WEBSERVER__WORKERS +# +workers = 4 + +# The worker class gunicorn should use. Choices include +# ``sync`` (default), ``eventlet``, ``gevent``. +# +# .. warning:: +# +# When using ``gevent`` you might also want to set the ``_AIRFLOW_PATCH_GEVENT`` +# environment variable to ``"1"`` to make sure gevent patching is done as early as possible. +# +# See related Issues / PRs for more details: +# +# * https://github.com/benoitc/gunicorn/issues/2796 +# * https://github.com/apache/airflow/issues/8212 +# * https://github.com/apache/airflow/pull/28283 +# +# Variable: AIRFLOW__WEBSERVER__WORKER_CLASS +# +worker_class = sync + +# Log files for the gunicorn webserver. '-' means log to stderr. +# +# Variable: AIRFLOW__WEBSERVER__ACCESS_LOGFILE +# +access_logfile = - + +# Log files for the gunicorn webserver. '-' means log to stderr. +# +# Variable: AIRFLOW__WEBSERVER__ERROR_LOGFILE +# +error_logfile = - + +# Access log format for gunicorn webserver. +# default format is ``%%(h)s %%(l)s %%(u)s %%(t)s "%%(r)s" %%(s)s %%(b)s "%%(f)s" "%%(a)s"`` +# See `Gunicorn Settings: 'access_log_format' Reference +# `__ for more details +# +# Variable: AIRFLOW__WEBSERVER__ACCESS_LOGFORMAT +# +access_logformat = + +# Expose the configuration file in the web server. Set to ``non-sensitive-only`` to show all values +# except those that have security implications. ``True`` shows all values. ``False`` hides the +# configuration completely. +# +# Variable: AIRFLOW__WEBSERVER__EXPOSE_CONFIG +# +expose_config = False + +# Expose hostname in the web server +# +# Variable: AIRFLOW__WEBSERVER__EXPOSE_HOSTNAME +# +expose_hostname = False + +# Expose stacktrace in the web server +# +# Variable: AIRFLOW__WEBSERVER__EXPOSE_STACKTRACE +# +expose_stacktrace = False + +# Default DAG view. Valid values are: ``grid``, ``graph``, ``duration``, ``gantt``, ``landing_times`` +# +# Variable: AIRFLOW__WEBSERVER__DAG_DEFAULT_VIEW +# +dag_default_view = grid + +# Default DAG orientation. Valid values are: +# ``LR`` (Left->Right), ``TB`` (Top->Bottom), ``RL`` (Right->Left), ``BT`` (Bottom->Top) +# +# Variable: AIRFLOW__WEBSERVER__DAG_ORIENTATION +# +dag_orientation = LR + +# Sorting order in grid view. Valid values are: ``topological``, ``hierarchical_alphabetical`` +# +# Variable: AIRFLOW__WEBSERVER__GRID_VIEW_SORTING_ORDER +# +grid_view_sorting_order = topological + +# The amount of time (in secs) webserver will wait for initial handshake +# while fetching logs from other worker machine +# +# Variable: AIRFLOW__WEBSERVER__LOG_FETCH_TIMEOUT_SEC +# +log_fetch_timeout_sec = 5 + +# Time interval (in secs) to wait before next log fetching. +# +# Variable: AIRFLOW__WEBSERVER__LOG_FETCH_DELAY_SEC +# +log_fetch_delay_sec = 2 + +# Distance away from page bottom to enable auto tailing. +# +# Variable: AIRFLOW__WEBSERVER__LOG_AUTO_TAILING_OFFSET +# +log_auto_tailing_offset = 30 + +# Animation speed for auto tailing log display. +# +# Variable: AIRFLOW__WEBSERVER__LOG_ANIMATION_SPEED +# +log_animation_speed = 1000 + +# By default, the webserver shows paused DAGs. Flip this to hide paused +# DAGs by default +# +# Variable: AIRFLOW__WEBSERVER__HIDE_PAUSED_DAGS_BY_DEFAULT +# +hide_paused_dags_by_default = False + +# Consistent page size across all listing views in the UI +# +# Variable: AIRFLOW__WEBSERVER__PAGE_SIZE +# +page_size = 100 + +# Define the color of navigation bar +# +# Variable: AIRFLOW__WEBSERVER__NAVBAR_COLOR +# +navbar_color = #fff + +# Define the color of text in the navigation bar +# +# Variable: AIRFLOW__WEBSERVER__NAVBAR_TEXT_COLOR +# +navbar_text_color = #51504f + +# Define the color of navigation bar links when hovered +# +# Variable: AIRFLOW__WEBSERVER__NAVBAR_HOVER_COLOR +# +navbar_hover_color = #eee + +# Define the color of text in the navigation bar when hovered +# +# Variable: AIRFLOW__WEBSERVER__NAVBAR_TEXT_HOVER_COLOR +# +navbar_text_hover_color = #51504f + +# Define the color of the logo text +# +# Variable: AIRFLOW__WEBSERVER__NAVBAR_LOGO_TEXT_COLOR +# +navbar_logo_text_color = #51504f + +# Default dagrun to show in UI +# +# Variable: AIRFLOW__WEBSERVER__DEFAULT_DAG_RUN_DISPLAY_NUMBER +# +default_dag_run_display_number = 25 + +# Enable werkzeug ``ProxyFix`` middleware for reverse proxy +# +# Variable: AIRFLOW__WEBSERVER__ENABLE_PROXY_FIX +# +enable_proxy_fix = False + +# Number of values to trust for ``X-Forwarded-For``. +# See `Werkzeug: X-Forwarded-For Proxy Fix +# `__ for more details. +# +# Variable: AIRFLOW__WEBSERVER__PROXY_FIX_X_FOR +# +proxy_fix_x_for = 1 + +# Number of values to trust for ``X-Forwarded-Proto``. +# See `Werkzeug: X-Forwarded-For Proxy Fix +# `__ for more details. +# +# Variable: AIRFLOW__WEBSERVER__PROXY_FIX_X_PROTO +# +proxy_fix_x_proto = 1 + +# Number of values to trust for ``X-Forwarded-Host``. +# See `Werkzeug: X-Forwarded-For Proxy Fix +# `__ for more details. +# +# Variable: AIRFLOW__WEBSERVER__PROXY_FIX_X_HOST +# +proxy_fix_x_host = 1 + +# Number of values to trust for ``X-Forwarded-Port``. +# See `Werkzeug: X-Forwarded-For Proxy Fix +# `__ for more details. +# +# Variable: AIRFLOW__WEBSERVER__PROXY_FIX_X_PORT +# +proxy_fix_x_port = 1 + +# Number of values to trust for ``X-Forwarded-Prefix``. +# See `Werkzeug: X-Forwarded-For Proxy Fix +# `__ for more details. +# +# Variable: AIRFLOW__WEBSERVER__PROXY_FIX_X_PREFIX +# +proxy_fix_x_prefix = 1 + +# Set secure flag on session cookie +# +# Variable: AIRFLOW__WEBSERVER__COOKIE_SECURE +# +cookie_secure = False + +# Set samesite policy on session cookie +# +# Variable: AIRFLOW__WEBSERVER__COOKIE_SAMESITE +# +cookie_samesite = Lax + +# Default setting for wrap toggle on DAG code and TI log views. +# +# Variable: AIRFLOW__WEBSERVER__DEFAULT_WRAP +# +default_wrap = False + +# Allow the UI to be rendered in a frame +# +# Variable: AIRFLOW__WEBSERVER__X_FRAME_ENABLED +# +x_frame_enabled = True + +# Send anonymous user activity to your analytics tool +# choose from ``google_analytics``, ``segment``, ``metarouter``, or ``matomo`` +# +# Variable: AIRFLOW__WEBSERVER__ANALYTICS_TOOL +# +# analytics_tool = + +# Unique ID of your account in the analytics tool +# +# Variable: AIRFLOW__WEBSERVER__ANALYTICS_ID +# +# analytics_id = + +# Your instances url, only applicable to Matomo. +# +# Example: analytics_url = https://your.matomo.instance.com/ +# +# Variable: AIRFLOW__WEBSERVER__ANALYTICS_URL +# +# analytics_url = + +# 'Recent Tasks' stats will show for old DagRuns if set +# +# Variable: AIRFLOW__WEBSERVER__SHOW_RECENT_STATS_FOR_COMPLETED_RUNS +# +show_recent_stats_for_completed_runs = True + +# The UI cookie lifetime in minutes. User will be logged out from UI after +# ``[webserver] session_lifetime_minutes`` of non-activity +# +# Variable: AIRFLOW__WEBSERVER__SESSION_LIFETIME_MINUTES +# +session_lifetime_minutes = 43200 + +# Sets a custom page title for the DAGs overview page and site title for all pages +# +# Variable: AIRFLOW__WEBSERVER__INSTANCE_NAME +# +# instance_name = + +# Whether the custom page title for the DAGs overview page contains any Markup language +# +# Variable: AIRFLOW__WEBSERVER__INSTANCE_NAME_HAS_MARKUP +# +instance_name_has_markup = False + +# How frequently, in seconds, the DAG data will auto-refresh in graph or grid view +# when auto-refresh is turned on +# +# Variable: AIRFLOW__WEBSERVER__AUTO_REFRESH_INTERVAL +# +auto_refresh_interval = 3 + +# Boolean for displaying warning for publicly viewable deployment +# +# Variable: AIRFLOW__WEBSERVER__WARN_DEPLOYMENT_EXPOSURE +# +warn_deployment_exposure = True + +# Comma separated string of view events to exclude from dag audit view. +# All other events will be added minus the ones passed here. +# The audit logs in the db will not be affected by this parameter. +# +# Example: audit_view_excluded_events = cli_task_run,running,success +# +# Variable: AIRFLOW__WEBSERVER__AUDIT_VIEW_EXCLUDED_EVENTS +# +# audit_view_excluded_events = + +# Comma separated string of view events to include in dag audit view. +# If passed, only these events will populate the dag audit view. +# The audit logs in the db will not be affected by this parameter. +# +# Example: audit_view_included_events = dagrun_cleared,failed +# +# Variable: AIRFLOW__WEBSERVER__AUDIT_VIEW_INCLUDED_EVENTS +# +# audit_view_included_events = + +# Boolean for running SwaggerUI in the webserver. +# +# Variable: AIRFLOW__WEBSERVER__ENABLE_SWAGGER_UI +# +enable_swagger_ui = True + +# Boolean for running Internal API in the webserver. +# +# Variable: AIRFLOW__WEBSERVER__RUN_INTERNAL_API +# +run_internal_api = False + +# The caching algorithm used by the webserver. Must be a valid hashlib function name. +# +# Example: caching_hash_method = sha256 +# +# Variable: AIRFLOW__WEBSERVER__CACHING_HASH_METHOD +# +caching_hash_method = md5 + +# Behavior of the trigger DAG run button for DAGs without params. ``False`` to skip and trigger +# without displaying a form to add a **dag_run.conf**, ``True`` to always display the form. +# The form is displayed always if parameters are defined. +# +# Variable: AIRFLOW__WEBSERVER__SHOW_TRIGGER_FORM_IF_NO_PARAMS +# +show_trigger_form_if_no_params = False + +# Number of recent DAG run configurations in the selector on the trigger web form. +# +# Example: num_recent_configurations_for_trigger = 10 +# +# Variable: AIRFLOW__WEBSERVER__NUM_RECENT_CONFIGURATIONS_FOR_TRIGGER +# +num_recent_configurations_for_trigger = 5 + +# A DAG author is able to provide any raw HTML into ``doc_md`` or params description in +# ``description_md`` for text formatting. This is including potentially unsafe javascript. +# Displaying the DAG or trigger form in web UI provides the DAG author the potential to +# inject malicious code into clients browsers. To ensure the web UI is safe by default, +# raw HTML is disabled by default. If you trust your DAG authors, you can enable HTML +# support in markdown by setting this option to ``True``. +# +# This parameter also enables the deprecated fields ``description_html`` and +# ``custom_html_form`` in DAG params until the feature is removed in a future version. +# +# Example: allow_raw_html_descriptions = False +# +# Variable: AIRFLOW__WEBSERVER__ALLOW_RAW_HTML_DESCRIPTIONS +# +allow_raw_html_descriptions = False + +# The maximum size of the request payload (in MB) that can be sent. +# +# Variable: AIRFLOW__WEBSERVER__ALLOWED_PAYLOAD_SIZE +# +allowed_payload_size = 1.0 + +# Require confirmation when changing a DAG in the web UI. This is to prevent accidental changes +# to a DAG that may be running on sensitive environments like production. +# When set to ``True``, confirmation dialog will be shown when a user tries to Pause/Unpause, +# Trigger a DAG +# +# Variable: AIRFLOW__WEBSERVER__REQUIRE_CONFIRMATION_DAG_CHANGE +# +require_confirmation_dag_change = False + +[email] +# Configuration email backend and whether to +# send email alerts on retry or failure + +# Email backend to use +# +# Variable: AIRFLOW__EMAIL__EMAIL_BACKEND +# +email_backend = airflow.utils.email.send_email_smtp + +# Email connection to use +# +# Variable: AIRFLOW__EMAIL__EMAIL_CONN_ID +# +email_conn_id = smtp_default + +# Whether email alerts should be sent when a task is retried +# +# Variable: AIRFLOW__EMAIL__DEFAULT_EMAIL_ON_RETRY +# +default_email_on_retry = True + +# Whether email alerts should be sent when a task failed +# +# Variable: AIRFLOW__EMAIL__DEFAULT_EMAIL_ON_FAILURE +# +default_email_on_failure = True + +# File that will be used as the template for Email subject (which will be rendered using Jinja2). +# If not set, Airflow uses a base template. +# +# Example: subject_template = /path/to/my_subject_template_file +# +# Variable: AIRFLOW__EMAIL__SUBJECT_TEMPLATE +# +# subject_template = + +# File that will be used as the template for Email content (which will be rendered using Jinja2). +# If not set, Airflow uses a base template. +# +# Example: html_content_template = /path/to/my_html_content_template_file +# +# Variable: AIRFLOW__EMAIL__HTML_CONTENT_TEMPLATE +# +# html_content_template = + +# Email address that will be used as sender address. +# It can either be raw email or the complete address in a format ``Sender Name `` +# +# Example: from_email = Airflow +# +# Variable: AIRFLOW__EMAIL__FROM_EMAIL +# +# from_email = + +# ssl context to use when using SMTP and IMAP SSL connections. By default, the context is "default" +# which sets it to ``ssl.create_default_context()`` which provides the right balance between +# compatibility and security, it however requires that certificates in your operating system are +# updated and that SMTP/IMAP servers of yours have valid certificates that have corresponding public +# keys installed on your machines. You can switch it to "none" if you want to disable checking +# of the certificates, but it is not recommended as it allows MITM (man-in-the-middle) attacks +# if your infrastructure is not sufficiently secured. It should only be set temporarily while you +# are fixing your certificate configuration. This can be typically done by upgrading to newer +# version of the operating system you run Airflow components on,by upgrading/refreshing proper +# certificates in the OS or by updating certificates for your mail servers. +# +# Example: ssl_context = default +# +# Variable: AIRFLOW__EMAIL__SSL_CONTEXT +# +ssl_context = default + +[smtp] +# If you want airflow to send emails on retries, failure, and you want to use +# the airflow.utils.email.send_email_smtp function, you have to configure an +# smtp server here + +# Specifies the host server address used by Airflow when sending out email notifications via SMTP. +# +# Variable: AIRFLOW__SMTP__SMTP_HOST +# +smtp_host = localhost + +# Determines whether to use the STARTTLS command when connecting to the SMTP server. +# +# Variable: AIRFLOW__SMTP__SMTP_STARTTLS +# +smtp_starttls = True + +# Determines whether to use an SSL connection when talking to the SMTP server. +# +# Variable: AIRFLOW__SMTP__SMTP_SSL +# +smtp_ssl = False + +# Username to authenticate when connecting to smtp server. +# +# Example: smtp_user = airflow +# +# Variable: AIRFLOW__SMTP__SMTP_USER +# +# smtp_user = + +# Password to authenticate when connecting to smtp server. +# +# Example: smtp_password = airflow +# +# Variable: AIRFLOW__SMTP__SMTP_PASSWORD +# +# smtp_password = + +# Defines the port number on which Airflow connects to the SMTP server to send email notifications. +# +# Variable: AIRFLOW__SMTP__SMTP_PORT +# +smtp_port = 25 + +# Specifies the default **from** email address used when Airflow sends email notifications. +# +# Variable: AIRFLOW__SMTP__SMTP_MAIL_FROM +# +smtp_mail_from = airflow@example.com + +# Determines the maximum time (in seconds) the Apache Airflow system will wait for a +# connection to the SMTP server to be established. +# +# Variable: AIRFLOW__SMTP__SMTP_TIMEOUT +# +smtp_timeout = 30 + +# Defines the maximum number of times Airflow will attempt to connect to the SMTP server. +# +# Variable: AIRFLOW__SMTP__SMTP_RETRY_LIMIT +# +smtp_retry_limit = 5 + +[sentry] +# `Sentry `__ integration. Here you can supply +# additional configuration options based on the Python platform. +# See `Python / Configuration / Basic Options +# `__ for more details. +# Unsupported options: ``integrations``, ``in_app_include``, ``in_app_exclude``, +# ``ignore_errors``, ``before_breadcrumb``, ``transport``. + +# Enable error reporting to Sentry +# +# Variable: AIRFLOW__SENTRY__SENTRY_ON +# +sentry_on = false + +# +# Variable: AIRFLOW__SENTRY__SENTRY_DSN +# +sentry_dsn = + +# Dotted path to a before_send function that the sentry SDK should be configured to use. +# +# Variable: AIRFLOW__SENTRY__BEFORE_SEND +# +# before_send = + +[scheduler] +# Task instances listen for external kill signal (when you clear tasks +# from the CLI or the UI), this defines the frequency at which they should +# listen (in seconds). +# +# Variable: AIRFLOW__SCHEDULER__JOB_HEARTBEAT_SEC +# +job_heartbeat_sec = 5 + +# The scheduler constantly tries to trigger new tasks (look at the +# scheduler section in the docs for more information). This defines +# how often the scheduler should run (in seconds). +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULER_HEARTBEAT_SEC +# +scheduler_heartbeat_sec = 5 + +# The frequency (in seconds) at which the LocalTaskJob should send heartbeat signals to the +# scheduler to notify it's still alive. If this value is set to 0, the heartbeat interval will default +# to the value of ``[scheduler] scheduler_zombie_task_threshold``. +# +# Variable: AIRFLOW__SCHEDULER__LOCAL_TASK_JOB_HEARTBEAT_SEC +# +local_task_job_heartbeat_sec = 0 + +# The number of times to try to schedule each DAG file +# -1 indicates unlimited number +# +# Variable: AIRFLOW__SCHEDULER__NUM_RUNS +# +num_runs = -1 + +# Controls how long the scheduler will sleep between loops, but if there was nothing to do +# in the loop. i.e. if it scheduled something then it will start the next loop +# iteration straight away. +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULER_IDLE_SLEEP_TIME +# +scheduler_idle_sleep_time = 1 + +# Number of seconds after which a DAG file is parsed. The DAG file is parsed every +# ``[scheduler] min_file_process_interval`` number of seconds. Updates to DAGs are reflected after +# this interval. Keeping this number low will increase CPU usage. +# +# Variable: AIRFLOW__SCHEDULER__MIN_FILE_PROCESS_INTERVAL +# +min_file_process_interval = 30 + +# How often (in seconds) to check for stale DAGs (DAGs which are no longer present in +# the expected files) which should be deactivated, as well as datasets that are no longer +# referenced and should be marked as orphaned. +# +# Variable: AIRFLOW__SCHEDULER__PARSING_CLEANUP_INTERVAL +# +parsing_cleanup_interval = 60 + +# How long (in seconds) to wait after we have re-parsed a DAG file before deactivating stale +# DAGs (DAGs which are no longer present in the expected files). The reason why we need +# this threshold is to account for the time between when the file is parsed and when the +# DAG is loaded. The absolute maximum that this could take is ``[core] dag_file_processor_timeout``, +# but when you have a long timeout configured, it results in a significant delay in the +# deactivation of stale dags. +# +# Variable: AIRFLOW__SCHEDULER__STALE_DAG_THRESHOLD +# +stale_dag_threshold = 50 + +# How often (in seconds) to scan the DAGs directory for new files. Default to 5 minutes. +# +# Variable: AIRFLOW__SCHEDULER__DAG_DIR_LIST_INTERVAL +# +dag_dir_list_interval = 300 + +# How often should stats be printed to the logs. Setting to 0 will disable printing stats +# +# Variable: AIRFLOW__SCHEDULER__PRINT_STATS_INTERVAL +# +print_stats_interval = 30 + +# How often (in seconds) should pool usage stats be sent to StatsD (if statsd_on is enabled) +# +# Variable: AIRFLOW__SCHEDULER__POOL_METRICS_INTERVAL +# +pool_metrics_interval = 5.0 + +# If the last scheduler heartbeat happened more than ``[scheduler] scheduler_health_check_threshold`` +# ago (in seconds), scheduler is considered unhealthy. +# This is used by the health check in the **/health** endpoint and in ``airflow jobs check`` CLI +# for SchedulerJob. +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_THRESHOLD +# +scheduler_health_check_threshold = 30 + +# When you start a scheduler, airflow starts a tiny web server +# subprocess to serve a health check if this is set to ``True`` +# +# Variable: AIRFLOW__SCHEDULER__ENABLE_HEALTH_CHECK +# +enable_health_check = False + +# When you start a scheduler, airflow starts a tiny web server +# subprocess to serve a health check on this host +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_SERVER_HOST +# +scheduler_health_check_server_host = 0.0.0.0 + +# When you start a scheduler, airflow starts a tiny web server +# subprocess to serve a health check on this port +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULER_HEALTH_CHECK_SERVER_PORT +# +scheduler_health_check_server_port = 8974 + +# How often (in seconds) should the scheduler check for orphaned tasks and SchedulerJobs +# +# Variable: AIRFLOW__SCHEDULER__ORPHANED_TASKS_CHECK_INTERVAL +# +orphaned_tasks_check_interval = 300.0 + +# Determines the directory where logs for the child processes of the scheduler will be stored +# +# Variable: AIRFLOW__SCHEDULER__CHILD_PROCESS_LOG_DIRECTORY +# +child_process_log_directory = /opt/airflow/logs/scheduler + +# Local task jobs periodically heartbeat to the DB. If the job has +# not heartbeat in this many seconds, the scheduler will mark the +# associated task instance as failed and will re-schedule the task. +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULER_ZOMBIE_TASK_THRESHOLD +# +scheduler_zombie_task_threshold = 300 + +# How often (in seconds) should the scheduler check for zombie tasks. +# +# Variable: AIRFLOW__SCHEDULER__ZOMBIE_DETECTION_INTERVAL +# +zombie_detection_interval = 10.0 + +# Turn off scheduler catchup by setting this to ``False``. +# Default behavior is unchanged and +# Command Line Backfills still work, but the scheduler +# will not do scheduler catchup if this is ``False``, +# however it can be set on a per DAG basis in the +# DAG definition (catchup) +# +# Variable: AIRFLOW__SCHEDULER__CATCHUP_BY_DEFAULT +# +catchup_by_default = True + +# Setting this to ``True`` will make first task instance of a task +# ignore depends_on_past setting. A task instance will be considered +# as the first task instance of a task when there is no task instance +# in the DB with an execution_date earlier than it., i.e. no manual marking +# success will be needed for a newly added task to be scheduled. +# +# Variable: AIRFLOW__SCHEDULER__IGNORE_FIRST_DEPENDS_ON_PAST_BY_DEFAULT +# +ignore_first_depends_on_past_by_default = True + +# This changes the batch size of queries in the scheduling main loop. +# This should not be greater than ``[core] parallelism``. +# If this is too high, SQL query performance may be impacted by +# complexity of query predicate, and/or excessive locking. +# Additionally, you may hit the maximum allowable query length for your db. +# Set this to 0 to use the value of ``[core] parallelism`` +# +# Variable: AIRFLOW__SCHEDULER__MAX_TIS_PER_QUERY +# +max_tis_per_query = 16 + +# Should the scheduler issue ``SELECT ... FOR UPDATE`` in relevant queries. +# If this is set to ``False`` then you should not run more than a single +# scheduler at once +# +# Variable: AIRFLOW__SCHEDULER__USE_ROW_LEVEL_LOCKING +# +use_row_level_locking = True + +# Max number of DAGs to create DagRuns for per scheduler loop. +# +# Variable: AIRFLOW__SCHEDULER__MAX_DAGRUNS_TO_CREATE_PER_LOOP +# +max_dagruns_to_create_per_loop = 10 + +# How many DagRuns should a scheduler examine (and lock) when scheduling +# and queuing tasks. +# +# Variable: AIRFLOW__SCHEDULER__MAX_DAGRUNS_PER_LOOP_TO_SCHEDULE +# +max_dagruns_per_loop_to_schedule = 20 + +# Should the Task supervisor process perform a "mini scheduler" to attempt to schedule more tasks of the +# same DAG. Leaving this on will mean tasks in the same DAG execute quicker, but might starve out other +# dags in some circumstances +# +# Variable: AIRFLOW__SCHEDULER__SCHEDULE_AFTER_TASK_EXECUTION +# +schedule_after_task_execution = True + +# The scheduler reads dag files to extract the airflow modules that are going to be used, +# and imports them ahead of time to avoid having to re-do it for each parsing process. +# This flag can be set to ``False`` to disable this behavior in case an airflow module needs +# to be freshly imported each time (at the cost of increased DAG parsing time). +# +# Variable: AIRFLOW__SCHEDULER__PARSING_PRE_IMPORT_MODULES +# +parsing_pre_import_modules = True + +# The scheduler can run multiple processes in parallel to parse dags. +# This defines how many processes will run. +# +# Variable: AIRFLOW__SCHEDULER__PARSING_PROCESSES +# +parsing_processes = 2 + +# One of ``modified_time``, ``random_seeded_by_host`` and ``alphabetical``. +# The scheduler will list and sort the dag files to decide the parsing order. +# +# * ``modified_time``: Sort by modified time of the files. This is useful on large scale to parse the +# recently modified DAGs first. +# * ``random_seeded_by_host``: Sort randomly across multiple Schedulers but with same order on the +# same host. This is useful when running with Scheduler in HA mode where each scheduler can +# parse different DAG files. +# * ``alphabetical``: Sort by filename +# +# Variable: AIRFLOW__SCHEDULER__FILE_PARSING_SORT_MODE +# +file_parsing_sort_mode = modified_time + +# Whether the dag processor is running as a standalone process or it is a subprocess of a scheduler +# job. +# +# Variable: AIRFLOW__SCHEDULER__STANDALONE_DAG_PROCESSOR +# +standalone_dag_processor = False + +# Only applicable if ``[scheduler] standalone_dag_processor`` is true and callbacks are stored +# in database. Contains maximum number of callbacks that are fetched during a single loop. +# +# Variable: AIRFLOW__SCHEDULER__MAX_CALLBACKS_PER_LOOP +# +max_callbacks_per_loop = 20 + +# Only applicable if ``[scheduler] standalone_dag_processor`` is true. +# Time in seconds after which dags, which were not updated by Dag Processor are deactivated. +# +# Variable: AIRFLOW__SCHEDULER__DAG_STALE_NOT_SEEN_DURATION +# +dag_stale_not_seen_duration = 600 + +# Turn off scheduler use of cron intervals by setting this to ``False``. +# DAGs submitted manually in the web UI or with trigger_dag will still run. +# +# Variable: AIRFLOW__SCHEDULER__USE_JOB_SCHEDULE +# +use_job_schedule = True + +# Allow externally triggered DagRuns for Execution Dates in the future +# Only has effect if schedule_interval is set to None in DAG +# +# Variable: AIRFLOW__SCHEDULER__ALLOW_TRIGGER_IN_FUTURE +# +allow_trigger_in_future = False + +# How often to check for expired trigger requests that have not run yet. +# +# Variable: AIRFLOW__SCHEDULER__TRIGGER_TIMEOUT_CHECK_INTERVAL +# +trigger_timeout_check_interval = 15 + +# Amount of time a task can be in the queued state before being retried or set to failed. +# +# Variable: AIRFLOW__SCHEDULER__TASK_QUEUED_TIMEOUT +# +task_queued_timeout = 600.0 + +# How often to check for tasks that have been in the queued state for +# longer than ``[scheduler] task_queued_timeout``. +# +# Variable: AIRFLOW__SCHEDULER__TASK_QUEUED_TIMEOUT_CHECK_INTERVAL +# +task_queued_timeout_check_interval = 120.0 + +# The run_id pattern used to verify the validity of user input to the run_id parameter when +# triggering a DAG. This pattern cannot change the pattern used by scheduler to generate run_id +# for scheduled DAG runs or DAG runs triggered without changing the run_id parameter. +# +# Variable: AIRFLOW__SCHEDULER__ALLOWED_RUN_ID_PATTERN +# +allowed_run_id_pattern = ^[A-Za-z0-9_.~:+-]+$ + +# Whether to create DAG runs that span an interval or one single point in time for cron schedules, when +# a cron string is provided to ``schedule`` argument of a DAG. +# +# * ``True``: **CronDataIntervalTimetable** is used, which is suitable +# for DAGs with well-defined data interval. You get contiguous intervals from the end of the previous +# interval up to the scheduled datetime. +# * ``False``: **CronTriggerTimetable** is used, which is closer to the behavior of cron itself. +# +# Notably, for **CronTriggerTimetable**, the logical date is the same as the time the DAG Run will +# try to schedule, while for **CronDataIntervalTimetable**, the logical date is the beginning of +# the data interval, but the DAG Run will try to schedule at the end of the data interval. +# +# Variable: AIRFLOW__SCHEDULER__CREATE_CRON_DATA_INTERVALS +# +create_cron_data_intervals = True + +[triggerer] +# How many triggers a single Triggerer will run at once, by default. +# +# Variable: AIRFLOW__TRIGGERER__DEFAULT_CAPACITY +# +default_capacity = 1000 + +# How often to heartbeat the Triggerer job to ensure it hasn't been killed. +# +# Variable: AIRFLOW__TRIGGERER__JOB_HEARTBEAT_SEC +# +job_heartbeat_sec = 5 + +# If the last triggerer heartbeat happened more than ``[triggerer] triggerer_health_check_threshold`` +# ago (in seconds), triggerer is considered unhealthy. +# This is used by the health check in the **/health** endpoint and in ``airflow jobs check`` CLI +# for TriggererJob. +# +# Variable: AIRFLOW__TRIGGERER__TRIGGERER_HEALTH_CHECK_THRESHOLD +# +triggerer_health_check_threshold = 30 + +[kerberos] +# Location of your ccache file once kinit has been performed. +# +# Variable: AIRFLOW__KERBEROS__CCACHE +# +ccache = /tmp/airflow_krb5_ccache + +# gets augmented with fqdn +# +# Variable: AIRFLOW__KERBEROS__PRINCIPAL +# +principal = airflow + +# Determines the frequency at which initialization or re-initialization processes occur. +# +# Variable: AIRFLOW__KERBEROS__REINIT_FREQUENCY +# +reinit_frequency = 3600 + +# Path to the kinit executable +# +# Variable: AIRFLOW__KERBEROS__KINIT_PATH +# +kinit_path = kinit + +# Designates the path to the Kerberos keytab file for the Airflow user +# +# Variable: AIRFLOW__KERBEROS__KEYTAB +# +keytab = airflow.keytab + +# Allow to disable ticket forwardability. +# +# Variable: AIRFLOW__KERBEROS__FORWARDABLE +# +forwardable = True + +# Allow to remove source IP from token, useful when using token behind NATted Docker host. +# +# Variable: AIRFLOW__KERBEROS__INCLUDE_IP +# +include_ip = True + +[sensors] +# Sensor default timeout, 7 days by default (7 * 24 * 60 * 60). +# +# Variable: AIRFLOW__SENSORS__DEFAULT_TIMEOUT +# +default_timeout = 604800 + +[common.io] +# Common IO configuration section + +# Path to a location on object storage where XComs can be stored in url format. +# +# Example: xcom_objectstorage_path = s3://conn_id@bucket/path +# +# Variable: AIRFLOW__COMMON.IO__XCOM_OBJECTSTORAGE_PATH +# +xcom_objectstorage_path = + +# Threshold in bytes for storing XComs in object storage. -1 means always store in the +# database. 0 means always store in object storage. Any positive number means +# it will be stored in object storage if the size of the value is greater than the threshold. +# +# Example: xcom_objectstorage_threshold = 1000000 +# +# Variable: AIRFLOW__COMMON.IO__XCOM_OBJECTSTORAGE_THRESHOLD +# +xcom_objectstorage_threshold = -1 + +# Compression algorithm to use when storing XComs in object storage. Supported algorithms +# are a.o.: snappy, zip, gzip, bz2, and lzma. If not specified, no compression will be used. +# Note that the compression algorithm must be available in the Python installation (e.g. +# python-snappy for snappy). Zip, gz, bz2 are available by default. +# +# Example: xcom_objectstorage_compression = gz +# +# Variable: AIRFLOW__COMMON.IO__XCOM_OBJECTSTORAGE_COMPRESSION +# +xcom_objectstorage_compression = + +[fab] +# This section contains configs specific to FAB provider. + +# Boolean for enabling rate limiting on authentication endpoints. +# +# Variable: AIRFLOW__FAB__AUTH_RATE_LIMITED +# +auth_rate_limited = True + +# Rate limit for authentication endpoints. +# +# Variable: AIRFLOW__FAB__AUTH_RATE_LIMIT +# +auth_rate_limit = 5 per 40 second + +# Update FAB permissions and sync security manager roles +# on webserver startup +# +# Variable: AIRFLOW__FAB__UPDATE_FAB_PERMS +# +update_fab_perms = True + +[imap] +# Options for IMAP provider. + +# ssl_context = + +[smtp_provider] +# Options for SMTP provider. + +# ssl context to use when using SMTP and IMAP SSL connections. By default, the context is "default" +# which sets it to ``ssl.create_default_context()`` which provides the right balance between +# compatibility and security, it however requires that certificates in your operating system are +# updated and that SMTP/IMAP servers of yours have valid certificates that have corresponding public +# keys installed on your machines. You can switch it to "none" if you want to disable checking +# of the certificates, but it is not recommended as it allows MITM (man-in-the-middle) attacks +# if your infrastructure is not sufficiently secured. It should only be set temporarily while you +# are fixing your certificate configuration. This can be typically done by upgrading to newer +# version of the operating system you run Airflow components on,by upgrading/refreshing proper +# certificates in the OS or by updating certificates for your mail servers. +# +# If you do not set this option explicitly, it will use Airflow "email.ssl_context" configuration, +# but if this configuration is not present, it will use "default" value. +# +# Example: ssl_context = default +# +# Variable: AIRFLOW__SMTP_PROVIDER__SSL_CONTEXT +# +# ssl_context = + +# Allows overriding of the standard templated email subject line when the SmtpNotifier is used. +# Must provide a path to the template. +# +# Example: templated_email_subject_path = path/to/override/email_subject.html +# +# Variable: AIRFLOW__SMTP_PROVIDER__TEMPLATED_EMAIL_SUBJECT_PATH +# +# templated_email_subject_path = + +# Allows overriding of the standard templated email path when the SmtpNotifier is used. Must provide +# a path to the template. +# +# Example: templated_html_content_path = path/to/override/email.html +# +# Variable: AIRFLOW__SMTP_PROVIDER__TEMPLATED_HTML_CONTENT_PATH +# +# templated_html_content_path = + +processor_log_folder = /opt/airflow/logs/scheduler diff --git a/AgCloud/airflow_bundle/leaf-pipeline/airflow/dags/configs/config.docker.yaml b/AgCloud/airflow_bundle/leaf-pipeline/airflow/dags/configs/config.docker.yaml new file mode 100644 index 000000000..465a10ca7 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/airflow/dags/configs/config.docker.yaml @@ -0,0 +1,67 @@ +# io: +# # IMPORTANT: use the Docker service name of Postgres (from your compose): +# postgres_url: "postgresql+psycopg2://missions_user:pg123@postgres:5432/missions_db" +io: + postgres_url: "postgresql+psycopg2://postgres:postgres@agcloud-postgres:5432/postgres" + + +windows: + frequency: "D" + timezone: "UTC" + +source_mapping: + entity_dim: "mission" # or "region"/"device" + area_strategy: "none" # or "region_area" (requires regions table/geom) + filters: + start_time: null + end_time: null + anomaly_codes: null + +baseline: + method: "median" + lookback_periods: 28 + min_history: 7 + seasonality: null + +rules: + count_anomaly: + enabled: true + method: "zscore" + z_threshold: 3.0 + iqr_k: 1.5 + min_count: 3 + worsening: + enabled: true + method: "slope" + slope_lookback: 7 + slope_min: 0.02 + min_periods: 5 + ewma_span: 7 + ewma_threshold: 0.6 + +alerting: + dedup_cooldown_windows: 3 + resolve_after_no_anomaly: 3 + rate_limit_per_run: 100 + group_by_window: true + +delivery: + slack: + enabled: false + webhook_url: "" + webhook: + enabled: false + url: "" + headers: {} + email: + enabled: false + smtp_host: "" + smtp_port: 587 + username: "" + password_env: "SMTP_PASSWORD" + from_addr: "" + to_addrs: [] + +run: + dry_run: false + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/airflow/dags/leaf_pipeline_dag.py b/AgCloud/airflow_bundle/leaf-pipeline/airflow/dags/leaf_pipeline_dag.py new file mode 100644 index 000000000..29d7896cf --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/airflow/dags/leaf_pipeline_dag.py @@ -0,0 +1,536 @@ +from __future__ import annotations +from datetime import datetime +import pendulum +from airflow import DAG +from airflow.operators.bash import BashOperator +from airflow.providers.docker.operators.docker import DockerOperator + + + +PROJECT_ROOT = "/opt/leaf-pipeline/projects/leaf-counting" + +PYTHON_BIN = "python" +WEIGHTS = f"{PROJECT_ROOT}/weights/best.pt" + + +OUT_RUN = f"{PROJECT_ROOT}/runs_local/airflow_run" +STAGING_DIR = "/opt/airflow/staging/input" + +tz = pendulum.timezone("Asia/Jerusalem") + +with DAG( + dag_id="leaf_pipeline_v2", + start_date=datetime(2025, 10, 1, tzinfo=tz), + schedule=None, + catchup=False, + default_args={"owner": "leafcounting", "retries": 0}, + tags=["leaf-counting", "detect", "pwb", "crop", "minio"], +) as dag: + + + RUN_ID_DATE = "{{ dag_run.conf.get('run_id') or logical_date.in_timezone('Asia/Jerusalem').strftime('%Y/%m/%d/%H%M') }}" + + # ----------------------------- + # STAGE INPUT + # ----------------------------- + stage_input = BashOperator( + task_id="stage_input", + bash_command=""" +set -euo pipefail +python -m pip install --no-cache-dir -q \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org \ + awscli \ +|| apt-get update && apt-get install -y -qq ca-certificates awscli \ +|| python -m pip install --no-cache-dir -q \ + --index-url http://pypi.org/simple \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org --trusted-host pypi.python.org \ + awscli +STAGING_DIR='{{ params.staging_dir }}' +INPUT_MODE='minio' +mkdir -p "$STAGING_DIR"; rm -rf "$STAGING_DIR"/* +if [ "$INPUT_MODE" = 'minio' ]; then + SRC_BUCKET='{{ dag_run.conf.get("src_bucket", var.value.leaf_minio_bucket | default("imagery")) }}' + SRC_PREFIX='leaves/examples' + ENDPOINT_URL='{{ conn.minio_s3.extra_dejson.endpoint_url | default("http://host.docker.internal:9001") }}' + export AWS_ACCESS_KEY_ID='{{ conn.minio_s3.login }}' + export AWS_SECRET_ACCESS_KEY='{{ conn.minio_s3.password }}' + export AWS_DEFAULT_REGION='{{ conn.minio_s3.extra_dejson.region_name or "us-east-1" }}' + export AWS_S3_FORCE_PATH_STYLE=true + export AWS_EC2_METADATA_DISABLED=true + echo "[stage] source=minio s3://$SRC_BUCKET/$SRC_PREFIX -> $STAGING_DIR (endpoint=$ENDPOINT_URL)" + python -m awscli s3 sync "s3://$SRC_BUCKET/$SRC_PREFIX" "$STAGING_DIR" --endpoint-url "$ENDPOINT_URL" +else + INPUT_DIR='{{ params.project_root }}/demo_images' + echo "[stage] source=local $INPUT_DIR -> $STAGING_DIR" + rsync -a --delete "$INPUT_DIR"/ "$STAGING_DIR"/ +fi +""", + params={"staging_dir": STAGING_DIR, "project_root": PROJECT_ROOT}, + env={"PYTHONUNBUFFERED": "1"}, + ) + + # ----------------------------- + # DETECT -> imagery/leaves///
//detect/ + # ----------------------------- + detect = BashOperator( + task_id="detect", + bash_command=""" +set -euo pipefail + +PROJECT_ROOT='{{ params.project_root }}' +PY='{{ params.python_bin }}'; if ! command -v "$PY" >/dev/null 2>&1; then PY='python'; fi + +export PYTHONEXECUTABLE="$PY" + +INPUT_DIR='{{ params.staging_dir }}' +OUT_LOCAL_DET='{{ params.out_run }}/detect' +WEIGHTS='{{ params.weights }}' + +DATE_ONLY='{{ dag_run.conf.get("run_id") or logical_date.in_timezone("Asia/Jerusalem").strftime("%Y/%m/%d/%H%M") }}' + +DEST_PREFIX="leaves/${DATE_ONLY}/detect" + +# MinIO: +# ל-SDK (minio-py) +ENDPOINT_HOSTPORT='{{ (conn.minio_s3.host or "host.docker.internal") }}:{{ (conn.minio_s3.port or 9001) }}' +# ל-awscli צריך URL מלא: +ENDPOINT_URL='{{ conn.minio_s3.extra_dejson.endpoint_url | default("http://host.docker.internal:9001") }}' +BUCKET='{{ var.value.leaf_minio_bucket | default("imagery") }}' +export AWS_ACCESS_KEY_ID='{{ conn.minio_s3.login }}' +export AWS_SECRET_ACCESS_KEY='{{ conn.minio_s3.password }}' +export AWS_DEFAULT_REGION='us-east-1' +export AWS_S3_FORCE_PATH_STYLE=true + +# mkdir -p "$OUT_LOCAL_DET" +OUT_LOCAL_DET='{{ params.out_run }}/detect' +mkdir -p "$OUT_LOCAL_DET" +rm -rf "$OUT_LOCAL_DET"/* 2>/dev/null || true +cd "$PROJECT_ROOT" +$PY src/detect_only.py \ + --input "$INPUT_DIR" \ + --out "$OUT_LOCAL_DET" \ + --weights "$WEIGHTS" \ + --conf 0.25 --imgsz 896 --device cpu \ + --run-id "detect" + +# יישור קו לנתיב המדויק: +pip install -q awscli || true +python -m awscli s3 sync "$OUT_LOCAL_DET"/ "s3://$BUCKET/$DEST_PREFIX/" --endpoint-url "$ENDPOINT_URL" +python -m awscli s3 ls "s3://$BUCKET/$DEST_PREFIX/" --recursive --endpoint-url "$ENDPOINT_URL" || true +""", + params={ + "project_root": PROJECT_ROOT, + # "python_bin": PYTHON_BIN, + "python_bin": "/usr/local/bin/python", + "staging_dir": STAGING_DIR, + "out_run": OUT_RUN, + "weights": WEIGHTS, + "run_id_date": RUN_ID_DATE, + }, + env={"PYTHONUNBUFFERED": "1"}, + ) + + # ----------------------------- + # PREDICT_PWB -> imagery/leaves///
//pwb/ + # ----------------------------- + pwb = BashOperator( + task_id="predict_pwb", + bash_command=""" +set -euo pipefail + +PROJECT_ROOT='{{ params.project_root }}' +PY='{{ params.python_bin }}'; if ! command -v "$PY" >/dev/null 2>&1; then PY='python'; fi +INPUT_DIR='{{ params.staging_dir }}' +OUT_LOCAL_PWB='{{ params.out_run }}/pwb' +WEIGHTS='{{ params.weights }}' + +DATE_ONLY='{{ dag_run.conf.get("run_id") or logical_date.in_timezone("Asia/Jerusalem").strftime("%Y/%m/%d/%H%M") }}' + +DEST_PREFIX="leaves/${DATE_ONLY}/pwb" + +ENDPOINT_HOSTPORT='{{ (conn.minio_s3.host or "host.docker.internal") }}:{{ (conn.minio_s3.port or 9001) }}' +ENDPOINT_URL='{{ conn.minio_s3.extra_dejson.endpoint_url | default("http://host.docker.internal:9001") }}' +BUCKET='{{ var.value.leaf_minio_bucket | default("imagery") }}' +export AWS_ACCESS_KEY_ID='{{ conn.minio_s3.login }}' +export AWS_SECRET_ACCESS_KEY='{{ conn.minio_s3.password }}' +export AWS_DEFAULT_REGION='us-east-1' +export AWS_S3_FORCE_PATH_STYLE=true + +mkdir -p "$OUT_LOCAL_PWB" + +cd "$PROJECT_ROOT" +$PY src/predict_pyramid_wbf.py \ + --input "$INPUT_DIR" \ + --out "$OUT_LOCAL_PWB" \ + --weights "$WEIGHTS" \ + --scales 0.75,1.0,1.25 --conf 0.25 --iou 0.55 --imgsz 896 --device cpu \ + --run-id "pwb" + +pip install -q awscli || true +python -m awscli s3 sync "$OUT_LOCAL_PWB"/ "s3://$BUCKET/$DEST_PREFIX/" --endpoint-url "$ENDPOINT_URL" +python -m awscli s3 ls "s3://$BUCKET/$DEST_PREFIX/" --recursive --endpoint-url "$ENDPOINT_URL" || true +""", + params={ + "project_root": PROJECT_ROOT, + # "python_bin": PYTHON_BIN, + "python_bin": "/usr/local/bin/python", + + "staging_dir": STAGING_DIR, + "out_run": OUT_RUN, + "weights": WEIGHTS, + "run_id_date": RUN_ID_DATE, + }, + env={"PYTHONUNBUFFERED": "1"}, + ) + + + crop = BashOperator( + task_id="crop", + bash_command=""" + set -euo pipefail + + PROJECT_ROOT='{{ params.project_root }}' + PY='{{ params.python_bin }}'; if ! command -v "$PY" >/dev/null 2>&1; then PY='python'; fi + OUT_LOCAL_CROP='{{ params.out_run }}/crop' + PWB_LOCAL='{{ params.out_run }}/pwb' + RUN_ID_DATE='{{ dag_run.conf.get("run_id") or logical_date.in_timezone("Asia/Jerusalem").strftime("%Y/%m/%d/%H%M") }}' + + RUN_ID_CROP="${RUN_ID_DATE}/crop" + + ENDPOINT_URL='{{ conn.minio_s3.extra_dejson.endpoint_url | default("http://host.docker.internal:9001") }}' + BUCKET='{{ var.value.leaf_minio_bucket | default("imagery") }}' + export AWS_ACCESS_KEY_ID='{{ conn.minio_s3.login }}' + export AWS_SECRET_ACCESS_KEY='{{ conn.minio_s3.password }}' + export AWS_DEFAULT_REGION='us-east-1' + export AWS_S3_FORCE_PATH_STYLE=true + + + export DEVICE_ID="${DEVICE_ID:-dev1}" + + mkdir -p "$OUT_LOCAL_CROP" + + # 1)crop + if [ -f "$PROJECT_ROOT/src/crop_only.py" ]; then + cd "$PROJECT_ROOT" + $PY src/crop_only.py --input "$PWB_LOCAL" --out "$OUT_LOCAL_CROP" + elif [ -f "$PROJECT_ROOT/src/crop_from_meta.py" ]; then + cd "$PROJECT_ROOT" + $PY src/crop_from_meta.py --input "$PWB_LOCAL" --out "$OUT_LOCAL_CROP" + else + echo "[crop] No crop script found; will only sync if $OUT_LOCAL_CROP has files." + fi + + # 2) (: _TZ[ _suffix].ext) + + python -m pip install -q pillow piexif || true + export OUT_LOCAL_CROP + python - <<'PY' +import os, re, sys, time +from datetime import datetime, timezone +OUT = os.environ.get("OUT_LOCAL_CROP", "") +DEVICE = os.environ.get("DEVICE_ID", "dev1") + +if not OUT or not os.path.isdir(OUT): + sys.exit(0) + +IMG_EXT = {".jpg",".jpeg",".png",".webp",".tif",".tiff",".bmp"} +iso_re = re.compile(r"^[A-Za-z0-9\-]+_\d{8}T\d{6}Z(?:[ _][^/\\\\]+)?\\.[A-Za-z0-9]+$") + +def get_ts_from_exif(path): + try: + import piexif + from PIL import Image + with Image.open(path) as im: + exif = im.info.get("exif") + if not exif: + return None + exif_dict = piexif.load(exif) + dt = exif_dict["Exif"].get(piexif.ExifIFD.DateTimeOriginal) or \ + exif_dict["Exif"].get(piexif.ExifIFD.DateTimeDigitized) or \ + exif_dict["0th"].get(piexif.ImageIFD.DateTime) + if not dt: + return None + # EXIF: "YYYY:MM:DD HH:MM:SS" + s = dt.decode() if isinstance(dt, bytes) else dt + dt_obj = datetime.strptime(s, "%Y:%m:%d %H:%M:%S").replace(tzinfo=timezone.utc) + return dt_obj + except Exception: + return None + +def ts_for_file(path): + dt = get_ts_from_exif(path) + if dt is None: + # fallback: mtime כ-UTC + mt = os.path.getmtime(path) + dt = datetime.fromtimestamp(mt, tz=timezone.utc) + return dt + +renamed = 0 +skipped = 0 +for root, _, files in os.walk(OUT): + for f in files: + ext = os.path.splitext(f)[1].lower() + if ext not in IMG_EXT: + continue + if iso_re.match(f): + skipped += 1 + continue + old = os.path.join(root, f) + dt = ts_for_file(old) + ts = dt.strftime("%Y%m%dT%H%M%SZ") + # suffix + base = os.path.splitext(f)[0] + suffix = "" + if base and base.lower() not in {"img","image","photo","dsc","dscn"}: + + cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", base).strip("-_.") + if cleaned and cleaned != ts: + suffix = f"_{cleaned}" + new_name = f"{DEVICE}_{ts}{suffix}{ext}" + new = os.path.join(root, new_name) + if new == old: + skipped += 1 + continue + + i = 1 + new_final = new + while os.path.exists(new_final): + new_final = os.path.join(root, f"{os.path.splitext(new_name)[0]}_{i}{ext}") + i += 1 + os.rename(old, new_final) + print(f"[crop][rename] {f} -> {os.path.basename(new_final)}") + renamed += 1 + +print(f"[crop][rename] done: renamed={renamed}, already_ok={skipped}") +PY + + # 3)MinIO + pip install -q awscli || true + if [ -d "$OUT_LOCAL_CROP" ] && [ "$(ls -A "$OUT_LOCAL_CROP" || true)" ]; then + python -m awscli s3 sync "$OUT_LOCAL_CROP"/ "s3://$BUCKET/leaves/$RUN_ID_CROP/" --endpoint-url "$ENDPOINT_URL" + else + echo "[crop] WARNING: no local crops found to upload." + fi + + python -m awscli s3 ls "s3://$BUCKET/leaves/$RUN_ID_CROP/" --recursive --endpoint-url "$ENDPOINT_URL" || true + """, + params={ + "project_root": PROJECT_ROOT, + "python_bin": PYTHON_BIN, + "out_run": OUT_RUN, + "run_id_date": RUN_ID_DATE, + }, + env={"PYTHONUNBUFFERED": "1"}, +) + + + detection_jobs = DockerOperator( + task_id="detection_jobs", + image="detection-jobs:cpu-lts", + docker_url="unix://var/run/docker.sock", + api_version="auto", + auto_remove=True, + mount_tmp_dir=False, + working_dir="/app", + network_mode="ag_cloud", + environment={ + "MINIO_ENDPOINT": "{{ conn.minio_s3.extra_dejson.endpoint_url | default('http://minio-hot:9001') }}", + "AWS_ACCESS_KEY_ID": "{{ conn.minio_s3.login }}", + "AWS_SECRET_ACCESS_KEY": "{{ conn.minio_s3.password }}", + "AWS_S3_FORCE_PATH_STYLE": "true", + "AWS_DEFAULT_REGION": "us-east-1", + "DATABASE_URL": "postgresql+psycopg2://missions_user:pg123@postgres:5432/missions_db", + "USER": "root", + "HOME": "/root", + }, + command=[ + "/bin/bash","-lc", r''' +set -euo pipefail +echo "[DJ] START"; whoami; pwd; python3 -V +python3 -m pip install --no-cache-dir -q awscli || true + +RID='{{ dag_run.conf.get("run_id") or logical_date.in_timezone("Asia/Jerusalem").strftime("%Y/%m/%d/%H%M") }}' +export MINIO_RID="$RID" +echo "[DJ] MINIO_RID=$MINIO_RID" +BUCKET='{{ var.value.leaf_minio_bucket | default("imagery") }}' +SRC="s3://${BUCKET}/leaves/${RID}/crop/" +ENDPOINT="${MINIO_ENDPOINT:-http://minio-hot:9001}" + +mkdir -p /work/in /work/out +echo "[DJ] sync from ${SRC} via ${ENDPOINT}" +python3 -m awscli s3 cp --recursive "$SRC" /work/in --endpoint-url "$ENDPOINT" || true + +IN_DIR="/work/in" +READY_DIR="/work/in_ready" +DEVICE_ID="${DEVICE_ID:-dev1}" +rm -rf "$READY_DIR" && mkdir -p "$READY_DIR" + +while IFS= read -r -d '' f; do + base="$(basename "$f")" + stem="$(printf '%s\n' "$base" | sed -n 's/^\([A-Za-z0-9-]\+_[0-9]\{8\}T[0-9]\{6\}Z\).*/\1/p')" + [ -n "$stem" ] || { echo "[DJ][skip] no stem in $base"; continue; } + outdir="$READY_DIR/$stem" + mkdir -p "$outdir" + cp -p "$f" "$outdir/$base" +done < <(find "$IN_DIR" -type f -print0) + +echo "[DJ][ready] tree under: $READY_DIR" +FLAT_DIR="/work/in_flat" +rm -rf "$FLAT_DIR" && mkdir -p "$FLAT_DIR" + +MANIFEST="$FLAT_DIR/_origin_map.tsv" +: > "$MANIFEST" + +find "$IN_DIR" -type f \( -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.png' -o -iname '*.webp' -o -iname '*.tif' -o -iname '*.tiff' -o -iname '*.bmp' \) -print0 \ +| while IFS= read -r -d '' f; do + base="$(basename "$f")" + parent="$(basename "$(dirname "$f")")" + out="$FLAT_DIR/$base"; i=1 + while [ -e "$out" ]; do + ext="${base##*.}"; stem="${base%.*}" + out="$FLAT_DIR/${stem}_$i.$ext"; i=$((i+1)) + done + cp -p "$f" "$out" + printf '%s\t%s\n' "$(basename "$out")" "$parent" >> "$MANIFEST" +done + +export ORIGIN_MANIFEST="$MANIFEST" +echo "[DJ] origin manifest at: $ORIGIN_MANIFEST (lines: $(wc -l < "$MANIFEST")))" + +ls -1 "$FLAT_DIR" | sed -n '1,50p' +export INPUT_DIR_FOR_RUNNER="$FLAT_DIR" +# === DB bootstrap: ensure required table exists === +python3 - <<'PY' +import os +from sqlalchemy import create_engine, text + + +ddl = """ +CREATE TABLE IF NOT EXISTS public.leaf_disease_types ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL +); + +CREATE TABLE IF NOT EXISTS public.leaf_reports ( + id BIGSERIAL PRIMARY KEY, + device_id TEXT NOT NULL, + leaf_disease_type_id INTEGER NOT NULL REFERENCES public.leaf_disease_types(id) ON DELETE RESTRICT, + ts TIMESTAMPTZ NOT NULL, + confidence DOUBLE PRECISION NOT NULL, + sick BOOLEAN NOT NULL +); + +-- אינדקסים קלים לשאילתות/דשבורדים +CREATE INDEX IF NOT EXISTS ix_leaf_reports_ts ON public.leaf_reports (ts); +CREATE INDEX IF NOT EXISTS ix_leaf_reports_type ON public.leaf_reports (leaf_disease_type_id); +CREATE INDEX IF NOT EXISTS ix_leaf_reports_device_ts ON public.leaf_reports (device_id, ts); +""" + +url = os.environ["DATABASE_URL"] +eng = create_engine(url, future=True) +with eng.begin() as conn: + conn.execute(text(ddl)) +print("[DJ][db] ensured table public.leaf_disease_types") +PY + + +export PYTHONPATH=/app +python3 - <<'PY' +import os, sys, importlib +os.environ.setdefault("DATABASE_URL", os.environ.get("DATABASE_URL","")) +inp = os.environ.get('INPUT_DIR_FOR_RUNNER','/work/in_flat') +print("[DJ] runner input dir:", inp) + +try: + files = [f for f in os.listdir(inp) if os.path.isfile(os.path.join(inp,f))] + print(f"[DJ] flat file count: {len(files)}") + for f in files[:10]: + print("[DJ] sample:", f) +except Exception as e: + print("[DJ] listdir failed:", e) + +mod = importlib.import_module('agri_baseline.src.batch_runner') +sys.argv = ['batch_runner.py', '--input', inp, '--mission','1'] +exit_code = 0 +try: + mod.main() +except SystemExit as e: + exit_code = int(e.code) if isinstance(e.code, int) else 1 +sys.exit(exit_code) +PY + +echo "[DJ] DONE" + ''' + ], +) + + + + disease_monitor = DockerOperator( + task_id="disease_monitor", + image="disease-monitor:cpu-lts", + entrypoint=["/bin/sh","-c"], + command=[r''' +set -eu +echo "[DM] START"; whoami; pwd; python3 -V || true + +echo "[DM][env] DATABASE_URL=$DATABASE_URL" +echo "[DM][env] Dropping PG* env if present (to avoid overrides)" +unset PGHOST PGPORT PGDATABASE PGPASSWORD PGUSER 2>/dev/null || true + +# ---- DDL via DATABASE_URL only ---- +python3 - <<'PY' +import os, sys, time +import psycopg2 + +DDL = """ +CREATE TABLE IF NOT EXISTS alerts_leaves ( + id bigserial PRIMARY KEY, + entity_id text NOT NULL, + rule text NOT NULL, + window_start timestamptz NOT NULL, + window_end timestamptz NOT NULL, + score double precision NOT NULL, + first_seen timestamptz NOT NULL, + last_seen timestamptz NOT NULL, + status text NOT NULL CHECK (status IN ('OPEN','ACK','RESOLVED')), + meta_json jsonb +); +CREATE INDEX IF NOT EXISTS ix_alerts_leaves_entity_rule ON alerts_leaves(entity_id, rule); +CREATE INDEX IF NOT EXISTS ix_alerts_leaves_status ON alerts_leaves(status); +""" + +dsn = os.environ["DATABASE_URL"].replace("postgresql+psycopg2://", "postgresql://", 1) +print("[DM][db] Using DSN:", dsn.replace(os.environ.get("DATABASE_URL",""), "***redacted***")) + +for i in range(12): + try: + with psycopg2.connect(dsn, connect_timeout=4) as conn: + with conn.cursor() as cur: + cur.execute(DDL) + print("[DM][db] DDL applied OK.") + break + except Exception as e: + print(f"[DM][db] retry {i+1}/12: {e}") + time.sleep(5) +else: + sys.exit("[DM][db] DDL failed after retries") +PY + +exec python -m disease_monitor.cli --config /app/configs/config.docker.yaml --log-level INFO + +'''], + environment={ + "DATABASE_URL": "postgresql://missions_user:pg123@postgres:5432/missions_db", + }, + working_dir="/app", + docker_url="unix://var/run/docker.sock", + api_version="auto", + auto_remove=True, + network_mode="ag_cloud", + dag=dag, +) + + + + stage_input >> detect >> pwb >> crop >> detection_jobs>>disease_monitor diff --git a/AgCloud/airflow_bundle/leaf-pipeline/airflow/webserver_config.py b/AgCloud/airflow_bundle/leaf-pipeline/airflow/webserver_config.py new file mode 100644 index 000000000..3048bb21f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/airflow/webserver_config.py @@ -0,0 +1,132 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Default configuration for the Airflow webserver.""" + +from __future__ import annotations + +import os + +from flask_appbuilder.const import AUTH_DB + +# from airflow.www.fab_security.manager import AUTH_LDAP +# from airflow.www.fab_security.manager import AUTH_OAUTH +# from airflow.www.fab_security.manager import AUTH_OID +# from airflow.www.fab_security.manager import AUTH_REMOTE_USER + + +basedir = os.path.abspath(os.path.dirname(__file__)) + +# Flask-WTF flag for CSRF +WTF_CSRF_ENABLED = True +WTF_CSRF_TIME_LIMIT = None + +# ---------------------------------------------------- +# AUTHENTICATION CONFIG +# ---------------------------------------------------- +# For details on how to set up each of the following authentication, see +# http://flask-appbuilder.readthedocs.io/en/latest/security.html# authentication-methods +# for details. + +# The authentication type +# AUTH_OID : Is for OpenID +# AUTH_DB : Is for database +# AUTH_LDAP : Is for LDAP +# AUTH_REMOTE_USER : Is for using REMOTE_USER from web server +# AUTH_OAUTH : Is for OAuth +AUTH_TYPE = AUTH_DB + +# Uncomment to setup Full admin role name +# AUTH_ROLE_ADMIN = 'Admin' + +# Uncomment and set to desired role to enable access without authentication +# AUTH_ROLE_PUBLIC = 'Viewer' + +# Will allow user self registration +# AUTH_USER_REGISTRATION = True + +# The recaptcha it's automatically enabled for user self registration is active and the keys are necessary +# RECAPTCHA_PRIVATE_KEY = PRIVATE_KEY +# RECAPTCHA_PUBLIC_KEY = PUBLIC_KEY + +# Config for Flask-Mail necessary for user self registration +# MAIL_SERVER = 'smtp.gmail.com' +# MAIL_USE_TLS = True +# MAIL_USERNAME = 'yourappemail@gmail.com' +# MAIL_PASSWORD = 'passwordformail' +# MAIL_DEFAULT_SENDER = 'sender@gmail.com' + +# The default user self registration role +# AUTH_USER_REGISTRATION_ROLE = "Public" + +# When using OAuth Auth, uncomment to setup provider(s) info +# Google OAuth example: +# OAUTH_PROVIDERS = [{ +# 'name':'google', +# 'token_key':'access_token', +# 'icon':'fa-google', +# 'remote_app': { +# 'api_base_url':'https://www.googleapis.com/oauth2/v2/', +# 'client_kwargs':{ +# 'scope': 'email profile' +# }, +# 'access_token_url':'https://accounts.google.com/o/oauth2/token', +# 'authorize_url':'https://accounts.google.com/o/oauth2/auth', +# 'request_token_url': None, +# 'client_id': GOOGLE_KEY, +# 'client_secret': GOOGLE_SECRET_KEY, +# } +# }] + +# When using LDAP Auth, setup the ldap server +# AUTH_LDAP_SERVER = "ldap://ldapserver.new" + +# When using OpenID Auth, uncomment to setup OpenID providers. +# example for OpenID authentication +# OPENID_PROVIDERS = [ +# { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, +# { 'name': 'AOL', 'url': 'http://openid.aol.com/' }, +# { 'name': 'Flickr', 'url': 'http://www.flickr.com/' }, +# { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }] + +# ---------------------------------------------------- +# Theme CONFIG +# ---------------------------------------------------- +# Flask App Builder comes up with a number of predefined themes +# that you can use for Apache Airflow. +# http://flask-appbuilder.readthedocs.io/en/latest/customizing.html#changing-themes +# Please make sure to remove "navbar_color" configuration from airflow.cfg +# in order to fully utilize the theme. (or use that property in conjunction with theme) +# APP_THEME = "bootstrap-theme.css" # default bootstrap +# APP_THEME = "amelia.css" +# APP_THEME = "cerulean.css" +# APP_THEME = "cosmo.css" +# APP_THEME = "cyborg.css" +# APP_THEME = "darkly.css" +# APP_THEME = "flatly.css" +# APP_THEME = "journal.css" +# APP_THEME = "lumen.css" +# APP_THEME = "paper.css" +# APP_THEME = "readable.css" +# APP_THEME = "sandstone.css" +# APP_THEME = "simplex.css" +# APP_THEME = "slate.css" +# APP_THEME = "solar.css" +# APP_THEME = "spacelab.css" +# APP_THEME = "superhero.css" +# APP_THEME = "united.css" +# APP_THEME = "yeti.css" diff --git a/AgCloud/airflow_bundle/leaf-pipeline/dags/configs/disease_monitor.yaml b/AgCloud/airflow_bundle/leaf-pipeline/dags/configs/disease_monitor.yaml new file mode 100644 index 000000000..832d6daf7 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/dags/configs/disease_monitor.yaml @@ -0,0 +1,68 @@ +io: + # IMPORTANT: use the Docker service name of Postgres (from your compose): + postgres_url: "postgresql+psycopg2://missions_user:pg123@postgres:5432/missions_db" + +windows: + frequency: "D" + timezone: "UTC" + +source_mapping: + entity_dim: "device" + area_strategy: "none" # or "region_area" (requires regions table/geom) + filters: + start_time: null + end_time: null + anomaly_codes: null + +baseline: + method: "median" + lookback_periods: 28 + min_history: 7 + seasonality: null + +rules: + count_anomaly: + enabled: true + method: "zscore" + z_threshold: 3.0 + iqr_k: 1.5 + min_count: 3 + worsening: + enabled: true + method: "slope" + slope_lookback: 7 + slope_min: 0.02 + min_periods: 5 + ewma_span: 7 + ewma_threshold: 0.6 + +alerting: + dedup_cooldown_windows: 3 + resolve_after_no_anomaly: 3 + rate_limit_per_run: 100 + group_by_window: true + +delivery: + kafka: + enabled: false + brokers: "kafka:9092" + topic: "alerts" + slack: + enabled: false + webhook_url: "" + webhook: + enabled: false + url: "" + headers: {} + email: + enabled: false + smtp_host: "" + smtp_port: 587 + username: "" + password_env: "SMTP_PASSWORD" + from_addr: "" + to_addrs: [] + +run: + dry_run: false + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/docker-compose.yml b/AgCloud/airflow_bundle/leaf-pipeline/docker-compose.yml new file mode 100644 index 000000000..5ededec6f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/docker-compose.yml @@ -0,0 +1,73 @@ + +version: "3.8" + +x-airflow-common: &airflow-common + image: leaf-airflow:2.9.3-fixed + build: + context: . + dockerfile: Dockerfile + environment: + AIRFLOW__CORE__LOAD_EXAMPLES: "False" + AIRFLOW__CORE__EXECUTOR: "SequentialExecutor" + AIRFLOW_HOME: /opt/airflow + LEAF_MINIO_ENDPOINT: "http://minio-hot:9000" + volumes: + - ./airflow:/opt/airflow + - ./projects/leaf-counting:/opt/leaf-pipeline/projects/leaf-counting + - /var/run/docker.sock:/var/run/docker.sock + user: "${AIRFLOW_UID:-50000}:${AIRFLOW_GID:-0}" + restart: unless-stopped + +services: + # --- Build-only images for DockerOperator tasks (exit 0 right away) --- + build_detection_jobs: + profiles: ["images"] + image: detection-jobs:cpu-lts + build: + context: ./projects/Detection_Jobs/Detection_Jobs + dockerfile: dockerfile + command: ["sh", "-c", "echo built detection-jobs && true"] + restart: "no" + networks: [agcloud_ag_cloud] + + build_disease_monitor: + profiles: ["images"] + image: disease-monitor:cpu-lts + build: + context: ./projects/disease-monitor/disease-monitor + dockerfile: Dockerfile + entrypoint: ["/bin/sh", "-c"] + command: ["sh", "-c", "echo built disease-monitor && true"] + restart: "no" + networks: [agcloud_ag_cloud] + + # --- Airflow runtime --- + scheduler: + <<: *airflow-common + command: ["airflow", "scheduler"] + user: "0:0" + depends_on: + build_detection_jobs: + condition: service_completed_successfully + build_disease_monitor: + condition: service_completed_successfully + networks: [agcloud_ag_cloud] + + webserver: + <<: *airflow-common + command: ["airflow", "webserver"] + user: "0:0" + ports: + - "8081:8080" + depends_on: + scheduler: + condition: service_started + build_detection_jobs: + condition: service_completed_successfully + build_disease_monitor: + condition: service_completed_successfully + networks: [agcloud_ag_cloud] + +networks: + agcloud_ag_cloud: + external: true diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/.gitignore b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/.gitignore new file mode 100644 index 000000000..06593e32e --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/.gitignore @@ -0,0 +1,55 @@ +# ==== OS / IDE ==== +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# ==== Node ==== +node_modules/ +dist/ + +# ==== Python ==== +__pycache__/ +*.py[cod] +*.pyc +*.pyo +*.so +*.dylib + +# ==== Virtual envs ==== +.venv/ +venv/ +ENV/ +env/ + +# ==== Packaging / build ==== +build/ +*.egg-info/ + +# ==== Environment / Secrets ==== +.env +.env.* + +# ==== Data / Notebooks / Logs ==== +*.log +*.ipynb +.ipynb_checkpoints/ + +# ==== Artifacts / Wheels / Models ==== +artifacts/ +.wheels/ +wheels/ +*.whl +*.pt +*.pth +*.bin + +# ==== Coverage reports ==== +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ + +# ==== gRPC generated (נוצרים בבילד דוקר) ==== +server/embed_pb2.py +server/embed_pb2_grpc.py diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.dockerignore b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.dockerignore new file mode 100644 index 000000000..641f56876 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.dockerignore @@ -0,0 +1,33 @@ +# Python cache +__pycache__/ +*.pyc +*.pyo + +# Virtual environments +.env +.venv/ +venv/ + +# IDE +.idea/ +.vscode/ + +# Node / Frontend +node_modules/ +dist/ + +# Test / Coverage +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ + +# Local databases +*.db +agri.db + +# Data outputs +data/ +data_balanced/ +data_baseline/ +*.csv diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.gitignore b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.gitignore new file mode 100644 index 000000000..b0e9b0028 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.gitignore @@ -0,0 +1,25 @@ +# === Python cache === +__pycache__/ +*.pyc +*.pyo + +# === Virtual environments === +.env +.venv/ +venv/ + +# === IDE / Editors === +.idea/ +.vscode/ + + +# === Test / Coverage === +.pytest_cache/ + +# === Local databases === +*.db +agri.db + +# === Data outputs === +data_balanced/ +data/ \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/README.md b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/README.md new file mode 100644 index 000000000..b2e46aec5 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/README.md @@ -0,0 +1,115 @@ +🌿 Agri Baseline – Disease Detection Pipeline + +This project runs an end-to-end disease detection pipeline for agricultural images. +It supports both local and MinIO-based storage backends, and processes entire folders of plant images using trained CNN models. + +🚀 Quick Start +1️⃣ Setup Environment +cp agri_baseline/.env.example agri_baseline/.env +pip install -r agri_baseline/requirements.txt + +2️⃣ Run the Pipeline + +Now the pipeline fetches images directly from MinIO, not from a local folder. + +docker compose up -d +docker compose logs -f app + + +The service automatically connects to your configured MinIO bucket, downloads the images to a cache directory, and processes them. + +3️⃣ Run Tests + +To verify the system: + +docker compose run --rm app pytest -q + +📂 Project Structure +Detection_Jobs/ +│ +├── agri_baseline/ +│ ├── scripts/ +│ │ └── run_batch.py # Run the pipeline on MinIO or local images +│ │ +│ ├── src/ +│ │ ├── detectors/ # CNN models and detectors +│ │ │ ├── base.py # Base Detector/Detection classes +│ │ │ ├── cnn_multi_classifier.py +│ │ │ ├── disease_model.py # Wraps CNN model as a Detector +│ │ │ ├── train/ +│ │ │ │ └── dictionary.py +│ │ │ +│ │ ├── pipeline/ +│ │ │ ├── config.py +│ │ │ ├── db.py # DB connection via SQLAlchemy +│ │ │ ├── logging_setup.py +│ │ │ └── utils.py # Helper functions (image loading, bbox, etc.) +│ │ │ +│ │ ├── storage/ +│ │ │ ├── minio_client.py +│ │ │ └── minio_sync.py # MinIO download helpers +│ │ │ +│ │ └── validator/ +│ │ ├── rules.py # Validation rules +│ │ └── validator.py # QA manager, writes to event logs +│ │ +│ ├── batch_runner.py # Orchestrates the full pipeline +│ ├── .env # Local config (not committed) +│ ├── .env.example # Example configuration file +│ ├── requirements.txt # Python dependencies +│ └── README.md +│ +├── models/ # Trained model weights (not in git) +│ ├── resnet18-f37072fd.pth +│ ├── cnn_multi_stage3.pth +│ └── multi_classes.pth +│ +├── docker-compose.yml # Runs pipeline + MinIO connection +├── dockerfile +├── tests/ # Unit and integration tests +│ ├── test_batch_runner.py +│ ├── test_disease_model.py +│ ├── test_run_detectors.py +│ ├── test_utils.py +│ └── test_validator.py +│ +└── ressearch/ # Experimental models and training + ├── detectors/ + │ ├── models/ + │ │ ├── cnn_binary.pth + │ │ ├── cnn_multi_finetuned.pth + │ │ └── cnn_multi.pth + │ ├── train/ + │ │ ├── disease.py + │ │ ├── eval_multi_levels.py + │ │ ├── finetune_multi_stage3.py + │ │ ├── finetune_multi.py + │ │ └── train_binary_multi.py + │ ├── cnn_binary_classifier.py + │ └── dataset_binary.py + +🧩 Models + +All trained models are stored under models/ and are not committed to Git: + +cnn_multi.pth – Base multi-class CNN + +cnn_multi_finetuned.pth – Fine-tuned on additional data + +cnn_multi_stage3.pth – Advanced fine-tuning with crop-specific data + +multi_classes.pth – Unified class mapping + +🧪 Testing + +Run all integration and unit tests using Docker: + +docker compose run --rm app pytest -q + +📌 Notes + +The pipeline now supports MinIO integration via environment variables in .env. + +Make sure your .env file includes all required MINIO_* variables (endpoint, bucket, credentials). + +Avoid committing .env or model files to the repository. \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/requirements.txt b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/requirements.txt new file mode 100644 index 000000000..8df4df0ba --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/requirements.txt @@ -0,0 +1,47 @@ +# ---------------------------- +# Core scientific stack +# ---------------------------- +numpy==1.26.4 +pandas==2.0.3 +scipy==1.12.0 + +# ---------------------------- +# Image processing +# ---------------------------- +opencv-python-headless==4.9.0.80 +Pillow==10.4.0 +albumentations==1.4.3 + +# ---------------------------- +# Database & configuration +# ---------------------------- +SQLAlchemy==1.4.52 +psycopg2-binary==2.9.9 +python-dotenv==1.0.1 +minio==7.2.9 # MinIO SDK for connecting to object storage + +# ---------------------------- +# Testing +# ---------------------------- +pytest + +# ---------------------------- +# Typing helpers +# ---------------------------- +typing-extensions>=4.9.0 +# Deep learning frameworks +torch==2.2.0 +torchvision==0.17.0 +torchaudio==2.2.0 + + +# ---------------------------- +# Training & monitoring tools +# ---------------------------- +tensorboard>=2.16 + +# ---------------------------- +# Visualization & ML utilities +# ---------------------------- +matplotlib>=3.7 +scikit-learn>=1.3 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/run_batch.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/run_batch.py new file mode 100644 index 000000000..666ed466e --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/run_batch.py @@ -0,0 +1,137 @@ +""" +run_batch.py + +Purpose: +- Run the disease-detection batch pipeline either from a LOCAL folder of images + or from a MinIO bucket (objects are first downloaded to a local cache dir, + then processed exactly like local files). + +Usage examples: +1) Local folder (backward-compatible): + python -m agri_baseline.scripts.run_batch --storage local --images ./data/images + +2) MinIO (reads config from ENV and optional CLI flags): + python -m agri_baseline.scripts.run_batch --storage minio --minio-prefix "" + +Environment variables (typical .env): +- STORAGE_BACKEND=minio|local +- MINIO_ENDPOINT=127.0.0.1:9000 +- MINIO_ACCESS_KEY=minioadmin +- MINIO_SECRET_KEY=minioadmin +- MINIO_BUCKET=leaves +- MINIO_SECURE=false +- MINIO_PREFIX=mission-123/ (optional) +- MINIO_CACHE_DIR=./data/_minio_cache +""" + +import argparse +import os +from pathlib import Path + +from agri_baseline.src.pipeline.logging_setup import setup_logging +from agri_baseline.src.pipeline import config +from agri_baseline.src.batch_runner import BatchRunner + +# MinIO helpers provided in your project +from agri_baseline.src.storage.minio_client import load_minio_config # loads config from ENV +from agri_baseline.src.storage.minio_sync import download_prefix_to_dir, ensure_bucket + + +def run_local(images_dir: Path) -> None: + """ + LOCAL mode: + - Run the batch pipeline over a local folder of images. + - This preserves the original behavior for backward compatibility. + """ + runner = BatchRunner() + runner.run_folder(images_dir) + + +def run_minio(prefix: str, cache_dir: Path) -> None: + """ + MINIO mode: + - Pull objects from a MinIO bucket (based on ENV config). + - Download them to a local cache directory. + - Run the batch pipeline over the downloaded files. + """ + cfg = load_minio_config() + ensure_bucket(cfg) # Safety: create the bucket if it doesn't exist + + cache_dir.mkdir(parents=True, exist_ok=True) + + # Download objects under 'prefix' into the local cache folder + downloaded = download_prefix_to_dir(cfg, prefix=prefix, local_dir=cache_dir) + if not downloaded: + raise SystemExit( + f"No objects found in bucket '{cfg.bucket}' with prefix '{prefix}'." + ) + + runner = BatchRunner() + runner.run_folder(cache_dir) + + +def parse_args() -> argparse.Namespace: + """ + Parse CLI arguments and provide sensible defaults from ENV where applicable. + """ + ap = argparse.ArgumentParser(description="Run batch pipeline (local/minio).") + + # Backward-compatible local images folder + ap.add_argument( + "--images", + default=config.IMAGES_DIR, + help="Folder of input images (LOCAL mode)", + ) + + # Storage backend selector + ap.add_argument( + "--storage", + choices=["local", "minio"], + default=os.getenv("STORAGE_BACKEND", "local").lower(), + help="Where to read images from (local|minio).", + ) + + # MinIO options (with ENV fallbacks) + ap.add_argument( + "--minio-prefix", + default=os.getenv("MINIO_PREFIX", ""), + help="Object prefix inside the bucket (e.g. 'mission-123/').", + ) + ap.add_argument( + "--minio-cache", + default=os.getenv("MINIO_CACHE_DIR", "./data/_minio_cache"), + help="Local temp folder used to download MinIO objects before processing.", + ) + + return ap.parse_args() + + +def main() -> None: + """ + Entry point: + - Logs chosen backend. + - Dispatches to local/minio flows. + - Keeps logs concise and informative for CI/ops. + """ + log = setup_logging() + args = parse_args() + + log.info(f"Storage backend: {args.storage}") + + if args.storage == "local": + images_dir = Path(args.images) + log.info(f"Starting batch over LOCAL folder: {images_dir}") + run_local(images_dir) + log.info("Batch done (local).") + else: + cache_dir = Path(args.minio_cache) + log.info( + "Starting batch over MINIO: " + f"bucket from ENV, prefix='{args.minio_prefix}', cache='{cache_dir}'" + ) + run_minio(prefix=args.minio_prefix, cache_dir=cache_dir) + log.info("Batch done (minio).") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/batch_runner.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/batch_runner.py new file mode 100644 index 000000000..8e4005c33 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/batch_runner.py @@ -0,0 +1,525 @@ +from __future__ import annotations + +from sqlalchemy import text +import os +import re +import json +from dataclasses import asdict, is_dataclass +from datetime import datetime, timezone, timedelta +from pathlib import Path +from typing import Tuple +from zoneinfo import ZoneInfo + +from agri_baseline.src.pipeline.utils import ( + load_image, + image_id_from_path, + clamp_bbox, +) +from agri_baseline.src.pipeline.db import ( + get_engine, +) +from agri_baseline.src.detectors.disease_model import DiseaseDetector + +# ----------------------------------- +# SQL +# ----------------------------------- + +# anomalies insert (unchanged) +INSERT_ANOMALY = text( + """ + INSERT INTO public.anomalies + (mission_id, device_id, ts, anomaly_type_id, severity, details, geom) + VALUES + ( + :mission_id, + :device_id, + :ts, + :anomaly_type_id, + :severity, + CAST(:details AS JSONB), + ST_SetSRID(ST_GeomFromText(:wkt_geom), 4326) + ) + """ +) + +# NEW: leaf_reports insert (always written) +INSERT_LEAF_REPORT = text( + """ + INSERT INTO public.leaf_reports + (device_id, leaf_disease_type_id, ts, confidence, sick) + VALUES + (:device_id, :leaf_disease_type_id, :ts, :confidence, :sick) + """ +) + +# NEW: upsert/get id for leaf_disease_types by name (case-insensitive) +UPSERT_LEAF_DISEASE_TYPE = text( + """ + WITH ins AS ( + INSERT INTO public.leaf_disease_types (name) + VALUES (:name) + ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name + RETURNING id + ) + SELECT id FROM ins + UNION ALL + SELECT id FROM public.leaf_disease_types WHERE name = :name + LIMIT 1 + """ +) + +INSERT_MISSION_FULL = text( + """ + INSERT INTO public.missions (mission_id, start_time, end_time, area_geom) + VALUES ( + :mission_id, + :start_time, + :end_time, + ST_SetSRID(ST_GeomFromText(:wkt_poly), 4326) + ) + ON CONFLICT (mission_id) DO NOTHING + """ +) + + +class BatchRunner: + """ + End-to-end runner: + - Parse device & timestamp from file name: _TZ[ _suffix].ext + - Run disease detector + - ALWAYS write a row into public.leaf_reports for each detection + - Write into public.anomalies ONLY if label is 'sick' (i.e., does NOT contain 'healthy') + - Ensure supporting FKs exist (devices:, missions: fixed 60, leaf_disease_types:) + + Notes: + * mission_id is fixed to 60 per requirement. + * geom is the pixel-center point of the detection bbox (WKT, SRID 4326). + """ + + # Fixed mission per request + FIXED_MISSION_ID = 60 + + def __init__(self, mission_id: int | None = None, device_id: str = "device-1") -> None: + # mission_id ignored; always use 60, but keep signature for CLI compatibility + self.mission_id = BatchRunner.FIXED_MISSION_ID + self.fallback_device_id = device_id # used only if filename parsing fails + self.engine = get_engine() + self.detector = DiseaseDetector() + self.origin_map = self._load_origin_map(os.getenv("ORIGIN_MANIFEST")) + + # anomaly_types entry for LEAF_DISEASE (used only for anomalies table) + self.leaf_anomaly_type_id = self._ensure_anomaly_type( + code="LEAF_DISEASE", description="Leaf disease detected" + ) + + # ---------------------------- + # Public API + # ---------------------------- + @staticmethod + def _load_origin_map(path: str | None) -> dict[str, str]: + """ + קורא קובץ טאב: \t. + מחזיר {} אם אין קובץ/כשל. + """ + mapping: dict[str, str] = {} + if not path or not os.path.exists(path): + return mapping + try: + with open(path, "r", encoding="utf-8") as f: + for line in f: + line = line.rstrip("\n") + if not line or "\t" not in line: + continue + fname, inner = line.split("\t", 1) + if fname and inner: + mapping[fname] = inner + except Exception: + pass + return mapping + + @staticmethod + def _parse_device_and_ts_from_name(img_path: Path) -> tuple[str, datetime]: + """ + Accepts: + _TZ. + _TZ_. + Returns (device_id, ts_utc). Raises ValueError if the pattern doesn't match. + """ + stem = img_path.stem + parts = stem.split("_") + if len(parts) < 2: + raise ValueError( + f"Filename '{img_path.name}' must be '_TZ[ _suffix].ext'" + ) + device = parts[0] + ts_str = parts[1] + if not re.fullmatch(r"\d{8}T\d{6}Z", ts_str): + raise ValueError( + f"Filename '{img_path.name}' must include timestamp as TZ" + ) + ts = datetime.strptime(ts_str, "%Y%m%dT%H%M%SZ").replace(tzinfo=timezone.utc) + return device, ts + + def run_folder(self, folder: Path | str) -> None: + """ + Run pipeline on all images within a folder (non-recursive). + """ + folder = Path(folder) + assert folder.exists(), f"Folder not found: {folder.resolve()}" + + image_paths = sorted( + p for p in folder.iterdir() if p.suffix.lower() in {".jpg", ".jpeg", ".png"} + ) + + total, total_dets = 0, 0 + for img_path in image_paths: + try: + n = self.process_image(img_path) + total += 1 + total_dets += n + except Exception as ex: + print(f"[WARN] Failed on {img_path.name}: {ex}") + + print(f"Processed {total} images, wrote {total_dets} detections") + + def process_image(self, img_path: Path | str) -> int: + """ + Run pipeline on a single image and insert rows into leaf_reports (always) + and anomalies (only if sick). Returns number of detections processed. + """ + img_path = Path(img_path) + + source_path = str(img_path.resolve()) + + # img_path = Path(img_path) + +# Parse from filename (with fallback for your current crop file names) + try: + device_id, det_ts = self._parse_device_and_ts_from_name(img_path) + except Exception: + device_id = self.fallback_device_id + # timestamp: file mtime if available, otherwise now (UTC) + try: + det_ts = datetime.fromtimestamp(img_path.stat().st_mtime, tz=timezone.utc) + except Exception: + det_ts = datetime.now(timezone.utc) + + + device_id, det_ts = self._parse_device_and_ts_from_name(img_path) + + + local_tz = os.getenv("LOCAL_TZ", "Asia/Jerusalem") + ts_local = det_ts.astimezone(ZoneInfo(local_tz)) + date_path = ts_local.strftime("%Y/%m/%d") # YYYY/MM/DD + + + rid_env = (os.getenv("MINIO_RID") or os.getenv("RID") or "").strip("/") + date_path = None + run_id = None + if rid_env: + parts = rid_env.split("/") + if len(parts) == 4 and all(parts): + y, m, d, hhmm = parts + date_path = f"{y}/{m}/{d}" + run_id = hhmm + + + if not date_path or not run_id: + local_tz = os.getenv("LOCAL_TZ", "Asia/Jerusalem") + ts_local = det_ts.astimezone(ZoneInfo(local_tz)) + date_path = ts_local.strftime("%Y/%m/%d") # YYYY/MM/DD + run_id = os.getenv("RUN_ID") or os.getenv("MINIO_RUN_ID") + if not run_id: + mp = (os.getenv("MINIO_PREFIX") or "").strip("/") + last = mp.split("/")[-1] if mp else "" + if last and len(last) == 4 and last.isdigit(): + run_id = last + if not run_id: + run_id = ts_local.strftime("%H%M") + + bucket = os.getenv("MINIO_BUCKET", "imagery") + prefix_root = os.getenv("MINIO_PREFIX_ROOT", "leaves") + + # Ensure FKs exist + self._ensure_device(device_id) + self._ensure_mission_full(self.mission_id, det_ts) + + # Load image & run detector + img, W, H = load_image(img_path) + image_id = image_id_from_path(img_path) + dets = self.detector.run(img) + + print(f"{image_id}: found {len(dets)} detections") + + written = 0 + for d in dets: + x, y, w, h = self._extract_bbox(d) + x, y, w, h = clamp_bbox(int(x), int(y), int(w), int(h), W, H) + cx = x + w / 2.0 + cy = y + h / 2.0 + + area = float(getattr(d, "area", w * h)) + label = str(getattr(d, "label", "disease")) + conf = float(getattr(d, "confidence", 1.0)) + + # key בפורמט: imagery/leaves/YYYY/MM/DD/RUNID/crop/leaf{index}/ + # leaf_folder = f"leaf{written + 1}" + # minio_key = f"{bucket}/{prefix_root}/{date_path}/{run_id}/crop/{leaf_folder}/{img_path.name}" + # minio_url = self._minio_url_from_key(minio_key) + # קבלת שם התיקייה הפנימית מהמניפסט (הקובץ נשאר בשם המקורי!) + inner_dir = self.origin_map.get(img_path.name) + + if inner_dir: + minio_key = f"{bucket}/{prefix_root}/{date_path}/{run_id}/crop/{inner_dir}/{img_path.name}" + else: + # fallback נדיר אם אין במניפסט (עדיין עובד, פשוט בלי התיקייה): + minio_key = f"{bucket}/{prefix_root}/{date_path}/{run_id}/crop/{img_path.name}" + + minio_url = self._minio_url_from_key(minio_key) + + # Build details JSON (used only in anomalies) + details = { + "image_id": image_id, + "label": label, + "bbox": [x, y, w, h], + "area": area, + "confidence": conf, + "device_id": device_id, + "ts": det_ts.isoformat(), + "source_path": source_path, + "minio_key": minio_key, + } + if minio_url: + details["minio_url"] = minio_url + details.setdefault("crop_type", None) + details.setdefault("disease_type", label) + if is_dataclass(d): + details["raw_detection"] = asdict(d) + + # Decide sick/healthy by label + sick = not self._is_healthy_label(label) + + # Map label → disease_type_name (part after "__" if present) + disease_type_name = self._disease_type_from_label(label) + + with self.engine.begin() as conn: + # ensure disease type exists and get id + leaf_type_id = self._ensure_leaf_disease_type(conn, disease_type_name) + + # 1) ALWAYS insert a leaf report + conn.execute( + INSERT_LEAF_REPORT, + dict( + device_id=device_id, + leaf_disease_type_id=leaf_type_id, + ts=det_ts, + confidence=conf, + sick=sick, + ), + ) + + # 2) Insert anomaly ONLY if sick + if sick: + conn.execute( + INSERT_ANOMALY, + dict( + mission_id=self.mission_id, + device_id=device_id, + ts=det_ts, + anomaly_type_id=self.leaf_anomaly_type_id, + severity=conf, + details=json.dumps(details), + wkt_geom=f"POINT({cx} {cy})", + ), + ) + + written += 1 + + return written + + # ---------------------------- + # Internals + # ---------------------------- + + @staticmethod + def _is_healthy_label(label: str) -> bool: + """Return True if label contains 'healthy' (case-insensitive).""" + return "healthy" in label.lower() + + @staticmethod + def _disease_type_from_label(label: str) -> str: + """ + Extract disease type token from label. If label contains 'a__b', return 'b'; else return label. + Keeps underscores as-is for consistency with the model outputs. + """ + if "__" in label: + return label.split("__", 1)[1] + return label + + def _ensure_anomaly_type(self, code: str, description: str) -> int: + """Return anomaly_type_id for `code`, inserting if needed (idempotent).""" + with self.engine.begin() as conn: + row = conn.execute( + text("SELECT anomaly_type_id FROM public.anomaly_types WHERE code = :c"), + {"c": code}, + ).first() + if row: + return int(row[0]) + + row = conn.execute( + text( + """ + INSERT INTO public.anomaly_types (code, description) + VALUES (:c, :d) + ON CONFLICT (code) + DO UPDATE SET description = EXCLUDED.description + RETURNING anomaly_type_id + """ + ), + {"c": code, "d": description}, + ).first() + return int(row[0]) + + def _ensure_leaf_disease_type(self, conn, name: str) -> int: + """ + Ensure a row exists in public.leaf_disease_types for the given name and return its id. + Uses an upsert with RETURNING to be idempotent. + """ + row = conn.execute(UPSERT_LEAF_DISEASE_TYPE, {"name": name}).first() + return int(row[0]) + + def _ensure_device(self, device_id: str) -> None: + """Ensure a row exists in public.devices (TEXT PK/UNIQUE).""" + with self.engine.begin() as conn: + conn.execute( + text( + """ + INSERT INTO public.devices (device_id) + VALUES (:d) + ON CONFLICT (device_id) DO NOTHING + """ + ), + {"d": device_id}, + ) + + def _ensure_mission_full(self, mission_id: int, ts: datetime) -> None: + """ + Ensure mission row exists and matches your table shape. + If not exists: start_time=ts, end_time=ts+1h, area=default 1x1° square near (0,0). + """ + with self.engine.begin() as conn: + exists = conn.execute( + text("SELECT 1 FROM public.missions WHERE mission_id = :id"), + {"id": mission_id}, + ).first() + if exists: + return + start = ts + end = ts + timedelta(hours=1) + wkt_poly = "POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" + conn.execute( + INSERT_MISSION_FULL, + { + "mission_id": mission_id, + "start_time": start, + "end_time": end, + "wkt_poly": wkt_poly, + }, + ) + + @staticmethod + def _extract_bbox(d) -> Tuple[float, float, float, float]: + """ + Normalize bbox to (x, y, w, h). Supports multiple field layouts. + """ + if all(hasattr(d, a) for a in ("x", "y", "w", "h")): + return float(d.x), float(d.y), float(d.w), float(d.h) + + if hasattr(d, "bbox"): + bx = list(d.bbox) + if len(bx) != 4: + raise ValueError(f"Unexpected bbox length: {len(bx)} in {bx}") + x, y, w, h = map(float, bx) + return x, y, w, h + + if all(hasattr(d, a) for a in ("xmin", "ymin", "xmax", "ymax")): + x1, y1, x2, y2 = float(d.xmin), float(d.ymin), float(d.xmax), float(d.ymax) + return x1, y1, max(0.0, x2 - y1), max(0.0, y2 - y1) + + if all(hasattr(d, a) for a in ("left", "top", "width", "height")): + return float(d.left), float(d.top), float(d.width), float(d.height) + + raise AttributeError( + "Detection bbox fields missing. Supported: " + "(x,y,w,h) or bbox or (xmin,ymin,xmax,ymax) or (left,top,width,height)." + ) + + @staticmethod + def _minio_url_from_key(key: str) -> str | None: + """ + בונה URL מלא. אם ה-key כבר מתחיל בשם הבאקט (למשל 'imagery/...'), + לא נוסיף את הבאקט שוב. + """ + endpoint = os.getenv("MINIO_ENDPOINT") + bucket = os.getenv("MINIO_BUCKET") + if not endpoint or not bucket: + return None + endpoint = endpoint.rstrip("/") + + if key.startswith(f"{bucket}/"): + return f"{endpoint}/{key}" + return f"{endpoint}/{bucket}/{key}" + + @staticmethod + def _minio_key_from_source_path(source_path: str) -> str: + """ + ממיר את נתיב המקור המקומי ל-key (עם נרמול ל'/' בלבד), + ומשלב MINIO_PREFIX אם הוגדר. + """ + prefix = os.getenv("MINIO_PREFIX", "").strip("/") + posix = source_path.replace("\\", "/") + posix = posix.lstrip("/") + return f"{prefix}/{posix}" if prefix else posix + + @staticmethod + def _minio_url(img_path: Path) -> str | None: + """ + Build a MinIO object URL if MINIO_* env vars are provided. + """ + endpoint = os.getenv("MINIO_ENDPOINT") + bucket = os.getenv("MINIO_BUCKET") + prefix = os.getenv("MINIO_PREFIX", "").strip("/") + if not endpoint or not bucket: + return None + endpoint = endpoint.rstrip("/") + key = f"{prefix}/{img_path.name}" if prefix else img_path.name + return f"{endpoint}/{bucket}/{key}" + + +# ------------- CLI helper ------------- + +def main() -> None: + """ + Local runner: + python -m agri_baseline.src.batch_runner --input + """ + import argparse + + parser = argparse.ArgumentParser( + description="Run disease detection pipeline: leaf_reports (always), anomalies (sick only)." + ) + parser.add_argument("--input", type=str, required=True, help="Image file or folder") + parser.add_argument("--mission", type=int, default=60, help="Ignored; always fixed to 60") + parser.add_argument("--device", type=str, default="device-1", help="Fallback device (unused)") + args = parser.parse_args() + + runner = BatchRunner(mission_id=args.mission, device_id=args.device) + in_path = Path(args.input) + if in_path.is_dir(): + runner.run_folder(in_path) + else: + runner.process_image(in_path) + + +if __name__ == "__main__": + main() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/base.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/base.py new file mode 100644 index 000000000..3eede7361 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/base.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple, Protocol + + +@dataclass(frozen=True) +class Detection: + """ + Model-agnostic detection container. + + Canonical storage: + - bbox: (x, y, w, h) in pixel coordinates. + - confidence: float in [0, 1]. + - label: class/code string. + + Notes: + - Properties expose a stable attribute API (.x/.y/.w/.h/.area etc.) + so downstream code can use either bbox or attributes. + - The class is frozen (immutable) to avoid accidental mutations + during processing and logging. + """ + label: str + confidence: float + bbox: Tuple[float, float, float, float] + meta: Optional[Dict] = None # optional extra data (e.g., model logits) + + # ---- Convenience constructors ------------------------------------------------- + + @staticmethod + def from_xywh( + label: str, + confidence: float, + x: float, + y: float, + w: float, + h: float, + meta: Optional[Dict] = None, + ) -> "Detection": + """Create a Detection from explicit x/y/w/h values.""" + return Detection(label=label, confidence=float(confidence), bbox=(x, y, w, h), meta=meta) + + # ---- Attribute-style view over bbox ------------------------------------------ + + @property + def x(self) -> float: + return float(self.bbox[0]) + + @property + def y(self) -> float: + return float(self.bbox[1]) + + @property + def w(self) -> float: + return float(self.bbox[2]) + + @property + def h(self) -> float: + return float(self.bbox[3]) + + @property + def xmin(self) -> float: + return self.x + + @property + def ymin(self) -> float: + return self.y + + @property + def xmax(self) -> float: + return self.x + self.w + + @property + def ymax(self) -> float: + return self.y + self.h + + @property + def area(self) -> float: + # Clamp at zero to avoid negative area if w/h are negative by mistake. + return max(0.0, self.w) * max(0.0, self.h) + + +class Detector(Protocol): + """ + Base detector interface. + + Implementors must return a list of Detection objects given a BGR image + (numpy array with shape (H, W, 3), dtype=uint8). + """ + name: str + + def run(self, bgr_image) -> List[Detection]: + """Run inference on a BGR image and return model detections.""" + ... diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/cnn_multi_classifier.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/cnn_multi_classifier.py new file mode 100644 index 000000000..6a2d5f3a3 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/cnn_multi_classifier.py @@ -0,0 +1,12 @@ +# agri-baseline/src/detectors/cnn_multi_classifier.py +import torch.nn as nn +from torchvision import models + +def build_multi_model(num_classes: int, pretrained: bool = True) -> nn.Module: + """ + Builds a ResNet18 model for multi-class disease classification. + """ + model = models.resnet18(weights="IMAGENET1K_V1" if pretrained else None) + in_features = model.fc.in_features + model.fc = nn.Linear(in_features, num_classes) + return model diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/disease_model.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/disease_model.py new file mode 100644 index 000000000..a9f94ebae --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/disease_model.py @@ -0,0 +1,127 @@ +# agri_baseline/src/detectors/disease_model.py +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Tuple + +import cv2 +import numpy as np +import torch +import albumentations as A +from albumentations.pytorch import ToTensorV2 + +from agri_baseline.src.detectors.cnn_multi_classifier import build_multi_model +from agri_baseline.src.detectors.train.dictionary import CLASS_MAPPING + + +@dataclass +class Detection: + """Simple container for a single detection box.""" + bbox: Tuple[int, int, int, int] # x, y, w, h + confidence: float + label: str = "disease" + + @property + def area(self) -> int: + x, y, w, h = self.bbox + return int(w * h) + + +def _ensure_bgr_uint8(img: np.ndarray) -> np.ndarray: + """ + Normalize any input image to BGR uint8 with 3 channels. + Prevents cvtColor from crashing with color.simd_helpers.hpp:94. + + Rules: + - None / empty -> ValueError + - GRAY (H,W) -> BGR + - BGRA (H,W,4) -> BGR + - dtype != uint8 -> convert to uint8 (clip to [0..255]) + """ + if img is None or getattr(img, "size", 0) == 0: + raise ValueError("DiseaseDetector: empty/None image given") + + # If grayscale -> convert to BGR + if img.ndim == 2: + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + + # If BGRA -> drop alpha + elif img.ndim == 3 and img.shape[2] == 4: + img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) + + # Validate shape now + if img.ndim != 3 or img.shape[2] != 3: + raise ValueError(f"DiseaseDetector: unexpected image shape {img.shape}") + + # Ensure uint8 + if img.dtype != np.uint8: + img = np.clip(img, 0, 255).astype(np.uint8) + + # Ensure non-zero size + h, w = img.shape[:2] + if h == 0 or w == 0: + raise ValueError("DiseaseDetector: zero-sized image") + + return img + + +class DiseaseDetector: + """ + CNN-based disease classifier. + - Normalizes input to BGR uint8 (3-ch) to avoid OpenCV color conversion crashes. + - Converts BGR->RGB before Albumentations (Normalize + ToTensorV2). + """ + + name = "disease" + + def __init__(self, model_path: str = "models/cnn_multi_stage3.pth", device: str | None = None) -> None: + # choose device + self.device = device or ("cuda" if torch.cuda.is_available() else "cpu") + + # build model according to class mapping + self.classes = sorted(set(CLASS_MAPPING.values())) + self.model = build_multi_model(num_classes=len(self.classes)).to(self.device) + + # load trained weights + state = torch.load(model_path, map_location=self.device) + self.model.load_state_dict(state) + self.model.eval() + + # same validation transforms used in training + self.transform = A.Compose( + [ + A.Resize(224, 224), + A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), + ToTensorV2(), + ] + ) + + def run(self, img: np.ndarray) -> List[Detection]: + """ + Run the classifier on a single image. + :param img: np.ndarray from OpenCV (BGR or GRAY/BGRA/float) — any shape/dtype. + :return: list with a single full-frame Detection carrying predicted label/confidence. + """ + # 1) Normalize input so cvtColor is safe + img = _ensure_bgr_uint8(img) + + # 2) Convert to RGB for the model pipeline + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + # 3) Albumentations -> tensor + aug = self.transform(image=img_rgb) + tensor = aug["image"].unsqueeze(0).to(self.device) + + # 4) Model inference + with torch.no_grad(): + logits = self.model(tensor) + probs = torch.softmax(logits, dim=1)[0] + conf_t, cls_t = torch.max(probs, dim=0) + + label = self.classes[cls_t.item()] + confidence = float(conf_t.item()) + + # 5) Return a single detection that spans the whole image (classifier) + h, w = img.shape[:2] + det = Detection(bbox=(0, 0, w, h), confidence=confidence, label=label) + return [det] diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/train/dictionary.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/train/dictionary.py new file mode 100644 index 000000000..1d0671026 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/train/dictionary.py @@ -0,0 +1,36 @@ +CLASS_MAPPING = { + # 🍅 Tomato + "tomato_healthy": "tomato__healthy", + "tomato_leaf": "tomato__healthy", + "tomato_bacterial_spot": "tomato__bacterial_spot", + "tomato_leaf_bacterial_spot": "tomato__bacterial_spot", + "tomato_early_blight": "tomato__early_blight", + "tomato_early_blight_leaf": "tomato__early_blight", + "tomato_late_blight": "tomato__late_blight", + "tomato_leaf_late_blight": "tomato__late_blight", + "tomato_leaf_mold": "tomato__leaf_mold", + "tomato_mold_leaf": "tomato__leaf_mold", + "tomato_septoria_leaf_spot": "tomato__septoria_leaf_spot", + "tomato_spider_mites_two_spotted_spider_mite": "tomato__spider_mites", + "tomato_spider_mites": "tomato__spider_mites", + "tomato_target_spot": "tomato__target_spot", + "tomato_tomato_mosaic_virus": "tomato__mosaic_virus", + "tomato_tomato_yellowleaf_curl_virus": "tomato__yellowleaf_curl_virus", + "tomato_leaf_mosaic_virus": "tomato__mosaic_virus", + "tomato_leaf_yellow_virus": "tomato__yellowleaf_curl_virus", + + + # 🥔 Potato + "potato_healthy": "potato__healthy", + "potato_leaf": "potato__healthy", + "potato_early_blight": "potato__early_blight", + "potato_leaf_early_blight": "potato__early_blight", + "potato_late_blight": "potato__late_blight", + "potato_leaf_late_blight": "potato__late_blight", + + # 🌶️ Pepper + "pepper_bell_healthy": "pepper__healthy", + "bell_pepper_leaf": "pepper__healthy", + "pepper_bell_bacterial_spot": "pepper__bacterial_spot", + "bell_pepper_leaf_spot": "pepper__bacterial_spot", +} diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/config.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/config.py new file mode 100644 index 000000000..18d696e0a --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/config.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import os +from pathlib import Path + +# Try to load env files both from project root and from agri_baseline/.env +try: + from dotenv import load_dotenv # type: ignore + load_dotenv(dotenv_path=Path("agri_baseline/.env"), override=False) + load_dotenv(override=False) +except Exception: + pass + +# Prefer standard name DATABASE_URL; fallback to DB_URL; finally default to localhost:5432 +DB_URL: str = ( + os.getenv("DATABASE_URL") + or os.getenv("DB_URL") + or "postgresql+psycopg2://missions_user:pg123@localhost:5432/missions_db" +) + +IMAGES_DIR = os.getenv("IMAGES_DIR", "./data/images") +BATCH_SIZE = int(os.getenv("BATCH_SIZE", 64)) +MAX_WORKERS = int(os.getenv("MAX_WORKERS", 4)) +MIN_BBOX_AREA = int(os.getenv("MIN_BBOX_AREA", 60)) +MIN_COMPONENT_AREA = int(os.getenv("MIN_COMPONENT_AREA", 200)) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/db.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/db.py new file mode 100644 index 000000000..8c69e24f3 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/db.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from sqlalchemy import create_engine, text, bindparam +from sqlalchemy.engine import Engine + +from . import config + +_engine: Engine | None = None + +def get_engine() -> Engine: + """Return a singleton SQLAlchemy engine for the configured DB.""" + global _engine + if _engine is None: + _engine = create_engine( + config.DB_URL, + pool_pre_ping=True, # keep-alive for flaky networks/tests + future=True, + connect_args={"connect_timeout": 5} # fail fast on bad host/port + ) + return _engine + +# === Inserts mapped to RelDB schema === + +# detections → anomalies +INSERT_DET = text( + """ + INSERT INTO anomalies(mission_id, device_id, ts, anomaly_type_id, severity, details, geom) + VALUES (:mission_id, :device_id, :ts, :anomaly_type_id, :severity, CAST(:details AS jsonb), + ST_GeomFromText(:wkt_geom, 4326)); + """ +) + +# counts → tile_stats +INSERT_COUNT = text( + """ + INSERT INTO tile_stats(mission_id, tile_id, anomaly_score, geom) + VALUES (:mission_id, :tile_id, :anomaly_score, ST_GeomFromText(:wkt_geom, 4326)) + ON CONFLICT (mission_id, tile_id) DO UPDATE + SET anomaly_score = excluded.anomaly_score; + """ +) + +# validator findings → event_logs +INSERT_FINDING = ( + text( + """ + INSERT INTO event_logs(ts, level, source, message, details) + VALUES (CURRENT_TIMESTAMP, :level, 'validator', :message, CAST(:details AS jsonb)); + """ + ) + # Defaults if the caller does not send the parameters + .bindparams( + bindparam("level", value="INFO"), + bindparam("message", value=""), + bindparam("details", value="{}"), + ) +) + + + +# QA metrics → event_logs +INSERT_QA = text( + """ + INSERT INTO event_logs(ts, level, source, message, details) + VALUES (CURRENT_TIMESTAMP, 'INFO', 'qa', 'QA metrics recorded', CAST(:details AS jsonb)); + """ +) \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/logging_setup.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/logging_setup.py new file mode 100644 index 000000000..06193027f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/logging_setup.py @@ -0,0 +1,9 @@ +import logging + + +def setup_logging(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", + ) + return logging.getLogger("agri") \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/utils.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/utils.py new file mode 100644 index 000000000..0b99245e9 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/utils.py @@ -0,0 +1,62 @@ +# agri_baseline/src/pipeline/utils.py +# Max line length: 100 + +from __future__ import annotations + +import hashlib +from pathlib import Path +from typing import Tuple + +import cv2 +import numpy as np + + +class ImageLoadError(Exception): + """Raised when an image cannot be decoded or is empty.""" + + +def load_image(path: str | Path) -> Tuple[np.ndarray, int, int]: + """ + Load an image from disk as BGR uint8 and return (img, width, height). + + Rules: + - Always read as color to ensure 3 channels (BGR). + - Raise FileNotFoundError if the path doesn't exist. + - Raise ImageLoadError if decode fails or the image is empty. + - Convert dtype to uint8 if needed. + - Normalize channel count: grayscale -> BGR, BGRA -> BGR. + """ + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"Image not found: {p.resolve()}") + + # Always load as color to ensure 3 channels (BGR) + img = cv2.imread(str(p), cv2.IMREAD_COLOR) + if img is None or img.size == 0: + raise ImageLoadError(f"Failed to decode image (or empty): {p.resolve()}") + + if img.dtype != np.uint8: + img = cv2.convertScaleAbs(img) + + # Guard channel count (should be 3 after IMREAD_COLOR, but just in case) + if img.ndim == 2: + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + elif img.ndim == 3 and img.shape[2] == 4: + img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) + + h, w = img.shape[:2] + return img, w, h + + +def image_id_from_path(path: str | Path) -> str: + p = Path(path) + digest = hashlib.sha1(str(p.resolve()).encode()).hexdigest()[:16] + return f"{p.stem}_{digest}" + + +def clamp_bbox(x: int, y: int, w: int, h: int, W: int, H: int) -> Tuple[int, int, int, int]: + x = max(0, min(x, W - 1)) + y = max(0, min(y, H - 1)) + w = max(1, min(w, W - x)) + h = max(1, min(h, H - y)) + return x, y, w, h diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_client.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_client.py new file mode 100644 index 000000000..dd5effd69 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_client.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass +from minio import Minio + + +@dataclass(frozen=True) +class MinioConfig: + endpoint: str + access_key: str + secret_key: str + bucket: str + secure: bool + + +def load_minio_config() -> MinioConfig: + endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000") + access_key = os.getenv("MINIO_ACCESS_KEY", "") + secret_key = os.getenv("MINIO_SECRET_KEY", "") + bucket = os.getenv("MINIO_BUCKET", "my-bucket") + secure = os.getenv("MINIO_SECURE", "false").lower() == "true" + + if not access_key or not secret_key: + raise ValueError("Missing MINIO_ACCESS_KEY / MINIO_SECRET_KEY.") + return MinioConfig(endpoint, access_key, secret_key, bucket, secure) + + +def build_client(cfg: MinioConfig) -> Minio: + return Minio( + endpoint=cfg.endpoint, + access_key=cfg.access_key, + secret_key=cfg.secret_key, + secure=cfg.secure, + ) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_sync.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_sync.py new file mode 100644 index 000000000..8c6c2b6a1 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_sync.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +import os +from io import BytesIO +from pathlib import Path +from typing import Iterable + +from .minio_client import MinioConfig, build_client + + +def ensure_bucket(cfg: MinioConfig) -> None: + """ + Ensure the target bucket exists; create it if it does not. + """ + client = build_client(cfg) + if not client.bucket_exists(cfg.bucket): + client.make_bucket(cfg.bucket) + + +def download_prefix_to_dir(cfg: MinioConfig, prefix: str, local_dir: Path) -> list[Path]: + """ + Download all objects under the given `prefix` to the local directory. + Returns a list of local file paths that were downloaded. + """ + client = build_client(cfg) + local_dir.mkdir(parents=True, exist_ok=True) + + downloaded: list[Path] = [] + for obj in client.list_objects(cfg.bucket, prefix=prefix, recursive=True): + # Skip entries that represent "virtual folders" + name = obj.object_name + if name.endswith("/") or not name: + continue + + # Simplify: save using the file's basename only. + # If you need to preserve the full hierarchy, use: local_dir.joinpath(name) + target = local_dir.joinpath(Path(name).name) + + response = client.get_object(cfg.bucket, name) + try: + data = response.read() + finally: + response.close() + response.release_conn() + + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + downloaded.append(target) + + return downloaded + + +def upload_dir_to_prefix(cfg: MinioConfig, local_dir: Path, prefix: str) -> list[str]: + """ + Upload all files from the local directory under the given `prefix`. + Returns a list of object names that were uploaded. + """ + client = build_client(cfg) + ensure_bucket(cfg) + + uploaded: list[str] = [] + for path in local_dir.rglob("*"): + if not path.is_file(): + continue + + rel = path.relative_to(local_dir).as_posix() + object_name = f"{prefix.rstrip('/')}/{rel}" + data = path.read_bytes() + bio = BytesIO(data) + + client.put_object(cfg.bucket, object_name, bio, length=len(data)) + uploaded.append(object_name) + + return uploaded diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/rules.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/rules.py new file mode 100644 index 000000000..afb6318a7 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/rules.py @@ -0,0 +1,65 @@ +from __future__ import annotations +import json +from dataclasses import dataclass +from typing import Iterable, Optional + +from sqlalchemy import text +from agri_baseline.src.pipeline.db import get_engine, INSERT_FINDING, INSERT_QA + + +@dataclass +class Finding: + scope: str + image_id: str + rule: str + severity: str + message: str + details: Optional[dict] = None + + +# ---- Image-level checks ---- + +def check_bbox_bounds(image_id: str, width: int, height: int, dets: list[dict]) -> list[Finding]: + out: list[Finding] = [] + for d in dets: + x, y, w, h = d["bbox_x"], d["bbox_y"], d["bbox_w"], d["bbox_h"] + if x < 0 or y < 0 or x + w > width or y + h > height: + out.append(Finding("image", image_id, "bbox_oob", "warn", + f"BBox out-of-bounds: {(x, y, w, h)}")) + if w * h <= 0 or d["area_px"] <= 0: + out.append(Finding("image", image_id, "bbox_area_zero", "error", + "Non-positive area")) + if d["confidence"] < 0 or d["confidence"] > 1: + out.append(Finding("image", image_id, "conf_oob", "error", + f"Confidence out of range: {d['confidence']:.3f}")) + return out + + +def check_counts_reasonable(image_id: str, disease: int) -> list[Finding]: + out: list[Finding] = [] + if disease < 0: + out.append(Finding("image", image_id, "negative_counts", "error", + f"Negative count: disease={disease}")) + if disease == 0: + out.append(Finding("image", image_id, "all_zero_counts", "warn", + "Disease count is zero")) + if disease > 10000: + out.append(Finding("image", image_id, "count_too_high", "warn", + f"Suspiciously high disease count: {disease}")) + return out + + +# ---- Batch-level checks ---- + +def check_batch_error_rate(total: int, errored: int, threshold: float = 0.05) -> list[Finding]: + rate = 0.0 if total == 0 else errored / total + sev = "warn" if rate <= threshold else "error" + return [Finding("batch", None, "error_rate", sev, + f"Batch error rate={rate:.3%}, threshold={threshold:.0%}")] + + +def check_batch_no_detections(total: int, sum_dets: int) -> list[Finding]: + if total > 0 and sum_dets == 0: + return [Finding("batch", None, "no_detections", "warn", + "Pipeline produced zero detections for the entire batch")] + return [] diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/validator.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/validator.py new file mode 100644 index 000000000..3c970190c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/validator.py @@ -0,0 +1,94 @@ +from __future__ import annotations +import json +from dataclasses import dataclass +from typing import Iterable, List, Optional +from sqlalchemy import text + +from agri_baseline.src.pipeline.db import get_engine, INSERT_FINDING, INSERT_QA + + +@dataclass +class Finding: + """Single validation finding.""" + scope: str # e.g., "image" + image_id: str # logical id per image + rule: str # rule code/name + severity: str # DEBUG/INFO/WARN/ERROR + message: str # human-readable message + details: Optional[dict] = None + + +class Validator: + """ + Collects validation findings and writes batch summaries. + """ + def image_findings(self, findings: Iterable[Finding]) -> None: + """Write image-level findings into event_logs table.""" + with get_engine().begin() as conn: + for f in findings: + details_dict = { + "scope": f.scope, + "rule": f.rule, + "image_id": f.image_id, + **(f.details or {}), + } + conn.execute( + INSERT_FINDING, + { + "level": f.severity.upper(), + "message": f.message, + # Passes as a JSON string because SQL does CAST(... AS jsonb) "details": json.dumps(details_dict), + }, + ) + + + def batch_summary(self) -> None: + """ + Aggregate anomalies → tile_stats by image_id (from anomalies.details->>'image_id'). + For each (mission_id, image_id): + - anomaly_score = count of anomalies + - geom = envelope of a small expanded collect of points (Polygon, 4326) + Idempotent via ON CONFLICT (mission_id, tile_id). + """ + sql = text( + """ + WITH per_image AS ( + SELECT + a.mission_id, + a.details->>'image_id' AS tile_id, + COUNT(*)::real AS anomaly_score, + -- produce Polygon in 4326 directly (no WKT roundtrip) + ST_Envelope( + ST_Expand( + ST_Collect(a.geom), + 0.0005 -- ~50m at equator; tweak if needed + ) + )::geometry(Polygon, 4326) AS poly + FROM anomalies a + WHERE a.geom IS NOT NULL + AND a.details ? 'image_id' + GROUP BY a.mission_id, tile_id + ) + INSERT INTO tile_stats (mission_id, tile_id, anomaly_score, geom) + SELECT mission_id, tile_id, anomaly_score, poly + FROM per_image + ON CONFLICT (mission_id, tile_id) DO UPDATE + SET anomaly_score = EXCLUDED.anomaly_score, + geom = EXCLUDED.geom; + """ + ) + + with get_engine().begin() as conn: + conn.execute(sql) + + # optional: record a QA info log (pass JSON as string) + with get_engine().begin() as conn: + conn.execute( + INSERT_QA, + { + "details": json.dumps({ + "source": "batch_summary", + "note": "tile_stats updated from anomalies by image_id", + }) + }, + ) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/docker-compose.yml b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/docker-compose.yml new file mode 100644 index 000000000..18e1cc31c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/docker-compose.yml @@ -0,0 +1,25 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: agri_app + # exec-form to avoid spacing/quoting issues + command: ["python", "-m", "agri_baseline.scripts.run_batch", "--storage", "minio"] + env_file: + - agri_baseline/.env + volumes: + - ./agri_baseline:/app/agri_baseline + - ./tests:/app/tests + - ./data:/app/data + - ./models:/root/.cache/torch/hub/checkpoints + networks: + - agri_net + - minio_net # ← MinIO network + +networks: + agri_net: + external: true + minio_net: + external: true + name: storage_with_mqtt_minionet # ← MinIO network name diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/dockerfile b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/dockerfile new file mode 100644 index 000000000..06550cea8 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/dockerfile @@ -0,0 +1,183 @@ +# # # ============================== +# # # Based on PyTorch with CUDA +# # # ============================== +# # # FROM pytorch/pytorch:2.2.0-cuda12.1-cudnn8-runtime +# # ARG BASE_IMAGE=pytorch/pytorch:2.2.0-cpu +# # FROM ${BASE_IMAGE} + +# # # # --- NETFREE CERT INSTALL --- +# # # ADD https://netfree.link/dl/unix-ca.sh /home/netfree-unix-ca.sh +# # # RUN bash /home/netfree-unix-ca.sh \ +# # # && update-ca-certificates +# # # ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +# # # ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +# # # ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt +# # # --- NETFREE CERT INSTALL (optional) --- +# # ARG INSTALL_NETFREE_CA=0 +# # # אל תעשי ADD מהאינטרנט (זה מה שנפל) +# # # במקום זה, רק אם תרצי – נוריד בזמן הבנייה עם curl (כש-INSTALL_NETFREE_CA=1) +# # RUN if [ "$INSTALL_NETFREE_CA" = "1" ]; then \ +# # apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \ +# # curl -fsSL --retry 5 https://netfree.link/dl/unix-ca.sh -o /home/netfree-unix-ca.sh && \ +# # bash /home/netfree-unix-ca.sh && update-ca-certificates && \ +# # rm -rf /var/lib/apt/lists/* ; \ +# # else echo "Skipping NetFree CA install"; fi + +# # # Force pip to trust PyPI +# # RUN pip config set global.trusted-host "pypi.org files.pythonhosted.org pypi.python.org" +# # RUN pip config set global.cert /etc/ssl/certs/ca-certificates.crt +# # # --- END NETFREE CERT INSTALL --- + +# # # ============================== +# # # Install system packages +# # # ============================== +# # RUN apt-get update && apt-get install -y --no-install-recommends \ +# # libgl1-mesa-glx \ +# # libglib2.0-0 \ +# # libsm6 \ +# # libxext6 \ +# # libxrender1 \ +# # libgtk2.0-0 \ +# # libcanberra-gtk-module \ +# # libcanberra-gtk3-module \ +# # && rm -rf /var/lib/apt/lists/* + +# # # ============================== +# # # Working directory +# # # ============================== +# # ==== Portable CPU base (works everywhere) ==== +# FROM python:3.10-slim + +# ENV PIP_NO_CACHE_DIR=1 \ +# PYTHONDONTWRITEBYTECODE=1 \ +# PYTHONUNBUFFERED=1 + +# # System deps מינימליים ל-CV/IO +# RUN apt-get update && apt-get install -y --no-install-recommends \ +# git ffmpeg libsm6 libxext6 libgl1 ca-certificates \ +# && rm -rf /var/lib/apt/lists/* +# # הוספת תעודת NetFree ל־trust store של המערכת +# COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +# RUN update-ca-certificates + +# # לוודא שכלי רשת/פייתון משתמשים ב־CA המעודכן +# ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +# ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +# ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# # Torch/torchvision/torchaudio גרסאות CPU יציבות מה-PyTorch index +# ARG TORCH_VERSION=2.2.1 +# ARG TORCHVISION_VERSION=0.17.1 +# ARG TORCHAUDIO_VERSION=2.2.1 +# RUN python -m pip install --upgrade pip && \ +# python -m pip install --index-url https://download.pytorch.org/whl/cpu \ +# torch==${TORCH_VERSION} \ +# torchvision==${TORCHVISION_VERSION} \ +# torchaudio==${TORCHAUDIO_VERSION} + +# # (מבטל תלות ב-NetFree בזמן build; אין ADD/curl מהאינטרנט בשלב הזה) +# # ==== END portable header ==== + +# # ============================== +# # Working directory +# # ============================== +# # WORKDIR /app + +# WORKDIR /app + +# # Update pip +# RUN pip install --upgrade pip + +# # ============================== +# # Install dependencies +# # ============================== +# COPY agri_baseline/requirements.txt /app/requirements.txt +# RUN pip install --no-cache-dir --upgrade "numpy==1.26.4" +# RUN pip install --no-cache-dir --force-reinstall "opencv-python-headless==4.9.0.80" + +# RUN pip install --no-cache-dir -r /app/requirements.txt + +# # ============================== +# # Copy source code +# # ============================== +# COPY agri_baseline /app/agri_baseline +# COPY models /app/models +# # Copy tests folder +# COPY tests /app/tests + +# # Set PYTHONPATH +# ENV PYTHONPATH=/app:$PYTHONPATH + +# # ============================== +# # Entry point +# # ============================== +# CMD ["python", "agri_baseline/src/batch_runner.py"] +# syntax=docker/dockerfile:1.6 + +FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye + +ENV PIP_NO_CACHE_DIR=0 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# 1) חבילות מערכת בסיסיות +RUN apt-get update && apt-get install -y --no-install-recommends \ + git ffmpeg libsm6 libxext6 libgl1 ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# 2) הוספת תעודת NetFree שהכנת (הקובץ יושב לצד ה-dockerfile) +# COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +# RUN update-ca-certificates + +# 3) לוודא שכלי רשת/פייתון משתמשים ב-CA של המערכת +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# 4) התקנת Torch CPU מהאינדקס של PyTorch עם cache +ARG TORCH_VERSION=2.2.1 +ARG TORCHVISION_VERSION=0.17.1 +ARG TORCHAUDIO_VERSION=2.2.1 +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --upgrade pip && \ + python -m pip install --index-url https://download.pytorch.org/whl/cpu \ + torch==${TORCH_VERSION} \ + torchvision==${TORCHVISION_VERSION} \ + torchaudio==${TORCHAUDIO_VERSION} + +# 5) ספריות פייתון נוספות (עם cache + בלי --no-cache-dir) +# WORKDIR /app +# COPY Detection_Jobs/agri_baseline/requirements.txt /app/requirements.txt + +# RUN --mount=type=cache,target=/root/.cache/pip \ +# pip install --upgrade pip && \ +# pip install "numpy==1.26.4" && \ +# pip install --force-reinstall "opencv-python-headless==4.9.0.80" && \ +# pip install --retries 10 --timeout 120 -r /app/requirements.txt +# 5) ספריות פייתון נוספות (עם cache + סינון GPU) +WORKDIR /app +COPY agri_baseline/requirements.txt /app/requirements.txt + +# מסננים תלויות GPU כדי שלא ימשכו CUDA +RUN awk '!/^(torch|torchvision|torchaudio)[[:space:]=<>!~]*$/ \ + && !/^pytorch-cuda/ \ + && !/^xformers/ \ + && !/^cupy-cuda/ \ + && !/^nvidia[-_]/' /app/requirements.txt > /app/requirements.cpu.txt + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + PIP_INDEX_URL=https://download.pytorch.org/whl/cpu \ + PIP_EXTRA_INDEX_URL=https://pypi.org/simple \ + pip install --retries 10 --timeout 120 -r /app/requirements.cpu.txt + +# 6) קוד המקור +COPY agri_baseline /app/agri_baseline +COPY models /app/models +COPY tests /app/tests + +# 7) PYTHONPATH – בלי ההפניה למשתנה שאינו קיים בבילד +ENV PYTHONPATH=/app + +# 8) Entry +CMD ["python", "agri_baseline/src/batch_runner.py"] diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/pytest.ini b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/pytest.ini new file mode 100644 index 000000000..89313dd9b --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +pythonpath = . +testpaths = tests +addopts = -v diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/cnn_binary_classifier.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/cnn_binary_classifier.py new file mode 100644 index 000000000..898c2c918 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/cnn_binary_classifier.py @@ -0,0 +1,12 @@ +# agri-baseline/src/detectors/cnn_binary_classifier.py +import torch.nn as nn +from torchvision import models + +def build_binary_model(pretrained: bool = True) -> nn.Module: + """ + Builds a ResNet18 model for binary classification (healthy vs diseased). + """ + model = models.resnet18(weights="IMAGENET1K_V1" if pretrained else None) + in_features = model.fc.in_features + model.fc = nn.Linear(in_features, 2) # healthy / diseased + return model diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/dataset_binary.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/dataset_binary.py new file mode 100644 index 000000000..d63bf5208 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/dataset_binary.py @@ -0,0 +1,36 @@ +# agri-baseline/src/detectors/dataset_binary.py +import os +from torch.utils.data import Dataset +from PIL import Image + +class BinaryDiseaseDataset(Dataset): + """ + Dataset wrapper that maps: + - healthy folders -> label 0 + - all disease folders -> label 1 + Keeps also the original folder name for optional subtype info. + """ + def __init__(self, root: str, transform=None): + self.samples = [] + + self.targets = [] + self.transform = transform + for cls in os.listdir(root): + path = os.path.join(root, cls) + if not os.path.isdir(path): + continue + label = 0 if "healthy" in cls.lower() else 1 + for f in os.listdir(path): + if f.lower().endswith((".jpg", ".png", ".jpeg")): + self.samples.append((os.path.join(path, f), label, cls)) + self.targets.append(label) + + def __len__(self): + return len(self.samples) + + def __getitem__(self, idx): + path, label, cls_name = self.samples[idx] + img = Image.open(path).convert("RGB") + if self.transform: + img = self.transform(img) + return img, label, cls_name diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/disease.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/disease.py new file mode 100644 index 000000000..653f673ae --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/disease.py @@ -0,0 +1,202 @@ +import cv2 +import numpy as np + +from ...agri_baseline.src.detectors.base import Detection +from ..pipeline import config + + +class DiseaseDetector: + """ + Improved disease detector: + - Leaf mask (HSV/LAB) to isolate plant tissue. + - Candidate lesion detection: + 1) Yellow/Brown in HSV (stress/necrosis). + 2) Dark + Brown in LAB (low L, high b). + - Noise cleaning and merging. + - Shape filtering by circularity (detect "spots"). + - Confidence weighted by darkness, saturation, and circularity. + """ + + name = "disease" + + # HSV thresholds for yellow/brown (tunable) + HSV_YELLOW = ((10, 50, 40), (45, 255, 255)) + HSV_BROWN1 = ((0, 80, 30), (10, 255, 200)) + HSV_BROWN2 = ((160, 80, 30), (179, 255, 200)) + + # LAB thresholds for dark/brown lesions (tunable) + LAB_L_MAX_DARK = 145 # Lower L means darker + LAB_B_MIN_BROWN = 135 # Higher b means more yellow/brown + + # Shape filtering + MIN_CIRCULARITY = 0.22 # 4πA/P^2; range 0..1 + MAX_ASPECT_RATIO = 2.2 # Avoid elongated regions + DILATE_MERGE_RADIUS = 4 + + def __init__(self): + # Minimum area from config (fallback to default if missing) + self.min_area = int(getattr(config, "MIN_BBOX_AREA", 60)) + + def run(self, bgr_image: np.ndarray) -> list[Detection]: + h, w = bgr_image.shape[:2] + + # ---------- 1) Leaf isolation ---------- + hsv = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2HSV) + lab = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2LAB) + H, S, V = cv2.split(hsv) + L, A, B = cv2.split(lab) + + # Green mask in HSV (broad range for leaf tissue) + green1 = cv2.inRange(hsv, (35, 30, 30), (85, 255, 255)) + green2 = cv2.inRange(hsv, (25, 25, 40), (95, 255, 255)) + leaf_mask = cv2.bitwise_or(green1, green2) + + # Contrast enhancement with CLAHE on L channel + clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + L_eq = clahe.apply(L) + + # Basic cleaning of leaf mask + leaf_mask = cv2.medianBlur(leaf_mask, 5) + leaf_mask = cv2.morphologyEx( + leaf_mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8), iterations=1 + ) + + # ---------- 2) Lesion candidates ---------- + # (a) Yellow/Brown in HSV + yellow = cv2.inRange(hsv, self.HSV_YELLOW[0], self.HSV_YELLOW[1]) + brown1 = cv2.inRange(hsv, self.HSV_BROWN1[0], self.HSV_BROWN1[1]) + brown2 = cv2.inRange(hsv, self.HSV_BROWN2[0], self.HSV_BROWN2[1]) + hsv_spots = cv2.bitwise_or(yellow, cv2.bitwise_or(brown1, brown2)) + + # (b) Dark + Brownish in LAB + dark = cv2.threshold(L_eq, self.LAB_L_MAX_DARK, 255, cv2.THRESH_BINARY_INV)[1] + brownish = cv2.threshold(B, self.LAB_B_MIN_BROWN, 255, cv2.THRESH_BINARY)[1] + lab_spots = cv2.bitwise_and(dark, brownish) + + # Combine HSV and LAB candidates, restricted to leaf mask + candidates = cv2.bitwise_or(hsv_spots, lab_spots) + candidates = cv2.bitwise_and(candidates, leaf_mask) + + # ---------- 3) Cleaning & merging ---------- + candidates = cv2.medianBlur(candidates, 3) + candidates = cv2.morphologyEx( + candidates, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8), iterations=1 + ) + + # Dilate slightly to merge nearby spots + if self.DILATE_MERGE_RADIUS > 0: + k = cv2.getStructuringElement( + cv2.MORPH_ELLIPSE, + (2 * self.DILATE_MERGE_RADIUS + 1, 2 * self.DILATE_MERGE_RADIUS + 1), + ) + candidates = cv2.dilate(candidates, k, iterations=1) + + # ---------- 4) Contours & filtering ---------- + cnts, _ = cv2.findContours(candidates, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + dets = [] + for c in cnts: + area = cv2.contourArea(c) + if area < self.min_area: + continue + + x, y, bw, bh = cv2.boundingRect(c) + + # Circularity: 4πA / P^2 + perim = cv2.arcLength(c, True) + circularity = (4.0 * np.pi * area / (perim ** 2 + 1e-6)) if perim > 0 else 0.0 + if circularity < self.MIN_CIRCULARITY: + continue + + # Aspect ratio filtering + ar = max(bw, bh) / (min(bw, bh) + 1e-6) + if ar > self.MAX_ASPECT_RATIO: + continue + + # Extract subregion for scoring + hsv_box = hsv[y : y + bh, x : x + bw] + lab_box = lab[y : y + bh, x : x + bw] + + Lb = lab_box[:, :, 0].astype(np.float32) + Sb = hsv_box[:, :, 1].astype(np.float32) + + # Darkness score (lower L → higher score) + dark_score = np.clip((180.0 - float(np.mean(Lb))) / 180.0, 0.0, 1.0) + # Saturation score (higher S → higher score) + sat_score = np.clip(float(np.mean(Sb)) / 255.0, 0.0, 1.0) + + # Final weighted confidence + conf = 0.45 * dark_score + 0.35 * sat_score + 0.20 * np.clip(circularity, 0.0, 1.0) + conf = float(np.clip(conf, 0.0, 1.0)) + + dets.append( + Detection( + label="disease_spot", + confidence=conf, + x=int(x), + y=int(y), + w=int(bw), + h=int(bh), + area=int(area), + ) + ) + + # ---------- 5) Merge overlapping boxes ---------- + dets = self._merge_overlaps(dets, iou_thresh=0.5) + return dets + + # ---------- IoU helper ---------- + @staticmethod + def _iou(a, b): + ax1, ay1, ax2, ay2 = a.x, a.y, a.x + a.w, a.y + a.h + bx1, by1, bx2, by2 = b.x, b.y, b.x + b.w, b.y + b.h + inter_x1, inter_y1 = max(ax1, bx1), max(ay1, by1) + inter_x2, inter_y2 = min(ax2, bx2), min(ay2, by2) + iw, ih = max(0, inter_x2 - inter_x1), max(0, inter_y2 - inter_y1) + inter = iw * ih + if inter == 0: + return 0.0 + area_a = a.w * a.h + area_b = b.w * b.h + return inter / float(area_a + area_b - inter + 1e-6) + + def _merge_overlaps(self, dets, iou_thresh=0.5): + if not dets: + return dets + dets = sorted(dets, key=lambda d: d.confidence, reverse=True) + kept = [] + while dets: + base = dets.pop(0) + to_merge = [base] + remain = [] + for d in dets: + if self._iou(base, d) >= iou_thresh: + to_merge.append(d) + else: + remain.append(d) + dets = remain + + # Merge into one bounding box + xs = [d.x for d in to_merge] + ys = [d.y for d in to_merge] + x2s = [d.x + d.w for d in to_merge] + y2s = [d.y + d.h for d in to_merge] + x = int(min(xs)) + y = int(min(ys)) + w = int(max(x2s) - x) + h = int(max(y2s) - y) + + # Average confidence + conf = float(np.mean([d.confidence for d in to_merge])) + area = int(w * h) + kept.append( + Detection( + label="disease_spot", + confidence=conf, + x=x, + y=y, + w=w, + h=h, + area=area, + ) + ) + return kept diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/eval_multi_levels.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/eval_multi_levels.py new file mode 100644 index 000000000..c39ea4e5a --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/eval_multi_levels.py @@ -0,0 +1,167 @@ +# eval_multi_levels.py +import torch +import numpy as np +from sklearn.metrics import accuracy_score, confusion_matrix, f1_score, classification_report +from torch.utils.data import DataLoader +import cv2 +import albumentations as A +from albumentations.pytorch import ToTensorV2 + +from agri_baseline.src.detectors.train.dictionary import CLASS_MAPPING +from agri_baseline.src.detectors.cnn_multi_classifier import build_multi_model +from torchvision import datasets +import seaborn as sns +import matplotlib.pyplot as plt + +# ------------------------ +# Paths +# ------------------------ +DATA_DIR = "data_balanced/PlantDoc/test" +MODEL_PATH = "models/cnn_multi_stage3.pth" + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# ------------------------ +# Transforms +# ------------------------ +val_transforms = A.Compose([ + A.Resize(224, 224), + A.Normalize(mean=(0.485, 0.456, 0.406), + std=(0.229, 0.224, 0.225)), + ToTensorV2() +]) + +# ------------------------ +# Dataset wrapper +# ------------------------ +class AlbumentationsDataset(torch.utils.data.Dataset): + def __init__(self, dataset, transform=None): + self.dataset = dataset + self.transform = transform + + def __getitem__(self, idx): + path, label = self.dataset.samples[idx] + image = cv2.imread(path) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + if self.transform: + image = self.transform(image=image)["image"] + return image, label + + def __len__(self): + return len(self.dataset) + + +# ------------------------ +# Prepare dataset +# ------------------------ +dataset = datasets.ImageFolder(DATA_DIR) +canonical_classes = sorted(set(CLASS_MAPPING.values())) +class_to_idx = {cls: i for i, cls in enumerate(canonical_classes)} + +new_samples, new_targets = [], [] +for path, label_idx in dataset.samples: + raw_name = dataset.classes[label_idx].lower().replace(" ", "_") + canonical_label = CLASS_MAPPING.get(raw_name) + if canonical_label is None: + raise ValueError(f"Class {raw_name} not found in CLASS_MAPPING") + new_samples.append((path, class_to_idx[canonical_label])) + new_targets.append(class_to_idx[canonical_label]) + +dataset.samples = new_samples +dataset.targets = new_targets +dataset.classes = canonical_classes +dataset.class_to_idx = class_to_idx + +val_dataset = AlbumentationsDataset(dataset, transform=val_transforms) +val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False) + +# ------------------------ +# Load model +# ------------------------ +model = build_multi_model(num_classes=len(canonical_classes)).to(device) +state_dict = torch.load(MODEL_PATH, map_location=device) +model.load_state_dict(state_dict) +model.eval() + +# ------------------------ +# Evaluation +# ------------------------ +all_preds, all_labels = [], [] +with torch.no_grad(): + for images, labels in val_loader: + images, labels = images.to(device), labels.to(device) + outputs = model(images) + _, preds = outputs.max(1) + all_preds.extend(preds.cpu().numpy()) + all_labels.extend(labels.cpu().numpy()) + +all_preds = np.array(all_preds) +all_labels = np.array(all_labels) + +# ------------------------ +# Grouping +# ------------------------ +def to_healthy_sick(cls: str): + return "healthy" if "healthy" in cls else "sick" + +def to_crop(cls: str): + if cls.startswith("tomato"): return "tomato" + if cls.startswith("potato"): return "potato" + if cls.startswith("pepper"): return "pepper" + return "other" + +def to_disease(cls: str): + if "bacterial_spot" in cls: return "bacterial_spot" + if "early_blight" in cls: return "early_blight" + if "late_blight" in cls: return "late_blight" + if "leaf_mold" in cls: return "leaf_mold" + if "septoria_leaf_spot" in cls: return "septoria_leaf_spot" + if "spider_mites" in cls: return "spider_mites" + if "target_spot" in cls: return "target_spot" + if "mosaic_virus" in cls: return "mosaic_virus" + if "yellowleaf_curl_virus" in cls: return "yellowleaf_curl_virus" + return "none" + +idx_to_class = {v: k for k, v in class_to_idx.items()} + +y_true_cls = [idx_to_class[i] for i in all_labels] +y_pred_cls = [idx_to_class[i] for i in all_preds] + +# ------------------------ +# Evaluation per level +# ------------------------ +def evaluate_level(name, y_true, y_pred, labels=None): + acc = accuracy_score(y_true, y_pred) + f1 = f1_score(y_true, y_pred, average="weighted") + print(f"\n===== {name} =====") + print(f"Accuracy: {acc:.4f}") + print(f"F1-score (weighted): {f1:.4f}") + print(classification_report(y_true, y_pred, digits=4)) + cm = confusion_matrix(y_true, y_pred, labels=labels) + if labels: + plt.figure(figsize=(8, 6)) + sns.heatmap(cm, annot=True, fmt="d", xticklabels=labels, yticklabels=labels, cmap="Blues") + plt.title(f"Confusion Matrix - {name}") + plt.xlabel("Predicted") + plt.ylabel("True") + plt.show() + +# Healthy vs Sick +evaluate_level("Healthy vs Sick", + [to_healthy_sick(c) for c in y_true_cls], + [to_healthy_sick(c) for c in y_pred_cls], + labels=["healthy", "sick"]) + +# Crop type +evaluate_level("Crop type", + [to_crop(c) for c in y_true_cls], + [to_crop(c) for c in y_pred_cls], + labels=["tomato", "potato", "pepper", "other"]) + +# Disease type +evaluate_level("Disease type", + [to_disease(c) for c in y_true_cls], + [to_disease(c) for c in y_pred_cls], + labels=["bacterial_spot","early_blight","late_blight","leaf_mold", + "septoria_leaf_spot","spider_mites","target_spot", + "mosaic_virus","yellowleaf_curl_virus","none"]) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi.py new file mode 100644 index 000000000..e3e5457cd --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi.py @@ -0,0 +1,242 @@ +# finetune_multi.py +import torch +import torch.nn as nn +import torch.optim as optim +from torchvision import datasets +import os +from sklearn.metrics import f1_score +from torch.utils.data import DataLoader, random_split, WeightedRandomSampler +from torch.optim.lr_scheduler import ReduceLROnPlateau +import albumentations as A +from albumentations.pytorch import ToTensorV2 +import cv2 +import numpy as np + +from agri_baseline.src.detectors.train.dictionary import CLASS_MAPPING +from agri_baseline.src.detectors.cnn_multi_classifier import build_multi_model + + +# ------------------------ +# MixUp +# ------------------------ +def mixup_data(x, y, alpha=1.0): + if alpha > 0: + lam = np.random.beta(alpha, alpha) + else: + lam = 1 + batch_size = x.size()[0] + index = torch.randperm(batch_size).to(x.device) + + mixed_x = lam * x + (1 - lam) * x[index, :] + y_a, y_b = y, y[index] + return mixed_x, y_a, y_b, lam + +def mixup_criterion(criterion, pred, y_a, y_b, lam): + return lam * criterion(pred, y_a) + (1 - lam) * criterion(pred, y_b) + + +# ------------------------ +# Paths +# ------------------------ +DATA_DIR = "data_balanced/PlantDoc" +MODEL_PATH = "models/cnn_multi.pth" +SAVE_PATH = "models/cnn_multi_finetuned.pth" + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + +# ------------------------ +# Augmentations +# ------------------------ +train_transforms = A.Compose([ + A.RandomResizedCrop(size=(224, 224), scale=(0.7, 1.0), p=1.0), + A.HorizontalFlip(p=0.5), + A.VerticalFlip(p=0.3), + A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.2, rotate_limit=30, p=0.7), + A.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, p=0.5), + A.RandomBrightnessContrast(p=0.5), + A.GaussianBlur(p=0.3), + A.CoarseDropout(max_height=32, max_width=32, max_holes=1, p=0.3), + A.Normalize(mean=(0.485, 0.456, 0.406), + std=(0.229, 0.224, 0.225)), + ToTensorV2() +]) + +val_transforms = A.Compose([ + A.Resize(224, 224), + A.Normalize(mean=(0.485, 0.456, 0.406), + std=(0.229, 0.224, 0.225)), + ToTensorV2() +]) + + +# ------------------------ +# Albumentations Dataset +# ------------------------ +class AlbumentationsDataset(torch.utils.data.Dataset): + def __init__(self, dataset, transform=None): + self.dataset = dataset + self.transform = transform + + def __getitem__(self, idx): + path, label = self.dataset.samples[idx] + image = cv2.imread(path) + image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + if self.transform: + image = self.transform(image=image)["image"] + return image, label + + def __len__(self): + return len(self.dataset) + + +# ------------------------ +# Prepare Dataset +# ------------------------ +def prepare_multi_dataset(path): + dataset = datasets.ImageFolder(path) + new_samples, new_targets = [], [] + canonical_classes = sorted(set(CLASS_MAPPING.values())) + class_to_idx = {cls: i for i, cls in enumerate(canonical_classes)} + + for sample_path, label_idx in dataset.samples: + raw_name = dataset.classes[label_idx].lower().replace(" ", "_") + canonical_label = CLASS_MAPPING.get(raw_name) + if canonical_label is None: + raise ValueError(f"Class {raw_name} not found in CLASS_MAPPING") + new_samples.append((sample_path, class_to_idx[canonical_label])) + new_targets.append(class_to_idx[canonical_label]) + + dataset.samples = new_samples + dataset.targets = new_targets + dataset.classes = canonical_classes + dataset.class_to_idx = class_to_idx + return dataset + + +# ------------------------ +# Load dataset +# ------------------------ +full_dataset = prepare_multi_dataset(os.path.join(DATA_DIR, "train")) +print("Classes:", full_dataset.classes) +print("Total samples:", len(full_dataset)) + +train_size = int(0.8 * len(full_dataset)) +val_size = len(full_dataset) - train_size +train_dataset, val_dataset = random_split(full_dataset, [train_size, val_size]) + +train_dataset = AlbumentationsDataset(train_dataset.dataset, transform=train_transforms) +val_dataset = AlbumentationsDataset(val_dataset.dataset, transform=val_transforms) + +class_counts = np.bincount(full_dataset.targets) +class_weights = 1. / class_counts +sample_weights = [class_weights[t] for t in full_dataset.targets] + +sampler = WeightedRandomSampler(weights=sample_weights, + num_samples=len(sample_weights), + replacement=True) + +train_loader = DataLoader(train_dataset, batch_size=32, sampler=sampler) +val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False) + + +# ------------------------ +# Model +# ------------------------ +model = build_multi_model(num_classes=len(full_dataset.classes)).to(device) +state_dict = torch.load(MODEL_PATH, map_location=device) +filtered_state_dict = {k: v for k, v in state_dict.items() if not k.startswith("fc.")} +model.load_state_dict(filtered_state_dict, strict=False) +print("✅ Loaded pretrained backbone") + + +# ------------------------ +# Training setup +# ------------------------ +criterion = nn.CrossEntropyLoss() +optimizer = optim.Adam([ + {"params": model.fc.parameters(), "lr": 1e-3}, +], lr=1e-3) + +scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=3, verbose=True) +best_val_f1 = 0.0 +patience, counter = 5, 0 + + +# ------------------------ +# Gradual Unfreeze +# ------------------------ +def unfreeze(epoch): + if epoch == 5: + for name, param in model.named_parameters(): + if "layer4" in name: + param.requires_grad = True + if epoch == 10: + for param in model.parameters(): + param.requires_grad = True + + +# ------------------------ +# Training Loop +# ------------------------ +EPOCHS = 20 +for epoch in range(EPOCHS): + unfreeze(epoch) + model.train() + total_loss, correct, total = 0.0, 0, 0 + for images, labels in train_loader: + images, labels = images.to(device), labels.to(device) + optimizer.zero_grad() + images, targets_a, targets_b, lam = mixup_data(images, labels, alpha=0.4) + outputs = model(images) + loss = mixup_criterion(criterion, outputs, targets_a, targets_b, lam) + loss.backward() + optimizer.step() + total_loss += loss.item() * images.size(0) + _, preds = outputs.max(1) + correct += preds.eq(labels).sum().item() + total += labels.size(0) + + train_acc = correct / total + train_loss = total_loss / total + + # Validation + model.eval() + all_preds, all_labels = [], [] + val_loss, val_correct, val_total = 0.0, 0, 0 + with torch.no_grad(): + for images, labels in val_loader: + images, labels = images.to(device), labels.to(device) + outputs = model(images) + loss = criterion(outputs, labels) + val_loss += loss.item() * images.size(0) + _, preds = outputs.max(1) + val_correct += preds.eq(labels).sum().item() + val_total += labels.size(0) + all_preds.extend(preds.cpu().numpy()) + all_labels.extend(labels.cpu().numpy()) + + val_acc = val_correct / val_total + val_loss /= val_total + val_f1 = f1_score(all_labels, all_preds, average="weighted") + + print(f"Epoch {epoch+1}/{EPOCHS} " + f"Train Loss: {train_loss:.4f}, Train Acc: {train_acc:.4f} " + f"Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.4f}, Val F1: {val_f1:.4f}") + + scheduler.step(val_loss) + + # Save by F1 + if val_f1 > best_val_f1: + best_val_f1 = val_f1 + counter = 0 + torch.save(model.state_dict(), SAVE_PATH) + print("💾 Model improved (F1) and saved!") + else: + counter += 1 + print(f"⏳ No improvement. EarlyStopping counter: {counter}/{patience}") + if counter >= patience: + print("🛑 Early stopping triggered!") + break + +print(f"✅ Training finished. Best model saved to {SAVE_PATH}") diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi_stage3.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi_stage3.py new file mode 100644 index 000000000..eee68bd67 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi_stage3.py @@ -0,0 +1,191 @@ +# finetune_multi_stage3.py +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, random_split +import albumentations as A +from albumentations.pytorch import ToTensorV2 +import cv2, os +import numpy as np +from sklearn.metrics import f1_score + +from agri_baseline.src.detectors.train.dictionary import CLASS_MAPPING +from agri_baseline.src.detectors.cnn_multi_classifier import build_multi_model +from torchvision import datasets + +# ========================= +# Config +# ========================= +DATA_DIR = "data_balanced/PlantDoc" +PREV_MODEL = "models/cnn_multi_finetuned.pth" +SAVE_PATH = "models/cnn_multi_stage3.pth" + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + +# ========================= +# Augmentations +# ========================= +train_tfms = A.Compose([ + A.RandomResizedCrop(size=(224, 224), scale=(0.6, 1.0), p=1.0), + A.HorizontalFlip(p=0.5), + A.VerticalFlip(p=0.3), + A.RandomBrightnessContrast(p=0.4), + A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.2, rotate_limit=30, p=0.5), + A.GaussianBlur(p=0.2), + A.RandomGamma(p=0.3), + A.Normalize(mean=(0.485, 0.456, 0.406), + std=(0.229, 0.224, 0.225)), + ToTensorV2() +]) + +val_tfms = A.Compose([ + A.Resize(224, 224), + A.Normalize(mean=(0.485, 0.456, 0.406), + std=(0.229, 0.224, 0.225)), + ToTensorV2() +]) + + +# ========================= +# Dataset wrapper +# ========================= +class AlbumentationsDataset(torch.utils.data.Dataset): + def __init__(self, dataset, transform=None): + self.dataset = dataset + self.transform = transform + + def __getitem__(self, idx): + path, label = self.dataset.samples[idx] + img = cv2.imread(path) + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + if self.transform: + img = self.transform(image=img)["image"] + return img, label + + def __len__(self): + return len(self.dataset) + + +def prepare_dataset(path): + ds = datasets.ImageFolder(path) + new_samples, new_targets = [], [] + canonical = sorted(set(CLASS_MAPPING.values())) + class_to_idx = {cls: i for i, cls in enumerate(canonical)} + + for pth, idx in ds.samples: + raw = ds.classes[idx].lower().replace(" ", "_") + canon = CLASS_MAPPING.get(raw) + if canon is None: + raise ValueError(f"Class {raw} missing in CLASS_MAPPING") + new_samples.append((pth, class_to_idx[canon])) + new_targets.append(class_to_idx[canon]) + + ds.samples = new_samples + ds.targets = new_targets + ds.classes = canonical + ds.class_to_idx = class_to_idx + return ds + + +# ========================= +# Progressive unfreezing +# ========================= +def unfreeze_layers(model, stages): + """ + stages: List of layer names to release (e.g.: ["layer3", "layer2"]) + """ + for name, param in model.named_parameters(): + for stage in stages: + if stage in name: + param.requires_grad = True + + +# ========================= +# Training loop +# ========================= +def train_stage3(model, train_loader, val_loader, epochs=20): + criterion = nn.CrossEntropyLoss() + optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4) + + best_f1, patience, counter = 0, 5, 0 + for epoch in range(epochs): + model.train() + total_loss, total_correct, total = 0, 0, 0 + for xb, yb in train_loader: + xb, yb = xb.to(device), yb.to(device) + optimizer.zero_grad() + out = model(xb) + loss = criterion(out, yb) + loss.backward() + optimizer.step() + total_loss += loss.item() * xb.size(0) + _, preds = out.max(1) + total_correct += preds.eq(yb).sum().item() + total += yb.size(0) + + train_acc = total_correct / total + train_loss = total_loss / total + + # Validation + model.eval() + val_loss, val_correct, val_total = 0, 0, 0 + all_preds, all_labels = [], [] + with torch.no_grad(): + for xb, yb in val_loader: + xb, yb = xb.to(device), yb.to(device) + out = model(xb) + loss = criterion(out, yb) + val_loss += loss.item() * xb.size(0) + _, preds = out.max(1) + val_correct += preds.eq(yb).sum().item() + val_total += yb.size(0) + all_preds.extend(preds.cpu().numpy()) + all_labels.extend(yb.cpu().numpy()) + + val_acc = val_correct / val_total + val_loss /= val_total + val_f1 = f1_score(all_labels, all_preds, average="weighted") + + print(f"Epoch {epoch+1}/{epochs} | Train Loss: {train_loss:.4f} Acc: {train_acc:.3f} " + f"| Val Loss: {val_loss:.4f} Acc: {val_acc:.3f} F1: {val_f1:.3f}") + + if val_f1 > best_f1: + best_f1 = val_f1 + counter = 0 + torch.save(model.state_dict(), SAVE_PATH) + print(f"💾 Model improved (F1={val_f1:.3f}) and saved!") + else: + counter += 1 + if counter >= patience: + print("🛑 EarlyStopping triggered.") + break + + +# ========================= +# Main +# ========================= +if __name__ == "__main__": + full_ds = prepare_dataset(os.path.join(DATA_DIR, "train")) + train_size = int(0.8 * len(full_ds)) + val_size = len(full_ds) - train_size + train_ds, val_ds = random_split(full_ds, [train_size, val_size]) + + train_ds = AlbumentationsDataset(train_ds.dataset, transform=train_tfms) + val_ds = AlbumentationsDataset(val_ds.dataset, transform=val_tfms) + + train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) + val_loader = DataLoader(val_ds, batch_size=32) + + model = build_multi_model(num_classes=len(full_ds.classes)).to(device) + model.load_state_dict(torch.load(PREV_MODEL, map_location=device)) + + # In step 3 we will release additional layers beyond layer4 + for p in model.parameters(): + p.requires_grad = False + for stage in ["layer3", "layer4", "fc"]: + unfreeze_layers(model, [stage]) + print(f"🔓 Unfroze {stage}") + + train_stage3(model, train_loader, val_loader, epochs=15) + print(f"✅ Training done. Best model saved to {SAVE_PATH}") diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/train_binary_multi.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/train_binary_multi.py new file mode 100644 index 000000000..0d48afb36 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/train_binary_multi.py @@ -0,0 +1,152 @@ +# agri-baseline/src/detectors/train_binary_multi.py +import argparse +import os +import torch +import torch.nn as nn +from torch.utils.data import DataLoader, WeightedRandomSampler +from torchvision import datasets, transforms +from torch.optim.lr_scheduler import ReduceLROnPlateau +import numpy as np + +from ...agri_baseline.src.detectors.cnn_binary_classifier import build_binary_model +from ...agri_baseline.src.detectors.cnn_multi_classifier import build_multi_model +from ...agri_baseline.src.detectors.dataset_binary import BinaryDiseaseDataset + + +def train_model(model, dataloader, val_dl, device, epochs, lr, out_path): + opt = torch.optim.Adam(model.parameters(), lr=lr) + loss_fn = nn.CrossEntropyLoss() + scheduler = ReduceLROnPlateau(opt, mode="min", factor=0.5, patience=3, verbose=True) + + best_val_loss = float("inf") + patience, counter = 5, 0 + + for epoch in range(epochs): + model.train() + running_loss, correct, total = 0.0, 0, 0 + for batch in dataloader: + if len(batch) == 3: + xb, yb, _ = batch + else: + xb, yb = batch + xb, yb = xb.to(device), yb.to(device) + + opt.zero_grad() + preds = model(xb) + loss = loss_fn(preds, yb) + loss.backward() + opt.step() + + running_loss += loss.item() * xb.size(0) + _, predicted = preds.max(1) + correct += predicted.eq(yb).sum().item() + total += yb.size(0) + + acc = correct / total + + # Validation + val_loss, val_acc = evaluate(model, val_dl, device, loss_fn) + print(f"Epoch {epoch+1}/{epochs} " + f"Train Loss={running_loss/total:.4f} Train Acc={acc:.3f} " + f"Val Loss={val_loss:.4f} Val Acc={val_acc:.3f}") + + scheduler.step(val_loss) + + # EarlyStopping + if val_loss < best_val_loss: + best_val_loss = val_loss + counter = 0 + torch.save(model.state_dict(), out_path) + print(f"💾 Saved best model {out_path}") + else: + counter += 1 + print(f"⏳ EarlyStopping counter {counter}/{patience}") + if counter >= patience: + print("🛑 Early stopping triggered") + break + + +def evaluate(model, dataloader, device, loss_fn): + model.eval() + correct, total, total_loss = 0, 0, 0.0 + with torch.no_grad(): + for batch in dataloader: + if len(batch) == 3: + xb, yb, _ = batch + else: + xb, yb = batch + xb, yb = xb.to(device), yb.to(device) + preds = model(xb) + loss = loss_fn(preds, yb) + total_loss += loss.item() * xb.size(0) + _, predicted = preds.max(1) + correct += predicted.eq(yb).sum().item() + total += yb.size(0) + return total_loss/total, correct/total + + +def make_sampler(targets): + class_counts = np.bincount(targets) + class_weights = 1. / class_counts + sample_weights = [class_weights[t] for t in targets] + return WeightedRandomSampler(weights=sample_weights, + num_samples=len(sample_weights), + replacement=True) + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--data", required=True, help="Dataset root (with train/val/test)") + p.add_argument("--out", default="./models") + p.add_argument("--epochs", type=int, default=10) + p.add_argument("--batch", type=int, default=32) + p.add_argument("--lr", type=float, default=1e-3) + p.add_argument("--device", default="cpu") + args = p.parse_args() + + device = torch.device(args.device) + + # Augmentations + train_tfms = transforms.Compose([ + transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.RandomRotation(15), + transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), + transforms.ToTensor(), + transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) + ]) + test_tfms = transforms.Compose([ + transforms.Resize((224,224)), + transforms.ToTensor(), + transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) + ]) + + # Binary dataset + train_bin = BinaryDiseaseDataset(os.path.join(args.data,"train"), transform=train_tfms) + val_bin = BinaryDiseaseDataset(os.path.join(args.data,"val"), transform=test_tfms) + + sampler_bin = make_sampler(train_bin.targets) + train_dl_bin = DataLoader(train_bin, batch_size=args.batch, sampler=sampler_bin) + val_dl_bin = DataLoader(val_bin, batch_size=args.batch) + + model_bin = build_binary_model().to(device) + train_model(model_bin, train_dl_bin, val_dl_bin, device, args.epochs, args.lr, + os.path.join(args.out, "cnn_binary.pth")) + + # Multi-class dataset + train_multi = datasets.ImageFolder(os.path.join(args.data,"train"), transform=train_tfms) + val_multi = datasets.ImageFolder(os.path.join(args.data,"val"), transform=test_tfms) + + sampler_multi = make_sampler([y for _, y in train_multi.samples]) + train_dl_multi = DataLoader(train_multi, batch_size=args.batch, sampler=sampler_multi) + val_dl_multi = DataLoader(val_multi, batch_size=args.batch) + + model_multi = build_multi_model(num_classes=len(train_multi.classes)).to(device) + train_model(model_multi, train_dl_multi, val_dl_multi, device, args.epochs, args.lr, + os.path.join(args.out, "cnn_multi.pth")) + + torch.save({"classes": train_multi.classes}, + os.path.join(args.out,"multi_classes.pth")) + +if __name__=="__main__": + main() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/conftest.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/conftest.py new file mode 100644 index 000000000..486e4fe52 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/conftest.py @@ -0,0 +1,29 @@ +# tests/conftest.py +import os +import pytest +from sqlalchemy import text +from agri_baseline.src.pipeline.db import get_engine + +@pytest.fixture(autouse=True, scope="function") +def _ensure_local_db_url(monkeypatch): + """ + Guarantee DATABASE_URL exists for tests. + """ + monkeypatch.setenv( + "DATABASE_URL", + os.getenv( + "DATABASE_URL", + "postgresql+psycopg2://missions_user:pg123@localhost:5432/missions_db", + ), + ) + +@pytest.fixture(autouse=True) +def _clean_tables_before_test(): + """ + Clean key tables before each test so counts can increase deterministically. + Adjust the list to your schema. + """ + tables = ["anomalies", "tile_stats", "event_logs"] + with get_engine().begin() as conn: + for t in tables: + conn.execute(text(f"DELETE FROM {t}")) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_batch_runner.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_batch_runner.py new file mode 100644 index 000000000..db88f63bc --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_batch_runner.py @@ -0,0 +1,68 @@ +# Purpose: End-to-end tests for the BatchRunner pipeline. +# Verifies that running on image folders or single images correctly writes results to the database. + +import pytest +from pathlib import Path +from sqlalchemy import text + +from agri_baseline.src.batch_runner import BatchRunner +from agri_baseline.src.pipeline.db import get_engine + + +@pytest.fixture +def folder_with_images() -> Path: + """ + Return a folder that contains a few test images. + Adjust the path if your dataset sits elsewhere. + """ + folder = Path("./data_balanced/PlantDoc/train/Bell_pepper leaf") + assert folder.exists(), f"Images folder not found: {folder.resolve()}" + return folder + + +def _count(conn, sql: str, params: dict | None = None) -> int: + """ + Small helper: run a COUNT(*) query safely with SQLAlchemy 2.0. + """ + return conn.execute(text(sql), params or {}).scalar() or 0 + + +def test_run_batch_on_images_folder(folder_with_images: Path): + """ + End-to-end: run the batch pipeline on a folder and verify DB writes happened. + We compare counts before/after instead of relying on specific image_id values. + """ + runner = BatchRunner() + + with get_engine().begin() as conn: + before = _count(conn, "SELECT COUNT(1) FROM anomalies") + + runner.run_folder(folder_with_images) + + with get_engine().begin() as conn: + after = _count(conn, "SELECT COUNT(1) FROM anomalies") + + assert after > before, "No detections were written to the database." + + +def test_process_single_image(): + """ + Process a single image and assert the DB anomalies count has increased. + This avoids fragile assumptions on the exact image_id in the DB. + """ + image_path = Path( + "./data_balanced/PlantDoc/train/Bell_pepper leaf/0f3s5A.jpg" + ) + assert image_path.exists(), f"Test image not found: {image_path.resolve()}" + + runner = BatchRunner() + + with get_engine().begin() as conn: + before = _count(conn, "SELECT COUNT(1) FROM anomalies") + + runner.process_image(image_path) + + with get_engine().begin() as conn: + after = _count(conn, "SELECT COUNT(1) FROM anomalies") + + assert after > before, "Single image was not processed correctly." diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_disease_model.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_disease_model.py new file mode 100644 index 000000000..c2cb78625 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_disease_model.py @@ -0,0 +1,17 @@ +# Purpose: Unit tests for the DiseaseDetector class. +# Ensures the model loads successfully and returns valid detections on dummy input. + +import pytest +from agri_baseline.src.detectors.disease_model import DiseaseDetector +import numpy as np + +def test_disease_detector_model_loads(): + detector = DiseaseDetector(model_path="models/cnn_multi_stage3.pth") + assert detector.model is not None, "Model failed to load correctly." + +def test_disease_detector_predicts(): + detector = DiseaseDetector() + img = np.zeros((224, 224, 3)) # Dummy image for testing + detections = detector.run(img) + assert len(detections) > 0, "Model did not return any detections." + assert detections[0].confidence > 0, "Detection confidence should be greater than 0." diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_minio_integration_mock.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_minio_integration_mock.py new file mode 100644 index 000000000..369b7070c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_minio_integration_mock.py @@ -0,0 +1,120 @@ +# Purpose: Mock-based integration tests for MinIO storage. +# Simulates MinIO object downloads, saves them locally, and verifies images can be loaded successfully. + +from __future__ import annotations + +from io import BytesIO +from pathlib import Path +from typing import Dict, Iterable + +import pytest +from PIL import Image + +from agri_baseline.src.storage import minio_sync +from agri_baseline.src.storage.minio_client import MinioConfig +from agri_baseline.src.pipeline.utils import load_image + + +class _FakeObj: + """Mimics the object returned by client.list_objects().""" + + def __init__(self, object_name: str) -> None: + self.object_name = object_name + + +class _FakeResponse: + """ + Minimal MinIO get_object-like response object. + + Provides: + - read(amt: int | None = None) -> bytes + - close() -> None + - release_conn() -> None + + This mirrors what MinIO/urllib3 responses typically expose, so production code + that calls release_conn() won't fail under the mock. + """ + + def __init__(self, data: bytes) -> None: + self._buf = BytesIO(data) + + def read(self, amt: int | None = None) -> bytes: + return self._buf.read() if amt is None else self._buf.read(amt) + + def close(self) -> None: + self._buf.close() + + def release_conn(self) -> None: + # In real clients this releases underlying HTTP resources. + # No-op here is fine for tests. + pass + + +class _FakeMinio: + """ + Fake MinIO client that supports the subset used by minio_sync: + - list_objects(bucket, prefix, recursive) -> Iterable[_FakeObj] + - get_object(bucket, key) -> _FakeResponse + """ + + def __init__(self, payload_by_key: Dict[str, bytes]) -> None: + self._payload_by_key = payload_by_key + + def list_objects(self, bucket: str, prefix: str, recursive: bool) -> Iterable[_FakeObj]: + for key in self._payload_by_key: + if key.startswith(prefix) and not key.endswith("/"): + yield _FakeObj(key) + + def get_object(self, bucket: str, key: str) -> _FakeResponse: + data = self._payload_by_key[key] + return _FakeResponse(data) + + +@pytest.fixture +def fake_jpeg() -> bytes: + """Create a tiny deterministic JPEG in-memory.""" + img = Image.new("RGB", (32, 24), (10, 20, 30)) + buf = BytesIO() + img.save(buf, format="JPEG") + return buf.getvalue() + + +def test_minio_download_and_load(monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + fake_jpeg: bytes) -> None: + """ + Flow under test: + 1) list prefix from MinIO (fake). + 2) download files to local cache dir. + 3) ensure those files exist and can be loaded with load_image. + """ + + # 1) Arrange fake MinIO payload (two images under mission-123/) + payload = { + "mission-123/imgA.jpg": fake_jpeg, + "mission-123/imgB.jpg": fake_jpeg, + } + fake_client = _FakeMinio(payload) + + # 2) Monkeypatch build_client to return our fake client + monkeypatch.setattr(minio_sync, "build_client", lambda cfg: fake_client, raising=True) + + # 3) Prepare config and download target folder + cfg = MinioConfig( + endpoint="127.0.0.1:9000", + access_key="minioadmin", + secret_key="minioadmin", + bucket="leaves", + secure=False, + ) + out_dir = tmp_path / "cache" + + # 4) Act: download objects to local dir + paths = minio_sync.download_prefix_to_dir(cfg, prefix="mission-123", local_dir=out_dir) + + # 5) Assert: files were written and are loadable + assert len(paths) == 2, f"Expected 2 files, got {len(paths)}" + for p in paths: + assert p.exists() and p.is_file(), f"Missing file: {p}" + img, w, h = load_image(str(p)) + assert img is not None and w > 0 and h > 0, f"Failed to load image {p}" diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_run_detectors.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_run_detectors.py new file mode 100644 index 000000000..6e0f13e26 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_run_detectors.py @@ -0,0 +1,21 @@ +# Purpose: Tests for running the DiseaseDetector model. +# Checks that detections are produced with valid confidence values on dummy images. + +import pytest +from agri_baseline.src.detectors.disease_model import DiseaseDetector +import numpy as np + +@pytest.fixture +def dummy_image(): + """Provide a dummy image for testing.""" + return np.zeros((224, 224, 3)) # Black dummy image + +def test_disease_detector_runs(dummy_image): + detector = DiseaseDetector() + detections = detector.run(dummy_image) + assert len(detections) > 0, "Disease detection did not return any detections." + assert detections[0].confidence > 0, "Detection confidence should be greater than 0." + +def test_disease_detector_model_loads(): + detector = DiseaseDetector(model_path="models/cnn_multi_stage3.pth") + assert detector.model is not None, "Model failed to load correctly." diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_utils_local.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_utils_local.py new file mode 100644 index 000000000..ab76fe8cf --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_utils_local.py @@ -0,0 +1,27 @@ +# Purpose: Local unit tests for utility functions. +# Covers image loading, image ID extraction, and bounding box clamping logic. + +from pathlib import Path +from PIL import Image +from agri_baseline.src.pipeline.utils import load_image, image_id_from_path, clamp_bbox + +def _write_test_image(tmp_dir: Path, name: str = "test.jpg") -> Path: + img = Image.new("RGB", (64, 48), (127, 200, 50)) + path = tmp_dir / name + img.save(path, format="JPEG") + return path + +def test_load_image_local(tmp_path: Path): + img_path = _write_test_image(tmp_path) + img, w, h = load_image(str(img_path)) + assert img is not None + assert (w, h) == (64, 48) + +def test_image_id_from_path_no_fs(tmp_path: Path): + fake_path = tmp_path / "nested" / "test.jpg" # no file needed + image_id = image_id_from_path(str(fake_path)) + assert isinstance(image_id, str) and image_id + +def test_clamp_bbox_pure(): + x, y, w, h = clamp_bbox(10, 10, 250, 250, 224, 224) + assert x >= 0 and y >= 0 and w <= 224 and h <= 224 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_validator.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_validator.py new file mode 100644 index 000000000..17957c530 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_validator.py @@ -0,0 +1,119 @@ +# Purpose: Integration tests for the Validator module. +# Verifies event logging from findings and correctness of batch summary generation in the database. + +import pytest +from sqlalchemy import text + +from agri_baseline.src.validator.validator import Validator +from agri_baseline.src.validator.rules import Finding +from agri_baseline.src.pipeline.db import get_engine +from agri_baseline.src.pipeline import config +from agri_baseline.src.pipeline.db import get_engine + +@pytest.fixture(autouse=True) +def _seed_anomalies_for_summary(): + """ + Ensure the DB has minimal data for batch_summary: + - device 'device-1' + - anomaly type id=1 + - mission id=1 with small polygon + - two anomalies with same image_id and non-null geom + Idempotent: safe to run before every test. + """ + with get_engine().begin() as conn: + conn.exec_driver_sql(""" + INSERT INTO devices(device_id, model, owner, active) + VALUES ('device-1','sim','lab',true) + ON CONFLICT (device_id) DO NOTHING; + """) + conn.exec_driver_sql(""" + INSERT INTO anomaly_types(anomaly_type_id, code, description) + VALUES (1,'disease_spot','Leaf disease spot') + ON CONFLICT (anomaly_type_id) DO NOTHING; + """) + conn.exec_driver_sql(""" + INSERT INTO missions(mission_id, start_time, area_geom) + VALUES (1, now(), ST_GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))',4326)) + ON CONFLICT (mission_id) DO NOTHING; + """) + conn.exec_driver_sql(""" + INSERT INTO anomalies(mission_id, device_id, ts, anomaly_type_id, severity, details, geom) + VALUES + (1, 'device-1', now(), 1, 0.6, + '{"image_id":"seed_img_for_summary"}'::jsonb, + ST_GeomFromText('POINT(0.50 0.50)',4326)), + (1, 'device-1', now(), 1, 0.7, + '{"image_id":"seed_img_for_summary"}'::jsonb, + ST_GeomFromText('POINT(0.55 0.52)',4326)) + ON CONFLICT DO NOTHING; + """) + yield + +@pytest.fixture +def dummy_finding() -> Finding: + """ + Create a minimal Finding to simulate a validator output. + Scope/value names should match your Validator implementation. + """ + return Finding( + scope="image", + image_id="test_image", + rule="bbox_oob", + severity="warn", + message="BBox out of bounds", + ) + + +def _count(conn, sql: str, params: dict | None = None) -> int: + """ + Small helper: run a COUNT(*) query safely with SQLAlchemy 2.0. + """ + return conn.execute(text(sql), params or {}).scalar() or 0 + + +def test_validator_image_findings(dummy_finding: Finding): + """ + Ensure validator writes a record into event_logs for the given finding. + We assert a strictly increasing count for the message we inserted. + """ + validator = Validator() + + with get_engine().begin() as conn: + before = _count( + conn, + "SELECT COUNT(1) FROM event_logs WHERE message = :msg", + {"msg": dummy_finding.message}, + ) + + validator.image_findings([dummy_finding]) + + with get_engine().begin() as conn: + after = _count( + conn, + "SELECT COUNT(1) FROM event_logs WHERE message = :msg", + {"msg": dummy_finding.message}, + ) + + assert after > before, "Finding was not written to event_logs." + + +def test_batch_summary(): + """ + Run batch_summary and verify tile_stats is populated or remains populated. + We allow idempotency (>=) but also require that there is some data (> 0). + """ + validator = Validator() + + with get_engine().begin() as conn: + print("DEBUG DB_URL:", config.DB_URL) + print("DEBUG anomalies:", conn.exec_driver_sql("SELECT COUNT(*) FROM anomalies").scalar()) + print("DEBUG tile_stats:", conn.exec_driver_sql("SELECT COUNT(*) FROM tile_stats").scalar()) + before = _count(conn, "SELECT COUNT(1) FROM tile_stats") + + validator.batch_summary() + + with get_engine().begin() as conn: + after = _count(conn, "SELECT COUNT(1) FROM tile_stats") + + assert after >= before, "tile_stats count unexpectedly decreased." + assert after > 0, "No images found in tile_stats for batch summary." diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Makefile b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Makefile new file mode 100644 index 000000000..94e2c9d4c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Makefile @@ -0,0 +1,7 @@ +.PHONY: ci-e2e ci-detection + +ci-e2e: + cd e2e_kafka_flink && pytest tests --cov=e2e_pipeline --cov-report=xml --maxfail=1 -v + +ci-detection: + cd agri-baseline && pytest tests --cov=agri_baseline --cov-report=xml --maxfail=1 -v diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/VENDORED_FROM.txt b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/VENDORED_FROM.txt new file mode 100644 index 000000000..b1786ac32 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/VENDORED_FROM.txt @@ -0,0 +1,4 @@ +VENDORED FROM (saved 2025-11-09T04:18:09+02:00) +------------------------------------- +origin https://github.com/KamaTechOrg/AgCloud.git (fetch) [blob:none] +origin https://github.com/KamaTechOrg/AgCloud.git (push) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/.gitignore b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/.gitignore new file mode 100644 index 000000000..06593e32e --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/.gitignore @@ -0,0 +1,55 @@ +# ==== OS / IDE ==== +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# ==== Node ==== +node_modules/ +dist/ + +# ==== Python ==== +__pycache__/ +*.py[cod] +*.pyc +*.pyo +*.so +*.dylib + +# ==== Virtual envs ==== +.venv/ +venv/ +ENV/ +env/ + +# ==== Packaging / build ==== +build/ +*.egg-info/ + +# ==== Environment / Secrets ==== +.env +.env.* + +# ==== Data / Notebooks / Logs ==== +*.log +*.ipynb +.ipynb_checkpoints/ + +# ==== Artifacts / Wheels / Models ==== +artifacts/ +.wheels/ +wheels/ +*.whl +*.pt +*.pth +*.bin + +# ==== Coverage reports ==== +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ + +# ==== gRPC generated (נוצרים בבילד דוקר) ==== +server/embed_pb2.py +server/embed_pb2_grpc.py diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/Makefile b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/Makefile new file mode 100644 index 000000000..94e2c9d4c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/Makefile @@ -0,0 +1,7 @@ +.PHONY: ci-e2e ci-detection + +ci-e2e: + cd e2e_kafka_flink && pytest tests --cov=e2e_pipeline --cov-report=xml --maxfail=1 -v + +ci-detection: + cd agri-baseline && pytest tests --cov=agri_baseline --cov-report=xml --maxfail=1 -v diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/VENDORED_FROM.txt b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/VENDORED_FROM.txt new file mode 100644 index 000000000..b1786ac32 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/VENDORED_FROM.txt @@ -0,0 +1,4 @@ +VENDORED FROM (saved 2025-11-09T04:18:09+02:00) +------------------------------------- +origin https://github.com/KamaTechOrg/AgCloud.git (fetch) [blob:none] +origin https://github.com/KamaTechOrg/AgCloud.git (push) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.dockerignore b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.dockerignore new file mode 100644 index 000000000..9bd273d4f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.dockerignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +.venv/ +venv/ + +# Tests and caches +.pytest_cache/ +tests/ + +# Local data / artifacts +data/ +alerts.db + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.gitignore b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.gitignore new file mode 100644 index 000000000..da73fe5e4 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.gitignore @@ -0,0 +1,2 @@ +# Ignore local data +data/ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile new file mode 100644 index 000000000..49d99c76c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile @@ -0,0 +1,34 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye + +WORKDIR /app + +# 1) Install CA tools and curl +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# 2) Add NetFree certificate and register in system trust store + +# Ensure Python, requests, and pip use the updated CA bundle +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# 3) Install Python dependencies (use trusted hosts to simplify NetFree path) +COPY requirements.txt /app/requirements.txt +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org --no-cache-dir -r requirements.txt + +# 4) Install the package (PEP517) with the same trusted hosts +COPY pyproject.toml README.md /app/ +COPY src /app/src +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org --no-cache-dir . + +# 5) Copy configs (can be overridden by a bind mount) +COPY configs /app/configs + +ENV PYTHONUNBUFFERED=1 + +ENTRYPOINT ["python", "-m", "disease_monitor.cli"] + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile.local b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile.local new file mode 100644 index 000000000..e9dfc92b9 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile.local @@ -0,0 +1,32 @@ +FROM docker.io/library/python@sha256:e0c4fae70d550834a40f6c3e0326e02cfe239c2351d922e1fb1577a3c6ebde02 + +WORKDIR /app + +# 1) כלים בסיסיים ותעודות + requests (דרך APT) בלי לפנות ל-PyPI עבור החבילה הזו +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl python3-requests \ + && rm -rf /var/lib/apt/lists/* + +# הגדרות SSL סטנדרטיות (משתמשים בתעודות מערכת) +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# 2) התקנת תלויות פייתון של הפרויקט +COPY requirements.txt /app/requirements.txt +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org --no-cache-dir -r requirements.txt +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org --no-cache-dir requests + +# 3) התקנת החבילה עצמה +COPY pyproject.toml README.md /app/ +COPY src /app/src +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org --no-cache-dir . + +# 4) קונפיגים +COPY configs /app/configs + +ENV PYTHONUNBUFFERED=1 +ENTRYPOINT ["python", "-m", "disease_monitor.cli"] diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/README.md b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/README.md new file mode 100644 index 000000000..a0eb9bda6 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/README.md @@ -0,0 +1,127 @@ +# Disease Monitor (Offline) + +Offline batch job that reads disease detections from **Postgres**, aggregates data, +builds baselines, detects anomalies/worsening, deduplicates & rate-limits alerts, +delivers notifications (Slack/Webhook/Email), and writes alerts back to **Postgres**. + +> **Note:** The pipeline uses **Postgres only** (both sources and sink). No CSV/SQLite. + +--- + +## Data Sources & Sink (Postgres) + +**Sources** +- `anomalies` — per-image detections (0..N rows per image) +- `tile_stats` — exactly 1 row per image (summary) +- `event_logs` — QA & validator logs (rules/metrics/errors) + +**Sink** +- `alerts` — unified alerts table (rules: `COUNT_SPIKE`, `WORSENING_TREND`) + +--- + +## Config (`configs/config.example.yaml`) + +Main sections: +- **io**: Postgres URL (e.g. `postgresql+psycopg2://user:pass@host:5432/db`) +- **windows**: frequency (`"D"`/`"W"`), timezone (e.g. `"UTC"`) +- **baseline**: method (`mean`/`median`), lookback, min_history, optional seasonality +- **rules**: thresholds & toggles for `count_anomaly` (zscore/iqr) and `worsening` (slope/ewma) +- **alerting**: dedup cooldown (windows), resolve-after-no-anomaly, per-run rate limit, group_by_window +- **delivery**: slack/webhook/email targets (can be disabled) +- **run**: `dry_run` and optional filters + +Example: +```yaml +io: + postgres_url: "postgresql+psycopg2://missions_user:pg123@localhost:5432/missions_db" + +windows: + frequency: "D" + timezone: "UTC" + +baseline: + method: "median" + lookback_periods: 28 + min_history: 7 + seasonality: null + +rules: + count_anomaly: + enabled: true + method: "zscore" + z_threshold: 3.0 + iqr_k: 1.5 + min_count: 3 + worsening: + enabled: true + method: "slope" + slope_lookback: 7 + slope_min: 0.02 + min_periods: 5 + ewma_span: 7 + ewma_threshold: 0.6 + +alerting: + dedup_cooldown_windows: 3 + resolve_after_no_anomaly: 3 + rate_limit_per_run: 100 + group_by_window: true + +delivery: + slack: + enabled: false + webhook_url: "" + webhook: + enabled: false + url: "" + headers: {} + email: + enabled: false + smtp_host: "" + smtp_port: 587 + username: "" + password_env: "SMTP_PASSWORD" + from_addr: "" + to_addrs: [] + +run: + dry_run: false +``` + +--- + +## Install & Run + +```bash +# Create & activate venv (Linux/Mac) +python -m venv .venv +source .venv/bin/activate + +# On Windows (PowerShell): +# python -m venv .venv +# .venv\Scripts\Activate.ps1 + +# Install +pip install -r requirements.txt + +# Run +python -m disease_monitor.cli --config configs/config.example.yaml --log-level INFO +``` + +--- + +## Tests + +```bash +pytest +``` + +--- + + +## Notes + +- Thresholds, lookbacks, and active rules are fully configurable from YAML. +- Logs and runtime counters are emitted to stdout. +- Extend notifiers in `src/disease_monitor/notifiers`. diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.docker.yaml b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.docker.yaml new file mode 100644 index 000000000..fb46acdfe --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.docker.yaml @@ -0,0 +1,64 @@ +io: + # IMPORTANT: use the Docker service name of Postgres (from your compose): + postgres_url: "postgresql+psycopg2://missions_user:pg123@postgres:5432/missions_db" + +windows: + frequency: "D" + timezone: "UTC" + +source_mapping: + entity_dim: "mission" # or "region"/"device" + area_strategy: "none" # or "region_area" (requires regions table/geom) + filters: + start_time: null + end_time: null + anomaly_codes: null + +baseline: + method: "median" + lookback_periods: 28 + min_history: 7 + seasonality: null + +rules: + count_anomaly: + enabled: true + method: "zscore" + z_threshold: 3.0 + iqr_k: 1.5 + min_count: 3 + worsening: + enabled: true + method: "slope" + slope_lookback: 7 + slope_min: 0.02 + min_periods: 5 + ewma_span: 7 + ewma_threshold: 0.6 + +alerting: + dedup_cooldown_windows: 3 + resolve_after_no_anomaly: 3 + rate_limit_per_run: 100 + group_by_window: true + +delivery: + slack: + enabled: false + webhook_url: "" + webhook: + enabled: false + url: "" + headers: {} + email: + enabled: false + smtp_host: "" + smtp_port: 587 + username: "" + password_env: "SMTP_PASSWORD" + from_addr: "" + to_addrs: [] + +run: + dry_run: false + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.example.yaml b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.example.yaml new file mode 100644 index 000000000..6d4d8d699 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.example.yaml @@ -0,0 +1,70 @@ +io: + postgres_url: "postgresql+psycopg2://missions_user:pg123@localhost:5432/missions_db" + +windows: + frequency: "D" + timezone: "UTC" + +source_mapping: + entity_dim: "mission" # or "region"/"device" + area_strategy: "none" # or "region_area" + filters: + start_time: null + end_time: null + anomaly_codes: null + +baseline: + method: "median" + lookback_periods: 28 + min_history: 7 + seasonality: null + +rules: + count_anomaly: + enabled: true + method: "zscore" + z_threshold: 3.0 + iqr_k: 1.5 + min_count: 3 + worsening: + enabled: true + method: "slope" + slope_lookback: 7 + slope_min: 0.02 + min_periods: 5 + ewma_span: 7 + ewma_threshold: 0.6 + +alerting: + dedup_cooldown_windows: 3 + resolve_after_no_anomaly: 3 + rate_limit_per_run: 100 + group_by_window: true + +delivery: + slack: + enabled: false + webhook_url: "" # paste Slack Webhook URL here if you want to enable + webhook: + enabled: false + url: "" # paste your Webhook URL here if you want to enable + headers: {} # optional headers map + email: + enabled: false + smtp_host: "" # paste your SMTP server address here if you want to enable + smtp_port: 587 + username: "" + password_env: "SMTP_PASSWORD" + from_addr: "" + to_addrs: [] + + alertmanager: + enabled: false + url: "http://localhost:9093" + default_severity: "warning" + extra_labels: + system: "disease-monitor" + team: "ag" + +run: + dry_run: false diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/disease_monitor.yaml b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/disease_monitor.yaml new file mode 100644 index 000000000..832d6daf7 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/disease_monitor.yaml @@ -0,0 +1,68 @@ +io: + # IMPORTANT: use the Docker service name of Postgres (from your compose): + postgres_url: "postgresql+psycopg2://missions_user:pg123@postgres:5432/missions_db" + +windows: + frequency: "D" + timezone: "UTC" + +source_mapping: + entity_dim: "device" + area_strategy: "none" # or "region_area" (requires regions table/geom) + filters: + start_time: null + end_time: null + anomaly_codes: null + +baseline: + method: "median" + lookback_periods: 28 + min_history: 7 + seasonality: null + +rules: + count_anomaly: + enabled: true + method: "zscore" + z_threshold: 3.0 + iqr_k: 1.5 + min_count: 3 + worsening: + enabled: true + method: "slope" + slope_lookback: 7 + slope_min: 0.02 + min_periods: 5 + ewma_span: 7 + ewma_threshold: 0.6 + +alerting: + dedup_cooldown_windows: 3 + resolve_after_no_anomaly: 3 + rate_limit_per_run: 100 + group_by_window: true + +delivery: + kafka: + enabled: false + brokers: "kafka:9092" + topic: "alerts" + slack: + enabled: false + webhook_url: "" + webhook: + enabled: false + url: "" + headers: {} + email: + enabled: false + smtp_host: "" + smtp_port: 587 + username: "" + password_env: "SMTP_PASSWORD" + from_addr: "" + to_addrs: [] + +run: + dry_run: false + diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/docker-compose.yml b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/docker-compose.yml new file mode 100644 index 000000000..15a0a0baa --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/docker-compose.yml @@ -0,0 +1,21 @@ +services: + disease-monitor: + build: + context: . + dockerfile: Dockerfile + image: disease-monitor:latest + command: ["--config", "/app/configs/config.docker.yaml", "--log-level", "INFO"] + environment: + TZ: "UTC" + # If you enable email delivery and use password_env=SMTP_PASSWORD: + # SMTP_PASSWORD: "your-smtp-password" + volumes: + - ./configs:/app/configs:ro + networks: + - worktree-main_ag_cloud + restart: on-failure + +networks: + # Use the external network created by your worktree-main compose + worktree-main_ag_cloud: + external: true diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/pyproject.toml b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/pyproject.toml new file mode 100644 index 000000000..063c0697f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "disease-monitor" +version = "0.1.0" +description = "Offline anomaly & worsening detection for disease cases in trees/plots/regions." +readme = "README.md" +requires-python = ">=3.10" + +[tool.pytest.ini_options] +pythonpath = ["src"] +addopts = "-q" diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/requirements.txt b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/requirements.txt new file mode 100644 index 000000000..46a776a5b --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/requirements.txt @@ -0,0 +1,11 @@ +pandas>=2.3.0,<2.4 +numpy>=2.2,<2.4 +pyyaml==6.0.2 +sqlalchemy==2.0.32 +pydantic==2.9.2 +scipy>=1.14.1,<1.15 +pytest==8.3.2 +python-dateutil==2.9.0.post0 +psycopg2-binary==2.9.7 +requests>=2.31 +kafka-python==2.0.2 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/__init__.py new file mode 100644 index 000000000..a9a2c5b3b --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/alerting.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/alerting.py new file mode 100644 index 000000000..fe3802cba --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/alerting.py @@ -0,0 +1,99 @@ +from __future__ import annotations +import logging +from datetime import datetime +from typing import Dict, Any, List, Tuple +import pandas as pd + +LOGGER = logging.getLogger(__name__) + +def _merge_reasons(s: pd.Series) -> list[str]: + items = [] + for x in s: + if isinstance(x, (list, tuple, set)): + items.extend(list(x)) + else: + items.append(str(x)) + return sorted(set(items)) + +def enforce_policies(candidates: pd.DataFrame, open_alerts_df: pd.DataFrame, + cfg: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + Deduplicate per (entity, rule) with cooldown; update OPEN alerts if still anomalous; + create RESOLVED entries after consecutive non-anomalous windows (handled by absence). + Rate limiting applied. + """ + if candidates.empty: + return [] + + candidates = candidates.copy() + candidates["window_start"] = pd.to_datetime(candidates["window"]) + candidates["window_end"] = pd.to_datetime(candidates["window_end"]) + candidates["first_seen"] = candidates["window_start"] + candidates["last_seen"] = candidates["window_end"] + candidates["status"] = "OPEN" + + # Dedup cooldown: skip if there is OPEN/ACK within last N windows for same (entity, rule) + cooldown = cfg["alerting"]["dedup_cooldown_windows"] + frequency = cfg["windows"]["frequency"] + + alerts_out: List[Dict[str, Any]] = [] + rate_limit = cfg["alerting"]["rate_limit_per_run"] + emitted = 0 + + # Grouping by window if requested + if cfg["alerting"]["group_by_window"]: + group_keys = ["entity_id", "rule", "window_start", "window_end"] + else: + group_keys = ["entity_id", "rule"] + + g = candidates.groupby(group_keys, as_index=False).agg({ + "score": "max", + "disease_count": "max", + "avg_severity": "max", + "affected_area": "max", + "reason": _merge_reasons +}) + + for _, row in g.iterrows(): + if emitted >= rate_limit: + LOGGER.warning("Rate limit reached (%d).", rate_limit) + break + entity, rule = row["entity_id"], row["rule"] + ws, we = row["window_start"], row["window_end"] + # Check cooldown against open alerts + if not open_alerts_df.empty: + same = open_alerts_df[(open_alerts_df["entity_id"] == entity) & + (open_alerts_df["rule"] == rule)] + # In cooldown if last_seen within last cooldown windows + recent = same[same["last_seen"] >= (ws - _windows_to_offset(frequency, cooldown))] + if not recent.empty: + LOGGER.info("Cooldown skip for %s/%s at %s.", entity, rule, ws) + continue + + meta = { + "reasons": row["reason"], + "disease_count": int(row["disease_count"]), + "avg_severity": float(row["avg_severity"]), + "affected_area": float(row["affected_area"]), + } + alerts_out.append({ + "entity_id": entity, + "rule": rule, + "window_start": ws.to_pydatetime(), + "window_end": we.to_pydatetime(), + "score": float(row["score"]), + "first_seen": ws.to_pydatetime(), + "last_seen": we.to_pydatetime(), + "status": "OPEN", + "meta": meta + }) + emitted += 1 + + return alerts_out + +def _windows_to_offset(freq: str, n: int) -> pd.Timedelta: + if n <= 0: + return pd.Timedelta(0) + if freq.upper().startswith("W"): + return pd.to_timedelta(7 * n, unit="D") + return pd.to_timedelta(n, unit="D") diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/baseline.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/baseline.py new file mode 100644 index 000000000..71e976c08 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/baseline.py @@ -0,0 +1,38 @@ +from __future__ import annotations +import pandas as pd +import numpy as np + +def compute_baseline(agg: pd.DataFrame, method: str, lookback: int, + min_history: int, seasonality: int | None) -> pd.DataFrame: + """ + Returns agg with baseline columns for disease_count, avg_severity, affected_area: + *_bl, *_std (or IQR helpers). + """ + df = agg.sort_values(["entity_id", "window"]).copy() + keys = ["entity_id"] + metrics = ["disease_count", "avg_severity", "affected_area"] + + # Optionally seasonal lag indexing + if seasonality and seasonality > 1: + df["season_index"] = df.groupby(keys)["window"].rank(method="first").astype(int) % seasonality + groupers = keys + ["season_index"] + else: + groupers = keys + + for m in metrics: + if method == "mean": + bl = df.groupby(groupers)[m].transform(lambda s: s.shift(1).rolling(lookback, min_periods=min_history).mean()) + sd = df.groupby(groupers)[m].transform(lambda s: s.shift(1).rolling(lookback, min_periods=min_history).std(ddof=0)) + else: + bl = df.groupby(groupers)[m].transform(lambda s: s.shift(1).rolling(lookback, min_periods=min_history).median()) + sd = df.groupby(groupers)[m].transform(lambda s: s.shift(1).rolling(lookback, min_periods=min_history).std(ddof=0)) + df[f"{m}_bl"] = bl.fillna(0.0) + df[f"{m}_std"] = sd.fillna(0.0) + + # IQR helpers + q1 = df.groupby(groupers)[m].transform(lambda s: s.shift(1).rolling(lookback, min_periods=min_history).quantile(0.25)) + q3 = df.groupby(groupers)[m].transform(lambda s: s.shift(1).rolling(lookback, min_periods=min_history).quantile(0.75)) + df[f"{m}_q1"] = q1.fillna(0.0) + df[f"{m}_q3"] = q3.fillna(0.0) + + return df diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/cli.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/cli.py new file mode 100644 index 000000000..74817d4cb --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/cli.py @@ -0,0 +1,121 @@ +import argparse +import json +import logging +from typing import Dict, Any, List + +import yaml +import pandas as pd + +from .logging_utils import setup_logging +from .config import AppConfig +from . import io as io_mod +from .baseline import compute_baseline +from .rules import apply_rules +from .alerting import enforce_policies +from .notifiers.base import Notifier +from .notifiers.slack import SlackNotifier +from .notifiers.webhook import WebhookNotifier +from .notifiers.emailer import EmailNotifier +from .notifiers.kafka_notifier import KafkaNotifier +from .io import load_inputs_from_postgres , upsert_alerts_pg , fetch_open_alerts_pg + + +LOGGER = logging.getLogger("disease_monitor") + + +def parse_args(): + parser = argparse.ArgumentParser(description="Offline disease anomaly detector") + parser.add_argument("--config", required=True, help="Path to config file (YAML)") + parser.add_argument("--log-level", default="INFO", help="Logging level") + return parser.parse_args() + + +def load_config(path: str) -> Dict[str, Any]: + with open(path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) + AppConfig(**cfg) # validation + return cfg + +def build_notifiers(cfg: Dict[str, Any]) -> List[Notifier]: + ns: List[Notifier] = [] + d = cfg.get("delivery", {}) + + kafka_cfg = d.get("kafka", {}) + if kafka_cfg.get("enabled"): + brokers = kafka_cfg["brokers"] + topic = kafka_cfg.get("topic", "alerts") + ns.append(KafkaNotifier(brokers, topic)) + LOGGER.info("Using KafkaNotifier to send alerts.") + return ns + + slack = d.get("slack", {}) + if slack.get("enabled") and slack.get("webhook_url"): + ns.append(SlackNotifier(slack["webhook_url"])) + + webhook = d.get("webhook", {}) + if webhook.get("enabled") and webhook.get("url"): + ns.append(WebhookNotifier(webhook["url"], webhook.get("headers") or {})) + + email = d.get("email", {}) + if email.get("enabled") and email.get("to_addrs"): + ns.append(EmailNotifier(email["smtp_host"], email["smtp_port"], email["username"], + email["password_env"], email["from_addr"], email["to_addrs"])) + return ns + +def main() -> None: + args = parse_args() + setup_logging(args.log_level) + cfg = load_config(args.config) + + tz = cfg["windows"]["timezone"] + freq = cfg["windows"]["frequency"] + + # Load inputs + det, reg = load_inputs_from_postgres(cfg["io"]["postgres_url"], tz, cfg) + + # Optional filters + run_cfg = cfg["run"] + if run_cfg.get("disease_filter"): + det = det[det["disease_type"].isin(run_cfg["disease_filter"])] + if run_cfg.get("limit_entities"): + keep = det["entity_id"].drop_duplicates().head(run_cfg["limit_entities"]).tolist() + det = det[det["entity_id"].isin(keep)] + + # Aggregation + baseline + agg = io_mod.aggregate(det, freq=freq) + agg_bl = compute_baseline( + agg, + method=cfg["baseline"]["method"], + lookback=cfg["baseline"]["lookback_periods"], + min_history=cfg["baseline"]["min_history"], + seasonality=cfg["baseline"]["seasonality"], + ) + + # Rules + candidates = apply_rules(agg_bl, cfg) + LOGGER.info("Candidate alerts: %d", 0 if candidates is None else len(candidates)) + + # Policies need knowledge of currently OPEN alerts from the chosen backend + open_alerts = fetch_open_alerts_pg(cfg["io"]["postgres_url"]) + alerts = enforce_policies(candidates, open_alerts, cfg) + LOGGER.info("Alerts after policies: %d", len(alerts)) + + # Delivery + notifiers = build_notifiers(cfg) + dry_run = cfg["run"]["dry_run"] + + if not dry_run and alerts: + io_mod.upsert_alerts_pg(cfg["io"]["postgres_url"], alerts) + for a in alerts: + for n in notifiers: + try: + n.send(a) + except Exception as ex: + LOGGER.error("Notifier failed: %s", ex) + else: + LOGGER.info("Dry-run or no alerts. Skipping DB write & delivery.") + LOGGER.info("Preview alerts: %s", json.dumps(alerts, default=str, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/config.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/config.py new file mode 100644 index 000000000..a2e0aa83f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/config.py @@ -0,0 +1,114 @@ +from pydantic import BaseModel, Field, model_validator +from typing import Optional, List, Dict, Any + +# ------------------------------ +# IO: Postgres-only +# ------------------------------ +class IOConfig(BaseModel): + postgres_url: str # required: Postgres-only + + @model_validator(mode="after") + def _ensure_pg_only(self): + url = self.postgres_url + if not isinstance(url, str) or not url.lower().startswith( + ("postgresql://", "postgresql+psycopg2://") + ): + raise ValueError("io.postgres_url is required and must be a PostgreSQL URL.") + return self + + +# ------------------------------ +# Windows/Baseline/Rules/Alerting +# ------------------------------ +class WindowsConfig(BaseModel): + frequency: str = "D" + timezone: str = "UTC" + +class BaselineConfig(BaseModel): + method: str = "median" + lookback_periods: int = 28 + min_history: int = 7 + seasonality: Optional[int] = None + +class CountAnomalyRule(BaseModel): + enabled: bool = True + method: str = "zscore" + z_threshold: float = 3.0 + iqr_k: float = 1.5 + min_count: int = 3 + +class WorseningRule(BaseModel): + enabled: bool = True + method: str = "slope" + slope_lookback: int = 7 + slope_min: float = 0.02 + min_periods: int = 5 + ewma_span: int = 7 + ewma_threshold: float = 0.6 + +class RulesConfig(BaseModel): + count_anomaly: CountAnomalyRule = Field(default_factory=CountAnomalyRule) + worsening: WorseningRule = Field(default_factory=WorseningRule) + +class AlertingConfig(BaseModel): + dedup_cooldown_windows: int = 3 + resolve_after_no_anomaly: int = 3 + rate_limit_per_run: int = 100 + group_by_window: bool = True + + +# ------------------------------ +# Delivery: add Alertmanager section +# ------------------------------ +class SlackConfig(BaseModel): + enabled: bool = False + webhook_url: Optional[str] = None + +class WebhookConfig(BaseModel): + enabled: bool = False + url: Optional[str] = None + headers: Dict[str, Any] = Field(default_factory=dict) + +class EmailConfig(BaseModel): + enabled: bool = False + smtp_host: str = "" + smtp_port: int = 587 + username: str = "" + password_env: str = "SMTP_PASSWORD" + from_addr: str = "" + to_addrs: List[str] = Field(default_factory=list) + +class AlertmanagerConfig(BaseModel): + enabled: bool = False + url: Optional[str] = None + default_severity: str = "warning" + extra_labels: Dict[str, str] = Field(default_factory=dict) + auth: Dict[str, Any] = Field(default_factory=lambda: {"type": "none"}) # {"type":"none"} or {"type":"basic",...} + +class DeliveryConfig(BaseModel): + slack: SlackConfig = Field(default_factory=SlackConfig) + webhook: WebhookConfig = Field(default_factory=WebhookConfig) + email: EmailConfig = Field(default_factory=EmailConfig) + alertmanager: AlertmanagerConfig = Field(default_factory=AlertmanagerConfig) + + +# ------------------------------ +# Run +# ------------------------------ +class RunConfig(BaseModel): + dry_run: bool = False + limit_entities: Optional[int] = None + disease_filter: Optional[List[str]] = None + + +# ------------------------------ +# AppConfig +# ------------------------------ +class AppConfig(BaseModel): + io: IOConfig + windows: WindowsConfig = Field(default_factory=WindowsConfig) + baseline: BaselineConfig = Field(default_factory=BaselineConfig) + rules: RulesConfig = Field(default_factory=RulesConfig) + alerting: AlertingConfig = Field(default_factory=AlertingConfig) + delivery: DeliveryConfig = Field(default_factory=DeliveryConfig) + run: RunConfig = Field(default_factory=RunConfig) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/io.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/io.py new file mode 100644 index 000000000..98b100f77 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/io.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import json +import logging +from typing import Tuple, Iterable, Dict, Any, List + +import pandas as pd +from sqlalchemy import create_engine, text + +LOGGER = logging.getLogger(__name__) + +# --------------------------------------------------------------------- +# Postgres sources: anomalies / anomaly_types / regions +# --------------------------------------------------------------------- + +_BASE_SQLS: Dict[str, str] = { + "device": """ + SELECT a.ts AS "timestamp", + a.device_id AS entity_id, + at.code AS disease_type, + COALESCE(a.severity::double precision, 0.0) AS severity, + 0.0 AS affected_area + FROM public.anomalies a + JOIN public.anomaly_types at ON at.anomaly_type_id = a.anomaly_type_id + WHERE a.ts IS NOT NULL + {AND_CODE_FILTER} + {AND_TIME_RANGE} + """, + "mission": """ + SELECT a.ts AS "timestamp", + a.mission_id::text AS entity_id, + at.code AS disease_type, + COALESCE(a.severity::double precision, 0.0) AS severity, + 0.0 AS affected_area + FROM public.anomalies a + JOIN public.anomaly_types at ON at.anomaly_type_id = a.anomaly_type_id + WHERE a.ts IS NOT NULL + {AND_CODE_FILTER} + {AND_TIME_RANGE} + """, + "region": """ + SELECT a.ts AS "timestamp", + r.id::text AS entity_id, + at.code AS disease_type, + COALESCE(a.severity::double precision, 0.0) AS severity, + {AREA_EXPR} AS affected_area + FROM public.anomalies a + JOIN public.anomaly_types at ON at.anomaly_type_id = a.anomaly_type_id + JOIN public.regions r ON ST_Contains(r.geom, a.geom) + WHERE a.ts IS NOT NULL AND a.geom IS NOT NULL + {AND_CODE_FILTER} + {AND_TIME_RANGE} + """, + +} + + +def _build_sql( + entity_dim: str, + area_strategy: str, + codes: List[str] | None, + start: str | None, + end: str | None, +) -> tuple[str, dict]: + """ + Build parametrized SQL for reading anomalies with chosen entity dimension and area strategy. + """ + sql = _BASE_SQLS[entity_dim] + area_expr = "0.0" + if entity_dim == "region" and area_strategy == "region_area": + area_expr = "ST_Area(r.geom::geography)::double precision" + + and_code = "" + params: Dict[str, Any] = {} + if codes: + and_code = "AND at.code = ANY(:codes)" + params["codes"] = codes + + and_time = "" + if start: + and_time += " AND a.ts >= :start_time" + params["start_time"] = start + if end: + and_time += " AND a.ts < :end_time" + params["end_time"] = end + + sql = ( + sql.replace("{AREA_EXPR}", area_expr) + .replace("{AND_CODE_FILTER}", and_code) + .replace("{AND_TIME_RANGE}", and_time) + ) + return sql, params + + +# --------------------------------------------------------------------- +# Postgres input (canonical) +# --------------------------------------------------------------------- + +def load_inputs_from_postgres(pg_url: str, tz: str, cfg: dict) -> Tuple[pd.DataFrame, pd.DataFrame]: + """ + Load inputs from Postgres (public.anomalies/anomaly_types/regions). + Controlled by cfg['source_mapping'] (entity_dim, area_strategy, filters, codes). + Returns: + det: columns [timestamp, entity_id, disease_type, severity, affected_area] + reg: columns [entity_id, entity_type] + """ + edim = cfg["source_mapping"]["entity_dim"] + area = cfg["source_mapping"].get("area_strategy", "none") + codes = cfg["source_mapping"].get("anomaly_codes") + filters = cfg["source_mapping"].get("filters") or {} + start = filters.get("start_time") + end = filters.get("end_time") + + sql, params = _build_sql(edim, area, codes, start, end) + + eng = create_engine(pg_url) + with eng.begin() as conn: + det = pd.read_sql(text(sql), conn, params=params) + reg = det[["entity_id"]].drop_duplicates().assign(entity_type=edim) + + det["timestamp"] = pd.to_datetime(det["timestamp"], utc=True).dt.tz_convert(tz) + + required = {"timestamp", "entity_id", "disease_type", "severity", "affected_area"} + if not required.issubset(det.columns): + missing = required - set(det.columns) + raise ValueError(f"det: missing {missing}") + if not {"entity_id", "entity_type"}.issubset(reg.columns): + raise ValueError("reg: missing cols") + + return det, reg + + +# --------------------------------------------------------------------- +# Aggregation +# --------------------------------------------------------------------- + +def aggregate(det: pd.DataFrame, freq: str) -> pd.DataFrame: + """ + Aggregate by entity_id + window and compute disease_count, avg_severity, affected_area. + """ + df = det.copy() + + # Normalize tz: drop tz-info to use pandas period-based bucketing safely + if pd.api.types.is_datetime64tz_dtype(df["timestamp"]): + df["timestamp"] = df["timestamp"].dt.tz_convert("UTC").dt.tz_localize(None) + + df["window"] = df["timestamp"].dt.to_period(freq).dt.start_time + grp = df.groupby(["entity_id", "window"], as_index=False).agg( + disease_count=("disease_type", "count"), + avg_severity=("severity", "mean"), + affected_area=("affected_area", "sum"), + ) + grp["window_end"] = grp["window"] + pd.tseries.frequencies.to_offset(freq) + return grp + + +# --------------------------------------------------------------------- +# Alerts: Postgres backend +# --------------------------------------------------------------------- + + +def fetch_open_alerts_pg(pg_url: str) -> pd.DataFrame: + eng = create_engine(pg_url) + sql = """ + SELECT id, entity_id, rule, window_start, window_end, score, + first_seen, last_seen, status, meta_json + FROM alerts_leaves + WHERE status IN ('OPEN','ACK') + """ + with eng.begin() as conn: + df = pd.read_sql(text(sql), conn) + if not df.empty: + for c in ("first_seen", "last_seen", "window_start", "window_end"): + # make tz-aware UTC then drop tz -> naive UTC + s = pd.to_datetime(df[c], utc=True) + df[c] = s.dt.tz_convert("UTC").dt.tz_localize(None) + + return df + + +def upsert_alerts_pg(pg_url: str, alerts: Iterable[Dict[str, Any]]) -> None: + rows = list(alerts) + if not rows: + return + eng = create_engine(pg_url) + sql = """ + INSERT INTO alerts_leaves + (entity_id, rule, window_start, window_end, score, + first_seen, last_seen, status, meta_json) + VALUES + (:entity_id, :rule, :window_start, :window_end, :score, + :first_seen, :last_seen, :status, CAST(:meta_json AS jsonb)) + """ + payload = [{ + "entity_id": a["entity_id"], + "rule": a["rule"], + "window_start": a["window_start"], + "window_end": a["window_end"], + "score": float(a["score"]), + "first_seen": a["first_seen"], + "last_seen": a["last_seen"], + "status": a["status"], + "meta_json": json.dumps(a["meta"], ensure_ascii=False), + } for a in rows] + + with eng.begin() as conn: + conn.execute(text(sql), payload) + LOGGER.info("Inserted %d alerts into Postgres.", len(rows)) \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/logging_utils.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/logging_utils.py new file mode 100644 index 000000000..f9618ff02 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/logging_utils.py @@ -0,0 +1,10 @@ +import logging +import sys + +def setup_logging(level: str = "INFO") -> None: + handler = logging.StreamHandler(sys.stdout) + fmt = "%(asctime)s %(levelname)s %(name)s - %(message)s" + handler.setFormatter(logging.Formatter(fmt)) + root = logging.getLogger() + root.setLevel(level.upper()) + root.handlers = [handler] diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/models.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/models.py new file mode 100644 index 000000000..4796fecd8 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/models.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import Optional, Dict +from datetime import datetime + +@dataclass +class Alert: + entity_id: str + rule: str + window_start: datetime + window_end: datetime + score: float + first_seen: datetime + last_seen: datetime + status: str # OPEN | ACK | RESOLVED + meta: Dict diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/base.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/base.py new file mode 100644 index 000000000..1f8bc409a --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/base.py @@ -0,0 +1,13 @@ +from __future__ import annotations +from typing import Dict, Any, List + +class Notifier: + def send(self, alert: Dict[str, Any]) -> None: + raise NotImplementedError + +def render_text(alert: Dict[str, Any]) -> str: + return ( + f"[{alert['status']}] {alert['rule']} for {alert['entity_id']} " + f"{alert['window_start']}..{alert['window_end']} " + f"score={alert['score']:.2f} reasons={alert['meta'].get('reasons')}" + ) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/emailer.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/emailer.py new file mode 100644 index 000000000..695e0ddfe --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/emailer.py @@ -0,0 +1,28 @@ +from __future__ import annotations +import os +import smtplib +from email.mime.text import MIMEText +from typing import Dict, Any, List +from .base import Notifier, render_text + +class EmailNotifier(Notifier): + def __init__(self, host: str, port: int, username: str, password_env: str, + from_addr: str, to_addrs: List[str]) -> None: + self.host = host + self.port = port + self.username = username + self.password_env = password_env + self.from_addr = from_addr + self.to_addrs = to_addrs + + def send(self, alert: Dict[str, Any]) -> None: + password = os.getenv(self.password_env, "") + msg = MIMEText(render_text(alert)) + msg["Subject"] = f"Alert: {alert['rule']} {alert['entity_id']}" + msg["From"] = self.from_addr + msg["To"] = ", ".join(self.to_addrs) + with smtplib.SMTP(self.host, self.port, timeout=10) as s: + s.starttls() + if self.username and password: + s.login(self.username, password) + s.sendmail(self.from_addr, self.to_addrs, msg.as_string()) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/kafka_notifier.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/kafka_notifier.py new file mode 100644 index 000000000..7552bfa1f --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/kafka_notifier.py @@ -0,0 +1,49 @@ +from __future__ import annotations +import json, uuid, datetime, logging +from kafka import KafkaProducer +from typing import Dict, Any +from .base import Notifier + +LOGGER = logging.getLogger(__name__) + +def _json_default(obj): + if isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + raise TypeError(f"Type {type(obj)} not serializable") + + +class KafkaNotifier(Notifier): + def __init__(self, brokers: str, topic: str): + self.producer = KafkaProducer( + bootstrap_servers=brokers.split(","), + value_serializer=lambda v: json.dumps(v, default=_json_default).encode("utf-8"), + ) + self.topic = topic + + def send(self, alert: Dict[str, Any]) -> None: + msg = { + "alert_id": alert.get("alert_id") or str(uuid.uuid4()), + "alert_type": alert.get("rule", "disease_detected"), + "device_id": alert.get("entity_id"), + "started_at": alert.get("window_start"), + "ended_at": alert.get("window_end"), + "confidence": alert.get("score"), + "severity": int(alert.get("meta", {}).get("severity", 1)), + "area": alert.get("meta", {}).get("area"), + "lat": alert.get("meta", {}).get("lat"), + "lon": alert.get("meta", {}).get("lon"), + "image_url": alert.get("meta", {}).get("image_url"), + "vod": alert.get("meta", {}).get("vod"), + "hls": alert.get("meta", {}).get("hls"), + "meta": alert.get("meta", {}), + } + + try: + self.producer.send(self.topic, msg) + self.producer.flush() + LOGGER.info( + "KafkaNotifier: sent alert %s to topic '%s' with rule '%s' (confidence=%.2f)", + msg["alert_id"], self.topic, msg["alert_type"], msg["confidence"] or 0, + ) + except Exception as e: + LOGGER.error("KafkaNotifier failed to send alert: %s", e) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/slack.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/slack.py new file mode 100644 index 000000000..68925060a --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/slack.py @@ -0,0 +1,15 @@ +from __future__ import annotations +import os +import json +import requests +from typing import Dict, Any +from .base import Notifier, render_text + +class SlackNotifier(Notifier): + def __init__(self, webhook_url: str) -> None: + self.webhook_url = webhook_url + + def send(self, alert: Dict[str, Any]) -> None: + text = render_text(alert) + payload = {"text": text} + requests.post(self.webhook_url, data=json.dumps(payload), timeout=10) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/webhook.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/webhook.py new file mode 100644 index 000000000..1e84232c7 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/webhook.py @@ -0,0 +1,13 @@ +from __future__ import annotations +import json +import requests +from typing import Dict, Any +from .base import Notifier + +class WebhookNotifier(Notifier): + def __init__(self, url: str, headers: Dict[str, str] | None = None) -> None: + self.url = url + self.headers = headers or {} + + def send(self, alert: Dict[str, Any]) -> None: + requests.post(self.url, json=alert, headers=self.headers, timeout=10) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/rules.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/rules.py new file mode 100644 index 000000000..eeba8ec4c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/rules.py @@ -0,0 +1,108 @@ +from __future__ import annotations +import logging +from typing import List, Dict, Any, Tuple +import pandas as pd +import numpy as np +from scipy import stats + +LOGGER = logging.getLogger(__name__) + +def zscore_anomalies(df: pd.DataFrame, threshold: float, min_count: int) -> pd.DataFrame: + s = df["disease_count"] + mu = df["disease_count_bl"] + # Use small epsilon for zero/NaN std to avoid z=0 + sd = df["disease_count_std"] + eps = 1e-6 + sd = sd.where(sd > 0, other=eps).fillna(eps) + + z = (s - mu) / sd + cond = (z >= threshold) & (s >= min_count) + + out = df.loc[cond].copy() + out["score"] = z.loc[cond] + out["rule"] = "COUNT_SPIKE" + out["reason"] = "zscore" + return out + +def iqr_anomalies(df: pd.DataFrame, k: float, min_count: int) -> pd.DataFrame: + q1 = df["disease_count_q1"] + q3 = df["disease_count_q3"] + iqr = (q3 - q1).replace(0, np.nan) + upper = q3 + k * iqr + cond = (df["disease_count"] > upper.fillna(float("inf"))) & (df["disease_count"] >= min_count) + out = df.loc[cond].copy() + out["score"] = (df["disease_count"] - upper).loc[cond].fillna(0.0) + out["rule"] = "COUNT_SPIKE" + out["reason"] = "iqr" + return out + +def slope_worsening(df: pd.DataFrame, metric: str, lookback: int, + slope_min: float, min_periods: int) -> pd.DataFrame: + # Per entity rolling slope (OLS) + rows = [] + for entity, g in df.groupby("entity_id"): + g = g.sort_values("window") + y = g[metric].rolling(lookback, min_periods=min_periods).apply(_rolling_slope, raw=False) + cond = y >= slope_min + sel = g.loc[cond].copy() + if sel.empty: + continue + sel["score"] = y.loc[cond] + sel["rule"] = "WORSENING_TREND" + sel["reason"] = f"slope_{metric}" + rows.append(sel) + return pd.concat(rows, ignore_index=True) if rows else pd.DataFrame(columns=df.columns.tolist() + ["score","rule","reason"]) + +def _rolling_slope(s: pd.Series) -> float: + x = np.arange(len(s)) + res = stats.linregress(x, s.values) + return float(res.slope) + +def ewma_worsening(df: pd.DataFrame, metric: str, span: int, threshold: float, min_periods: int) -> pd.DataFrame: + rows = [] + for entity, g in df.groupby("entity_id"): + g = g.sort_values("window").copy() + ew = g[metric].ewm(span=span, adjust=False).mean() + cond = (ew >= threshold) & (g[metric].rolling(span, min_periods=min_periods).count() >= min_periods) + sel = g.loc[cond].copy() + if sel.empty: + continue + sel["score"] = ew.loc[cond] + sel["rule"] = "WORSENING_TREND" + sel["reason"] = f"ewma_{metric}" + rows.append(sel) + return pd.concat(rows, ignore_index=True) if rows else pd.DataFrame(columns=df.columns.tolist() + ["score","rule","reason"]) + +def apply_rules(df: pd.DataFrame, cfg: Dict[str, Any]) -> pd.DataFrame: + results = [] + + # Count anomaly + rc = cfg["rules"]["count_anomaly"] + if rc["enabled"]: + if rc["method"] == "zscore": + results.append(zscore_anomalies(df, rc["z_threshold"], rc["min_count"])) + elif rc["method"] == "iqr": + results.append(iqr_anomalies(df, rc["iqr_k"], rc["min_count"])) + else: + # Placeholder: CUSUM can be added similarly + results.append(zscore_anomalies(df, rc["z_threshold"], rc["min_count"])) + + # Worsening trend on severity and area + rw = cfg["rules"]["worsening"] + if rw["enabled"]: + if rw["method"] == "slope": + for m in ["avg_severity", "affected_area"]: + results.append(slope_worsening(df, m, rw["slope_lookback"], rw["slope_min"], rw["min_periods"])) + else: + for m in ["avg_severity", "affected_area"]: + results.append(ewma_worsening(df, m, rw["ewma_span"], rw["ewma_threshold"], rw["min_periods"])) + + if not results: + return pd.DataFrame() + out = pd.concat([r for r in results if r is not None and not r.empty], ignore_index=True) \ + if any((r is not None and not r.empty) for r in results) else pd.DataFrame() + # Prepare common fields + if not out.empty: + out = out[["entity_id", "window", "window_end", "rule", "score", "reason", + "disease_count", "avg_severity", "affected_area"]].copy() + return out diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/conftest.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/conftest.py new file mode 100644 index 000000000..043dc1712 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/conftest.py @@ -0,0 +1,17 @@ +import pandas as pd +import numpy as np +from datetime import datetime, timedelta, timezone + +TZ = "UTC" + +def make_series(start: str, days: int, entity: str, base_count=1, bump_at=None, bump=5): + rows = [] + start_dt = pd.to_datetime(start).tz_localize("UTC") + for i in range(days): + ts = start_dt + pd.Timedelta(days=i) + count = base_count + if bump_at is not None and i in bump_at: + count = bump + rows.append({"timestamp": ts, "entity_id": entity, "disease_type": "x", + "severity": 0.1 * count, "affected_area": 2.0 * count}) + return pd.DataFrame(rows) diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_aggregation.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_aggregation.py new file mode 100644 index 000000000..bb0a42558 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_aggregation.py @@ -0,0 +1,17 @@ +import pandas as pd +from disease_monitor.io import aggregate + +def test_aggregate_basic(): + det = pd.DataFrame({ + "timestamp": pd.to_datetime(["2025-08-01", "2025-08-01", "2025-08-02"]).tz_localize("UTC"), + "entity_id": ["A","A","A"], + "disease_type": ["x","x","x"], + "severity": [0.2, 0.4, 0.3], + "affected_area": [1,2,3], + }) + out = aggregate(det, "D") + assert len(out) == 2 + d1 = out[out["window"] == pd.to_datetime("2025-08-01")] + assert int(d1["disease_count"].iloc[0]) == 2 + assert abs(float(d1["avg_severity"].iloc[0]) - 0.3) < 1e-9 + assert int(d1["affected_area"].iloc[0]) == 3 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_alerting.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_alerting.py new file mode 100644 index 000000000..48126beaf --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_alerting.py @@ -0,0 +1,34 @@ +import pandas as pd +from disease_monitor.alerting import enforce_policies + +def test_dedup_cooldown(): + candidates = pd.DataFrame({ + "entity_id": ["A","A"], + "window": pd.to_datetime(["2025-08-10","2025-08-11"]), + "window_end": pd.to_datetime(["2025-08-11","2025-08-12"]), + "rule": ["COUNT_SPIKE","COUNT_SPIKE"], + "score": [3.1, 3.2], + "reason": [["zscore"],["zscore"]], + "disease_count": [10, 9], + "avg_severity": [0.5, 0.4], + "affected_area": [10.0, 9.0], + }) + open_alerts = pd.DataFrame({ + "entity_id": ["A"], + "rule": ["COUNT_SPIKE"], + "last_seen": pd.to_datetime(["2025-08-10"]), + "window_start": pd.to_datetime(["2025-08-10"]), + "window_end": pd.to_datetime(["2025-08-11"]), + "first_seen": pd.to_datetime(["2025-08-10"]), + "status": ["OPEN"], + "id": [1], + "score": [3.1] + }) + cfg = { + "alerting": {"dedup_cooldown_windows": 3, "resolve_after_no_anomaly": 3, + "rate_limit_per_run": 10, "group_by_window": True}, + "windows": {"frequency": "D"} + } + res = enforce_policies(candidates, open_alerts, cfg) + # Second day should be skipped due to cooldown + assert len(res) == 0 or len(res) == 1 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_anomaly_rules.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_anomaly_rules.py new file mode 100644 index 000000000..23f595a7a --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_anomaly_rules.py @@ -0,0 +1,22 @@ +import pandas as pd +from disease_monitor.baseline import compute_baseline +from disease_monitor.rules import apply_rules + +def test_zscore_spike_detected(): + # mostly low counts, then spike + det = [] + for d in range(10): + det.append({"window": pd.to_datetime(f"2025-08-{d+1:02d}"), + "entity_id": "E1", + "disease_count": 1 if d < 8 else (10 if d==8 else 1), + "avg_severity": 0.2, "affected_area": 2.0}) + df = pd.DataFrame(det) + df["window_end"] = df["window"] + pd.Timedelta(days=1) + bl = compute_baseline(df.rename(columns={"window":"window"}), "median", 7, 3, None) + cfg = { + "rules": {"count_anomaly": {"enabled": True, "method": "zscore", "z_threshold": 2.5, "min_count": 3}, + "worsening": {"enabled": False}}, + } + out = apply_rules(bl, cfg) + assert not out.empty + assert "COUNT_SPIKE" in out["rule"].unique() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_worsening_rules.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_worsening_rules.py new file mode 100644 index 000000000..e17b34e57 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_worsening_rules.py @@ -0,0 +1,24 @@ +import pandas as pd +from disease_monitor.baseline import compute_baseline +from disease_monitor.rules import apply_rules + +def test_worsening_slope_on_severity(): + rows = [] + for i in range(10): + rows.append({ + "window": pd.to_datetime(f"2025-08-{i+1:02d}"), + "entity_id": "E1", + "disease_count": 1, + "avg_severity": 0.1 + 0.03*i, + "affected_area": 2 + i + }) + df = pd.DataFrame(rows) + df["window_end"] = df["window"] + pd.Timedelta(days=1) + bl = compute_baseline(df, "median", 7, 3, None) + cfg = {"rules": + {"count_anomaly": {"enabled": False}, + "worsening": {"enabled": True, "method": "slope", + "slope_lookback": 7, "slope_min": 0.02, "min_periods": 5}}} + out = apply_rules(bl, cfg) + assert not out.empty + assert "WORSENING_TREND" in out["rule"].unique() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/05.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/05.jpg new file mode 100644 index 000000000..09be3be5f Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/05.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/bacterialspot3_600px.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/bacterialspot3_600px.jpg new file mode 100644 index 000000000..c56ceecd3 Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/bacterialspot3_600px.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/DSCN3768.JPG.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/DSCN3768.JPG.jpg new file mode 100644 index 000000000..1dd73ab87 Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/DSCN3768.JPG.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/IMG_3891.JPG_1492073147.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/IMG_3891.JPG_1492073147.jpg new file mode 100644 index 000000000..4acbdc891 Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/IMG_3891.JPG_1492073147.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/Late-blight-infected-potato-plants_2.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/Late-blight-infected-potato-plants_2.jpg new file mode 100644 index 000000000..238c58e4e Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/Late-blight-infected-potato-plants_2.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/blight-on-potato-leaves.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/blight-on-potato-leaves.jpg new file mode 100644 index 000000000..311a114ce Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/blight-on-potato-leaves.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Early blight leaf/dscn3175.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Early blight leaf/dscn3175.jpg new file mode 100644 index 000000000..a1014b817 Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Early blight leaf/dscn3175.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato-badleaves.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato-badleaves.jpg new file mode 100644 index 000000000..545773c4c Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato-badleaves.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato_septoria_05_zoom.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato_septoria_05_zoom.jpg new file mode 100644 index 000000000..9ca14479a Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato_septoria_05_zoom.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/late_blight_tomato_leaf4x1200.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/late_blight_tomato_leaf4x1200.jpg new file mode 100644 index 000000000..83ea0c4f6 Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/late_blight_tomato_leaf4x1200.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/russian-2-319-dt-2010-leaves-high-tunnel-9-29-2014-c.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/russian-2-319-dt-2010-leaves-high-tunnel-9-29-2014-c.jpg new file mode 100644 index 000000000..ca68fc44c Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/russian-2-319-dt-2010-leaves-high-tunnel-9-29-2014-c.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/Leaf-mold3.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/Leaf-mold3.jpg new file mode 100644 index 000000000..5c7238de3 Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/Leaf-mold3.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/tomato_plants_1_original.JPG_1407178095.jpg b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/tomato_plants_1_original.JPG_1407178095.jpg new file mode 100644 index 000000000..d8215f30d Binary files /dev/null and b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/tomato_plants_1_original.JPG_1407178095.jpg differ diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.orig.txt b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.orig.txt new file mode 100644 index 000000000..81958653a --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.orig.txt @@ -0,0 +1,5 @@ + +ultralytics>=8.1.0 +opencv-python-headless>=4.9.0.80 +numpy>=1.23.0 +minio>=7.1.15 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.txt b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.txt new file mode 100644 index 000000000..0c27d4902 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.txt @@ -0,0 +1,9 @@ +--extra-index-url https://download.pytorch.org/whl/cpu +torch==2.6.0+cpu +torchvision==0.21.0+cpu +torchaudio==2.6.0+cpu + +ultralytics>=8.1.0 +opencv-python-headless>=4.9.0.80 +numpy>=1.23.0 +minio>=7.1.15 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/__init__.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/__init__.py new file mode 100644 index 000000000..ee49d4339 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/__init__.py @@ -0,0 +1,5 @@ +# decompyle3 version 3.9.3 +# Python bytecode version base 3.12.0 (3531) +# Decompiled from: Python 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] +# Embedded file name: /home/user/ml-workspace/projects/leaf-counting/src/__init__.py +# Compiled at: 2025-10-20 13:47:51 diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/common.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/common.py new file mode 100644 index 000000000..ea30f64ea --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/common.py @@ -0,0 +1,35 @@ +from __future__ import annotations +from pathlib import Path +import cv2 +import numpy as np + +IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"} + +def is_image(path: Path) -> bool: + return path.suffix.lower() in IMG_EXTS + +def iter_images(inp: Path): + p = Path(inp) + if p.is_file() and is_image(p): + yield p + elif p.is_dir(): + for q in sorted(p.rglob("*")): + if q.is_file() and is_image(q): + yield q + +def ensure_dir(p: Path) -> Path: + Path(p).mkdir(parents=True, exist_ok=True) + return Path(p) + +def draw_boxes(img_bgr: np.ndarray, boxes, color=(0,255,0), thickness=2): + h, w = img_bgr.shape[:2] + out = img_bgr.copy() + for (x1,y1,x2,y2,conf,cls_id) in boxes: + x1 = max(0, min(w-1, int(x1))) + y1 = max(0, min(h-1, int(y1))) + x2 = max(0, min(w-1, int(x2))) + y2 = max(0, min(h-1, int(y2))) + cv2.rectangle(out, (x1,y1), (x2,y2), color, thickness) + label = f"{int(cls_id)}:{conf:.2f}" + cv2.putText(out, label, (x1, max(0, y1-5)), cv2.FONT_HERSHEY_SIMPLEX, 0.45, color, 1, cv2.LINE_AA) + return out \ No newline at end of file diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/crop_only.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/crop_only.py new file mode 100644 index 000000000..c54066f4c --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/crop_only.py @@ -0,0 +1,143 @@ +from __future__ import annotations +import json, argparse +from pathlib import Path +from typing import Optional +import cv2 +from common import ensure_dir +from datetime import datetime + +try: + from minio_io import get_client, ensure_bucket, put_png +except Exception: + get_client = ensure_bucket = put_png = None + + +def _load_jsons(inp: Path): + jdir = inp / "json" + if not jdir.exists(): + raise SystemExit(f"[ERR] Expected JSON dir not found: {jdir} (run detect_only.py first)") + + for jp in sorted(jdir.rglob("*.json")): + with jp.open("r", encoding="utf-8") as f: + j = json.load(f) + yield jp, j + +def _safe_crop(img, x1, y1, x2, y2): + h, w = img.shape[:2] + x1 = max(0, min(w-1, int(x1))); y1 = max(0, min(h-1, int(y1))) + x2 = max(0, min(w-1, int(x2))); y2 = max(0, min(h-1, int(y2))) + if x2 <= x1: x2 = min(w-1, x1+1) + if y2 <= y1: y2 = min(h-1, y1+1) + return img[y1:y2, x1:x2] + + +def run_crop(inp: Path, out_dir: Path, size: int=224, margin: float=0.1, min_wh: int=8, + orig_dir: Optional[Path]=None, flat: bool=False, + minio_endpoint: Optional[str]=None, minio_access: Optional[str]=None, + minio_secret: Optional[str]=None, minio_bucket: Optional[str]=None, + minio_prefix: str="CROP", minio_secure: bool=False, + run_id: Optional[str]=None): + run_id = run_id or datetime.now().strftime("%Y/%m/%d/%H%M") + out_dir = ensure_dir(out_dir) + + cli = None + if minio_endpoint and minio_access and minio_secret and minio_bucket: + if get_client is None: + raise SystemExit("[ERR] חסר minio או minio_io.") + cli = get_client(minio_endpoint, minio_access, minio_secret, secure=minio_secure) + ensure_bucket(cli, minio_bucket) + + count = 0 + for jp, j in _load_jsons(inp): + + if "source_path" in j: + img_path = Path(j["source_path"]) + rel_path = j.get("rel_path", j["image"]) + elif "rel_path" in j: + if orig_dir is None: + raise SystemExit("[ERR] JSON מכיל רק rel_path; ספקי --orig כדי למצוא את קובץ המקור") + img_path = Path(orig_dir) / j["rel_path"] + rel_path = j["rel_path"] + else: + if orig_dir is None: + raise SystemExit("[ERR] JSON חסר source_path/rel_path; ספקי --orig ותתאימי לשמות image") + img_path = Path(orig_dir) / j["image"] + rel_path = j["image"] + + if not img_path.exists(): + print(f"[WARN] Original image not found: {img_path}, skipping") + continue + + img = cv2.imread(str(img_path)) + if img is None: + print(f"[WARN] Can't read image: {img_path}") + continue + + rel_parent = str(Path(rel_path).parent) + rel_stem = Path(rel_path).stem + + + if flat: + dest_dir = ensure_dir(out_dir) + minio_subprefix = minio_prefix + else: + dest_dir = ensure_dir(out_dir / rel_parent / rel_stem) + minio_subprefix = f"{minio_prefix}/{rel_parent}/{rel_stem}" if rel_parent != "." else f"{minio_prefix}/{rel_stem}" + + for i, (x1,y1,x2,y2,conf,cls_id) in enumerate(j.get("boxes", [])): + w = x2 - x1; h = y2 - y1 + if w < min_wh or h < min_wh: + continue + cx = (x1 + x2) * 0.5; cy = (y1 + y2) * 0.5 + half = max(w, h) * 0.5 * (1.0 + margin) + crop = _safe_crop(img, cx-half, cy-half, cx+half, cy+half) + if crop.size == 0: + continue + crop_resized = cv2.resize(crop, (size, size), interpolation=cv2.INTER_AREA) + out_name = f"det{i:03d}_cls{int(cls_id)}_{conf:.2f}.png" + cv2.imwrite(str(dest_dir / out_name), crop_resized) + count += 1 + + if cli: + base = f"{run_id}/{minio_prefix}" # תאריך/שעה קודם, אח"כ CROP + key = f"{base}/{rel_parent}/{rel_stem}/{out_name}" if rel_parent != "." else f"{base}/{rel_stem}/{out_name}" + put_png(cli, minio_bucket, key, crop_resized) + + put_png(cli, minio_bucket, f"{minio_subprefix}/{out_name}", crop_resized) + + print(f"[DONE] Saved {count} crops under: {out_dir} (flat={flat})") + +def main(): + ap = argparse.ArgumentParser(description="Create square crops from detection JSON results (+optional MinIO).") + ap.add_argument("--input", required=True) + ap.add_argument("--out", required=True) + ap.add_argument("--orig", default=None, help="דרוש רק אם JSON חסר source_path") + ap.add_argument("--size", type=int, default=224) + ap.add_argument("--margin", type=float, default=0.1) + ap.add_argument("--min-wh", type=int, default=8) + ap.add_argument("--flat", action="store_true") + + ap.add_argument("--minio-endpoint", default=None) + ap.add_argument("--minio-access", default=None) + ap.add_argument("--minio-secret", default=None) + ap.add_argument("--minio-bucket", default=None) + ap.add_argument("--minio-prefix", default="crops") + ap.add_argument("--minio-secure", action="store_true") + ap.add_argument("--run-id", default=None, help="תיקיית הריצה ב-MinIO (ברירת מחדל: YYYY/MM/DD/HHmm)") + + args = ap.parse_args() + run_id = args.run_id or datetime.now().strftime("%Y/%m/%d/%H%M") + run_crop( + inp=Path(args.input), out_dir=Path(args.out), + size=args.size, margin=args.margin, min_wh=args.min_wh, + orig_dir=Path(args.orig) if args.orig else None, flat=args.flat, + minio_endpoint=args.minio_endpoint, minio_access=args.minio_access, + minio_secret=args.minio_secret, minio_bucket=args.minio_bucket, + minio_prefix=args.minio_prefix, minio_secure=args.minio_secure, + run_id=run_id, + ) + + + +if __name__ == "__main__": + main() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/detect_only.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/detect_only.py new file mode 100644 index 000000000..707c88a66 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/detect_only.py @@ -0,0 +1,140 @@ + +from __future__ import annotations +import json, argparse +from pathlib import Path +from typing import Optional +from datetime import datetime + +# --- HARD PATCH: עקיפת cpuinfo/ultralytics באופן גורף (מונע Popen('')) --- +try: + import cpuinfo as _ci + _ci.get_cpu_info = (lambda: {"brand_raw": "unknown"}) +except Exception: + pass +try: + import ultralytics.utils.torch_utils as _tu + _tu.get_cpu_info = (lambda: "unknown") +except Exception: + pass +# --- end hard patch --- + +import cv2 +from ultralytics import YOLO +from common import iter_images, ensure_dir, draw_boxes + +import cpuinfo +try: + print("cpu brand:", cpuinfo.get_cpu_info().get("brand_raw")) +except Exception as e: + print("cpuinfo error:", repr(e)) + +try: + from minio_io import get_client, ensure_bucket, put_png, put_json +except Exception: + get_client = ensure_bucket = put_png = put_json = None + +def run_detect(inp: Path, out_dir: Path, weights: Path, + conf: float=0.25, imgsz: int=896, device: str="cpu", + minio_endpoint: Optional[str]=None, minio_access: Optional[str]=None, + minio_secret: Optional[str]=None, minio_bucket: Optional[str]=None, + minio_prefix: str="DETECT", minio_secure: bool=False, + run_id: Optional[str]=None): + + run_id = run_id or datetime.now().strftime("%Y/%m/%d/%H%M") + out_dir = ensure_dir(out_dir) + overlay_root = ensure_dir(out_dir / "overlay") + json_root = ensure_dir(out_dir / "json") + + cli = None + if minio_endpoint and minio_access and minio_secret and minio_bucket: + if get_client is None: + raise SystemExit("[ERR] חסר minio או minio_io.") + cli = get_client(minio_endpoint, minio_access, minio_secret, secure=minio_secure) + ensure_bucket(cli, minio_bucket) + + model = YOLO(str(weights)) + + + img_paths = list(iter_images(inp)) + if not img_paths: + raise SystemExit(f"[ERR] No images found under: {inp}") + + is_dir_input = Path(inp).is_dir() + + for img_path in img_paths: + + rel_path = img_path.name if not is_dir_input else str(img_path.relative_to(inp)) + rel_parent = "." if not is_dir_input else str(img_path.relative_to(inp).parent) + rel_stem = Path(rel_path).stem + + overlay_dir = ensure_dir(overlay_root / rel_parent) + json_dir = ensure_dir(json_root / rel_parent) + + img_bgr = cv2.imread(str(img_path)) + if img_bgr is None: + print(f"[WARN] can't read image: {img_path}") + continue + h, w = img_bgr.shape[:2] + + res = model.predict(source=img_bgr, conf=conf, imgsz=imgsz, device=device, verbose=False)[0] + + boxes_pix = [] + if res.boxes is not None and len(res.boxes) > 0: + for b in res.boxes: + xyxy = b.xyxy.cpu().numpy().reshape(-1) + conf_i = float(b.conf.cpu().numpy().reshape(-1)[0]) + cls_i = float(b.cls.cpu().numpy().reshape(-1)[0]) if b.cls is not None else 0.0 + x1,y1,x2,y2 = map(float, xyxy.tolist()) + boxes_pix.append([x1,y1,x2,y2,conf_i,cls_i]) + + j = { + "image": img_path.name, + "rel_path": rel_path, + "source_path": str(img_path.resolve()), + "width": w, "height": h, + "boxes": boxes_pix + } + json_path = json_dir / f"{rel_stem}.json" + json_path.write_text(json.dumps(j, ensure_ascii=False, indent=2), encoding="utf-8") + + overlay = draw_boxes(img_bgr, boxes_pix) + ov_path = overlay_dir / img_path.name + cv2.imwrite(str(ov_path), overlay) + + if cli: + base = f"{run_id}/{minio_prefix}" + minio_json_key = f"{base}/json/{rel_parent}/{rel_stem}.json" if rel_parent != "." else f"{base}/json/{rel_stem}.json" + minio_ov_key = f"{base}/overlay/{rel_parent}/{img_path.name}" if rel_parent != "." else f"{base}/overlay/{img_path.name}" + put_json(cli, minio_bucket, minio_json_key, j) + put_png(cli, minio_bucket, minio_ov_key, overlay) + + print(f"[OK] {rel_path} -> {json_path.relative_to(out_dir)}, boxes={len(boxes_pix)}") + +def main(): + ap = argparse.ArgumentParser(description="YOLO detect -> pixel JSON + overlay (+optional MinIO)") + ap.add_argument("--input", required=True) + ap.add_argument("--out", required=True) + ap.add_argument("--weights", required=True) + ap.add_argument("--conf", type=float, default=0.25) + ap.add_argument("--imgsz", type=int, default=896) + ap.add_argument("--device", default="cpu") + + ap.add_argument("--minio-endpoint", default=None) + ap.add_argument("--minio-access", default=None) + ap.add_argument("--minio-secret", default=None) + ap.add_argument("--minio-bucket", default=None) + ap.add_argument("--minio-prefix", default="detect") + ap.add_argument("--minio-secure", action="store_true") + ap.add_argument("--run-id", default=None, help="תיקיית הריצה ב-MinIO (ברירת מחדל: YYYY/MM/DD/HHmm)") + + args = ap.parse_args() + run_id = args.run_id or datetime.now().strftime("%Y/%m/%d/%H%M") + run_detect(Path(args.input), Path(args.out), Path(args.weights), + conf=args.conf, imgsz=args.imgsz, device=args.device, + minio_endpoint=args.minio_endpoint, minio_access=args.minio_access, + minio_secret=args.minio_secret, minio_bucket=args.minio_bucket, + minio_prefix=args.minio_prefix, minio_secure=args.minio_secure, + run_id=run_id) + +if __name__ == "__main__": + main() diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/minio_io.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/minio_io.py new file mode 100644 index 000000000..cda3c246d --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/minio_io.py @@ -0,0 +1,39 @@ +from __future__ import annotations +import io, json, os +from pathlib import Path +import cv2 +from minio import Minio +from minio.error import S3Error + +def get_client(endpoint: str, access_key: str, secret_key: str, secure: bool=False) -> Minio: + """ + דוגמה: + cli = get_client("localhost:9000", "minioadmin", "minioadmin", secure=False) + """ + return Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=secure) + +def ensure_bucket(cli: Minio, bucket: str): + found = cli.bucket_exists(bucket) + if not found: + cli.make_bucket(bucket) + +def put_png(cli: Minio, bucket: str, key: str, img_bgr): + """ + מעלה תמונת PNG מתוך np.ndarray (BGR של OpenCV). + """ + Path(key).parent and os.makedirs(Path(key).parent, exist_ok=True) # לא חובה, לשקט נפשי מקומי + ok, buf = cv2.imencode(".png", img_bgr) + if not ok: + raise RuntimeError("cv2.imencode PNG failed") + bio = io.BytesIO(buf.tobytes()) + bio.seek(0) + cli.put_object(bucket, key, bio, length=len(bio.getvalue()), content_type="image/png") + +def put_json(cli: Minio, bucket: str, key: str, obj): + """ + מעלה JSON (dict/list). + """ + js = json.dumps(obj, ensure_ascii=False, indent=2).encode("utf-8") + bio = io.BytesIO(js) + bio.seek(0) + cli.put_object(bucket, key, bio, length=len(js), content_type="application/json") diff --git a/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/predict_pyramid_wbf.py b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/predict_pyramid_wbf.py new file mode 100644 index 000000000..292829ba1 --- /dev/null +++ b/AgCloud/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/predict_pyramid_wbf.py @@ -0,0 +1,233 @@ + +from __future__ import annotations +import argparse, json +from pathlib import Path +from typing import List, Tuple, Optional +from datetime import datetime + +# --- HARD PATCH: עקיפת cpuinfo/ultralytics (מונע Popen('')) --- +try: + import cpuinfo as _ci + _ci.get_cpu_info = (lambda: {"brand_raw": "unknown"}) +except Exception: + pass +try: + import ultralytics.utils.torch_utils as _tu + _tu.get_cpu_info = (lambda: "unknown") +except Exception: + pass +# --- end hard patch --- + +import cv2 +import numpy as np +from ultralytics import YOLO + +from common import iter_images, ensure_dir, draw_boxes + +try: + from minio_io import get_client, ensure_bucket, put_png, put_json +except Exception: + get_client = ensure_bucket = put_png = put_json = None + + +# ----------------- WBF utils ----------------- +def iou_xyxy(a: np.ndarray, b: np.ndarray) -> float: + ax1, ay1, ax2, ay2 = a + bx1, by1, bx2, by2 = b + ix1, iy1 = max(ax1, bx1), max(ay1, by1) + ix2, iy2 = min(ax2, bx2), min(ay2, by2) + iw, ih = max(0.0, ix2 - ix1), max(0.0, iy2 - iy1) + inter = iw * ih + area_a = max(0.0, ax2 - ax1) * max(0.0, ay2 - ay1) + area_b = max(0.0, bx2 - bx1) * max(0.0, by2 - by1) + union = area_a + area_b - inter + 1e-9 + return inter / union + + +def wbf(boxes: List[np.ndarray], scores: List[float], iou_thr: float = 0.55) -> tuple[list[np.ndarray], list[float]]: + """Very small WBF: קיבוץ לפי IoU>=thr, ממוצע משוקלל לפי conf.""" + used = [False] * len(boxes) + out_boxes, out_scores = [], [] + for i in range(len(boxes)): + if used[i]: + continue + group_idxs = [i] + used[i] = True + for j in range(i + 1, len(boxes)): + if used[j]: + continue + if iou_xyxy(boxes[i], boxes[j]) >= iou_thr: + group_idxs.append(j) + used[j] = True + bs = np.array([boxes[k] for k in group_idxs], dtype=float) + ws = np.array([scores[k] for k in group_idxs], dtype=float) + wsum = ws.sum() + 1e-9 + avg = (bs * ws[:, None]).sum(axis=0) / wsum + out_boxes.append(avg) + out_scores.append(float(ws.max())) + return out_boxes, out_scores + + +# ----------------- multi-scale predict ----------------- +def predict_at_scales(model: YOLO, img_bgr: np.ndarray, scales: List[float], conf: float, imgsz: int, device: str): + H, W = img_bgr.shape[:2] + all_boxes, all_scores, all_classes = [], [], [] + for s in scales: + if s == 1.0: + resized = img_bgr + rx, ry = 1.0, 1.0 + else: + newW, newH = int(W * s), int(H * s) + resized = cv2.resize(img_bgr, (newW, newH), interpolation=cv2.INTER_LINEAR) + rx, ry = 1.0 / s, 1.0 / s + + res = model.predict(source=resized, conf=conf, imgsz=imgsz, device=device, verbose=False)[0] + if res.boxes is None or len(res.boxes) == 0: + continue + for b in res.boxes: + xyxy = b.xyxy.cpu().numpy().reshape(-1) + conf_i = float(b.conf.cpu().numpy().reshape(-1)[0]) + cls_i = float(b.cls.cpu().numpy().reshape(-1)[0]) if b.cls is not None else 0.0 + x1, y1, x2, y2 = xyxy + # החזרה לקואורדינטות המקור + x1, y1, x2, y2 = x1 * rx, y1 * ry, x2 * rx, y2 * ry + all_boxes.append(np.array([x1, y1, x2, y2], dtype=float)) + all_scores.append(conf_i) + all_classes.append(int(cls_i)) + return all_boxes, all_scores, all_classes + + +# ----------------- main runner ----------------- +def run(inp: Path, out_dir: Path, weights: Path, + scales: List[float], conf: float = 0.25, iou_thr: float = 0.55, + imgsz: int = 896, device: str = "cpu", + minio_endpoint: Optional[str] = None, minio_access: Optional[str] = None, + minio_secret: Optional[str] = None, minio_bucket: Optional[str] = None, + minio_prefix: str = "PREDICT_PWB", minio_secure: bool = False, + run_id: Optional[str] = None): + + run_id = run_id or datetime.now().strftime("%Y/%m/%d/%H%M") + + out_dir = ensure_dir(out_dir) + overlay_root = ensure_dir(out_dir / "overlay") + json_root = ensure_dir(out_dir / "json") + + cli = None + if minio_endpoint and minio_access and minio_secret and minio_bucket: + if get_client is None: + raise SystemExit("[ERR] חסר minio או minio_io.") + cli = get_client(minio_endpoint, minio_access, minio_secret, secure=minio_secure) + ensure_bucket(cli, minio_bucket) + + model = YOLO(str(weights)) + images = list(iter_images(inp)) + if not images: + raise SystemExit(f"[ERR] No images under: {inp}") + + for p in images: + img = cv2.imread(str(p)) + if img is None: + print(f"[WARN] can't read: {p}") + continue + H, W = img.shape[:2] + + + rel_path = str(p.relative_to(inp)) if inp.is_dir() else p.name + rel_parent = str(Path(rel_path).parent) + rel_stem = Path(rel_path).stem + + boxes, scores, classes = predict_at_scales(model, img, scales, conf, imgsz, device) + + + merged = [] + for cls in sorted(set(classes)): + idxs = [i for i, c in enumerate(classes) if c == cls] + if not idxs: + continue + bcls = [boxes[i] for i in idxs] + scls = [scores[i] for i in idxs] + mbox, mscore = wbf(bcls, scls, iou_thr=iou_thr) + for bb, ss in zip(mbox, mscore): + x1, y1, x2, y2 = [float(max(0, v)) for v in bb] + x1, y1 = min(x1, W - 1), min(y1, H - 1) + x2, y2 = min(x2, W - 1), min(y2, H - 1) + merged.append([x1, y1, x2, y2, float(ss), float(cls)]) + + + overlay_dir = ensure_dir(overlay_root / rel_parent) + json_dir = ensure_dir(json_root / rel_parent) + + j = { + "image": p.name, + "rel_path": rel_path, + "source_path": str(p.resolve()), + "width": W, "height": H, + "boxes": merged + } + jpath = json_dir / f"{rel_stem}.json" + jpath.write_text(json.dumps(j, ensure_ascii=False, indent=2), encoding="utf-8") + + overlay = draw_boxes(img, merged) + cv2.imwrite(str(overlay_dir / p.name), overlay) + + + if cli: + base = f"{run_id}/{minio_prefix}" + json_key = f"{base}/json/{rel_parent}/{rel_stem}.json" if rel_parent != "." else f"{base}/json/{rel_stem}.json" + ov_key = f"{base}/overlay/{rel_parent}/{p.name}" if rel_parent != "." else f"{base}/overlay/{p.name}" + put_json(cli, minio_bucket, json_key, j) + put_png(cli, minio_bucket, ov_key, overlay) + + print(f"[OK] {rel_path} WBF boxes={len(merged)} -> {jpath.relative_to(out_dir)}") + + +def parse_scales(s: str) -> List[float]: + return [float(x) for x in s.split(",") if x.strip()] + + +def main(): + ap = argparse.ArgumentParser(description="YOLO multi-scale + WBF (+optional MinIO)") + ap.add_argument("--input", required=True) + ap.add_argument("--out", required=True) + ap.add_argument("--weights", required=True) + ap.add_argument("--scales", default="0.75,1.0,1.25", help="comma-separated, e.g. 0.5,1.0,1.5") + ap.add_argument("--conf", type=float, default=0.25) + ap.add_argument("--iou", type=float, default=0.55, help="WBF IoU threshold") + ap.add_argument("--imgsz", type=int, default=896) + ap.add_argument("--device", default="cpu") + + # MinIO + ap.add_argument("--minio-endpoint", default=None) + ap.add_argument("--minio-access", default=None) + ap.add_argument("--minio-secret", default=None) + ap.add_argument("--minio-bucket", default=None) + ap.add_argument("--minio-prefix", default="PREDICT_PWB") + ap.add_argument("--minio-secure", action="store_true") + + # Run grouping + ap.add_argument("--run-id", default=None, help="תיקיית הריצה ב-MinIO (ברירת מחדל: YYYY/MM/DD/HHmm)") + + args = ap.parse_args() + + run_id = args.run_id or datetime.now().strftime("%Y/%m/%d/%H%M") + run( + inp=Path(args.input), + out_dir=Path(args.out), + weights=Path(args.weights), + scales=parse_scales(args.scales), + conf=args.conf, + iou_thr=args.iou, + imgsz=args.imgsz, + device=args.device, + minio_endpoint=args.minio_endpoint, + minio_access=args.minio_access, + minio_secret=args.minio_secret, + minio_bucket=args.minio_bucket, + minio_prefix=args.minio_prefix, + minio_secure=args.minio_secure, + run_id=run_id, + ) + + +if __name__ == "__main__": + main() diff --git a/AgCloud/docker-compose.yml b/AgCloud/docker-compose.yml new file mode 100644 index 000000000..1bb720727 --- /dev/null +++ b/AgCloud/docker-compose.yml @@ -0,0 +1,1825 @@ +# ========================== +# Docker Compose - AG Cloud +# ========================== + +# version: "3.9" +# -------------------------- +# Networks +# -------------------------- +networks: + ag_cloud: + name: ag_cloud + driver: bridge + flink-net: + driver: bridge + +# -------------------------- +# Secrets +# -------------------------- +secrets: + slack_webhook: + file: ./secrets/slack_webhook.url + +# -------------------------- +# Volumes +# -------------------------- +volumes: + postgres_data: + wal_archive: + backups: + gui_data: + minio-hot-data: {} + minio-cold-data: {} + contracts: {} + +# ========================== +# Services +# ========================== +services: + + # -------------------------- + # RelDB / Postgres + # -------------------------- + postgres: + build: ./RelDB + container_name: postgres + environment: + POSTGRES_USER: missions_user + POSTGRES_PASSWORD: pg123 + POSTGRES_DB: missions_db + PGHOST: 0.0.0.0 + PGPORT: 5432 + PGDATA: /var/lib/postgresql/data + WAL_DIR: /var/lib/postgresql/wal_archive + BACKUP_DIR: /var/lib/postgresql/backups + RETENTION: 7 + TZ: Asia/Jerusalem + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - wal_archive:/var/lib/postgresql/wal_archive + - backups:/var/lib/postgresql/backups + healthcheck: + test: [ "CMD", "pg_isready", "-U", "missions_user", "-d", "missions_db" ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - ag_cloud + restart: unless-stopped + + postgres_exporter: + image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0 + environment: + DATA_SOURCE_NAME: "postgresql://missions_user:pg123@postgres:5432/missions_db?sslmode=disable" + command: + - "--extend.query-path=/etc/postgres-queries.yml" + volumes: + - ./RelDB/graphs/postgres-queries.yml:/etc/postgres-queries.yml + depends_on: + - postgres + ports: + - "9187:9187" + networks: + - ag_cloud + # ------------------------- + # Sound Metrics Service + # ------------------------- + + sound_metrics: + build: + context: ./services/sound_metrics + dockerfile: Dockerfile + environment: + - ADDR=0.0.0.0 + - PORT=8005 + - USE_UTC=false + - WINDOW_MIN=1 + - STABLE_SEC=1 + - PYTHONUNBUFFERED=1 + + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MINIO_BUCKET=sound + - MINIO_PREFIX=sounds/, plants/ + + command: [ "python", "-u", "src/metrics.py" ] + ports: + - "8005:8005" + depends_on: + - minio-hot + networks: + - ag_cloud + restart: unless-stopped + + + # ------------------------- + # Plant Stress Daily Batch + # ------------------------- + + plant_stress_daily: + build: ./services/plant_stress + environment: + TZ: "Asia/Jerusalem" + MODEL_DIR: /models + CONFIDENCE_THRESHOLD: "0.60" + TF_CPP_MIN_LOG_LEVEL: "2" + TIMEZONE: Asia/Jerusalem + POSTGRES_DSN: postgresql://missions_user:pg123@postgres:5432/missions_db + MINIO_ENDPOINT: minio-hot:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + MINIO_BUCKET: sound + MINIO_PREFIX: plants/ + MINIO_SECURE: "false" + DEFAULT_AREA: unknown + DEFAULT_LAT: 0.0 + DEFAULT_LON: 0.0 + DEFAULT_IMAGE_URL: https://example.com/placeholder.jpg + DEFAULT_VOD: https://example.com/placeholder.mp4 + DEFAULT_HLS: https://example.com/placeholder.m3u8 + ENABLE_ALERTS: "true" + KAFKA_BOOTSTRAP: "kafka:9092" + ALERT_TOPIC: "alerts" + ALERT_TYPE: "plant_drought_detected" + KAFKA_CLIENT_ID: "plant-stress-producer" + volumes: + - "./services/plant_stress/models:/models:ro" + depends_on: + postgres: + condition: service_healthy + minio-hot: + condition: service_healthy + mc-bootstrap: + condition: service_started + kafka: + condition: service_healthy + networks: [ag_cloud] + restart: unless-stopped + + # ------------------------- + # MQTT + Kafka + MQTT-router + # ------------------------- + kafka: + build: + context: ./mqtt_and_kafka/kafka + dockerfile: dockerfile + container_name: kafka + environment: + - ALLOW_PLAINTEXT_LISTENER=yes + - KAFKA_ENABLE_KRAFT=yes + - KAFKA_CFG_PROCESS_ROLES=broker,controller + - KAFKA_CFG_NODE_ID=1 + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT + - KAFKA_CFG_LISTENERS=INTERNAL://:9092,EXTERNAL://:9094,CONTROLLER://:9093 + - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9092,EXTERNAL://localhost:29092 + - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL + - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false + - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 + - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 + - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 + ports: + - "9092:9092" + - "29092:29092" + networks: + - ag_cloud + healthcheck: + test: [ "CMD-SHELL", "/opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list >/dev/null 2>&1 || exit 1" ] + interval: 10s + timeout: 5s + retries: 20 + + mosquitto: + image: eclipse-mosquitto:2.0 + container_name: mosquitto + command: [ "mosquitto", "-c", "/mqtt_and_kafka/mosquitto/config/mosquitto.conf" ] + ports: + - "1883:1883" + volumes: + - ./mqtt_and_kafka/mosquitto/config:/mqtt_and_kafka/mosquitto/config:ro + depends_on: + kafka: + condition: service_healthy + networks: + - ag_cloud + healthcheck: + test: [ "CMD", "mosquitto_sub", "-h", "localhost", "-p", "1883", "-t", "$$SYS/#", "-C", "1", "-W", "15" ] + interval: 10s + timeout: 5s + retries: 12 + + mqtt-router: + build: + context: ./mqtt_and_kafka/mqtt-router + image: local/mqtt-router:1.0.0 + depends_on: + kafka: + condition: service_healthy + mosquitto: + condition: service_healthy + environment: + - MQTT_HOST=mosquitto + - MQTT_PORT=1883 + - MQTT_TOPIC_FILTER=mqtt/# + + - KAFKA_BOOTSTRAP=kafka:9092 + - CREATE_TOPICS=false + - DEFAULT_PARTITIONS=1 + - DEFAULT_REPLICATION=1 + networks: + - ag_cloud + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import socket; socket.create_connection(('mosquitto',1883),3); socket.create_connection(('kafka',9092),3)"] + interval: 15s + timeout: 5s + retries: 5 + + # -------------------------- + # GUI / Runner / Gateway + # -------------------------- + runner: + build: + context: ./GUI + dockerfile: src/vast/runner/Dockerfile + args: + USE_NETFREE: ${USE_NETFREE:-true} + container_name: runner + environment: + - RUNNER_MODE=real + - SQLITE_DB=/data/app.db + - LOG_LEVEL=INFO + volumes: + - ./GUI/data:/data:ro + ports: + - "50051:50051" + restart: unless-stopped + + gateway: + container_name: gateway + build: + context: ./GUI + dockerfile: src/vast/gateway/Dockerfile + args: + USE_NETFREE: ${USE_NETFREE:-true} + environment: + - RUNNER_ADDR=runner:50051 + ports: + - "8000:8000" + depends_on: + - runner + restart: unless-stopped + + sensors_metrics: + build: + context: ./GUI + dockerfile: src/vast/services/Dockerfile + container_name: sensors_metrics + environment: + - SQLITE_DB=/data/app.db + - GATEWAY_URL=http://gateway:8000 + volumes: + - ./GUI/data:/data:ro + depends_on: + - gateway + networks: + - ag_cloud + restart: unless-stopped + + # -------------------------- + # Prometheus / Grafana + # -------------------------- + prometheus: + image: prom/prometheus:latest + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - ./prometheus/prometheus-recording.rules.yml:/etc/prometheus/prometheus-recording.rules.yml:ro + - ./prometheus/postgres-alerts.yml:/etc/prometheus/postgres-alerts.yml:ro + ports: + - "9090:9090" + depends_on: + - postgres_exporter + - minio-hot + - minio-cold + networks: + - ag_cloud + + grafana: + image: grafana/grafana-oss:latest + environment: + GF_SECURITY_ALLOW_EMBEDDING: "true" + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer + GF_USERS_DEFAULT_THEME: light + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + volumes: + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro + - ./grafana/provisioning:/etc/grafana/provisioning:ro + ports: + - "3000:3000" + depends_on: + - prometheus + networks: + - ag_cloud + + pushgateway: + image: prom/pushgateway:v1.8.0 + container_name: pushgateway + ports: + - "9091:9091" + networks: + - ag_cloud + restart: unless-stopped + + # -------------------------- + # Desktop App + # -------------------------- + desktop_app: + build: + context: ./GUI + dockerfile: src/vast/desktop/Dockerfile + container_name: desktop_app + environment: + - NO_VNC_PORT=8080 + - DISPLAY=host.docker.internal:0.0 + - GATEWAY_URL=http://sensors_metrics:8000 + - NOTIFICATION_API_URL=http://notification_api:5000 + + - API_BASE_URL=http://db_api_service:8001 + - AUTH_BOOTSTRAP_URL=http://db_api_service:8001/auth/_dev_bootstrap + - ALERTS_WS_URL=ws://alerts-gateway:8000/ws/alerts + ports: + - "5900:5900" + - "8080:8080" + depends_on: + - db_api_service + - notification_api + - alerts-gateway + volumes: + - ./GUI/src/vast:/app/src/vast + - ./templates:/app/templates:ro + networks: + - ag_cloud + restart: unless-stopped + + # -------------------------- + # Large Mosquitto + # -------------------------- + large-mosquitto: + container_name: large-mosquitto + image: eclipse-mosquitto:2 + restart: unless-stopped + volumes: + - ./storage_with_mqtt/mqtt_images/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf + ports: + - "1885:1885" + networks: + - ag_cloud + + # -------------------------- + # MinIO: hot + cold + bootstrap + # -------------------------- + minio-hot: + build: + context: ./storage_with_mqtt/storage/minio-storage + container_name: minio-hot + environment: + MINIO_PROMETHEUS_AUTH_TYPE: public + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + + # ===== IMAGE NOTIFIERS ===== + MINIO_NOTIFY_KAFKA_ENABLE_aerial: "on" + MINIO_NOTIFY_KAFKA_BROKERS_aerial: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_aerial: "image.new.aerial" + + MINIO_NOTIFY_KAFKA_ENABLE_air: "on" + MINIO_NOTIFY_KAFKA_BROKERS_air: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_air: "image.new.air" + + MINIO_NOTIFY_KAFKA_ENABLE_fruits: "on" + MINIO_NOTIFY_KAFKA_BROKERS_fruits: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_fruits: "image.new.fruits" + + MINIO_NOTIFY_KAFKA_ENABLE_leaves: "on" + MINIO_NOTIFY_KAFKA_BROKERS_leaves: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_leaves: "image.new.leaves" + + MINIO_NOTIFY_KAFKA_ENABLE_ground: "on" + MINIO_NOTIFY_KAFKA_BROKERS_ground: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_ground: "image.new.ground" + + MINIO_NOTIFY_KAFKA_ENABLE_field: "on" + MINIO_NOTIFY_KAFKA_BROKERS_field: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_field: "image.new.field" + + # ===== SOUND NOTIFIERS ===== + MINIO_NOTIFY_KAFKA_ENABLE_plants: "on" + MINIO_NOTIFY_KAFKA_BROKERS_plants: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_plants: "sound.new.plants" + + MINIO_NOTIFY_KAFKA_ENABLE_sounds: "on" + MINIO_NOTIFY_KAFKA_BROKERS_sounds: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_sounds: "sound.new.sounds" + + # ===== SECURITY NOTIFIER ===== + MINIO_NOTIFY_KAFKA_ENABLE_security: "on" + MINIO_NOTIFY_KAFKA_BROKERS_security: "kafka:9092" + MINIO_NOTIFY_KAFKA_TOPIC_security: "image.new.security" + ports: + - "9001:9000" # HOT S3 + - "9002:9001" # HOT Console + networks: [ ag_cloud ] + healthcheck: + test: [ "CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready" ] + interval: 3s + timeout: 2s + retries: 40 + volumes: + - minio-hot-data:/data + + minio-cold: + build: + context: ./storage_with_mqtt/storage/minio-storage + container_name: minio-cold + environment: + MINIO_PROMETHEUS_AUTH_TYPE: public + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + ports: + - "9101:9000" # COLD S3 + - "9102:9001" # COLD Console + networks: [ ag_cloud ] + healthcheck: + test: [ "CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready" ] + interval: 3s + timeout: 2s + retries: 40 + volumes: + - minio-cold-data:/data + + mc-bootstrap: + build: + context: ./storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap + container_name: mc-bootstrap + volumes: + - ./storage_with_mqtt/storage/combined_minio_setup/config:/config:ro + - ./storage_with_mqtt/data/config:/config + depends_on: + minio-hot: + condition: service_healthy + minio-cold: + condition: service_healthy + kafka: + condition: service_healthy + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + HOT_ENDPOINT: http://minio-hot:9000 + COLD_ENDPOINT: http://minio-cold:9000 + MC_ALIAS_HOT: hot + MC_ALIAS_COLD: cold + BUCKET_IMAGERY: imagery + BUCKET_SOUND: sound + networks: [ ag_cloud ] + restart: unless-stopped + + # -------------------------- + # MQTT Ingest & Publisher + # -------------------------- + mqtt_ingest: + build: + context: ./storage_with_mqtt/mqtt_images/mqtt_ingest + container_name: mqtt_ingest + environment: + MINIO_ENDPOINT: http://minio-hot:9000 + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + BUCKET_IMAGERY: imagery + BUCKET_SOUND: sound + MQTT_BROKER: large-mosquitto + MQTT_PORT: 1885 + MQTT_TOPIC: MQTT/imagery/# + MQTT_PUB_TOPIC: imagery/ingested + DUMMY_DB: 0 + DB_API_BASE: http://db_api_service:8001 + DB_API_TOKEN: auto + OUTBOX_DIR: /app/outbox + DB_API_AUTH_MODE: service + DB_API_SERVICE_NAME: mqtt_ingest + INGEST_WORKERS: 8 + volumes: + - ./storage_with_mqtt/mqtt_images/outbox:/app/outbox + depends_on: + large-mosquitto: + condition: service_started + minio-hot: + condition: service_healthy + mc-bootstrap: + condition: service_started + db_api_service: + condition: service_started + networks: + - ag_cloud + restart: unless-stopped + + mqtt_ingest_sound: + build: + context: ./storage_with_mqtt/mqtt_images/mqtt_ingest + container_name: mqtt_ingest_sound + environment: + MINIO_ENDPOINT: http://minio-hot:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + S3_BUCKET: sound + MQTT_BROKER: large-mosquitto + MQTT_PORT: 1885 + MQTT_TOPIC: MQTT/sounds/# + MQTT_PUB_TOPIC: sound/sounds/ingested + DEFAULT_PREFIX: MIC-01 + CAMERA_PREFIX: camera + MICROPHONE_PREFIX: microphone + DUMMY_DB: 0 + DB_API_BASE: http://db_api_service:8001 + DB_API_TOKEN: auto + OUTBOX_DIR: /app/outbox + DB_API_AUTH_MODE: service + DB_API_SERVICE_NAME: mqtt_ingest_sound + INGEST_WORKERS: 8 + volumes: + - ./storage_with_mqtt/mqtt_images/outbox:/app/outbox + depends_on: + large-mosquitto: + condition: service_started + minio-hot: + condition: service_healthy + mc-bootstrap: + condition: service_started + db_api_service: + condition: service_started + networks: + - ag_cloud + restart: unless-stopped + + mqtt_ingest_sounds_ultra: + build: + context: ./storage_with_mqtt/mqtt_images/mqtt_ingest + container_name: mqtt_ingest_sounds_ultra + environment: + MINIO_ENDPOINT: http://minio-hot:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + S3_BUCKET: sound + MQTT_BROKER: large-mosquitto + MQTT_PORT: 1885 + MQTT_TOPIC: MQTT/sounds_ultra/# + MQTT_PUB_TOPIC: sound/sounds_ultra/ingested + DEFAULT_PREFIX: MIC-02 + CAMERA_PREFIX: microphone + MICROPHONE_PREFIX: microphone + DUMMY_DB: 0 + DB_API_BASE: http://db_api_service:8001 + DB_API_TOKEN: auto + OUTBOX_DIR: /app/outbox + DB_API_AUTH_MODE: service + DB_API_SERVICE_NAME: mqtt_ingest_sounds_ultra + INGEST_WORKERS: 8 + ULTRA_DIR_PREFIX: plants + volumes: + - ./storage_with_mqtt/mqtt_images/outbox:/app/outbox + depends_on: + large-mosquitto: + condition: service_started + minio-hot: + condition: service_healthy + mc-bootstrap: + condition: service_started + db_api_service: + condition: service_started + networks: + - ag_cloud + restart: unless-stopped + + mqtt_publisher: + build: + context: ./storage_with_mqtt/mqtt_images/mqtt_publisher + container_name: mqtt_publisher + environment: + - MQTT_HOST=large-mosquitto + - MQTT_PORT=1885 + - MQTT_TOPIC_BASE=MQTT/imagery + - IMAGES_DIR=/images + - CAMERA_ID=camera-01 + - LIMIT=0 + - SHUFFLE=1 + - MQTT_QOS=2 + - PUBLISH_DELAY_MS=100 + volumes: + - ./storage_with_mqtt/mqtt_images/data/real_images:/images:ro + depends_on: + - large-mosquitto + - mqtt_ingest + networks: + - ag_cloud + + mqtt_gateway: + build: + context: ./services/mqtt_gateway + dockerfile: Dockerfile + environment: + MINIO_ENDPOINT: http://minio-hot:9000 + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + MINIO_BUCKET: imagery + KAFKA_BOOTSTRAP: kafka:9092 + KAFKA_TOPIC: rover.images.meta.v1 + MQTT_HOST: large-mosquitto + MQTT_PORT: 1885 + MQTT_TOPIC: MQTT/imagery/# + MQTT_TOPIC_TEL: MQTT/telemetry/# + TELEMETRY_TTL_SEC: 10 + MQTT_CLIENT_ID: mqtt-gateway + METRICS_PORT: 9110 + depends_on: + large-mosquitto: + condition: service_started + kafka: + condition: service_healthy + minio-hot: + condition: service_healthy + networks: + - ag_cloud + restart: unless-stopped + + + # ------------------------ + # Classifier - Sounds + # ------------------------ + sounds_classifier: + build: + context: ./services/sounds_classifier + dockerfile: Dockerfile.classifier-svc + container_name: sounds_classifier + restart: unless-stopped + environment: + # Runtime mode + - DEVICE=cpu + - BACKBONE=cnn14 + + # Model artifacts (must exist inside the image) + - CHECKPOINT=/app/classification/models/panns_data/Cnn14_mAP=0.431.pth + - HEAD=/app/classification/models/head/head_cnn14_rf.joblib + - HEAD_META=/app/classification/models/head/head_cnn14_rf.joblib.meta.json + + # DB + - WRITE_DB=false + - DB_URL=postgresql://missions_user:pg123@postgres:5432/missions_db + - DB_SCHEMA=agcloud_audio + - DB_RUN_ID=api-default + - FILES_SCHEMA=public + - FILES_TABLE=sound_new_sounds_connections + + # Kafka + - KAFKA_BROKERS=kafka:9092 + - ALERTS_TOPIC=alerts + - ENABLE_ALERTS=true + + # MinIO + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MINIO_SECURE=false + + # Request validation + - ALLOWED_BUCKETS=sound + - ALLOWED_CONTENT_TYPES=audio/wav,audio/x-wav,audio/mpeg,audio/flac,audio/ogg,audio/mp4 + - MAX_BYTES=104857600 + + # Tuning params + - UNKNOWN_THRESHOLD=0.4 + - WINDOW_SEC=2.0 + - HOP_SEC=0.5 + - PAD_LAST=true + - AGG=mean + depends_on: + postgres: + condition: service_healthy + kafka: + condition: service_healthy + mc-bootstrap: + condition: service_started + ports: + - "8088:8088" + networks: + - ag_cloud + healthcheck: + test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8088/health').read()" ] + interval: 45s + timeout: 5s + retries: 10 + start_period: 20s + + # -------------------------- + # DB API Service + # -------------------------- + + contracts-gen: + build: + context: ./services/db_api_service + dockerfile: app/contracts/Dockerfile + env_file: + - ./services/db_api_service/.env + environment: + DATABASE_URL: postgresql+psycopg://missions_user:pg123@postgres:5432/missions_db + depends_on: + postgres: + condition: service_healthy + volumes: + - contracts:/app/app/contracts + networks: + - ag_cloud + restart: "no" + + db_api_service: + build: + context: ./services/db_api_service + dockerfile: Dockerfile + container_name: db_api_service + env_file: + - ./services/db_api_service/.env + environment: + DB_DSN: postgresql+psycopg://missions_user:pg123@postgres:5432/missions_db + ENV: dev + JWT_SECRET: change-me-please-very-secret + JWT_ALGO: HS256 + ACCESS_TTL_MIN: 15 + REFRESH_TTL_DAYS: 14 + DEV_SA_NAME: my-ingest-service + ADDR: 0.0.0.0 + ports: + - "8001:8001" + volumes: + - ./services/db_api_service/app:/app/app + - contracts:/app/app/contracts + depends_on: + contracts-gen: + condition: service_completed_successfully + postgres: + condition: service_healthy + networks: + - ag_cloud + restart: unless-stopped + + notification_api: + build: + context: ./services/API-notifications/src + dockerfile: Dockerfile + container_name: notification_api + environment: + - FLASK_ENV=development + ports: + - "5000:5000" + depends_on: + - postgres + networks: + - ag_cloud + + ripeness-api: + build: + context: ./services/ripeness-ml + dockerfile: deploy/Dockerfile + image: ripeness-api:latest + environment: + - PGHOST=postgres + - PGPORT=5432 + - PGDATABASE=missions_db + - PGUSER=missions_user + - PGPASSWORD=pg123 + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_SECURE=false + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MODEL_NAME=best_conditional + - BATCH_LIMIT=500 + - FRUITS=Apple,Banana,Orange + depends_on: + - postgres + - minio-hot + volumes: + - ./services/ripeness-ml/checkpoints:/app/checkpoints + - ./services/ripeness-ml/configs:/app/configs + - ./services/ripeness-ml/model:/app/model + container_name: ripeness-api + networks: [ ag_cloud ] + ports: + - "8091:8088" + restart: unless-stopped + + # -------------------------- + # Flink JobManager & TaskManager + # -------------------------- + flink-jobmanager: + build: + context: ./streaming/flink + dockerfile: Dockerfile.flink-py + image: agcloud-flink-py:1.18 + container_name: flink-jobmanager + command: jobmanager + ports: + - "8081:8081" + networks: [ ag_cloud ] + environment: + - | + FLINK_PROPERTIES= + jobmanager.rpc.address: flink-jobmanager + parallelism.default: 2 + taskmanager.numberOfTaskSlots: 2 + jobmanager.memory.process.size: 1600m + taskmanager.memory.process.size: 1728m + s3.endpoint: http://minio-hot:9000 + s3.path.style.access: true + s3.access.key: minioadmin + s3.secret.key: minioadmin123 + fs.s3a.connection.ssl.enabled: false + python.client.executable: /usr/bin/python3 + python.executable: /usr/bin/python3 + - HTTP_INFER_URL=http://fruit-inference-http:8004/infer_json + volumes: + - ./streaming/flink/jobs:/opt/flink/jobs:ro + - ./streaming/flink/connectors/flink-json-1.18.1.jar:/opt/flink/lib/flink-json-1.18.1.jar:ro + - ./streaming/flink/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar:/opt/flink/lib/flink-sql-connector-kafka-3.2.0-1.18.jar:ro + - ./streaming/flink/connectors/flink-connector-kafka-3.2.0-1.18.jar:/opt/flink/lib/flink-connector-kafka-3.2.0-1.18.jar:ro + - ./streaming/flink/connectors/kafka-clients-3.2.3.jar:/opt/flink/lib/kafka-clients-3.2.3.jar:ro + - ./streaming/flink/connectors/lz4-java-1.8.0.jar:/opt/flink/lib/lz4-java-1.8.0.jar:ro + - ./streaming/flink/connectors/snappy-java-1.1.10.5.jar:/opt/flink/lib/snappy-java-1.1.10.5.jar:ro + restart: unless-stopped + + audio_compression: + build: + context: ./services/compression + dockerfile: Dockerfile + container_name: audio_compression + environment: + - RAW_MAX_AGE_DAYS=30 + - COMPRESSION_CODEC=opus + - COMPRESSED_MAX_AGE_DAYS=90 + - CHECK_INTERVAL_SECONDS=3600 + - MINIO_ENDPOINT=minio-hot:9000 + - ACCESS_KEY=minioadmin + - SECRET_KEY=minioadmin123 + - BUCKET_NAME=imagery + depends_on: + minio-hot: + condition: service_healthy + mc-bootstrap: + condition: service_started + networks: + - ag_cloud + restart: unless-stopped + + flink_writer_db: + build: + context: ./services/flink_writer_db + dockerfile: Dockerfile.flink + container_name: flink_writer_db + environment: + - KAFKA_BROKERS=kafka:9092 + - TOPICS=sensor_zone_stats,sensor_anomalies,image_new_security_connections,alerts,image_new_aerial_connections,aerial_images_metadata,aerial_image_object_detections,aerial_image_anomaly_detections,aerial_images_complete_metadata,aerial_image_segmentation,sound_new_sounds_connections,sound_new_plants_connections,sounds_metadata,sounds_ultra_metadata,sensors,sensors_anomalies_modal,event_logs_sensors + - DB_API_BASE=http://db_api_service:8001 + - DB_API_AUTH_MODE=service + - DB_API_SERVICE_NAME=flink-writer-db + - DB_API_TOKEN_FILE=/opt/app/secrets/db_api_token + - FLINK_PARALLELISM=1 + depends_on: + kafka: + condition: service_healthy + db_api_service: + condition: service_started + networks: + - ag_cloud + restart: unless-stopped + + flink-taskmanager: + image: agcloud-flink-py:1.18 + container_name: flink-taskmanager + command: taskmanager + depends_on: + flink-jobmanager: + condition: service_started + networks: [ ag_cloud ] + environment: + - | + FLINK_PROPERTIES= + jobmanager.rpc.address: flink-jobmanager + parallelism.default: 2 + taskmanager.numberOfTaskSlots: 2 + jobmanager.memory.process.size: 1600m + taskmanager.memory.process.size: 1728m + s3.endpoint: http://minio-hot:9000 + s3.path.style.access: true + s3.access.key: minioadmin + s3.secret.key: minioadmin123 + fs.s3a.connection.ssl.enabled: false + python.client.executable: /usr/bin/python3 + python.executable: /usr/bin/python3 + - HTTP_INFER_URL=http://fruit-inference-http:8004/infer_json + volumes: + - ./streaming/flink/connectors/flink-json-1.18.1.jar:/opt/flink/lib/flink-json-1.18.1.jar:ro + - ./streaming/flink/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar:/opt/flink/lib/flink-sql-connector-kafka-3.2.0-1.18.jar:ro + - ./streaming/flink/connectors/flink-connector-kafka-3.2.0-1.18.jar:/opt/flink/lib/flink-connector-kafka-3.2.0-1.18.jar:ro + - ./streaming/flink/connectors/kafka-clients-3.2.3.jar:/opt/flink/lib/kafka-clients-3.2.3.jar:ro + - ./streaming/flink/connectors/lz4-java-1.8.0.jar:/opt/flink/lib/lz4-java-1.8.0.jar:ro + - ./streaming/flink/connectors/snappy-java-1.1.10.5.jar:/opt/flink/lib/snappy-java-1.1.10.5.jar:ro + restart: unless-stopped + + # -------------------------- + # Fence Hole Detector (FastAPI + ONNX) + # -------------------------- + fence-hole-detector: + build: + context: ./services/fence_hole_detector + dockerfile: Dockerfile + container_name: fence-hole-detector + env_file: + - ./services/fence_hole_detector/.env + environment: + # Override anything needed for in-Docker networking + MINIO_ENDPOINT: minio-hot:9000 + MINIO_SECURE: "false" + # Use internal base URL so other containers can fetch the image + MINIO_PUBLIC_BASE_URL: http://minio-hot:9000/imagery + + # Kafka inside docker network + ALERT_ENABLED: "1" + KAFKA_BOOTSTRAP: kafka:9092 + ALERTS_TOPIC: alerts + ALERT_TYPE: fence_hole + depends_on: + kafka: + condition: service_healthy + minio-hot: + condition: service_healthy + networks: + - ag_cloud + ports: + - "8088:8088" # expose API to host (optional) + volumes: + - ./services/fence_hole_detector/weights:/app/services/fence_hole_detector/weights:ro + command: + [ + "uvicorn", + "services.fence_hole_detector.app:app", + "--host", "0.0.0.0", + "--port", "8088", + "--log-level", "info" + ] + + # -------------------------- + # Inference HTTP Service + # -------------------------- + fruit-inference-http: + build: + context: ./services/inference_http + dockerfile: Dockerfile + environment: + - TEAM=fruit + - WEIGHTS_PATH=/app/weights/fruit_cls_best.ts + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MINIO_SECURE=0 + volumes: + - ./services/inference_http/weights:/app/weights:ro + container_name: fruit-inference-http + networks: [ ag_cloud ] + ports: + - "8011:8004" + restart: unless-stopped + + camera-inference-http: + build: + context: ./services/inference_http + dockerfile: Dockerfile + environment: + - TEAM=camera + - WEIGHTS_PATH=/app/weights/yolov8-fruits.pt + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MINIO_SECURE=0 + volumes: + - ./services/inference_http/weights:/app/weights:ro + container_name: camera-inference-http + networks: [ag_cloud] + ports: + - "8012:8004" + restart: unless-stopped + soil-inference-http: + build: + context: ./services/inference_http + dockerfile: Dockerfile + environment: + + - TEAM=soil_moisture + - WEIGHTS_PATH=/app/weights/soil_moisture_best.onnx + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MINIO_SECURE=0 + - PG_DSN=postgresql://missions_user:pg123@postgres:5432/missions_db + - KAFKA_BROKERS=kafka:9092 + - KAFKA_TOPIC=irrigation.control + - KAFKA_DLT=irrigation.control.dlq + + + volumes: + - ./services/inference_http/weights:/app/weights:ro + - ./services/inference_http/adapters:/app/adapters + - ./services/inference_http/soil_moisture:/app/soil_moisture + depends_on: + - minio-hot + - postgres + ports: + - "8013:8004" + networks: [ag_cloud] + restart: unless-stopped + + # -------------------------- + # Flink Jobs + # -------------------------- + flink-dispatcher-fruit: + image: agcloud-flink-py:1.18 + container_name: flink-dispatcher-fruit + depends_on: + flink-jobmanager: { condition: service_started } + flink-taskmanager: { condition: service_started } + fruit-inference-http: { condition: service_started } + networks: [ ag_cloud ] + environment: + - KAFKA_BOOTSTRAP=kafka:9092 + - INPUT_TOPIC=imagery.new.fruit + - TEAM=fruit + - HTTP_URL=http://fruit-inference-http:8004/infer_json + - DLQ_TOPIC=dlq.inference.http + - GROUP_ID=http-dispatcher-fruit + - PARALLELISM=2 + - PYFLINK_CLIENT_EXECUTABLE=/usr/bin/python3 + volumes: + - ./streaming/flink/jobs:/opt/flink/jobs:ro + - ./streaming/flink/connectors/flink-connector-kafka-3.2.0-1.18.jar:/opt/flink/lib/flink-connector-kafka-3.2.0-1.18.jar:ro + - ./streaming/flink/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar:/opt/flink/lib/flink-sql-connector-kafka-3.2.0-1.18.jar:ro + - ./streaming/flink/connectors/flink-json-1.18.1.jar:/opt/flink/lib/flink-json-1.18.1.jar:ro + - ./streaming/flink/connectors/kafka-clients-3.2.3.jar:/opt/flink/lib/kafka-clients-3.2.3.jar:ro + - ./streaming/flink/connectors/lz4-java-1.8.0.jar:/opt/flink/lib/lz4-java-1.8.0.jar:ro + - ./streaming/flink/connectors/snappy-java-1.1.10.5.jar:/opt/flink/lib/snappy-java-1.1.10.5.jar:ro + command: [ "bash", "-lc", "set -e; echo 'Waiting for JobManager to accept commands...'; until /opt/flink/bin/flink list --jobmanager flink-jobmanager:8081 >/dev/null 2>&1; do echo 'still waiting...'; sleep 3; done; echo 'JobManager is ready!'; /opt/flink/bin/flink run -Dpython.client.executable=/usr/bin/python3 -Dpython.executable=/usr/bin/python3 -Dpipeline.jars=file:///opt/flink/lib/flink-connector-kafka-3.2.0-1.18.jar,file:///opt/flink/lib/flink-sql-connector-kafka-3.2.0-1.18.jar,file:///opt/flink/lib/flink-json-1.18.1.jar --jobmanager flink-jobmanager:8081 --detached --python /opt/flink/jobs/http_dispatcher.py -- --bootstrap kafka:9092 --input-topic imagery.new.fruit --team fruit --http-url http://fruit-inference-http:8004/infer_json --group-id http-dispatcher-fruit --dlq-topic dlq.inference.http; tail -f /dev/null" ] + restart: always + + flink-dispatcher-camera: + image: agcloud-flink-py:1.18 + container_name: flink-dispatcher-camera + depends_on: + flink-jobmanager: { condition: service_started } + flink-taskmanager: { condition: service_started } + camera-inference-http: { condition: service_started } + networks: [ag_cloud] + environment: + - KAFKA_BOOTSTRAP=kafka:9092 + - INPUT_TOPIC=imagery.new.camera + - TEAM=camera + - HTTP_URL=http://camera-inference-http:8004/infer_json + - DLQ_TOPIC=dlq.inference.http + - GROUP_ID=http-dispatcher-camera + - PARALLELISM=2 + - PYFLINK_CLIENT_EXECUTABLE=/usr/bin/python3 + volumes: + - ./streaming/flink/jobs:/opt/flink/jobs:ro + - ./streaming/flink/connectors:/opt/flink/lib/connectors:ro + command: [ "bash", "-lc", "set -e; echo 'Waiting for JobManager to accept commands...'; until /opt/flink/bin/flink list --jobmanager flink-jobmanager:8081 >/dev/null 2>&1; do echo 'still waiting...'; sleep 3; done; echo 'JobManager is ready!'; /opt/flink/bin/flink run -Dpython.client.executable=/usr/bin/python3 -Dpython.executable=/usr/bin/python3 -Dpipeline.jars=file:///opt/flink/lib/connectors/flink-connector-kafka-3.2.0-1.18.jar,file:///opt/flink/lib/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar,file:///opt/flink/lib/connectors/flink-json-1.18.1.jar --jobmanager flink-jobmanager:8081 --detached --python /opt/flink/jobs/http_dispatcher.py -- --bootstrap kafka:9092 --input-topic imagery.new.camera --team camera --http-url http://camera-inference-http:8004/infer_json --group-id http-dispatcher-camera --dlq-topic dlq.inference.http; tail -f /dev/null" ] + restart: always + + flink-dispatcher-soil: + image: agcloud-flink-py:1.18 + depends_on: + flink-jobmanager: { condition: service_started } + flink-taskmanager: { condition: service_started } + soil-inference-http: { condition: service_started } + networks: [ag_cloud] + environment: + - KAFKA_BOOTSTRAP=kafka:9092 + - INPUT_TOPIC=image.new.ground + - TEAM=soil_moisture + - HTTP_URL=http://soil-inference-http:8004/infer_json + - DLQ_TOPIC=dlq.inference.http + - GROUP_ID=http-dispatcher-soil + - PARALLELISM=1 + - PYFLINK_CLIENT_EXECUTABLE=/usr/bin/python3 + volumes: + - ./streaming/flink/jobs:/opt/flink/jobs:ro + - ./streaming/flink/connectors:/opt/flink/lib/connectors:ro + command: [ "bash", "-lc", "set -e; echo 'Waiting...'; until /opt/flink/bin/flink list --jobmanager flink-jobmanager:8081 >/dev/null 2>&1; do echo 'still waiting...'; sleep 3; done; echo 'JobManager is ready!'; /opt/flink/bin/flink run -Dpython.client.executable=/usr/bin/python3 -Dpython.executable=/usr/bin/python3 -Dpipeline.jars=file:///opt/flink/lib/connectors/... --jobmanager flink-jobmanager:8081 --detached --python /opt/flink/jobs/http_dispatcher.py -- --bootstrap kafka:9092 --input-topic image.new.ground --team soil_moisture --http-url http://soil-inference-http:8004/infer_json --group-id http-dispatcher-soil --dlq-topic dlq.inference.http; tail -f /dev/null" ] + + + flink-alerts-job: + build: + context: ./services/alerts_forwarder + dockerfile: Dockerfile.flink + container_name: alerts-forwarder + depends_on: + kafka: + condition: service_healthy + alertmanager_service: + condition: service_started + environment: + - PYTHONPATH=/opt/app + - KAFKA_BROKERS=kafka:9092 + - ALERTMANAGER_SERVICE_URL=http://alertmanager_service:8090/alerts + command: [ "python", "/opt/app/alerts_forwarder.py" ] + networks: + - ag_cloud + restart: unless-stopped + + alertmanager: + image: prom/alertmanager:v0.27.0 + container_name: alertmanager + command: + - "--config.file=/etc/alertmanager/alertmanager.yml" + - "--storage.path=/alertmanager" + - "--log.level=debug" + volumes: + - ./services/alertmanager_service/compose/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro + ports: + - "9093:9093" + networks: + - ag_cloud + restart: always + + alertmanager_service: + build: + context: ./services/alertmanager_service/src + dockerfile: Dockerfile + container_name: alertmanager_service + ports: + - "8090:8090" + command: [ "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090" ] + volumes: + - ./templates:/app/templates:ro + environment: + - CFG_PATH=/app/templates/templates.yml + - ALERTMANAGER_URL=http://alertmanager:9093 + - GATEWAY_URL=http://alerts-gateway:8000/internal/alert + depends_on: + - alertmanager + - alerts-gateway + networks: + - ag_cloud + + alerts-gateway: + build: + context: ./services/alertmanager_service/src + dockerfile: Dockerfile + container_name: alerts_gateway + command: [ "uvicorn", "gateway:app", "--host", "0.0.0.0", "--port", "8000" ] + ports: + - "8010:8000" + networks: + - ag_cloud + + image-linker-jobmanager: + build: + context: ./services/image-linker + dockerfile: Dockerfile.flink + container_name: image-linker-jobmanager + command: jobmanager + ports: + - "8084:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=image-linker-jobmanager + - KAFKA_BROKERS=kafka:9092 + - CONFIG_PATH=/opt/app/config/topics.yaml + networks: + - ag_cloud + + image-linker-taskmanager: + build: + context: ./services/image-linker + dockerfile: Dockerfile.flink + container_name: image-linker-taskmanager + command: taskmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=image-linker-jobmanager + - KAFKA_BROKERS=kafka:9092 + - CONFIG_PATH=/opt/app/config/topics.yaml + depends_on: + image-linker-jobmanager: + condition: service_started + networks: + - ag_cloud + + image-linker-submitter: + build: + context: ./services/image-linker + dockerfile: Dockerfile.flink + container_name: image-linker-submit + depends_on: + image-linker-jobmanager: + condition: service_started + command: > + bash -lc "sleep 10 && + flink run -m image-linker-jobmanager:8081 -py /opt/app/job_linker.py && + echo 'Image-Linker job submitted successfully' && + sleep 1" + networks: + - ag_cloud + + flink-sounds-http-jobmanager: + build: + context: ./services/sounds_flink + dockerfile: Dockerfile + container_name: flink-sounds-http-jobmanager + command: jobmanager + ports: + - "8083:8081" + environment: + JOB_MANAGER_RPC_ADDRESS: flink-sounds-http-jobmanager + KAFKA_BROKERS: kafka:9092 + SOURCE_TOPIC: sound_new_sounds_connections + SINK_TOPIC: "" + GROUP_ID: flink-classifier-sounds + CLASSIFIER_HTTP_URL: http://sounds_classifier:8088/classify + DEFAULT_PARALLELISM: 2 + KAFKA_START: earliest + PYTHON: /opt/venv/bin/python + FLINK_PYTHON: /opt/venv/bin/python + networks: + - ag_cloud + + flink-sounds-http-taskmanager: + build: + context: ./services/sounds_flink + dockerfile: Dockerfile + container_name: flink-sounds-http-taskmanager + command: taskmanager + depends_on: + flink-sounds-http-jobmanager: + condition: service_started + environment: + JOB_MANAGER_RPC_ADDRESS: flink-sounds-http-jobmanager + PYTHON: /opt/venv/bin/python + FLINK_PYTHON: /opt/venv/bin/python + FLINK_PROPERTIES: |- + jobmanager.rpc.address: flink-sounds-http-jobmanager + taskmanager.numberOfTaskSlots: 2 + networks: + - ag_cloud + + flink-sounds-http-submit: + build: + context: ./services/sounds_flink + dockerfile: Dockerfile + container_name: flink-sounds-http-submit + depends_on: + flink-sounds-http-jobmanager: + condition: service_started + flink-sounds-http-taskmanager: + condition: service_started + command: + - /opt/flink/bin/flink + - run + - -d + - -m + - flink-sounds-http-jobmanager:8081 + - -Dpython.client.executable=/opt/venv/bin/python + - -Dpython.executable=/opt/venv/bin/python + - -py + - /opt/app/flink_job.py + environment: + JOB_MANAGER_RPC_ADDRESS: flink-sounds-http-jobmanager + KAFKA_BROKERS: kafka:9092 + SOURCE_TOPIC: sound_new_sounds_connections + SINK_TOPIC: "" + GROUP_ID: flink-classifier-sounds + CLASSIFIER_HTTP_URL: http://sounds_classifier:8088/classify + DEFAULT_PARALLELISM: 2 + KAFKA_START: earliest + PYTHON: /opt/venv/bin/python + FLINK_PYTHON: /opt/venv/bin/python + networks: + - ag_cloud + + # -------------------------- + # Sensor Anomaly Pro + Flink + # -------------------------- + sensor_anomaly_pro: + build: + context: ./services/sensorAnomalyPro/sensorAnomalyPro + dockerfile: Dockerfile + container_name: sensor-anomaly-pro + volumes: + - ./services/sensorAnomalyPro/sensorAnomalyPro/data:/app/data + - ./services/sensorAnomalyPro/sensorAnomalyPro/reports:/app/reports + environment: + - DATA_PATH=/app/data/plant_health_data.csv + command: > + python analyze_sensors.py + networks: + - ag_cloud + + jobmanager: + build: + context: ./services/sensorAnomalyPro + dockerfile: Dockerfile.flink + container_name: jobmanager + command: > + bash -c " + /docker-entrypoint.sh jobmanager & + echo '⏳ Waiting for Flink JobManager startup...' && + sleep 10 && + echo '🕓 Waiting for reports to be generated...' && + while [ ! -d /opt/app/sensorAnomalyPro/reports ] || [ -z \"$(ls -A /opt/app/sensorAnomalyPro/reports 2>/dev/null)\" ]; do + echo ' ↳ reports directory empty, waiting...'; + sleep 5; + done && + echo '✅ Reports ready, submitting Flink job...' && + flink run -m localhost:8091 -py /opt/app/sensorAnomalyPro/app.py && + tail -f /dev/null" + depends_on: + - sensor_anomaly_pro + - kafka + ports: + - "8091:8091" + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=sensors + - OUT_TOPIC=sensor_anomalies + - ZONE_TOPIC=sensor_zone_stats + volumes: + - ./services/sensorAnomalyPro/sensorAnomalyPro/reports:/opt/app/sensorAnomalyPro/reports:rw + restart: unless-stopped + networks: + - ag_cloud + + taskmanager: + build: + context: ./services/sensorAnomalyPro + dockerfile: Dockerfile.flink + container_name: taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + depends_on: + - jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=sensors + - OUT_TOPIC=sensor_anomalies + - ZONE_TOPIC=sensor_zone_stats + - taskmanager.numberOfTaskSlots=4 + volumes: + - ./services/sensorAnomalyPro/sensorAnomalyPro/reports:/opt/app/sensorAnomalyPro/reports:rw + restart: unless-stopped + networks: + - ag_cloud + + + + vector_service: + build: ./services/vector_service + container_name: vector_service + environment: + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=missions_user + - DB_PASS=pg123 + - DB_NAME=missions_db + ports: + - "8005:8000" + depends_on: + - postgres + networks: + - ag_cloud + + + +# -------------------------- +# SensorGuard - Flink Job for Sensor Health Monitoring +# --------------------------sensorguard-jobmanager: + sensorguard-jobmanager: + build: + context: ./services/sensorGuard + dockerfile: Dockerfile.flink + container_name: sensorguard-jobmanager + ports: + - "8081:8081" + command: > + bash -c " + /docker-entrypoint.sh jobmanager & + echo 'Waiting for Flink JobManager startup...' && + sleep 15 && + echo 'Submitting sensorGuard Flink job...' && + flink run -m localhost:8081 -py /opt/app/main.py && + tail -f /dev/null" + environment: + - JOB_MANAGER_RPC_ADDRESS=sensorguard-jobmanager + - KAFKA_BROKERS=kafka:9092 + - KAFKA_IN_TOPIC=sensors + - KAFKA_OUT_TOPIC=event_logs_sensors + - KAFKA_GROUP_ID=sensorguard-flink-pipeline + - DB_API_BASE=http://db_api_service:8001 + - DB_API_AUTH_MODE=service + - DB_API_SERVICE_NAME=sensorguard-flink + - DB_API_TOKEN_FILE=/opt/app/secrets/db_api_token + depends_on: + kafka: + condition: service_healthy + db_api_service: + condition: service_started + networks: + - ag_cloud + volumes: + - ./services/sensorGuard/secrets:/opt/app/secrets + restart: unless-stopped + sensorguard-taskmanager: + build: + context: ./services/sensorGuard + dockerfile: Dockerfile.flink + container_name: sensorguard-taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + depends_on: + - sensorguard-jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=sensorguard-jobmanager + - KAFKA_BROKERS=kafka:9092 + - KAFKA_IN_TOPIC=sensors + - KAFKA_OUT_TOPIC=event_logs_sensors + - taskmanager.numberOfTaskSlots=4 + - KAFKA_GROUP_ID=sensorguard-flink-pipeline + - DB_API_BASE=http://db_api_service:8001 + - DB_API_AUTH_MODE=service + - DB_API_SERVICE_NAME=sensorguard-flink + networks: + - ag_cloud + volumes: + - ./services/sensorGuard/secrets:/opt/app/secrets + restart: unless-stopped + + # -------------------------- + # Flink Air Processing + # -------------------------- + + air-jobmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: air-jobmanager + command: jobmanager + ports: + - "8085:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=air-jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=image.new.aerial + - KAFKA_GROUP_ID=flink-air-device-pipeline + networks: + - flink-net + - ag_cloud + + air-taskmanager: + build: + context: ./services/air + dockerfile: Dockerfile.flink + container_name: air-taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + depends_on: + air-jobmanager: + condition: service_started + infer-api: + condition: service_healthy + anomaly-api: + condition: service_healthy + segmentation-api: + condition: service_healthy + environment: + - JOB_MANAGER_RPC_ADDRESS=air-jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=image.new.aerial + - OUT_TOPIC_OBJECT=aerial_image_object_detections + - OUT_TOPIC_ANOMALY=aerial_image_anomaly_detections + - OUT_TOPIC_SEGMENTATION=aerial_image_segmentation + - taskmanager.numberOfTaskSlots=4 + - KAFKA_GROUP_ID=flink-air-device-pipeline + - SEGMENTATION_URL=http://segmentation-api:8500/infer + - INFER_URL=http://infer-api:8000/infer + - ANOMALY_URL=http://anomaly-api:8010/predict + - INFER_CONF=0.25 + - INFER_IOU=0.45 + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + networks: + - flink-net + - ag_cloud + restart: unless-stopped + + infer-api: + build: + context: ./services/air/object_detection_api + dockerfile: Dockerfile.infer + container_name: infer-api + environment: + - WEIGHTS_PATH=/app/object_detection_api.pt + volumes: + - ./services/air/object_detection_api/model/object_detection_api.pt:/app/object_detection_api.pt:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8000/health"] + interval: 10s + timeout: 3s + retries: 15 + networks: + - flink-net + - ag_cloud + + anomaly-api: + build: + context: ./services/air/anomaly_detection_api + dockerfile: Dockerfile.anomaly + container_name: anomaly-api + environment: + - MODEL_PATH=/app/models/anomaly_detection_api.pt + volumes: + - ./services/air/anomaly_detection_api/models:/app/models:ro + ports: + - "8020:8010" + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8010/health"] + interval: 10s + timeout: 3s + retries: 15 + networks: + - flink-net + - ag_cloud + + segmentation-api: + build: + context: ./services/air/segmentation_api + dockerfile: dockerfile.segmentation + container_name: segmentation-api + environment: + - MODEL_PATH=/app/model/segmentation_api.pth + ports: + - "8500:8500" + volumes: + - ./services/air/segmentation_api/model:/app/model:ro + - ./services/air/segmentation_api/certs:/usr/local/share/ca-certificates/netfree:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8500/health"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - flink-net + - ag_cloud + + air-submit: + build: + context: ./services/air + dockerfile: Dockerfile.flink + depends_on: + air-jobmanager: + condition: service_started + command: > + bash -c " + sleep 10 && + flink run -m air-jobmanager:8081 -py /opt/app/job.py + " + networks: + - flink-net + - ag_cloud + + + # -------------------------- + # Security + # -------------------------- + + media-proxy: + build: + context: ./services/security + dockerfile: agguard/app/Dockerfile + command: uvicorn agguard.app.media_proxy:app --host 0.0.0.0 --port 8080 + environment: + - MEDIA_AUTH_TOKEN=CHANGE_ME + - PYTHONPATH=/app + ports: + - "8089:8080" + networks: + - ag_cloud + + + security-flink-jobmanager: + build: + context: ./services/security + dockerfile: agguard/pipeline/Dockerfile + image: agguard-flink:latest + container_name: security-flink-jobmanager + + command: > + bash -c " + /docker-entrypoint.sh jobmanager & + echo 'Waiting for Kafka...'; + sleep 15 && + echo '🚀 Submitting Flink job...' && + flink run -py /opt/app/agguard/pipeline/flink_job.py + " + + + environment: + - JOB_MANAGER_RPC_ADDRESS=security-flink-jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=image_new_security_connections + - OUT_TOPIC=alerts + - heartbeat.timeout=180000 + - heartbeat.interval=30000 + volumes: + - ./services/security/agguard/app:/app/agguard/app + - ./services/security/agguard/core:/app/agguard/core + - ./services/security/agguard/specialists:/app/agguard/specialists + - ./services/security/agguard/pipeline:/app/agguard/pipeline + - ./services/security/agguard/adapters:/app/agguard/adapters + - ./services/security/agguard/media:/app/agguard/media + - ./services/security/configs:/app/configs:ro + + depends_on: + kafka: + condition: service_healthy + mqtt-router: + condition: service_healthy + networks: + - ag_cloud + + security-flink-taskmanager: + image: agguard-flink:latest + container_name: security-flink-taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + environment: + - JOB_MANAGER_RPC_ADDRESS=security-flink-jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=image_new_security_connections + - OUT_TOPIC=alerts + - taskmanager.numberOfTaskSlots=4 + - heartbeat.timeout=180000 + - heartbeat.interval=30000 + volumes: + - ./services/security/agguard/app:/app/agguard/app + - ./services/security/agguard/core:/app/agguard/core + - ./services/security/agguard/specialists:/app/agguard/specialists + - ./services/security/agguard/pipeline:/app/agguard/pipeline + - ./services/security/agguard/adapters:/app/agguard/adapters + - ./services/security/agguard/media:/app/agguard/media + - ./services/security/configs:/app/configs:ro + depends_on: + security-flink-jobmanager: + condition: service_started + networks: + - ag_cloud + + + + + + animal-classifier: + build: + context: ./services/security + dockerfile: agguard/specialists/animal_service/Dockerfile.animal-classifier + image: agguard-animal-classifier:latest + environment: + - PORT=50064 + - METRICS_PORT=8008 + - MODEL_PATH=/app/weights/yolov8n-cls.pt + - DEVICE=cpu + volumes: + - ./services/security/weights:/app/weights:ro + - ./services/security/agguard/specialists/animal_service:/app/agguard/specialists/animal_service + expose: + - "50064" + - "8008" + networks: + - ag_cloud + + + mega-detector: + build: + context: ./services/security + dockerfile: agguard/specialists/megadetector_service/Dockerfile.mega-detector + image: mega-detector:latest + environment: + - PORT=50063 + - METRICS_PORT=8007 + - MODEL_NAME=MDV5A + - CONF_THRESH=0.2 + - DEVICE=cpu + volumes: + - ./services/security/agguard/specialists/megadetector_service:/app/agguard/specialists/megadetector_service + + expose: + - "50063" + - "8007" + container_name: mega-detector + networks: + - ag_cloud + + + anomalies-classifier: + build: + context: ./services/security + dockerfile: agguard/specialists/anomalies_service/Dockerfile.anomalies-classifier + image: agguard-anomalies-classifier:latest + container_name: clip-classifier + environment: + - PORT=50062 + - METRICS_PORT=8011 + - DEVICE=cpu + - CLIP_MODEL=RN50 + - CLIP_PRETRAINED=openai + - CLIP_INPUT_SIZE=224 + - CLIP_TEMPERATURE=100.0 + - CLIP_BATCH=32 + - ENABLE_MKLDNN=1 + - NUM_THREADS=6 + expose: + - "50062" + networks: + - ag_cloud + + + + mask-classifier: + build: + context: ./services/security + dockerfile: agguard/specialists/mask_service/Dockerfile.mask-classifier + image: agguard-mask-classifier:latest + environment: + - PORT=50061 + - METRICS_PORT=8012 + - BACKEND=onnx + - MODEL_PATH=/app/weights/mask_yolov8.onnx + - CLASSES=no_mask,mask + - IMGSZ=224 + - DEVICE=cpu + volumes: + - ./services/security/weights:/app/weights:ro + expose: + - "50061" + networks: + - ag_cloud + + + crosssensor-flink-jobmanager: + build: + context: ./services/Cross-Sensor System-Level Anomalies + dockerfile: Dockerfile.flink + container_name: crosssensor-flink-jobmanager + command: jobmanager + ports: + - "8086:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=crosssensor-flink-jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=sensors + - OUT_TOPIC=sensors_anomalies_modal + volumes: + - ./services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml:/opt/flink/conf/flink-conf.yaml + - ./services/Cross-Sensor System-Level Anomalies/flink_job.py:/opt/app/flink_job.py + - ./services/Cross-Sensor System-Level Anomalies/models:/opt/models + depends_on: + kafka: + condition: service_healthy + networks: + - ag_cloud + restart: unless-stopped + + crosssensor-flink-taskmanager: + build: + context: ./services/Cross-Sensor System-Level Anomalies + dockerfile: Dockerfile.flink + container_name: crosssensor-flink-taskmanager + command: taskmanager + depends_on: + - crosssensor-flink-jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=crosssensor-flink-jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=sensors + - OUT_TOPIC=sensors_anomalies_modal + volumes: + - ./services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml:/opt/flink/conf/flink-conf.yaml + - ./services/Cross-Sensor System-Level Anomalies/flink_job.py:/opt/app/flink_job.py + - ./services/Cross-Sensor System-Level Anomalies/models:/opt/models + networks: + - ag_cloud + restart: unless-stopped diff --git a/AgCloud/grafana/dashboards/grafana-dashboard-multi.json b/AgCloud/grafana/dashboards/grafana-dashboard-multi.json new file mode 100644 index 000000000..b871c38b4 --- /dev/null +++ b/AgCloud/grafana/dashboards/grafana-dashboard-multi.json @@ -0,0 +1,58 @@ +{ + "id": null, + "title": "Postgres – Selected Metrics", + "tags": ["postgres", "prometheus"], + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "10s", + "panels": [ + { + "type": "timeseries", + "title": "WAL Throughput (bytes/s)", + "datasource": "Prometheus", + "targets": [ + { "expr": "rate(pg_wal_stats_wal_bytes[30m])", "legendFormat": "WAL bytes/s" } + ], + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "BRIN Hit Ratio by Index", + "datasource": "Prometheus", + "targets": [ + { "expr": "pg_brin_index_io_brin_hit_ratio", "legendFormat": "{{schemaname}}.{{index_name}}" } + ], + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "Transactions per Second (TPS)", + "datasource": "Prometheus", + "targets": [ + { "expr": "rate(pg_stat_database_xact_commit[1m]) + rate(pg_stat_database_xact_rollback[1m])", "legendFormat": "TPS" } + ], + "gridPos": { "x": 0, "y": 8, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "Cache Hit Ratio", + "datasource": "Prometheus", + "targets": [ + { "expr": "pg_stat_database_blks_hit / (pg_stat_database_blks_hit + pg_stat_database_blks_read)", "legendFormat": "Hit Ratio" } + ], + "gridPos": { "x": 12, "y": 8, "w": 12, "h": 8 } + }, + { + "type": "timeseries", + "title": "Active Connections", + "datasource": "Prometheus", + "targets": [ + { "expr": "pg_stat_activity_count{state=\"active\"}", "legendFormat": "Active" }, + { "expr": "pg_stat_activity_count{state=\"idle\"}", "legendFormat": "Idle" } + ], + "gridPos": { "x": 0, "y": 16, "w": 24, "h": 8 } + } + ] + } + \ No newline at end of file diff --git a/AgCloud/grafana/dashboards/grafana-dashboard-simple.json b/AgCloud/grafana/dashboards/grafana-dashboard-simple.json new file mode 100644 index 000000000..8dc444f1c --- /dev/null +++ b/AgCloud/grafana/dashboards/grafana-dashboard-simple.json @@ -0,0 +1,56 @@ +{ + "id": null, + "title": "Postgres – WAL, Replication, BRIN", + "tags": ["postgres", "prometheus"], + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "5s", + "panels": [ + { + "type": "graph", + "title": "WAL Throughput (bytes/s)", + "targets": [ + { + "expr": "rate(pg_wal_stats_wal_bytes[30s])", + "legendFormat": "WAL bytes/s" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "Replication Lag (bytes)", + "targets": [ + { + "expr": "pg_replication_lag_bytes_primary", + "legendFormat": "{{application_name}}" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "Replication Lag (seconds)", + "targets": [ + { + "expr": "pg_replication_replay_lag_seconds_standby", + "legendFormat": "standby lag" + } + ], + "datasource": "Prometheus" + }, + { + "type": "graph", + "title": "BRIN Hit Ratio by Index", + "targets": [ + { + "expr": "pg_brin_index_io_brin_hit_ratio", + "legendFormat": "{{schemaname}}.{{index_name}}" + } + ], + "datasource": "Prometheus" + } + ] + } + \ No newline at end of file diff --git a/AgCloud/grafana/dashboards/grafana-dashboard.json b/AgCloud/grafana/dashboards/grafana-dashboard.json new file mode 100644 index 000000000..7798a2c77 --- /dev/null +++ b/AgCloud/grafana/dashboards/grafana-dashboard.json @@ -0,0 +1,246 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Dashboard showing MinIO bucket usage and PUT request latency\n", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "minio_s3_requests_ttfb_seconds_distribution{api=\"putobject\"}", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "PUT latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "minio_cluster_usage_total_bytes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "bew9cih96fx8gb" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "minio_cluster_usage_total_bytes", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Bucket size", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "MinIO Monitoring", + "uid": "83c84814-bdf4-4a16-b3a0-b2cf9ae50de8", + "version": 1 +} \ No newline at end of file diff --git a/AgCloud/grafana/dashboards/leaf-disease-dashboard.json b/AgCloud/grafana/dashboards/leaf-disease-dashboard.json new file mode 100644 index 000000000..f7314e183 --- /dev/null +++ b/AgCloud/grafana/dashboards/leaf-disease-dashboard.json @@ -0,0 +1,211 @@ +{ + "id": null, + "uid": "leaf-disease-detail", + "title": "Leaf Disease Analysis", + "tags": ["leaf", "disease", "agriculture"], + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "10s", + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Total Cases (Selected Disease)", + "gridPos": { "h": 6, "w": 6, "x": 0, "y": 0 }, + "targets": [ + { + "expr": "leaf_reports_by_disease_count{disease_id=\"$disease_id\"}", + "refId": "A", + "legendFormat": "Cases" + } + ], + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "options": { + "colorMode": "background", + "graphMode": "area", + "orientation": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 50 }, + { "color": "orange", "value": 100 }, + { "color": "red", "value": 200 } + ] + } + } + } + }, + { + "id": 2, + "type": "timeseries", + "title": "Daily Disease Progression (Last 90 Days)", + "gridPos": { "h": 10, "w": 18, "x": 6, "y": 0 }, + "targets": [ + { + "expr": "leaf_disease_daily_progression_sick_count{disease_id=\"$disease_id\"}", + "refId": "A", + "legendFormat": "{{disease_name}}" + } + ], + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom", "showLegend": true }, + "tooltip": { "mode": "multi", "sort": "desc" } + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "smooth", + "fillOpacity": 30, + "lineWidth": 2, + "showPoints": "auto" + }, + "color": { "mode": "palette-classic" }, + "unit": "short" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Hourly Detection Rate (Last 7 Days)", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 10 }, + "targets": [ + { + "expr": "leaf_disease_hourly_detection_count{disease_id=\"$disease_id\"}", + "refId": "A", + "legendFormat": "Detections/hour" + } + ], + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "bars", + "fillOpacity": 80, + "lineWidth": 0 + }, + "color": { "mode": "palette-classic" }, + "unit": "short" + } + } + }, + { + "id": 4, + "type": "bargauge", + "title": "Disease Severity by Device (%)", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 10 }, + "targets": [ + { + "expr": "leaf_disease_severity_by_device_sick_percentage{disease_id=\"$disease_id\"}", + "refId": "A", + "legendFormat": "Device {{device_id}}" + } + ], + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "options": { + "orientation": "horizontal", + "displayMode": "gradient", + "showUnfilled": true + }, + "fieldConfig": { + "defaults": { + "min": 0, + "max": 100, + "unit": "percent", + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 20 }, + { "color": "orange", "value": 50 }, + { "color": "red", "value": 70 } + ] + } + } + } + }, + { + "id": 5, + "type": "table", + "title": "Device Statistics", + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 18 }, + "targets": [ + { + "expr": "leaf_reports_by_device_sick_reports", + "refId": "A", + "format": "table", + "instant": true + } + ], + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "options": { "showHeader": true }, + "fieldConfig": { + "defaults": { + "custom": { "align": "auto" } + } + }, + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { "Time": true, "job": true, "instance": true }, + "renameByName": { + "device_id": "Device ID", + "Value": "Sick Reports" + } + } + } + ] + }, + { + "id": 6, + "type": "piechart", + "title": "All Diseases Distribution", + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 18 }, + "targets": [ + { + "expr": "leaf_reports_by_disease_count", + "refId": "A", + "legendFormat": "{{disease_name}}" + } + ], + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "options": { + "legend": { "displayMode": "table", "placement": "right", "showLegend": true }, + "pieType": "donut" + }, + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" } + } + } + } + ], + "templating": { + "list": [ + { + "name": "disease_id", + "type": "query", + "datasource": { "type": "prometheus", "uid": "prometheus" }, + "query": "label_values(leaf_reports_by_disease_count, disease_id)", + "refresh": 1, + "regex": "", + "sort": 1 + } + ] + } +} \ No newline at end of file diff --git a/AgCloud/grafana/dashboards/security.json b/AgCloud/grafana/dashboards/security.json new file mode 100644 index 000000000..6e6e0b728 --- /dev/null +++ b/AgCloud/grafana/dashboards/security.json @@ -0,0 +1,254 @@ +{ + "id": null, + "uid": "security-models", + "title": "Security – Model Inference Overview", + "tags": ["agguard", "models", "metrics"], + "timezone": "browser", + "schemaVersion": 39, + "version": 11, + "refresh": "10s", + "panels": [ + { + "type": "timeseries", + "title": "📈 Inference Throughput (req/s)", + "targets": [ + { + "expr": "sum(rate(inference_requests_total{service=~\"$service\"}[1m])) by (service)", + "legendFormat": "{{service}}" + } + ], + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 7 }, + "fieldConfig": { + "defaults": { + "unit": "req/s", + "color": { "mode": "palette-classic" }, + "custom": { "drawStyle": "lines", "lineWidth": 2 } + } + }, + "options": { + "legend": { "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + } + }, + { + "type": "gauge", + "title": "⚙️ Model Load Time (s)", + "targets": [ + { + "expr": "model_load_seconds{service=~\"$service\"}", + "legendFormat": "{{service}}" + } + ], + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 7 }, + "fieldConfig": { + "defaults": { + "min": 0, + "unit": "s", + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "orange", "value": 5 }, + { "color": "red", "value": 10 } + ] + } + } + }, + "options": { + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, + "orientation": "horizontal", + "showThresholdLabels": false, + "showThresholdMarkers": true + } + }, + { + "type": "stat", + "title": "🧮 Total Inference Requests", + "targets": [ + { + "expr": "sum(inference_requests_total{service=~\"$service\"})", + "legendFormat": "Total" + } + ], + "gridPos": { "x": 0, "y": 7, "w": 12, "h": 6 }, + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "orange", "value": 100 }, + { "color": "red", "value": 1000 } + ] + } + } + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "horizontal", + "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } + } + }, + { + "type": "timeseries", + "title": "🚨 Inference Error Rate (%)", + "targets": [ + { + "expr": "100 * sum(rate(inference_errors_total{service=~\"$service\"}[5m])) by (service) / sum(rate(inference_requests_total{service=~\"$service\"}[5m])) by (service)", + "legendFormat": "{{service}}" + } + ], + "gridPos": { "x": 12, "y": 7, "w": 12, "h": 6 }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { "mode": "continuous-GrYlRd" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "orange", "value": 1 }, + { "color": "red", "value": 5 } + ] + } + } + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + } + }, + { + "type": "timeseries", + "title": "⏱ Inference Latency (p50 & p90)", + "targets": [ + { + "expr": "histogram_quantile(0.5, sum(rate(inference_latency_seconds_bucket{service=~\"$service\"}[5m])) by (le, service))", + "legendFormat": "{{service}} p50" + }, + { + "expr": "histogram_quantile(0.9, sum(rate(inference_latency_seconds_bucket{service=~\"$service\"}[5m])) by (le, service))", + "legendFormat": "{{service}} p90" + } + ], + "gridPos": { "x": 0, "y": 13, "w": 24, "h": 7 }, + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { "mode": "palette-classic" }, + "custom": { "drawStyle": "lines", "lineWidth": 2 } + } + }, + "options": { + "legend": { "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + } + }, + { + "type": "timeseries", + "title": "🖥️ System & Process CPU Usage (%)", + "targets": [ + { + "expr": "system_cpu_usage_percent", + "legendFormat": "System CPU %" + }, + { + "expr": "process_cpu_usage_percent{service=~\"$service\"}", + "legendFormat": "{{service}} CPU %" + } + ], + "gridPos": { "x": 0, "y": 20, "w": 12, "h": 6 }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { "mode": "palette-classic" }, + "custom": { "drawStyle": "lines", "lineWidth": 2 } + } + }, + "options": { + "legend": { "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + } + }, + { + "type": "timeseries", + "title": "💾 Process Memory Usage (MB)", + "targets": [ + { + "expr": "process_memory_megabytes{service=~\"$service\"}", + "legendFormat": "{{service}} memory" + } + ], + "gridPos": { "x": 12, "y": 20, "w": 12, "h": 6 }, + "fieldConfig": { + "defaults": { + "unit": "megabytes", + "color": { "mode": "palette-classic" }, + "custom": { "drawStyle": "lines", "lineWidth": 2 } + } + }, + "options": { + "legend": { "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + } + }, + { + "type": "timeseries", + "title": "🎮 GPU Utilization & Memory (MB)", + "targets": [ + { + "expr": "gpu_utilization_percent{service=~\"$service\"}", + "legendFormat": "GPU {{gpu_id}} util %" + }, + { + "expr": "gpu_memory_used_megabytes{service=~\"$service\"}", + "legendFormat": "GPU {{gpu_id}} used" + }, + { + "expr": "gpu_memory_total_megabytes{service=~\"$service\"}", + "legendFormat": "GPU {{gpu_id}} total" + } + ], + "gridPos": { "x": 0, "y": 26, "w": 24, "h": 7 }, + "fieldConfig": { + "defaults": { + "unit": "short", + "color": { "mode": "palette-classic" }, + "custom": { "drawStyle": "lines", "lineWidth": 2 } + } + }, + "options": { + "legend": { "displayMode": "table", "placement": "bottom" }, + "tooltip": { "mode": "multi" } + } + } + ], + "templating": { + "list": [ + { + "name": "service", + "type": "query", + "datasource": "Prometheus", + "query": "label_values({service!=\"\"}, service)", + "refresh": 1, + "includeAll": true, + "multi": true, + "allValue": ".*", + "sort": 1, + "label": "Model Service" + } + ] + } + + + , + "time": { "from": "now-1h", "to": "now" }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h"] + } +} diff --git a/AgCloud/grafana/dashboards/sensors.json b/AgCloud/grafana/dashboards/sensors.json new file mode 100644 index 000000000..0546e5d2f --- /dev/null +++ b/AgCloud/grafana/dashboards/sensors.json @@ -0,0 +1,29 @@ +{ + "uid": "agcloud-sensors", + "title": "Sensors", + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "10s", + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Active Sensors", + "gridPos": { "h": 6, "w": 8, "x": 0, "y": 0 }, + "targets": [ + { "expr": "sum(sensor_status)", "instant": true, "refId": "A" } + ] + }, + { + "id": 2, + "type": "bargauge", + "title": "Sensor Status (1=active, 0=inactive)", + "gridPos": { "h": 10, "w": 16, "x": 8, "y": 0 }, + "options": { "displayMode": "lcd", "orientation": "horizontal" }, + "targets": [ + { "expr": "sensor_status", "instant": true, "refId": "A", "legendFormat": "{{sensor}}" } + ] + } + ] +} diff --git a/AgCloud/grafana/dashboards/sound_dashboard.json b/AgCloud/grafana/dashboards/sound_dashboard.json new file mode 100644 index 000000000..3f78e3ac8 --- /dev/null +++ b/AgCloud/grafana/dashboards/sound_dashboard.json @@ -0,0 +1,62 @@ +{ + "id": null, + "uid": "sound-combined", + "title": "Sound – Combined (Real MinIO Metrics)", + "tags": ["sound", "minio", "exporter"], + "timezone": "browser", + "schemaVersion": 38, + "version": 1, + "refresh": "10s", + "templating": { + "list": [ + { + "name": "mic_id", + "type": "query", + "datasource": "Prometheus", + "query": "label_values(sound_avg_volume, mic_id)", + "includeAll": true, + "multi": true, + "refresh": 1, + "current": {} + } + ] + }, + "panels": [ + { + "type": "timeseries", + "title": "Average RMS (5m window)", + "targets": [{ "expr": "sound_avg_volume{mic_id=~\"$mic_id\"}" , "legendFormat": "{{mic_id}}" }], + "options": { "noDataOptions": { "noValue": "hide" } }, + "gridPos": { "x": 0, "y": 0, "w": 12, "h": 7 } + }, + { + "type": "timeseries", + "title": "STD of RMS (5m window)", + "targets": [{ "expr": "sound_std_volume{mic_id=~\"$mic_id\"}", "legendFormat": "{{mic_id}}" }], + "options": { "noDataOptions": { "noValue": "hide" } }, + "gridPos": { "x": 12, "y": 0, "w": 12, "h": 7 } + }, + { + "type": "timeseries", + "title": "Uptime Ratio (0..1)", + "targets": [{ "expr": "sound_mic_uptime_ratio{mic_id=~\"$mic_id\"}", "legendFormat": "{{mic_id}}" }], + "options": { "noDataOptions": { "noValue": "hide" } }, + "gridPos": { "x": 0, "y": 7, "w": 12, "h": 7 } + }, + { + "type": "timeseries", + "title": "Uptime (seconds, derived)", + "targets": [{ "expr": "mic_uptime_seconds{mic_id=~\"$mic_id\"}", "legendFormat": "{{mic_id}}" }], + "options": { "noDataOptions": { "noValue": "hide" } }, + "gridPos": { "x": 12, "y": 7, "w": 12, "h": 7 } + }, + { + "type": "timeseries", + "title": "Volume (dB, derived from RMS)", + "targets": [{ "expr": "sound_volume_db{mic_id=~\"$mic_id\"}", "legendFormat": "{{mic_id}}" }], + "options": { "noDataOptions": { "noValue": "hide" } }, + "gridPos": { "x": 0, "y": 14, "w": 24, "h": 7 } + } + ] + } + \ No newline at end of file diff --git a/AgCloud/grafana/provisioning/dashboards/dashboards.yaml b/AgCloud/grafana/provisioning/dashboards/dashboards.yaml new file mode 100644 index 000000000..6300c162c --- /dev/null +++ b/AgCloud/grafana/provisioning/dashboards/dashboards.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 +providers: + - name: 'agcloud-dashboards' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards diff --git a/AgCloud/grafana/provisioning/datasources/prometheus.yaml b/AgCloud/grafana/provisioning/datasources/prometheus.yaml new file mode 100644 index 000000000..c6229c97c --- /dev/null +++ b/AgCloud/grafana/provisioning/datasources/prometheus.yaml @@ -0,0 +1,10 @@ +apiVersion: 1 +datasources: + - name: Prometheus + uid: prometheus + type: prometheus + access: proxy + isDefault: true + url: http://prometheus:9090 + jsonData: + httpMethod: GET diff --git a/AgCloud/grafana/sensors-to-pushgateway.ps1 b/AgCloud/grafana/sensors-to-pushgateway.ps1 new file mode 100644 index 000000000..2a87b1623 --- /dev/null +++ b/AgCloud/grafana/sensors-to-pushgateway.ps1 @@ -0,0 +1,36 @@ +# =============================================== +# Push local sensor JSON metrics to Pushgateway +# =============================================== + +# URL of Prometheus Pushgateway +$PushUrl = "http://pushgateway:9091/metrics/job/local_sensors" + +# Use relative path inside the repo or container (cross-platform) +# Example: if script is under grafana/, look for ./local_sensors/ +$BaseDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$SensorDir = Join-Path $BaseDir "local_sensors" + +Write-Host "Monitoring folder: $SensorDir" +Write-Host "Pushing metrics to: $PushUrl" + +while ($true) { + Get-ChildItem -Path $SensorDir -Filter "*.json" | ForEach-Object { + try { + $data = Get-Content $_.FullName | ConvertFrom-Json + $mic = $data.mic_id + $body = @" +sound_volume_db{mic_id="$mic"} $($data.volume_db) +classifier_rate{mic_id="$mic"} $($data.classifier_rate) +mic_uptime_seconds{mic_id="$mic"} $($data.uptime_sec) +anomaly_count{mic_id="$mic"} $($data.anomaly_count) +"@ + Invoke-RestMethod -Uri "$PushUrl/instance/$mic" -Method Put -Body ($body + "`n") -ContentType "text/plain" + Write-Host "✅ Pushed metrics for $mic" + } + catch { + Write-Warning "⚠️ Failed for file $($_.Name): $_" + } + } + + Start-Sleep -Seconds 5 +} diff --git a/AgCloud/grafana/simulate-sound-metrics.ps1 b/AgCloud/grafana/simulate-sound-metrics.ps1 new file mode 100644 index 000000000..769a954bf --- /dev/null +++ b/AgCloud/grafana/simulate-sound-metrics.ps1 @@ -0,0 +1,33 @@ +$job = "sound_dashboard" +$instance = "mic-001" + +$volume = 40 +$rate = 0.8 +$uptime = 0 +$anomalies = 0 + +while ($true) { + $volume += Get-Random -Minimum -3 -Maximum 3 + $rate += (Get-Random -Minimum -0.02 -Maximum 0.02) + $uptime += 5 + if ((Get-Random -Minimum 0 -Maximum 10) -gt 8) { $anomalies++ } + + if ($volume -lt 20) { $volume = 20 } + elseif ($volume -gt 90) { $volume = 90 } + + if ($rate -lt 0.5) { $rate = 0.5 } + elseif ($rate -gt 1.0) { $rate = 1.0 } + + $body = @" +sound_volume_db $volume +classifier_rate $rate +mic_uptime_seconds $uptime +app_anomaly_total $anomalies +"@ + + Invoke-RestMethod -Uri "http://pushgateway:9091/metrics/job/$job/instance/$instance" ` + -Method Put -Body ($body + "`n") -ContentType "text/plain" + + Write-Host "Pushed: V=$volume, R=$rate, U=$uptime, A=$anomalies" + Start-Sleep -Seconds 5 +} diff --git a/AgCloud/mqtt_and_kafka/.dockerignore b/AgCloud/mqtt_and_kafka/.dockerignore new file mode 100644 index 000000000..d9aad42f9 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/.dockerignore @@ -0,0 +1,20 @@ +** + +!docker-compose.yml +!docker-compose.override.yml +!kafka/** +!connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/** + +!Dockerfile +!values-kafka.yaml +!create-topics.sh +!ca/** + +!.gitignore +!.gitattributes +!.dockerignore +!.env + +data/ +logs/ +tmp/ \ No newline at end of file diff --git a/AgCloud/mqtt_and_kafka/.gitattributes b/AgCloud/mqtt_and_kafka/.gitattributes new file mode 100644 index 000000000..526c8a38d --- /dev/null +++ b/AgCloud/mqtt_and_kafka/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf \ No newline at end of file diff --git a/AgCloud/mqtt_and_kafka/.gitignore b/AgCloud/mqtt_and_kafka/.gitignore new file mode 100644 index 000000000..7c60622af --- /dev/null +++ b/AgCloud/mqtt_and_kafka/.gitignore @@ -0,0 +1,24 @@ +# Secrets & certs +*.key +*.csr +*.srl +*.crt + +# Runtime junk +data/ +logs/ +tmp/ +*.log +*.pid +.DS_Store +Thumbs.db +*.tar +*.zip +*.tgz +__pycache__/ + +# Ignore all .jar files +*.jar + +# Allow .jar files under lib/ directory +!connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/*.jar \ No newline at end of file diff --git a/AgCloud/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py b/AgCloud/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py new file mode 100644 index 000000000..b482705a8 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py @@ -0,0 +1,321 @@ +import csv +import json +import os +from datetime import datetime, timedelta, timezone +from collections import deque +from confluent_kafka import Consumer + +# ---------- CONFIG ---------- +TOPIC = 'summaries.5m' +BOOTSTRAP = 'kafka:9092' +GROUP = 'daily-aggregator' + +CSV_5MIN = 'aggregated_5min.csv' +CSV_DAILY = 'aggregated_daily.csv' + +# intervals +INTERVAL_5MIN = 5 # produce 5-min rows every 5 minutes +INTERVAL_DAILY = 15 # read last 15 minutes from 5-min CSV and write daily summary + +# ---------- Kafka consumer ---------- +conf = { + 'bootstrap.servers': BOOTSTRAP, + 'group.id': GROUP, + 'auto.offset.reset': 'earliest' +} +consumer = Consumer(conf) +consumer.subscribe([TOPIC]) + +# ---------- helpers ---------- +def now_utc(): + return datetime.now(timezone.utc) + +def iso_to_dt(s: str) -> datetime: + """Parse ISO timestamp; accept trailing Z or offset.""" + if not s: + return None + if s.endswith('Z'): + s = s[:-1] + '+00:00' + return datetime.fromisoformat(s) + +def write_csv_row(path, row, fieldnames): + new_file = not os.path.exists(path) or os.path.getsize(path) == 0 + with open(path, 'a', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + if new_file: + writer.writeheader() + writer.writerow(row) + +def read_5min_rows_between(cutoff_start_dt, cutoff_end_dt): + """Read aggregated_5min.csv and return list of parsed rows whose window_end is within [cutoff_start_dt, cutoff_end_dt].""" + rows = [] + if not os.path.exists(CSV_5MIN): + return rows + with open(CSV_5MIN, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + for r in reader: + # try to parse window_start/window_end; tolerate JSON mic_ids + try: + we = iso_to_dt(r.get('window_end') or r.get('window_end_iso') or '') + except Exception: + we = None + if we is None: + continue + # include row if window_end is inside window (alternatively could check overlap) + if cutoff_start_dt <= we <= cutoff_end_dt: + try: + avg_v = float(r.get('avg_volume_db') or 0.0) + except Exception: + avg_v = 0.0 + try: + max_v = float(r.get('max_volume_db') or 0.0) + except Exception: + max_v = 0.0 + try: + anom = int(r.get('anomaly_count') or r.get('total_anomaly_count') or 0) + except Exception: + anom = 0 + mic_ids_raw = r.get('mic_id') or r.get('mic_ids') or '' + # try parse JSON list, otherwise keep as string + mic_ids = None + try: + mic_ids = json.loads(mic_ids_raw) + except Exception: + mic_ids = [x.strip() for x in mic_ids_raw.split(',') if x.strip()] + rows.append({ + "window_start": r.get('window_start'), + "window_end": r.get('window_end'), + "avg_volume_db": avg_v, + "max_volume_db": max_v, + "anomaly_count": anom, + "mic_ids": mic_ids, + "raw": r + }) + return rows + +# ---------- runtime accumulators for 5-min summary ---------- +# we'll accumulate incoming messages and produce 5-min summary from these (not from file). +accum = { + "count": 0, + "sum_avg": 0.0, + "max_v": None, + "total_anom": 0, + "mic_ids_seen": [] +} +window_5min_start = now_utc() +next_5min_write = window_5min_start + timedelta(minutes=INTERVAL_5MIN) + +# prepare daily schedule +window_daily_next = now_utc() + timedelta(minutes=INTERVAL_DAILY) + +print("📥 Listening for messages... (CTRL+C to stop)") + +try: + while True: + msg = consumer.poll(1.0) + now = now_utc() + + # --- handle incoming kafka message --- + if msg is not None: + if msg.error(): + print("Kafka error:", msg.error()) + else: + try: + payload = json.loads(msg.value().decode('utf-8')) + except Exception as e: + print("JSON decode error, skipping:", e) + payload = None + + if payload: + metrics = payload.get('metrics', {}) + try: + avg_v = float(metrics.get('avg_volume_db', 0.0)) + max_v = float(metrics.get('max_volume_db', avg_v)) + anom = int(metrics.get('anomaly_count', 0)) + except Exception: + # skip malformed metric + continue + + mic_id = payload.get('mic_id') + + # update 5-min accumulators + accum["count"] += 1 + accum["sum_avg"] += avg_v + accum["max_v"] = max_v if accum["max_v"] is None else max(accum["max_v"], max_v) + accum["total_anom"] += anom + if mic_id and mic_id not in accum["mic_ids_seen"]: + accum["mic_ids_seen"].append(mic_id) + + # --- produce 5-min CSV row when it's time --- + if now >= next_5min_write: + window_end = now + window_start = window_5min_start + + if accum["count"] > 0: + row_5 = { + "mic_id": json.dumps(accum["mic_ids_seen"], ensure_ascii=False), + "window_start": window_start.isoformat(), + "window_end": window_end.isoformat(), + "avg_volume_db": round(accum["sum_avg"] / accum["count"], 2), + "max_volume_db": accum["max_v"], + "anomaly_count": accum["total_anom"] + } + else: + # no samples in this 5-min window + row_5 = { + "mic_id": json.dumps([]), + "window_start": window_start.isoformat(), + "window_end": window_end.isoformat(), + "avg_volume_db": "", + "max_volume_db": "", + "anomaly_count": 0 + } + + write_csv_row(CSV_5MIN, row_5, fieldnames=list(row_5.keys())) + print("Wrote 5-min row:", row_5) + + # reset accumulators for next 5-min block + accum = {"count": 0, "sum_avg": 0.0, "max_v": None, "total_anom": 0, "mic_ids_seen": []} + window_5min_start = now + next_5min_write = now + timedelta(minutes=INTERVAL_5MIN) + + # --- every INTERVAL_DAILY minutes: read last INTERVAL_DAILY minutes from CSV_5MIN and write to CSV_DAILY --- + if now >= window_daily_next: + import time + time.sleep(1) + + cutoff_start = now - timedelta(minutes=INTERVAL_DAILY) + cutoff_end = now + rows = read_5min_rows_between(cutoff_start, cutoff_end) + + if rows: + samples = len(rows) + avg_of_avgs = round(sum(r["avg_volume_db"] for r in rows) / samples, 2) + max_of_max = max(r["max_volume_db"] for r in rows) + total_anom = sum(r["anomaly_count"] for r in rows) + # union mic ids preserving order + seen = [] + for r in rows: + for m in (r["mic_ids"] or []): + if m not in seen: + seen.append(m) + mic_ids_out = seen + else: + samples = 0 + avg_of_avgs = "" + max_of_max = "" + total_anom = 0 + mic_ids_out = [] + + daily_row = { + "window_start": cutoff_start.isoformat(), + "window_end": cutoff_end.isoformat(), + "mic_ids": json.dumps(mic_ids_out, ensure_ascii=False), + "samples_5min_rows": samples, + "avg_of_avg_volume_db": avg_of_avgs, + "max_volume_db": max_of_max, + "total_anomaly_count": total_anom + } + write_csv_row(CSV_DAILY, daily_row, fieldnames=list(daily_row.keys())) + print("Wrote 15-min daily summary (from 5-min CSV):", daily_row) + + window_daily_next = now + timedelta(minutes=INTERVAL_DAILY) + +except KeyboardInterrupt: + print("Stopping consumer...") + +finally: + consumer.close() + print("Consumer closed.") + + +# import csv +# import json +# from datetime import datetime, timedelta +# from confluent_kafka import Consumer + +# # Kafka consumer configuration +# conf = { +# 'bootstrap.servers': 'kafka:9092', +# 'group.id': 'daily-aggregator', +# 'auto.offset.reset': 'earliest' +# } + +# consumer = Consumer(conf) +# consumer.subscribe(['summaries.5m']) + +# # CSV file paths +# csv_5min = 'aggregated_5min.csv' +# csv_daily = 'aggregated_daily.csv' + +# # Window settings +# window_5min_start = datetime.utcnow() +# window_daily_start = datetime.utcnow() +# results_5min = [] + +# print("📥 Listening for messages... (CTRL+C to stop)") + +# try: +# while True: +# msg = consumer.poll(5.0) +# if msg is None: +# continue +# if msg.error(): +# print("Error:", msg.error()) +# continue + +# # Decode message +# data = json.loads(msg.value().decode('utf-8')) + +# # Append raw metrics to results list +# results_5min.append(data) + +# now = datetime.utcnow() + +# # Write to 5-min CSV if 5 minutes passed +# if now - window_5min_start >= timedelta(minutes=5): +# if results_5min: +# avg_volume = sum(r['metrics']['avg_volume_db'] for r in results_5min) / len(results_5min) +# max_volume = max(r['metrics']['max_volume_db'] for r in results_5min) +# total_anomalies = sum(r['metrics']['anomaly_count'] for r in results_5min) + +# result = { +# "mic_id": ", ".join(set(r["mic_id"] for r in results_5min)), +# "window_start": window_5min_start.isoformat(), +# "window_end": now.isoformat(), +# "avg_volume_db": round(avg_volume, 2), +# "max_volume_db": max_volume, +# "anomaly_count": total_anomalies +# } + +# # Write to CSV +# with open(csv_5min, mode='a', newline='') as f: +# writer = csv.DictWriter(f, fieldnames=result.keys()) +# if f.tell() == 0: +# writer.writeheader() +# writer.writerow(result) + +# print("5-min summary written:", result) + +# results_5min = [] +# window_5min_start = now + +# # Write daily summary every 15 minutes (testing) +# if now - window_daily_start >= timedelta(minutes=15): +# # פה אפשר להכניס חישוב יומי אמיתי, לדוגמה ממוצע כל מה שנאסף +# daily_summary = result # בדוגמה הזאת, פשוט חוזר על התוצאה האחרונה +# with open(csv_daily, mode='a', newline='') as f: +# writer = csv.DictWriter(f, fieldnames=daily_summary.keys()) +# if f.tell() == 0: +# writer.writeheader() +# writer.writerow(daily_summary) + +# print("Daily summary written:", daily_summary) +# window_daily_start = now + +# except KeyboardInterrupt: +# print("Stopping consumer...") + +# finally: +# consumer.close() +# print("Consumer closed.") diff --git a/AgCloud/mqtt_and_kafka/Batch_reprocessing/send_messages.py b/AgCloud/mqtt_and_kafka/Batch_reprocessing/send_messages.py new file mode 100644 index 000000000..13030df86 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Batch_reprocessing/send_messages.py @@ -0,0 +1,26 @@ +from confluent_kafka import Producer +import json +import time +import random + +producer = Producer({'bootstrap.servers': 'kafka:9092'}) + +try: + while True: + msg = { + "mic_id": f"mic-{random.randint(1,3)}", + "window_start": "2025-09-29T00:00:00Z", + "window_end": "2025-09-29T00:05:00Z", + "metrics": { + "avg_volume_db": round(random.uniform(-30, 0), 1), + "max_volume_db": round(random.uniform(-10, 0), 1), + "anomaly_count": random.randint(0, 5) + } + } + producer.produce('summaries.5m', json.dumps(msg).encode('utf-8')) + producer.flush() + print("Sent message:", msg) + time.sleep(1) + +except KeyboardInterrupt: + print("Stopped sending messages.") diff --git a/AgCloud/mqtt_and_kafka/README.md b/AgCloud/mqtt_and_kafka/README.md new file mode 100644 index 000000000..ee83d5da5 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/README.md @@ -0,0 +1,231 @@ +# AgCloud-telemetrys + +## AgCloud – End-to-End MQTT → Kafka (Quickstart) + +This guide shows how to: +make the MQTT connector plugin available to Kafka Connect, +run the end-to-end test with full logs, +publish a test MQTT message and verify it lands in Kafka. +Works on Windows (PowerShell) with WSL, or on Linux/Git Bash. + +### Prerequisites + +Docker Desktop + Compose +WSL (Windows) or any Bash shell +Images will be pulled automatically on first run. +Project layout (relevant bits): +connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/... +connectors/mqtt-source.json # maps mqtt/# -> raw.mqtt.in +run_all.sh +docker-compose.yml + +### Make the MQTT Connector plugin available + +#### Permanent (recommended): Compose volume mapping + +Bring up the stack: + +```PowerShell +docker compose up -d connect +``` + +Check that the plugin is registered: + +```PowerShell +curl -s http://localhost:8083/connector-plugins | Select-String mqtt +``` + +Expected output includes: + +```PowerShell +"io.confluent.connect.mqtt.MqttSourceConnector" +``` + +### Run the full E2E flow with logs + +This runs the whole stack, waits for health, ensures the connector, publishes a test message, and consumes from Kafka. +It also saves a timestamped log file and keeps the window open so you can read errors. + +#### Windows PowerShell + +```PowerShell +wsl bash -lc 'set -x; ./run_all.sh 2>&1 | tee run_all.$(date +%Y%m%d_%H%M%S).log; read -rp "Press Enter to close..." _' +``` + +#### Linux / Git Bash + +```PowerShell +bash -x ./run_all.sh 2>&1 | tee run_all.$(date +%Y%m%d_%H%M%S).log +``` + +You should see lines like: +==> Checking connector plugins ... +MQTT plugin detected. +... +mqtt-source is RUNNING. +==> Publishing test MQTT message ... +==> Consuming from Kafka (kcat) ... +If it hangs on “Consuming from Kafka (kcat) …”, publish a message (next step). + +### Publish a test MQTT message (in a second window) + +Open another terminal and run: + +```PowerShell +docker exec -it mosquitto \ + mosquitto_pub -h mosquitto -p 1883 -t mqtt/test -m '{"hello":"world"}' +``` + +Go back to the first window and confirm a record was consumed. + +### Clean up + +```PowerShell +docker compose down -v +``` + +This stops and removes containers, network, and volumes. + +## Kafka Single – Kafka in Docker + +This project provides a simple Dockerfile to run Kafka (Bitnami image, single broker with topics and smoke test). + +### Run Instructions + +Run the project with the following commands: + +```bash +docker build -t kafka-single:local . + +docker run -d --name kafka-single -p 29092:9094 -p 9092:9092 --env-file .\kafka-files\kafka.env.example kafka-single:local + +docker logs --tail 200 kafka-single + +docker rm -f kafka-single +``` + +### Notes - kafka + +- The first command builds the image. + +- The second command runs the container. + +- The third command shows the logs – wait a few minutes before running it. If you don’t see ✅ or ❌, run the command again. + +- The fourth command removes the container. +If the container is running and you want to reset it, use this command. + +## AgCloud - Data Simulator + +This project provides a CLI tool that replays telemetry and sensor payloads from **CSV/Parquet** files at a fixed **QPS** to **MQTT** and/or **Kafka**. +It is used to validate throughput, stability, and reliability of the messaging stack. + +### Features + +- Publish messages to **MQTT**, **Kafka**, or both. +- Input from **CSV** or **Parquet** (Parquet recommended for high QPS). +- Metrics: sent / acked / lost, latency (p50/p95/p99), jitter. +- Stability profile: 60s @ 1k msg/s. +- Performance profile: 15m @ 10k msg/s. +- KPI: message loss ≤ **0.5%**. + +### Install + +```bash +python -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +``` + +### Quick Examples + +#### 1 Publish to both (MQTT + Kafka) + +```bash +python data_simulator.py --qps 100 --duration 30 --out both --file data/sample.csv --mqtt-host 127.0.0.1 --mqtt-port 1883 --mqtt-topic telemetry --kafka-bootstrap localhost:29092 --kafka-topic dev-robot-telemetry-raw +``` + +#### 2 MQTT only + +```bash +python data_simulator.py --qps 5 --duration 10 --out mqtt --file data/sample.csv --mqtt-host 127.0.0.1 --mqtt-port 1883 --mqtt-topic telemetry +``` + +#### 3 Kafka only + +```bash +python data_simulator.py --qps 5 --duration 10 --out kafka --file data/sample.csv --kafka-bootstrap localhost:29092 --kafka-topic dev-robot-telemetry-raw +``` + +#### 4 Stability profile (60s @ 1k msg/s) + +```bash +python data_simulator.py --stability --out kafka --file data/sample.csv --kafka-bootstrap localhost:29092 --kafka-topic dev-robot-telemetry-raw +``` + +#### 5 Performance profile (15m @ 10k msg/s) + +> Parquet is recommended for faster reads: + +```python +import pandas as pd; pd.read_csv("sample.csv").to_parquet("sample.parquet") +``` + +```bash +python data_simulator.py --perf --out kafka --file data/sample.parquet --kafka-bootstrap localhost:29092 --kafka-topic dev-robot-telemetry-raw +``` + +### Simulator CLI options + +- `--file ` + **Required**. Path to input CSV file. Must exist under `data/` (e.g. `data/sample.csv`). + +- `--qps ` + Optional. Messages per second to produce. Default: `1.0`. + +- `--duration ` + Optional. How long to run (in seconds). Default: `10`. + +- `--out ` + Optional. Where to send the records. Default: `kafka`. + +- `--loop` + Optional flag (no value). If set, replay file in a loop. Default: disabled. + +- `--bootstrap ` + Optional. Kafka bootstrap servers. Default: `localhost:29092`. + +- `--topic ` + Optional. Kafka topic name. Default: `dev-robot-telemetry-raw`. + +### Example Output + +After running a short test: + +```bash +python data_simulator.py --qps 5 --duration 5 --out both --file data/sample.csv --mqtt-host 127.0.0.1 --mqtt-port 1883 --mqtt-topic telemetry --kafka-bootstrap localhost:29092 --kafka-topic dev-robot-telemetry-raw +``` + +You may see a summary like: + +```bash +[summary] + total: sent=5 runtime=1.81s qps_avg≈2.76 + jitter (std of inter-arrival): 0.000782s + kafka: sent=5 acked=5 lost=0 p50/p95/p99=201.0ms/561.9ms/594.1ms + mqtt : sent=5 acked=5 lost=0 p50/p95/p99=3.8ms/5.3ms/5.5ms + kafka loss: 0.000% + mqtt loss: 0.000% +``` + +### Notes - data simulator + +- KPI target: **loss ≤ 0.5%** (PASS). +- CSV/Parquet input is required (`--file`). Install `pyarrow` or `fastparquet` for Parquet. +- Make sure your brokers are reachable (e.g., Mosquitto on 1883; Kafka advertised at 9094). + +## MQTT→Kafka Soak (Docker Compose) + +- Trigger: Actions → "Soak Test (MQTT to Kafka Bridge)" → Run workflow. +- Load: 150s @ 1000 msg/s to MQTT topic `soak/test`, consumed from Kafka topic `dev-robot-telemetry-raw`. +- Results: Check "MQTT to Kafka Soak Test Results" and download artifacts (logs, junit.xml, bridge.json). +- Pass/Fail: Job fails if loss_pct > 1.0%. diff --git a/AgCloud/mqtt_and_kafka/Sensor_edge_device/Crop_recommendationV2.csv b/AgCloud/mqtt_and_kafka/Sensor_edge_device/Crop_recommendationV2.csv new file mode 100644 index 000000000..3de7ccc07 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Sensor_edge_device/Crop_recommendationV2.csv @@ -0,0 +1,2201 @@ +N,P,K,temperature,humidity,ph,rainfall,label,soil_moisture,soil_type,sunlight_exposure,wind_speed,co2_concentration,organic_matter,irrigation_frequency,crop_density,pest_pressure,fertilizer_usage,growth_stage,urban_area_proximity,water_source_type,frost_risk,water_usage_efficiency +90,42,43,20.87974371,82.00274423,6.502985292,202.9355362,rice,29.44606392482905,2,8.67735526697563,10.109875244575115,435.61122566407204,3.121394502259622,4,11.743910096203432,57.60730813596583,188.19495775411988,1,2.7196142681382094,3,95.64998536684321,1.1932932982959272 +85,58,41,21.77046169,80.31964408,7.038096361,226.6555374,rice,12.851182636936997,3,5.754287955222303,12.048049798316917,401.45185974634256,2.142020928535281,4,16.797101236780506,74.73687900741903,70.96362942364794,1,4.714427327225068,2,77.26569365523096,1.7526716815799785 +60,55,44,23.00445915,82.3207629,7.840207144,263.9642476,rice,29.363912891054824,2,9.875230096038333,9.05134891346406,357.4179627074981,1.4749737247687815,1,12.654394579097698,1.034478009144435,191.97607728111194,1,30.431736475207316,2,18.192167864978813,3.035541019903109 +74,35,40,26.49109635,80.15836264,6.980400905,242.8640342,rice,26.20773239299878,3,8.023684684293785,7.963606057288741,363.6943055002588,8.393907171864402,1,10.864360184826305,24.091887934783962,55.76138848397649,3,10.861071276483274,3,82.81872017326596,1.2733406458545562 +78,42,42,20.13017482,81.60487287,7.628472891,262.7173405,rice,28.236236135835618,2,8.12051188272924,19.264133422858432,410.35645777701023,5.202285434682235,3,13.852910054226463,38.811481435085696,185.25970154082898,2,47.19077674711596,3,25.466498927055326,2.5786710849952157 +69,37,42,23.05804872,83.37011772,7.073453503,251.0549998,rice,23.61311519614218,3,10.873767569219407,2.728812124554161,428.72842969362614,5.958483178119839,5,13.478474458070083,82.07341331570409,135.92906584189868,3,42.15873477813557,3,43.42633430045652,1.7536156668614868 +69,55,38,22.70883798,82.63941394,5.70080568,271.3248604,rice,15.333693105308976,2,8.726839860577611,4.715023586814744,398.3170075705707,7.389915833434773,1,13.487610448270205,48.81462557502119,121.17365960158139,1,1.5921718254178319,3,50.01897066431516,3.171514124476316 +94,53,40,20.27774362,82.89408619,5.718627178,241.9741949,rice,20.835640482532128,3,10.719887142177978,6.627556793662457,379.8472815222341,1.2420726896565477,4,8.63078182173721,80.12783103951577,195.7361604567481,1,14.415279885864967,1,18.08996232939174,3.1795385786845385 +89,54,38,24.51588066,83.5352163,6.685346424,230.4462359,rice,26.64065576840069,2,11.600185637716663,7.309391590701411,446.3333843786954,8.821774607066256,2,11.260104812933562,89.87019212181914,73.44697633488323,3,48.13877175140959,3,98.37740877207062,1.5309800787118886 +68,58,38,23.22397386,83.03322691,6.336253525,221.2091958,rice,24.368852917432807,3,6.413153902771772,15.395845984336622,377.3990151601595,4.24170639150573,1,11.88543252673378,26.544257604904598,72.79170148098056,1,45.647152908162894,2,63.335751982038204,4.586856058247635 +91,53,40,26.52723513,81.41753846,5.386167788,264.6148697,rice,20.459145741082907,2,6.43288482293385,18.241282308646483,397.4717144170916,7.686282485746422,3,19.80376704152053,43.0149813897197,88.35389727182411,1,25.44276360101112,3,0.8030068386047917,1.4760491681677208 +90,46,42,23.97898217,81.45061596,7.50283396,250.0832336,rice,15.404600446017936,2,8.554866438403772,3.5335211917178566,387.838957847824,3.312374645893745,2,6.024622352933802,65.42929939559889,162.39579093341422,1,36.39975138070278,2,79.21176901091427,4.956311495518364 +78,58,44,26.80079604,80.88684822,5.108681786,284.4364567,rice,15.69649075752503,1,5.962473079366161,7.89643882264818,359.04279531520604,6.142806338720238,6,7.755604910749735,44.776936686283655,196.08297109004903,3,7.030797616012851,3,26.72412345028374,1.5690463286676493 +93,56,36,24.01497622,82.05687182,6.98435366,185.2773389,rice,18.102299560975805,3,10.509164776834663,4.249812524263021,439.9141828695301,3.208669383951548,2,7.552875738109826,80.6212931315953,83.06454514117044,1,40.4247179730376,2,12.45162810967173,1.0297367847931582 +94,50,37,25.66585205,80.66385045,6.94801983,209.5869708,rice,19.742522826826033,2,7.613462956316367,3.9259507614610967,431.68393229524645,1.1538330760013102,4,13.520434354100505,91.7188882485618,78.3083947509769,2,10.157776806077878,2,30.712435357097934,4.410523115565436 +60,48,39,24.28209415,80.30025587,7.042299069,231.0863347,rice,23.34511451117176,1,10.168697865057386,13.214305576708757,386.14283047459605,8.705518065479506,3,14.312229905712677,13.358098388005168,117.26676065919189,3,43.476465497635566,1,50.39889549176869,1.3745635802528167 +85,38,41,21.58711777,82.7883708,6.249050656,276.6552459,rice,12.287843010348427,1,10.748001538790213,18.997984889655246,435.4804751841295,8.55056918740796,1,15.303243639522,31.42938168793934,182.0508467996191,3,14.486082459985944,2,38.68300438556499,1.1359236150294585 +91,35,39,23.79391957,80.41817957,6.970859754,206.2611855,rice,20.505529046999705,2,8.914357978526676,5.605837157503199,362.0769125293712,5.083744266596145,5,6.886703068768259,14.483418321200748,74.4938501084522,3,35.14762403771211,3,77.8685812971585,1.085565894426281 +77,38,36,21.8652524,80.1923008,5.953933276,224.5550169,rice,19.86297771132577,3,9.604758001573376,15.037544987768992,449.09998238776404,3.287939301403511,6,8.66938933565357,50.16163399739286,75.75147164765943,2,4.852330346328937,1,83.16991783223679,2.290178559584014 +88,35,40,23.57943626,83.58760316,5.85393208,291.2986618,rice,25.75100657263861,1,5.156131899329995,11.966922691518535,387.84517226922674,8.826688210121965,6,13.41922724517027,21.42755696379507,87.37678676139276,2,24.83894964192667,2,49.834587199231606,1.3075942280167938 +89,45,36,21.32504158,80.47476396,6.442475375,185.4974732,rice,17.850955791197684,1,11.30220543702904,2.5680289175683435,386.91204857095914,8.137254880064633,2,13.331189009949806,90.87507422306251,145.35805048578587,2,43.275650169073835,1,3.070325598982515,1.4701086515910866 +76,40,43,25.15745531,83.11713476,5.070175667,231.3843163,rice,16.92544358289537,2,7.193711117792186,16.94274790637595,380.15457772902835,3.757578539131919,6,11.439381139697474,43.70821709513933,69.95604214677172,3,9.150663021420286,3,94.38189733419962,3.6723121619098236 +67,59,41,21.94766735,80.97384195,6.012632591,213.3560921,rice,13.9285897742515,3,8.401741241240257,17.444358494246778,427.32100344533956,4.9411167167386605,1,5.2613486650637284,37.08392899537516,177.95358783440952,3,23.13274544495867,3,15.47295955998289,2.4656465098439284 +83,41,43,21.0525355,82.67839517,6.254028451,233.1075816,rice,15.054079970805905,1,8.353858331438325,1.0286659731733572,422.16318039654476,5.1542050838430695,2,5.844103097052975,71.06154282829911,68.9503249773089,1,43.62347417344994,3,20.140472654441886,2.6868715107254615 +98,47,37,23.48381344,81.33265073,7.375482851,224.0581164,rice,21.665501586766386,3,11.400351466402661,7.1781252308063825,387.1073930580847,2.5247545824809117,3,7.674413593102753,66.68658019500681,56.29101402022205,3,33.55821259097579,2,77.15182533785733,2.6293142066193536 +66,53,41,25.0756354,80.52389148,7.778915154,257.0038865,rice,13.741746858175928,2,8.232858165322387,1.2902067748532486,434.99224295037806,7.417834013024693,2,13.420275630579985,66.96630853056874,161.99888092080016,1,21.855042906713884,1,75.12350816058637,3.9887219236789067 +97,59,43,26.35927159,84.04403589,6.286500176,271.3586137,rice,25.52548813642485,1,5.029698818479235,12.957018280144002,354.2222341724808,2.3172103907133668,2,10.720871310411214,89.94165562227481,123.91618268614569,1,15.840594303675386,3,80.56353348649273,2.7810452572861553 +97,50,41,24.52922681,80.54498576,7.070959995,260.2634026,rice,16.9004052999644,1,7.130601915609544,16.285840161183593,387.0109983219061,4.229277117008785,1,9.288550189993176,34.12657847522734,186.48685102113703,3,27.329056036699583,1,29.00824790733667,4.596199347451918 +60,49,44,20.77576147,84.49774397,6.244841491,240.0810647,rice,14.07087343441151,1,8.20556958897901,18.85925311258835,382.4049654362147,6.409720868261506,4,15.92940372453539,42.57121963780162,54.46000266994612,2,27.513657535428003,3,24.079101189899234,1.169038310400524 +84,51,35,22.30157427,80.64416466,6.043304899,197.9791215,rice,12.41872292580177,3,6.495707284772177,13.90703851581521,385.59068019596657,4.852641176937873,5,6.141819410846823,41.556331929529954,123.11369498702734,3,15.718680974054983,1,49.51670996926777,1.457174298152816 +73,57,41,21.44653958,84.94375962,5.824709117,272.2017204,rice,24.038531950479534,2,7.395417364492115,15.97704743199468,409.13507585559415,2.374012772166458,4,14.308048809199102,45.57980344027099,143.3450708523203,1,33.469149566509856,1,72.29219081889899,3.6672817146402275 +92,35,40,22.17931888,80.33127223,6.357389366,200.0882787,rice,11.78666410407552,2,7.827432567657639,19.205834810173457,449.75283667822123,1.1610994447862981,3,10.644278367228107,32.18107344470167,112.78792329339363,1,30.478580663185745,3,87.23012611655435,3.8583490632502904 +85,37,39,24.52783742,82.73685569,6.364134968,224.6757231,rice,18.580802806789414,1,10.681930603549745,3.936908658170577,440.59356041170236,4.719268902845018,6,15.78996607668547,5.000830207015228,73.55137622003463,1,22.972076616170384,2,11.030281219282845,1.2581960363365012 +98,53,38,20.26707606,81.63895217,5.01450727,270.4417274,rice,12.173006937040185,3,7.411662488078816,5.978189568747238,437.35228176667613,8.659881716344454,6,10.992251318755233,46.72542365743689,134.2455911192905,2,49.82165834015599,1,41.05329141913438,3.4668364446701974 +88,54,44,25.7354293,83.88266234,6.149410611,233.1321372,rice,23.33402741040184,3,11.435795412825094,1.123597347821832,428.43707637137913,5.1183852700980115,5,8.58871956139635,2.002447204415725,127.80651505647134,1,2.910249616931365,1,71.06733199806864,3.478428457663009 +95,55,42,26.79533926,82.1480873,5.950660556,193.3473987,rice,23.978828255346407,3,11.40265470663751,14.23982332784258,440.27705465686984,2.8606047983092937,6,19.940179865130624,18.592823085504907,148.6785351076055,1,29.808356985813404,3,87.44669457509315,4.937106996587655 +99,57,35,26.75754171,81.17734011,5.960370061,272.2999056,rice,20.463412227529027,1,6.2885136001290824,15.680680873219739,414.95432438786736,9.040013385591827,6,6.191903207615072,2.7200192410222424,185.9919749922708,2,2.1033043307092605,1,97.66742866354488,3.86799401207321 +95,39,36,23.86330467,83.15250801,5.561398642,285.2493645,rice,18.19548533667535,2,9.659394559081736,5.91500978313838,423.02488401737594,2.3715055511692955,1,7.624250507782482,62.71503007028074,57.9637403385222,3,10.362238021056097,3,6.8526759252996,1.049513577841978 +60,43,44,21.01944696,82.95221726,7.416245107,298.4018471,rice,19.47400022303122,3,10.173810986216726,5.6144207297556825,381.3204769233853,6.184190584365346,2,13.158529897373638,45.733507435124054,142.7926757284559,2,3.2353428707002307,2,21.146305191155513,2.208341569439201 +63,44,41,24.17298839,83.7287574,5.583370042,257.0343554,rice,10.470323050276075,3,6.112574760750782,11.560939120014613,446.1576903753747,5.1912928458118355,5,19.669808340988297,13.415560327065524,162.2816929151758,3,8.469410839964864,1,42.22708139720211,4.558693861518211 +62,42,36,22.78133816,82.06719137,6.430010215,248.7183228,rice,19.71000774484648,1,10.665139917708814,16.508163220734254,415.2461742627597,8.748923931901139,2,17.747714548222874,79.52545134432043,65.14449898572833,3,44.58429975024805,3,6.1138640539346145,3.6062501886256157 +64,45,43,25.62980105,83.52842314,5.534878156,209.9001977,rice,17.241898182189612,2,10.912255011011943,5.994164681549803,428.69964059252925,2.601890339800027,3,14.830346646255126,82.17204057073666,161.6658188784206,3,29.801272180384586,1,47.308195054078,1.0867395612698543 +83,60,36,25.59704938,80.14509262,6.903985986,200.834898,rice,23.532235559361865,3,8.76691037407468,8.119286466905507,401.04588920009286,8.410945628340926,1,5.545100233843027,86.37193496156841,74.7326110897238,1,6.334056913628555,1,88.2559796343018,4.937216070656682 +82,40,40,23.83067496,84.81360127,6.271478838,298.5601175,rice,22.691620214136808,1,6.60503715579402,15.553970905337561,437.10889660253844,7.030883615149051,2,7.031841761325804,43.38731405276066,195.8162786796658,1,3.5154381351317934,3,36.92147305256569,4.652537295264811 +85,52,45,26.31355498,82.36698992,7.224285503,265.5355937,rice,10.924939769484347,1,9.8176727910344,9.747403754434998,368.03569094118075,1.8143648596860777,6,14.080812219915186,90.09073038644414,181.7079578905352,1,22.939446748478254,3,15.530858697250661,2.8258265400350386 +91,35,38,24.8972823,80.52586088,6.13428721,183.6793207,rice,14.659362350623981,1,7.719460462143672,18.6770593044926,407.88658529231157,4.8469323795326815,6,8.775049596009735,98.9995425042727,130.2798842463123,2,21.9793870975708,2,10.359632793076035,3.257700872428641 +76,49,42,24.958779,84.47963372,5.206373153,196.9560008,rice,15.672750919557645,3,10.14681184399114,9.895260931811753,423.0962709665236,6.42222247707992,3,6.765426686979041,88.5524671721141,87.01920348090971,2,5.510014012877839,3,18.33425108498282,2.243055952362575 +74,39,38,23.24113501,84.59201843,7.782051313,233.0453455,rice,20.81748305905524,3,11.913349858198561,11.206828237810033,392.47297638412806,2.514791433556788,6,18.05209272255442,12.797245190699236,57.79366971753189,2,8.956987271527167,3,3.006486696932764,2.389991554637027 +79,43,39,21.66628296,80.70960551,7.062779015,210.8142087,rice,16.434935959029833,1,8.620858210010176,7.996159945564127,373.4682793057467,4.293714527293529,4,5.724663012476134,51.50982369776087,151.98828590293806,3,39.88277176919184,3,95.62224628259068,2.7800309771194263 +88,55,45,24.63544858,80.41363018,7.730367824,253.7202781,rice,14.83400005660129,3,7.0299315247365675,14.434281252135346,420.6499543076612,1.7059432979616953,6,15.951517619057757,11.010510353792712,98.02721357338355,1,17.10798930512149,2,11.032124827970403,4.338717263699402 +60,36,43,23.43121862,83.06310136,5.286203711,219.9048349,rice,24.8121987674652,1,7.1699310899608175,7.122173780010099,409.3179306793328,2.2645154872020052,6,14.054509318880132,41.56000215352197,90.25123166275117,1,4.335156741635321,3,40.45347159293695,1.179476422724584 +76,60,39,20.0454142,80.3477562,6.766240045,208.5810155,rice,10.907756040294556,2,9.518815468591033,4.538322562892398,446.2142258989603,9.711797807054227,2,12.277305467128615,98.53192199244982,126.16351024579018,3,6.659135697162805,1,41.29659021359268,3.3426584464698568 +93,56,42,23.85724032,82.22572988,7.382762603,195.0948311,rice,29.146041129266138,1,8.706770211778142,8.006397242835632,364.4324680916244,7.237875249463322,3,9.943805879583564,25.96809287969377,130.24024403885724,1,31.22242222634088,2,62.38411170401456,3.893675454174683 +65,60,43,21.97199397,81.89918197,5.658169482,227.3637009,rice,21.279364223543475,3,9.694557135285127,14.759346365481305,359.3430403515077,1.5614187930812968,3,15.134719809759043,70.62633951527407,101.3128211428695,1,10.19287102298922,3,49.998243880027594,4.331025133020088 +95,52,36,26.22916897,83.83625819,5.543360238,286.5083725,rice,16.404267961783816,1,9.506083980727048,9.96361333023617,410.9776457780935,3.7475816088492824,4,13.665584559067932,14.43284131608662,124.08871632013896,2,21.74750792119669,3,36.90025767066773,1.732768742010205 +75,38,39,23.44676801,84.79352417,6.215109715,283.9338466,rice,13.104904061824998,3,10.10608178157582,17.37441873678496,420.6999516017301,1.8355654130967771,2,11.00629674217113,99.43025731792928,141.51172728195272,1,14.240641354573762,2,78.3372724368921,3.6564828968883094 +74,54,38,25.65553461,83.47021081,7.120272972,217.3788583,rice,21.98743157620418,1,8.364997214765754,6.428312409954922,372.50119767131713,5.268051691122307,3,7.120670963023565,10.953229601268644,63.70842566808378,2,5.288644975579782,3,73.14725206396203,3.857388005841727 +91,36,45,24.44345477,82.45432595,5.950647577,267.9761948,rice,23.16205187173825,3,11.579201458855032,0.7639098737618966,414.95720791834634,3.5629031070467945,6,6.26214276716537,51.31985891047406,58.534785406313375,1,6.707805125271133,1,83.25806597493758,3.996544549895163 +71,46,40,20.2801937,82.1235421,7.236705436,191.9535738,rice,14.510480330649035,3,10.075854149612363,19.270908923947662,419.84281932107467,7.460449893967257,2,11.346599804821283,70.21030642246134,131.47925278847293,2,10.731507879482011,2,10.484498111926776,4.986762807502485 +99,55,35,21.7238313,80.2389895,6.501697816,277.9626192,rice,13.538539648190985,1,9.153951848687026,17.43376305411903,399.27840636225307,7.792042020666548,6,13.835956010995186,59.3651907576288,53.32331105162666,1,4.531077958351326,1,32.65593194168696,1.5620905056992695 +72,40,38,20.41447029,82.20802629,7.592490617,245.1511304,rice,14.592365270038037,3,10.994239280777903,1.789767905513986,393.2001556575748,3.9017389217755056,6,5.407176816711667,8.700531740345085,53.445356185109375,3,18.261436822772726,3,20.926994270124133,3.256415373141469 +83,58,45,25.75528612,83.51827127,5.875345751,245.6626799,rice,14.696973511495068,1,10.098612476063089,16.102923423795584,425.29909923701746,9.347480667753917,5,18.36459059319938,10.447536091915389,90.09117542879707,2,19.060004487984628,1,74.35069868011723,4.562012264508544 +93,58,38,20.61521424,83.77345559,6.932400225,279.5451717,rice,19.569122738360516,3,8.962662837776401,17.59924760989761,430.70688622839964,5.780798200037314,4,14.367410418916537,1.2100892407415187,67.82375808154919,3,7.734178463661928,2,30.764317142552855,1.1678093088284922 +70,36,42,21.84106875,80.72886384,6.946209881,202.3838319,rice,13.825855174688847,2,9.139950658853223,13.327711148386477,366.15574674408026,3.644763752764966,1,14.745989316097555,28.636245175283015,59.54664820922447,2,49.183980078509826,3,77.93952677708141,1.8753376905284456 +76,47,42,20.08369642,83.29114712,5.739175027,263.6372176,rice,12.798551225078885,1,8.493220515439155,14.793145743826779,361.02091567323566,1.985335032598622,4,7.303002128148399,13.445380845596954,134.0076579778979,2,42.93040374466491,3,66.24336098308976,3.4540275931594833 +99,41,36,24.45802087,82.74835604,6.738652179,182.5616319,rice,16.952351417059347,1,10.018050387435224,14.277774807927026,401.1430304659543,4.5256181184152835,3,11.81226410781256,47.84099194412511,170.64946473485531,3,33.346448676639675,1,23.656292621719523,3.625787958916628 +99,54,37,21.14347496,80.33502926,5.594819626,198.6730942,rice,15.393957784044227,3,9.361882068000092,2.6483106419485236,353.1164708535708,9.107231414495246,6,17.87947383561629,66.87910668138838,60.55799294468706,1,29.336491212143034,2,23.257368366471454,1.4600224768097783 +86,59,35,25.78720567,82.11124033,6.946636369,243.5120414,rice,10.308661233612547,1,7.191549350538006,8.510774774915603,401.4504766264109,1.241607294139976,4,7.32202807288316,23.521256255359802,71.62588155356148,2,33.87827329021119,2,1.9116639478860997,4.5372072686839875 +69,46,41,23.64124821,80.28597873,5.012139669,263.1103304,rice,18.880685233418696,2,10.831767020718933,15.844308334520589,428.42838833857763,8.275411802198768,6,7.0408597815728875,66.47197175806218,161.1511192985543,2,11.679116871575934,1,43.387930202188144,3.100221279464048 +91,56,37,23.43191632,80.56887849,6.363472208,269.5039162,rice,23.365152996616825,2,10.630830430978955,7.055707864747114,431.437947572399,4.595950250761159,4,13.757519351796471,32.29148179612109,139.16901748610525,1,40.37082457952749,3,58.590449975548765,2.538025948530431 +61,52,41,24.97669518,83.891805,6.880431223,204.8001847,rice,18.825945982287358,2,10.805946367333611,1.5682839911427404,426.82988928449737,2.775087902476876,5,15.476558419761076,41.72254045993387,188.18436225462816,2,6.203481960846608,3,75.91396560628976,4.2086307992241565 +67,45,38,22.72791041,82.1706881,7.300410836,260.8875056,rice,26.46537120009733,3,9.143613979505744,14.5416924741046,392.14531137505486,9.76133822014606,2,7.14944570440716,31.222512485403364,92.15260517357126,2,4.194985620642871,2,75.06507363965994,2.9699200803837047 +79,42,37,24.87300744,82.84022551,6.587918708,295.6094492,rice,28.479208747976088,1,9.982401859212954,16.820796568383592,352.6022202925,6.4963617277235715,6,11.314347532403602,85.20503231387296,108.03340693170699,2,29.125179044374285,2,52.85013703914488,3.748161739200655 +78,43,42,21.32376327,83.00320459,7.283736617,192.3197536,rice,26.958458153643253,3,11.513024035041527,13.171620444337286,405.7932062139446,8.959053347735628,6,5.5111304955159035,15.560573364494923,127.8709880901036,3,49.68373215866776,2,44.23937121236209,4.465167105403804 +75,54,36,26.29465461,84.56919326,7.023936392,257.4914906,rice,12.692405256703811,1,10.107386654563172,16.370263035927152,378.88278792276606,5.711948512456132,5,14.354384400048602,30.005963650270363,177.6444672214584,1,4.970428090272394,3,79.4798521508006,4.561682533023239 +97,36,45,22.2286982,81.85872947,6.939083505,278.0791793,rice,21.742934622322476,2,8.555142534523862,12.921630582099965,387.7311859883452,3.054852950064713,2,15.385554829755966,22.17683939975881,110.18419685355285,1,27.384108486113597,3,3.7155727391854687,4.975013598385738 +67,47,44,26.73072391,81.78596776,7.868474653,280.4044392,rice,11.75158593268405,2,7.738664534793937,8.817489638097593,416.6140621933331,7.950500850109946,6,11.479111183478434,47.623938328421666,104.4842425093012,1,6.983647225825362,1,62.49022737819724,3.767585951435315 +73,35,38,24.88921174,81.97927117,5.005306977,185.9461429,rice,16.512010148068555,3,10.916537773149614,2.8412736706849473,391.0619312352157,2.625693130482998,3,7.382253533992833,81.30788103914537,98.5371975997906,3,34.22210086121554,2,48.72180390853752,4.367224224978273 +77,36,37,26.88444878,81.46033732,6.136131869,194.5766559,rice,29.615404320651862,3,6.984510379676158,2.261604010333411,442.4042944499589,1.171969895212413,5,16.56948169978149,85.82209094624453,162.12832940612995,2,36.65283920631073,3,34.56122926492523,1.552999309360061 +81,41,38,22.67846116,83.72874389,7.524080076,200.9133156,rice,25.619096306119154,1,7.693171089062004,9.809661026988328,417.594528247409,5.675302324647516,4,18.79716620642527,20.122956874708365,198.32016072814443,3,38.47677764247125,3,91.77639502384389,2.5104863034093494 +68,57,43,26.08867875,80.37979919,5.706943251,182.9043504,rice,19.692628262369958,2,10.20137176731399,1.1079885820330926,424.7263248932942,4.2928318939608054,2,17.068146137334224,62.13732416015099,82.9523141465906,1,27.78524893870808,1,65.2888596700758,3.808248085280934 +72,45,35,25.42977518,82.94682591,5.758506323,195.3574542,rice,11.875026900296128,2,9.96686366052047,1.393476353571308,359.42617040101936,1.3295956163114628,2,7.40716422874189,74.64723652678093,135.7377885014937,3,36.01261617977819,1,33.10591165125892,3.181773323564408 +61,53,43,26.40323239,81.05635517,6.349606327,223.3671883,rice,28.761611146685205,1,10.212025418448317,5.067830179901323,387.74246291258265,9.60990064731802,4,17.63028251830553,6.947564227419035,94.50164703380148,3,31.017419993335356,1,57.30462164897895,4.556014599317104 +67,43,39,26.04371967,84.96907151,5.999969026,186.7536773,rice,16.7470551416293,1,11.322839349737604,3.009587967088847,365.87111715462606,2.3570732927293525,2,7.020516362405225,99.31161833505557,156.71569404413762,1,39.72124751888767,3,60.26723564198137,1.7535621686631453 +67,58,39,25.2827223,80.54372813,5.453592032,220.1156708,rice,12.612552024800824,1,8.168906404433727,16.111398131742526,361.39419335558557,1.400432280475421,3,8.36075029867662,5.256702169123928,192.22689965854005,2,31.935297187524963,1,75.56094196383658,4.843657322960674 +66,60,38,22.08576562,83.47038318,6.372576327,231.7364957,rice,28.298454436135017,2,9.381699483527832,17.497149089540137,371.36714979251565,1.978030141671313,5,17.25455607177455,12.726035399916547,80.39865991915731,3,17.068090813193482,1,62.23285165297112,1.7260055689391591 +82,43,38,23.28617173,81.43321641,5.105588355,242.3170629,rice,27.219011180701177,1,5.569152367667043,17.033311807471673,405.5040918139793,6.070042779991108,5,16.185987168945253,92.529345725829,194.8462494962094,2,32.031400995774945,1,63.603897970666736,4.69763303207872 +84,50,44,25.48591986,81.40633547,5.935344406,182.6549356,rice,17.8886838179578,2,8.449936908327008,10.593161540395338,388.47047426130456,3.940238365811812,1,5.183824357892965,79.06296965868911,197.88295846111782,1,30.201742560194123,3,79.19770372686067,3.741958327988631 +81,53,42,23.67575393,81.03569343,5.17782304,233.7034975,rice,25.949927940533676,2,11.85680927654324,6.485574932046092,415.95308242932083,3.2707015974798033,3,10.026937780528783,4.281976211538052,102.7245311499158,1,39.15509050264488,3,64.53883907891502,4.349212315669888 +91,50,40,20.82477109,84.1341879,6.462391607,230.2242223,rice,12.50657530419173,2,10.671771298160097,4.410430124774289,396.471249525371,6.972058538748989,5,11.432461995579867,45.20504156684125,136.02909931446464,2,2.854875355888659,2,52.18373864732552,1.1669671425606056 +93,53,38,26.92995077,81.91411159,7.069172227,290.6793783,rice,28.25910002250434,1,6.378760462518726,18.908676350006775,352.4409810980666,7.699751399011879,2,16.213774194747153,83.04973571425558,69.7809545775652,1,20.2378475395106,1,57.558778592883385,2.296473029385508 +90,44,38,23.83509503,83.88387074,7.473134377,241.2013513,rice,13.91157577786971,1,6.990586093261763,18.162174985921602,380.0805206318279,6.6768228384878165,4,17.141311350605257,3.976145351830307,186.9294298835475,2,24.12238200946591,1,98.19485078778193,2.5650189438392497 +81,45,35,26.52872817,80.12267476,6.158376967,218.9163567,rice,10.664596611430836,3,10.305123719250641,1.9802437947808715,383.25166153387386,5.786553038348169,2,12.463467033144376,21.71469305735925,165.79366141632315,1,12.37146533845328,2,12.092905072536308,2.5105887784096996 +78,40,38,26.46428311,83.85642678,7.549873681,248.2256491,rice,17.122876153973998,2,8.443883260881767,3.3772935322157904,399.3725140164827,2.710678978084017,4,14.859973248773601,89.47333635932176,163.60040788315183,3,41.684995362394204,1,1.9891436792416228,2.206593657295648 +60,51,36,22.69657794,82.81088865,6.028321558,256.9964761,rice,16.194472911082855,1,6.42389127289027,9.298748203306834,354.79099601781576,4.388515720640965,3,7.985093996001121,33.91551511398264,184.35492240009202,3,19.049281995863982,2,31.95272039870628,2.7106079870976036 +88,46,42,22.68319059,83.46358271,6.604993475,194.2651719,rice,23.81684773602596,1,5.122433172035966,13.975935842613731,357.58239368378196,4.551065766778282,1,7.284444179427627,88.79185465173238,118.59008039681912,3,29.93185195556929,2,48.16128032944297,2.4927184961230173 +93,47,37,21.53346343,82.14004101,6.500343222,295.9248796,rice,27.740009084178773,2,5.42947414856075,7.130239308961157,449.1218921098511,9.14554523917223,5,7.411669392147853,69.62919254848961,125.66994046168375,3,6.845270438197776,2,90.86432426533774,3.7703017173233935 +60,55,45,21.40865769,83.3293191,5.935745417,287.5766935,rice,11.523065573544693,3,6.462053281841766,18.371658193412344,392.9445046447296,1.2835124229095598,1,10.642500799949506,69.65347817072832,130.37475284044007,3,7.711555753164584,1,8.729263909014783,1.4643518692610047 +78,35,44,26.54348085,84.67353597,7.072655622,183.6222657,rice,14.777482361774299,1,6.99683917257488,14.834502651212185,388.5297715339604,2.054593533311688,3,15.945053506781143,5.211421652847859,172.4208991438148,1,24.699916131253584,2,19.9188175750872,2.7611641386973265 +65,37,40,23.35905428,83.59512273,5.333322606,188.413665,rice,28.968310478447773,1,10.339150689269,11.056683208838201,421.02356091318785,9.393009250160304,4,5.484538541649057,29.137157431355643,81.77991296914234,1,13.155469273392306,2,55.35620492885723,4.282289822503507 +71,54,16,22.61359953,63.69070564,5.749914421,87.75953857,maize,28.156114237523916,1,10.4576601616066,14.635145836663545,418.1292073546417,8.563344115067382,1,17.403104220998536,91.21045169141881,162.70904483875375,2,15.252876265844723,2,61.72390606857003,3.795894645249465 +61,44,17,26.10018422,71.57476937,6.931756558,102.2662445,maize,20.077735685013888,1,10.289859638018285,12.841988225487285,365.48489253268934,8.453087186021335,1,14.256065491378326,15.849372917478943,60.31843581473337,1,31.10022803850989,2,6.801556749727022,3.009500931401725 +80,43,16,23.55882094,71.59351368,6.657964753,66.71995467,maize,15.374348473394676,2,6.339511862572754,0.09573474609472443,418.97470876214027,1.0828255895455383,5,9.989453729079614,52.25777314483076,90.038070568537,2,29.017744501451453,3,77.9288728634444,4.85950927979083 +73,58,21,19.97215954,57.68272924,6.596060648,60.65171481,maize,27.030658271675122,2,6.344720978162365,16.29883251302163,409.4140461799825,5.0816083083204076,3,7.358802371161817,52.054374876759866,121.56979071519591,1,11.182231203060228,2,67.65339695294185,3.565479847440921 +61,38,20,18.47891261,62.69503871,5.970458434,65.43835393,maize,26.64690297120908,3,6.945164306014915,17.66803320023015,422.2249570742907,2.367882777802868,2,8.262650457055916,55.32698827597444,135.4694481586128,1,34.888152354961356,2,44.148870019065626,1.4113493815475193 +68,41,16,21.77689322,57.80840636,6.158830619,102.0861694,maize,16.916115717602835,3,9.282074793155324,5.5415496813857,427.29145013841145,7.9608652457013305,3,10.351651775804458,63.23212380945598,156.83349954557946,1,10.084132267556612,1,64.89237361237468,3.855729270734928 +93,41,17,25.6217169,66.50415474,6.047906679,105.4654703,maize,23.187377578327315,2,10.018629748924496,19.115083906627884,361.9329794791084,9.169777905327692,5,12.177475707156415,49.22027462850708,70.01148158526053,3,1.3197442252329161,2,2.8005041510896556,4.977450806470273 +89,60,19,25.19192419,66.6902901,5.913664501,78.06639649,maize,23.96198764939306,3,7.9743677613738235,12.406600507956444,375.04669574988264,6.035312524317499,4,13.496860600121634,62.613064678750376,69.1423489369196,3,1.8606160719431108,1,47.86273638760582,3.6201363654359566 +76,44,17,20.41683147,62.5542482,5.855442401,65.27798457,maize,25.43969221505493,1,8.128670502444741,11.342437973246067,365.60362795026225,8.104929796744013,3,5.778014622307045,73.55118791501147,127.23637324936807,2,48.569130915607374,2,13.021129364594408,2.9723159844104035 +67,60,25,24.92162194,66.78627406,5.750254943,109.2162279,maize,18.810809946739603,1,7.355747703474254,4.952370142135402,410.56049975561785,3.287584989466832,2,12.550304099257465,73.15099475398165,97.43424879026091,1,13.16491571416553,3,62.70727287874255,1.3781879213463348 +70,44,19,23.31689124,73.4541537,5.852607099,94.29712821,maize,18.35536638092298,3,9.938918604408634,10.706750345132791,394.4056601902954,3.2437906076985428,4,13.103252635674144,31.934164617380766,143.01517202938913,3,16.213898432091277,2,66.24277217211676,1.1993949698890676 +90,49,21,24.84016732,68.3584573,6.472523287,74.05474936,maize,15.475575411009304,3,11.285436917010383,13.773911528099836,366.49567072399606,1.03443993947727,5,8.319287108668323,45.22873452598076,130.07543366050697,1,31.587129294230742,1,31.914456082520182,1.210945364128046 +62,52,16,22.27526694,58.84015925,6.967057762,63.87020584,maize,28.818107434133015,1,9.36311331410015,6.868265821161971,362.51840622472565,4.311333351785014,6,12.549366890834857,46.382134147103194,156.5443354532003,2,4.844258560297576,3,17.86729936411161,4.357799119499675 +92,44,16,18.87751445,65.76816093,6.082973754,94.76189431,maize,16.51598934276396,3,9.541492150387434,5.342120631199774,411.56085829111623,3.6999827888842907,2,8.480648425783688,4.178685433798812,64.90979248812386,2,5.237154050854781,3,67.76069582065286,3.571591728432276 +66,54,21,25.19008683,60.2001687,5.919045532,72.12375573,maize,20.220159164922585,2,7.423576255500406,19.145718543100244,351.65914469014217,1.6325832025301086,2,14.146164946984477,70.05089554978348,163.91622978418593,3,5.792494299249879,1,52.42581981447745,4.794256796005639 +63,58,22,18.25405352,55.28220433,6.204747653,63.72358154,maize,29.564409215525536,2,11.192985136397244,6.4786868758872584,357.8076384307208,1.17251688037859,5,8.076160620007826,88.470864632309,83.2757211170948,1,10.756825058653646,1,14.994566171085177,3.8391866408203557 +70,47,17,24.6129118,70.4162444,6.600827017,104.1626147,maize,20.02109254320799,1,10.772228518270595,12.045762640050942,392.7139043478131,6.401624076069764,5,10.867535362615893,12.799899994777453,111.69765438364496,2,31.736820786813354,2,64.72764247946668,1.4137416188734484 +61,41,17,25.1420613,65.26185135,6.021902237,76.68456006,maize,29.564065781027193,1,9.571397394060487,12.421020602544797,395.6082094108026,4.8756386598676835,5,13.672334019978845,34.756039188882816,102.32510768412166,1,29.04560353566869,2,4.41334308885687,4.254663478706797 +66,53,19,23.09348056,60.1159381,6.033550195,65.49730729,maize,17.777308012949078,3,8.139861891424822,15.694184583565391,394.98459197401326,6.4197805860464054,5,17.545024846230945,25.570199686659855,166.57231522635095,3,33.10010699498457,2,57.63680063022102,2.4721631055805333 +74,55,19,18.05033737,62.89366992,6.28886807,84.23613484,maize,28.361883052679257,3,7.681525712895155,18.632775176054338,380.7963324132751,2.640846091740541,4,13.71699861924386,86.71488486432531,55.12069480853036,3,16.071264008579654,1,49.73906723092462,2.04522576754565 +77,57,21,24.9321581,73.80435276,6.550563823,79.74078719,maize,25.08406622790464,1,10.397123987862376,15.484854483085043,386.8619027811957,6.717954629661662,5,9.15477207376625,94.91474203223625,169.53659841875515,2,4.953312485140815,1,73.17456535822878,4.491385538631137 +99,50,15,18.14710054,71.09445342,5.573286437,88.07753741,maize,16.483831774064306,1,9.379188921868618,16.88292084900617,371.8112027959144,6.918472121701261,4,7.470003928218493,26.763543619603535,86.61070677800393,1,37.86246984239875,2,42.23117351554303,4.159630333864085 +74,56,22,18.28362235,66.65952796,6.829199275,80.97573281,maize,10.557327891943224,3,10.83192583282395,17.988080048208047,364.67934018627403,5.316890644844102,6,5.201145336322493,83.78298076978213,58.47081771157484,1,10.753839484866251,3,46.47527079661836,4.466445884409916 +83,45,21,18.83344471,58.75082029,5.716222912,79.7532896,maize,20.709097635329297,1,9.891744119864367,0.0028699831011058663,435.28434448948826,9.290833683244204,4,18.19873965724357,57.89451147131556,178.5840260359625,2,0.775878150027759,2,49.554728913830026,2.532466667747218 +100,48,16,25.71895816,67.22190688,5.54990242,74.51490791,maize,21.22619485890013,1,11.947405276289771,13.947580594371523,431.98059043394665,2.3667708969986765,1,13.41172579969479,40.98109660839617,179.69058134877503,2,44.03384055147534,3,82.44241544906274,1.492170729707151 +79,51,16,25.33797709,68.49835977,6.586244581,96.46380213,maize,26.550300867005102,1,11.9112554720343,12.725374709602653,417.6657456963728,8.410889148419427,2,9.752331183752151,72.02096613988199,128.99762556641122,3,37.18264427443312,1,72.44831092389418,2.498723847592176 +94,39,18,23.89114571,57.48775781,5.893093135,102.8301942,maize,19.783968908523512,3,10.335042866881452,14.65783855206299,414.8819147289122,3.3402567324689043,3,18.600387231393107,13.059975510986089,116.7154630883587,3,13.657851892941968,3,61.85552093917713,4.238195092373365 +75,49,15,21.53574127,71.50905983,5.918263801,102.4852929,maize,13.366998531307289,2,5.212174170651337,0.43516696816681577,393.0346068562641,4.107909745784703,6,15.237957415469394,9.183958155483774,109.0361329204643,1,3.224739326113818,3,9.174379745815454,2.4688512841863455 +78,48,22,23.08974909,63.10459626,5.588650585,70.43473609,maize,11.766911669745031,3,10.02693325768997,1.131060427028412,374.7561366651767,7.131052626309331,1,12.617373927469494,43.373526023124576,79.36313101485527,2,0.05100027656864681,1,84.8327609721675,4.434976689320607 +87,54,20,25.61707368,63.4711755,6.576418207,108.8303762,maize,20.70903958586095,1,11.925229223232812,0.7877098121617987,402.94879540520617,1.4514109657922438,2,10.481830006234032,62.5663983211316,194.46537178757615,2,27.61743818470897,2,52.211015030209495,3.333349557907917 +87,35,25,21.44526922,63.1621551,6.178056304,65.88951188,maize,12.024365136909857,2,7.025473918542891,9.636454275820574,437.66748810491094,9.09043841303276,1,5.136479448650119,1.4722843513942374,138.7770689068788,2,8.463016371367049,3,76.41745343467062,3.8996705146629873 +63,43,19,18.51816776,55.53128131,6.641906353,90.988051,maize,10.825667707107044,2,11.593728814565367,15.7705200275212,360.1340384618379,8.290345462926442,1,8.441099099484457,32.116045569709584,68.20600730927012,1,34.45421746666232,1,97.22599820831897,1.7256139508085835 +84,57,25,22.53510514,67.99257471,6.489040367,64.40866039,maize,10.31015108426444,3,5.321100794191025,12.331107898263793,393.38395943545436,7.908178584669154,2,12.315468996415266,68.69956434618702,62.52098834181867,2,42.75667705665569,1,20.84317515988414,1.9699483934582274 +64,35,23,23.02038334,61.89472002,5.680361038,63.03843397,maize,25.994938800495667,2,7.929225231230511,6.930290324954798,391.68983609791906,5.892073429989431,6,5.4727001471613175,72.31552202033662,98.300501869008,2,18.163902811084125,1,88.01318654695997,1.4725103509437498 +60,46,22,24.89364635,65.61418761,6.625404348,87.9298085,maize,16.11860233000442,2,9.35492288249931,5.655700625847202,390.82424230746005,9.110038220829544,3,7.909937165868556,15.725754102822108,138.24007836765517,1,28.89583402726419,2,46.756888703620945,2.3080612227485044 +98,44,21,25.77175115,74.089114,6.524478032,107.4931917,maize,16.18147559658525,2,7.139734066592972,15.377594807872253,380.3692118643177,7.4066962470237225,2,8.957046138169506,97.08582627962441,148.0560576055633,1,42.66952219581532,1,76.81332346451318,1.9610536963990355 +75,56,18,19.39851734,62.35750641,5.696205468,60.95197486,maize,15.509719611987599,3,5.693955811265353,11.440690741372029,403.2726882501837,9.441555378948523,4,13.591520198316868,38.869970227355196,181.35636770905035,1,49.07879005776542,1,97.46042299404664,1.3899369084084277 +86,55,21,21.54156232,59.64024162,6.803931519,109.7515385,maize,25.40648231124742,2,5.421761267587175,13.953796326708794,422.9332074015874,7.539780823844976,1,14.528664099794987,33.132219921217384,177.3899730581817,3,40.20983673008141,3,48.45018632155897,2.5564549267760683 +98,35,18,23.79746068,74.82913698,6.252797548,91.76337172,maize,22.6919053080773,1,7.98570892490657,5.310167614140995,431.1193253745016,6.160676196620697,1,19.932663074827303,53.36238643447064,175.05313499022634,1,9.796705667526378,1,1.292525891338303,4.450859980605886 +76,57,18,18.9802729,74.52600826,6.092725883,94.26249353,maize,28.784592006394575,3,10.550047413011114,10.823207722509274,449.69869557543313,1.5458254697100964,5,12.708877059990156,37.19198005428698,161.27349181695152,1,31.39389387190536,1,32.851751957607924,1.0800769088964608 +99,56,17,24.10859207,73.13112261,6.234330356,71.07562236,maize,14.69504496019837,3,8.677477727721634,12.647115269211902,428.6027601702945,9.333546192189281,3,7.805512640876092,76.31931037232788,79.44767278280503,2,27.400766660984655,1,93.1186238383711,4.454077982323488 +60,44,23,24.7947077,70.04556743,5.722579819,76.72860067,maize,17.626428704420114,2,10.166162207907812,6.967887724695856,366.41285411311605,8.579635315418768,4,7.04495444822971,0.5614210194318514,132.20877116174552,3,30.613258217909824,3,10.72321818421571,3.4127390044597345 +74,48,17,21.63162756,60.27766379,6.430616465,69.21803098,maize,25.58405607247876,3,8.843296433572885,3.353474748424652,424.67630974491885,3.747302982036841,2,9.30491297798995,0.03810333825923218,166.04727940670887,3,49.238614344595824,3,42.82098612293197,2.7049325457675977 +89,60,17,25.37548751,57.21025565,5.983952675,101.7004306,maize,24.3952801312897,1,5.585151224634395,12.193613817481113,397.44774475788194,1.1333539918115116,3,7.48417760834058,20.801617420111285,132.88521727386944,1,38.34958003502755,2,39.830284969298,4.356934287952017 +69,51,23,22.21738222,72.85462807,6.80163854,106.6213157,maize,22.423386416477808,1,11.840331199765423,7.247076081947183,420.94455947068315,1.4025280830112834,1,12.516766964632325,57.08770912648716,173.4000279177962,3,17.01691588992606,2,1.8351307727198174,2.7722565624488817 +96,46,22,20.58314011,69.00128641,6.499936446,66.29390357,maize,14.48966526248931,1,10.191230930589679,6.263832316038447,437.9304508700769,3.178371540879528,1,6.179459216687517,27.916454719985516,69.14680302954363,1,43.288909777026646,2,30.937605912470023,1.165076098841824 +61,60,15,24.87502824,68.74248334,6.265564338,91.26056654,maize,21.260414498467775,3,7.077418010925226,18.38791521505286,444.7979525227851,4.640238746835432,2,14.255651050360761,8.67008251978656,107.43116395731514,3,37.54157773631281,3,46.6022956628057,2.424669745784972 +74,58,18,20.03728219,56.35606753,6.727303282,109.024141,maize,25.00861027953608,1,8.508133170032366,11.605523824306518,417.0069817058546,4.610375789148433,1,14.898992302488635,79.4704144449591,115.02329779434895,1,19.150393686224916,1,60.18168394840999,2.6218159783552237 +74,43,23,25.95263264,61.89082199,6.325235159,99.57981207,maize,27.459627375694836,1,7.886227116144518,10.125153639385562,393.7830108957594,9.254137836951292,5,18.846196360555492,55.00597878693748,105.38261729102703,2,23.449073423157067,2,87.90359432778183,2.939576457583108 +63,43,17,19.28889933,65.47050802,6.807487794,71.3195307,maize,28.435890670149476,2,5.280586264089705,11.568013529157703,411.31808573902697,1.662285212306121,1,10.161683027589696,99.89974644462399,60.8791805192695,3,0.415997807100954,2,37.65267991942805,2.1206651641150205 +99,36,20,20.57981887,65.34583901,6.671085817,78.34604471,maize,25.405561379933715,1,9.201812197219205,17.266212873463843,352.76457613836914,4.567640039742477,6,15.44392517727786,35.86732171213733,64.62263325490517,3,7.016145355521291,3,25.793879587513313,2.0850432544323794 +77,36,23,24.71417533,56.73426469,6.648725327,88.45361858,maize,28.443769919769352,2,10.106009914388903,18.907173706193174,354.6669244953803,1.9579872652380221,2,10.690410886054764,62.755731142764446,128.23188523274808,2,12.888195788450934,2,8.233446456835347,4.422928323306293 +87,60,23,20.27317074,63.91281869,6.439071996,62.50351892,maize,17.177087805545685,3,5.817462300480618,8.715019816474074,430.7242209915556,5.375129377106358,5,17.843443008937925,99.8719349760149,149.42298760922466,2,9.70094513455853,3,76.39186277298457,1.8219530335234864 +60,38,17,18.41932981,64.23580251,6.474476516,76.41312437,maize,20.860039409441107,2,9.47085517222036,13.718946933245252,385.2710083445663,3.246225075939655,5,16.156766220071802,52.33152627394014,53.06463216807005,3,8.562627921297716,1,46.64749572651732,1.8470540287088064 +94,54,17,23.39128187,61.74427165,5.871647806,107.3198135,maize,20.17302945406609,1,5.311372444842171,2.1483431423102517,387.51908570009516,3.814075766569502,5,15.984332260680901,11.174649539939841,148.77804162927305,1,45.407355174881324,1,37.402087528159754,2.3117452723016947 +95,38,22,19.84939404,61.24500053,5.730617109,100.7689246,maize,27.24057149159221,1,10.281803884113703,17.994566879219242,422.457909913992,4.428488420270796,3,16.042398139731425,18.974444317620332,95.12348051277266,1,21.44507400630273,2,10.091052507469955,1.0804518903373794 +84,44,21,21.869274,61.91044947,5.850439831,107.2681929,maize,25.75189006255298,1,5.097418919103131,14.517392342677564,429.385195418356,7.232329331053059,5,17.830242001772373,20.01839654357567,151.19603335103704,2,6.918201163979182,1,49.486356806029406,3.1335152328474685 +77,58,19,22.8056033,56.50768935,5.791649933,101.5952794,maize,14.633805874356845,3,9.954512637879553,3.1753905233687707,421.1541811026011,1.2135070499086016,6,11.779800803995947,11.566829253446908,170.47018716051412,1,47.95011897171934,1,64.45366800148746,3.8524893551723816 +66,44,20,19.0781471,69.02298571,6.740000688,80.72515943,maize,18.419803551509652,2,10.19609968097122,14.580834811815995,395.02141470760546,4.5176373435578885,1,18.440338456991753,40.460341506329065,118.15717889321736,2,20.946101728117593,3,65.24624992163848,3.850412236324358 +63,35,16,22.02720976,65.35549924,6.272417541,83.73280082,maize,11.813093986606347,2,6.036377526282939,10.235403137206866,412.9796765255336,4.2463672030004735,3,6.7525777965247755,74.16172432657592,97.04732635043786,3,42.74847621343011,3,78.44525777731768,2.935125027092456 +79,45,20,23.80546189,59.24537979,5.715208817,89.9622014,maize,20.040852237917964,3,9.79578208333223,3.4866536347049926,361.78892261154516,1.9463361421270295,3,12.647647546034134,10.364412091450525,129.4272078394163,2,24.952145879349157,3,49.070343250123415,3.832164311714721 +72,60,25,18.52510753,69.0276233,5.773454729,88.10234397,maize,18.104902150279425,1,8.233334952892696,3.1383006712790062,436.45429971958566,8.915273023719175,6,12.357774060312721,14.65827099700151,111.92986900866882,1,3.287193052337367,2,79.35478134213156,1.0452981156980559 +67,51,24,23.50297882,61.32026065,5.584171461,64.77791424,maize,29.26467351986148,3,11.723327994690862,1.3117254962609337,370.47200441737675,8.769848378830542,3,17.65818882510448,50.94086544519703,185.46798667770727,2,7.825643121583337,3,17.055777258179205,1.7681207817849396 +86,36,24,26.54986394,72.89187265,5.787268394,73.33636055,maize,19.331725558632996,1,8.041045784160316,19.151375942437493,390.8523909704385,9.406392646116165,3,14.080907791274898,18.22983797970228,156.80611532643263,2,25.588450972813497,1,80.4747861467936,1.8969972900536152 +76,48,18,19.29563411,69.63481219,5.77597783,83.21030571,maize,16.30111470104408,2,10.958720364359795,17.95663201732216,363.0267580498317,2.0069585760048225,3,6.353349425990796,78.09352144477624,179.1024591202059,3,47.26280770116056,3,48.36953463874125,1.5303967710357687 +75,53,18,20.68899915,59.4375337,6.864793607,103.651438,maize,15.481196021626536,3,7.958415540105081,3.34396082120066,421.30261683249614,3.9099396124938948,3,15.249649775290163,98.6789299589765,96.76359102397306,1,5.056436652040841,1,67.94038247613823,2.3898966024658055 +81,45,23,19.32666088,68.034493,6.192360003,84.22969177,maize,26.570913509730723,3,5.187140972272441,6.686084000418142,377.58557225776354,8.673550763440826,5,6.289926997973194,83.34257056734201,74.37916665193858,3,48.975696363127156,1,60.998213770414736,1.9544220112424564 +73,45,21,24.60532218,73.58868502,6.636803223,96.59195302,maize,15.55441258248264,2,7.663928217899676,14.599401310253379,448.06664727103794,2.1835081036811514,3,7.028913355459454,81.54280710856095,156.22444744804625,1,17.633760302655915,3,18.153460725171733,3.5421068690387645 +71,35,24,22.27373646,59.52193158,5.826426917,67.96704792,maize,15.74570393886939,3,8.523533077749091,15.552504119160194,369.1809942625622,2.8698357838089,1,11.492621903101632,16.639564220213366,50.5137904119127,1,0.7452125730909365,2,7.521921937457732,1.1035824553247244 +96,54,22,25.70196694,61.33450447,6.960358276,83.20711308,maize,17.363971184414066,1,8.128524498155933,6.881733231566569,386.26761176327057,9.324581466058012,6,17.796992944319314,24.098825633487607,56.75320571605474,2,41.79832878736197,3,81.82954559295081,4.427178621334322 +99,39,18,19.20129357,68.30578978,6.11275104,87.85092352,maize,16.247215558153485,1,8.592736669086673,19.14323710780399,426.6477759505081,7.5340338706859304,4,8.19155341180029,39.94142728060136,80.9585165361021,3,34.24992127187122,3,21.849380643025608,2.6918938402807577 +62,48,20,21.70181447,60.47470519,6.708446922,95.71388473,maize,18.873230593947397,2,6.74855112668991,17.675737752571116,403.7226708414982,9.308117935977796,4,5.694703592205469,70.61923112928065,86.32779065484485,1,43.396310706341254,1,49.3664607137875,1.3242316175244104 +86,37,16,20.51716779,59.21235483,5.561510732,67.61013737,maize,24.77973159794391,2,11.122159845105017,14.859952978688689,366.9931264906183,8.784357098673777,1,12.021268024084389,47.362185695141804,125.67340631304897,2,45.26653983050325,1,7.374460104874214,1.192419163819559 +94,50,19,23.30355338,73.62548442,5.873242491,97.59081274,maize,15.79416857957826,3,6.683881117699223,3.9544340610647244,435.56820597269405,8.371270672689072,4,19.810718774191947,62.30045966805792,83.55117940975367,2,0.32283832237851584,2,95.28385816711362,2.244192422886826 +76,39,24,24.2547451,55.64709899,6.995843776,64.23845455,maize,15.119546282543407,2,8.456038632738139,4.804740482390574,424.6786165885767,4.043798906552058,4,11.716946760922799,51.11855813356543,72.83044951894448,3,41.52588147021503,2,55.53001222798426,2.039435540550364 +77,52,17,24.86374934,65.7420046,5.714799723,75.82270467,maize,11.787229925774788,3,5.89279330798145,12.592767775125523,448.923313693228,3.1897094180513594,6,10.358573006493405,10.030387975043597,118.75556175231566,2,46.38342263149758,2,39.942503331754274,4.068601268951715 +74,39,23,22.6265115,65.77472881,6.78073637,88.17251033,maize,27.32199653303682,3,11.193790191540508,7.629118354377753,352.904880652595,7.310568444434592,1,11.181807936951836,57.01158612337706,72.75606820523552,2,22.767171653057094,2,8.06113152367054,2.567881446027299 +81,49,20,18.04185513,60.61494304,5.513697923,104.2321615,maize,13.140268766996403,2,10.196607975259784,3.4832115534787733,397.4356174963996,3.2782065640579092,2,5.414770671610774,44.746384473151004,88.40605515252025,2,46.19669037166359,2,38.31235820233824,1.1168902416254172 +63,42,21,23.26237612,72.33125523,5.798423908,67.10225139,maize,17.31523561761122,1,7.849243177135962,4.095620810916083,365.4540671963635,1.9559260646112966,6,9.164329483559474,5.081291415735745,111.11511016724113,3,18.00706004626559,3,16.36278637811406,4.103625650038794 +99,38,21,22.88330922,71.59722446,6.352471866,67.72777298,maize,25.152751993732778,2,8.371245021323062,14.481232333635486,415.7635745919353,4.22405638094979,5,14.381723879647334,96.47137923862667,183.83601593606326,1,28.07849199312776,3,3.130629430080556,4.09170459542183 +90,52,25,25.97482359,69.36385721,6.822586546,103.2234212,maize,21.989640068600735,1,10.342484070593438,16.583664875326335,442.5406299173002,4.599840117844138,2,7.9111987154144074,50.71018392194593,117.77919195353641,3,42.68537362266174,3,89.51313080277625,2.874749671453941 +68,40,19,26.14384005,66.20569924,6.655426355,107.2361366,maize,24.61074581078945,3,6.174129143559844,9.881363275835515,418.1281007802853,4.2311166570450895,4,5.461923401636004,45.54954262915827,132.00618789090888,3,7.3265732997072615,2,4.535363379647251,3.0036833968613936 +60,57,24,18.66116213,61.55327249,6.121294041,75.03247667,maize,23.856300168189968,1,7.266250392248363,3.060514275280659,406.4379210866088,7.947931521547467,2,5.850574917101877,43.67301931020937,153.60052412097752,2,21.19415053899066,3,11.761352922430223,4.855731659520144 +71,52,18,25.10787449,55.97732754,5.790770203,78.16077693,maize,24.418857520447084,3,10.551891892896808,12.403388216696632,366.0808225771917,4.257854167941089,2,5.796895313422025,57.41985879079804,109.84339982647128,3,42.497834861926925,1,89.56139640297094,3.704657757419396 +61,59,17,23.33844615,59.24580604,6.47444292,105.0083144,maize,22.223938666202645,2,9.928580901950046,14.578745903626064,387.5294136828265,7.0157063581796315,5,16.94774083646296,4.494926318313763,191.48394745399847,3,10.23664028612304,3,68.86234029016124,2.7172288909959996 +88,38,15,25.08239719,65.92195844,6.455116637,62.49190812,maize,14.870988392069222,1,8.714014557173432,13.271228463631628,422.059478644224,3.5768913557038435,2,6.4368910550950975,94.98265612730185,132.18604929157254,2,0.9075737494833092,3,99.1862892798888,1.5964223622074538 +65,60,22,25.36768364,72.52054555,6.606984086,107.9124111,maize,15.390627022479427,1,11.342605622109431,13.228410890052553,408.2629918504056,7.741397000539391,2,16.914679824672614,83.86720481900898,163.20158796758875,2,43.69142349568697,1,31.129592541073205,4.86315181451786 +78,37,22,25.34217103,63.31801994,6.330554389,74.52082026,maize,25.52928577439126,2,8.27430762483107,16.000444339875692,449.11370290856553,4.996667305023241,6,13.233219736724235,31.699481285984543,184.61170512474547,1,49.47928111598519,3,33.96147441613348,2.8233235864546296 +78,58,15,25.00933355,67.816568,6.528631266,62.91359494,maize,21.418969534149536,2,7.381552115301711,14.920389000514367,382.73875023411034,9.32175524470922,5,8.082137765739834,2.4411191940279298,119.70615759111739,3,22.81370437253355,3,36.59740104019218,2.2958042811275843 +92,60,23,18.66746724,71.516474,5.721667141,69.93293255,maize,29.79614445339352,1,9.157163231052252,8.015793974322424,370.5752005160799,7.763379453224758,6,19.390152421220254,56.43846684781507,113.78725148276587,1,26.338107092766073,1,68.43907762157919,1.4017927449612841 +79,59,17,20.37999665,63.73849998,6.644205485,108.5054416,maize,22.522206330197566,2,9.02671098190191,11.163760408298602,377.1710347331981,6.831260208833757,2,15.069717386741758,76.4686880037692,79.49257075458524,3,24.74026417240806,2,44.73949871069311,3.245678594622081 +91,55,15,18.09300227,72.61024172,6.376651091,78.96159541,maize,22.160503048245175,2,9.293838829229468,1.4510100140192295,429.06159045266776,4.086805039836934,4,12.19923480815148,95.38151251706745,69.08408655433956,2,18.874662198070002,1,67.90228913508464,3.0462904527202275 +76,51,18,26.16985907,71.96246617,6.247040422,79.84925393,maize,25.203148573763222,2,6.433505948475184,15.85709417488098,357.1869299069623,7.055220948922785,2,11.609298480327528,73.88308625300832,51.708005261829,2,25.516598302408084,1,10.017235240924972,4.376680365359048 +87,48,25,18.65396672,61.37879671,6.656730008,93.62039175,maize,10.829340209454173,3,11.10019077910031,17.25430693483319,418.16223693941504,2.519905146189566,4,17.489674137213534,64.99441265306486,107.80064147950878,2,37.45337343765176,2,16.907763631837504,3.565962259806854 +71,60,22,26.07470121,59.37147589,6.2048017,85.75692395,maize,26.51320154509817,1,11.597738092839382,14.285398465332989,424.2189782376373,7.035000808853337,6,13.335002940254167,43.88541612350818,168.15983659460858,1,28.23930788314811,3,34.16548714568012,2.6256857111032224 +90,57,24,18.92851916,72.80086137,6.158860284,82.34162918,maize,14.204733068318305,1,7.511533173691788,2.905776711170591,357.474461591779,9.399769903758651,6,9.965544067494417,47.73270601351744,157.48038245153631,3,25.107784176170966,3,31.024511334985693,2.6750850648405997 +67,35,22,23.30546753,63.24648023,6.385684214,108.7603001,maize,14.644122612068678,2,5.673489469340444,5.600581230132036,442.15246295058324,1.9791372467954105,4,8.06972771146671,32.76954018189521,87.94052548675873,1,6.1797061831319,3,40.395851596587086,1.1305515437416358 +60,54,19,18.74826712,62.49878458,6.417820493,70.23401597,maize,27.443328277090266,1,7.0227758283696,19.398892142744344,427.49133042636936,8.568352691783055,3,5.1944866892726616,99.42325184651747,129.96462966860045,1,9.082351746830069,1,45.15901144503276,2.778748669210386 +83,58,23,19.74213321,59.66263104,6.381201909,65.50861389,maize,23.877505842772578,3,8.630778875689666,18.8342416418148,357.11508072541375,5.080115727781574,2,14.17929793257815,27.71774119138405,191.1797472721646,2,8.449766810802368,1,91.71148311263607,3.641138981661048 +83,57,19,25.73044432,70.74739256,6.877869005,98.73771338,maize,11.318500163338104,2,11.435626370270699,7.367576322865088,356.09461966300614,2.8785687512696008,6,6.310274384944956,99.38119141134656,155.84674722568252,1,21.194100477537358,1,25.961516341020385,4.1695748950379015 +40,72,77,17.02498456,16.98861173,7.485996067,88.55123143,chickpea,17.883769647178593,1,11.450264999979893,4.008740762379432,395.28055616597123,4.175852139267892,3,19.538137648105852,84.21808845378636,62.12247787492086,3,4.404796040752751,3,88.59923610206668,4.254476758311473 +23,72,84,19.02061277,17.13159126,6.920251378,79.92698081,chickpea,27.05509317878045,3,11.878308835344855,2.815813462829515,416.79872503934456,3.5492663410517467,6,9.493220478777129,33.115625927157,59.70228819804632,3,38.376003590536605,3,10.03302386589996,3.3074601310830722 +39,58,85,17.88776475,15.40589717,5.996932037,68.54932919,chickpea,10.333280500159196,3,9.041096449529492,18.263061222799116,386.13435748404925,9.379368392579806,3,17.48414845121379,38.18364849343664,144.33491944585325,2,41.70058183062172,3,81.63991031531191,3.851439578709019 +22,72,85,18.86805647,15.65809214,6.391173589,88.51048983,chickpea,19.18716957882736,1,10.862606230986676,12.997827664757295,351.4241212028633,3.1866846001678084,3,14.222679441745441,95.48733256486143,168.05913500315523,2,15.505807553254819,2,21.00468699185747,2.2100386315009573 +36,67,77,18.36952567,19.56381041,7.152811172,79.26357665,chickpea,17.398324281444683,3,8.922137133747439,5.893234491062076,358.1794686531044,1.791547617798237,4,6.416859995791264,91.48492904898315,50.23297973155482,2,34.14144683496081,2,90.47890613120232,4.345380240659991 +32,73,81,20.45078582,15.40312102,5.988992796,92.68373702,chickpea,17.39105459619854,3,7.319758147818643,12.36564398197296,409.70989206310054,9.925365521257197,4,19.202486716654903,64.85282445111888,83.42295509022486,2,26.06788480960312,1,18.190323031668797,2.2424091079834914 +58,70,84,20.6543203,16.60820843,6.231049028,74.6631118,chickpea,11.86449934520388,1,8.081814024118177,14.878447975920647,370.4081608733405,4.925810774280869,5,19.776496152204473,44.93988022978135,97.3670638513376,3,44.804873553847926,2,38.356629199866575,2.0813051934295324 +59,70,84,17.3348681,18.74926979,7.550808267,82.61734721,chickpea,20.80520694710316,1,6.061224857756229,9.744177629074498,410.0611703463969,4.286936211416223,4,7.529905341424893,66.08846416181758,129.0487393455678,1,8.3351726033142,3,75.23177871738217,1.495584903540391 +42,62,75,18.17912258,18.90426935,7.010570541,81.84997529,chickpea,16.855989834990762,2,10.898630488599764,18.717924563063054,363.1426364063716,8.710450464452705,5,12.162733880735614,27.82289426330574,124.79094402149043,3,25.919149052211477,3,76.09638378851825,4.29157406907057 +28,74,81,18.01272266,18.30968112,8.753795334,81.98568791,chickpea,28.194534550199293,1,11.020217163366166,0.7799968800970647,447.0634882386164,6.056725288855371,1,14.811510345687008,8.822399594300768,173.177902240913,2,43.72478954001057,2,53.22983104157473,2.4906675296205503 +58,66,79,20.99373558,19.33470387,8.718192847,93.55280105,chickpea,16.377314990448518,3,7.635890092464875,9.437445560045468,425.5362394391332,7.386713056513713,1,11.378194844023954,69.61771392718562,65.75033912259543,3,15.156948915941621,3,81.55634329958858,1.8551940428244382 +43,66,79,19.46233971,15.22538951,7.976607593,74.58565097,chickpea,18.649005552034154,3,8.678865601523844,7.768842035716236,392.423243434558,5.365131182907307,5,5.705618739932875,25.220063562570328,60.04976133364728,2,40.5491739950942,2,45.7308997454234,4.71097589643278 +58,63,81,19.81344531,14.69765308,6.515499549,78.96514709,chickpea,16.226574044304257,2,7.22876068290824,7.443739948013577,410.8708708885899,9.552020013630946,5,6.047475623132928,88.80300646686628,62.91307477345761,3,39.73238481231317,2,37.463930604593074,2.1273331264862088 +23,62,85,18.97424756,19.5161216,8.490127142,80.7108745,chickpea,25.460091273127905,3,8.790221039706333,0.8594327021561554,350.6622023844562,8.630685339710386,5,14.114808216871875,47.11808907845859,124.47594178376303,1,47.65292702206749,1,4.679212126598964,3.1638571195329495 +27,62,77,18.19737048,14.71070537,6.576415562,70.18185181,chickpea,16.66349434113963,3,11.798227277721587,16.23998215243586,447.03713568310536,3.5209287062451082,6,9.98946651264458,74.21731936393104,186.9701868718644,2,26.09824113735863,2,63.82922923090456,3.304724950605886 +28,72,84,18.72963144,19.18197264,6.481783043,71.58010169,chickpea,25.13860270697137,2,6.405853888337862,2.0205707301865927,391.68579272168597,3.6945398232044355,1,19.192294390421086,29.547742012218293,181.73522221021398,1,18.598374367275373,1,1.2339438196844466,3.373610863005012 +50,56,76,20.99502153,19.8601304,7.966605025,73.50734019,chickpea,18.08944050167041,3,5.381545148145534,6.6143837410652235,417.65177523123464,7.204964914929031,2,15.415367684821728,1.6583647777020705,198.7702532837622,1,30.831104140378862,2,3.007494111957776,1.275783348432701 +39,71,84,20.28155898,16.39535215,8.140825437,82.52339655,chickpea,17.54654752624201,2,10.421650684226343,19.693778168217243,398.7713767979736,8.300831404175232,6,10.466471737214551,66.88799366818279,119.37304155990783,3,5.598804003963193,1,49.23509262359846,1.0235188723538826 +25,78,76,17.48042641,15.7559405,7.228963452,66.96980581,chickpea,10.381990876237381,1,8.906908007224743,3.502025459572713,391.4503141600682,5.4842219398852965,1,5.84610629395161,73.59436066405578,195.96949710013843,1,32.76586513338042,3,55.02836477689708,1.2074488799675613 +31,70,77,20.88818675,14.32313811,6.492546046,90.46228334,chickpea,26.23268612268895,1,6.773495330284018,2.4256667455555636,360.0131126772707,8.593841230060736,6,19.87231327063354,94.37990031520002,133.98925020705144,3,24.055575755674575,3,3.0274411275390767,3.55293876431195 +26,80,83,17.08498521,16.14565756,7.528599957,71.31007253,chickpea,21.293774850946086,1,11.451315272126987,9.412980646594814,449.1563024127249,5.542487017792919,6,17.465459830598448,91.76380435918469,148.4426525548937,2,25.571018422622526,3,48.01280467311895,4.158550148423668 +25,68,77,20.09340593,15.11279612,7.701446446,85.74904898,chickpea,21.98104519816333,1,8.018334118522745,10.399693722488198,361.5517261115133,4.94166353804454,3,15.437716441205566,80.56162600819718,80.62196063019674,3,19.915956391241274,2,33.31988911005192,4.5617193924396435 +31,78,76,17.57212145,14.99927489,8.519975748,89.31050665,chickpea,10.139559488060783,2,10.554765223975345,18.263015405896326,363.8349607590321,6.153485072967357,5,10.991956128483594,15.333488382127069,142.25218145025667,3,1.6576727114613332,3,18.863385517796804,1.2909300856064343 +60,68,83,19.12065218,18.43475844,6.620900869,85.52950164,chickpea,21.162223655975843,2,10.407197816131974,6.778676684972607,427.21444324032865,4.967753295665488,5,12.368024962474003,54.18300583042846,114.0648347834285,1,23.72703730082816,2,1.933926945659592,1.8985416627632659 +59,62,83,18.57665902,19.22008229,8.104396058,72.94940441,chickpea,29.24766608564906,2,5.3058959378719965,15.467749192548176,363.2005448225677,9.283973810705183,5,18.047567243311452,74.5183297311113,83.18937561577343,1,9.501954565086946,2,37.49928518284125,4.26523446281083 +22,67,78,17.16606398,14.42457525,6.204090835,72.32667516,chickpea,12.749978307351926,2,5.253313913059135,8.10095786697239,435.3218480028378,7.738915752519131,4,13.63673071192061,78.92375477593997,174.6626620899322,2,37.322627004007934,3,45.04181595380484,2.7486067592081973 +36,65,80,18.2872007,16.67921616,6.051091339,74.87445574,chickpea,19.94660520390211,1,11.595418776858759,17.22929785325301,416.1453279400823,1.5847319028873834,5,19.860237805264497,41.021404124268976,73.87188460178672,1,12.33402567788746,1,60.3362807784382,1.9280267901203714 +59,60,84,19.03025305,18.66725565,7.690962338,94.70992037,chickpea,28.161800463685136,1,5.35860840343292,11.400663377093297,438.4098402351785,1.2909767978774576,1,13.401840125739605,62.45447661167408,68.57463046394965,1,48.66431634249764,1,74.93251760419156,1.550002163350534 +54,77,85,17.1418614,17.0662427,7.829211144,83.74606679,chickpea,14.94566795676447,1,6.3237446349480475,6.741236943789364,350.99621521094826,9.448286237607052,1,16.62088061236865,26.402047907932015,64.467382468398,1,41.23216857144729,2,73.9864136818539,4.242441075863542 +43,68,81,17.47809436,17.93253975,6.761599706,78.92060234,chickpea,29.54751947015624,1,10.923211331694134,3.7618393865345467,405.73129251034396,9.97077940130214,2,14.158485156909475,30.606500410386218,169.62259056532866,3,38.19704692810651,2,99.51482648398853,1.107302768652128 +28,76,82,20.56601874,14.25803981,6.654425315,83.75937135,chickpea,25.714360682572632,1,10.15340488219801,6.063440970586131,381.3442113540241,2.3394109563618617,5,7.560792781918695,1.0479509166304357,93.7207325793955,3,31.941322136311452,1,0.05491132086610229,4.100617267436344 +42,79,85,17.22385224,15.82069268,6.129533877,76.57580954,chickpea,18.753296686393078,3,6.570016163496534,6.712612112472782,367.359147468591,2.8232673945691253,1,18.263335641553503,46.804244810117545,152.1965332537667,1,45.03297141381157,3,64.5887153801255,3.4038757953687373 +32,60,83,19.69141713,19.44225438,8.829273328,91.76071648,chickpea,19.77662735497598,2,10.750109900367274,4.695382677631397,389.42411117733184,5.054603037725947,5,15.443946159831247,32.47549497326977,124.81861532958483,1,30.408181559796088,2,53.25354332039801,1.1421687978299002 +22,78,76,17.84851658,19.09172907,8.621662982,76.32470713,chickpea,25.01219432511382,2,10.001646028101856,11.70782119875091,379.8787571834583,4.137592729944506,6,5.138515867656392,28.750413183602387,150.64694806976306,2,19.35536419441675,3,68.83013440351641,4.543012802391944 +31,79,75,18.8202251,16.1074793,8.204862075,89.73119396,chickpea,23.165716293534224,2,7.453287233131013,8.154860995882178,401.0335580904291,7.705392048729591,5,11.862147575474378,39.970107618720796,84.67342716065369,2,18.849226113348692,3,67.16875836034355,2.978148693555786 +28,58,81,17.47500984,16.54314829,6.18042747,93.35034262,chickpea,12.154437978074892,1,6.789587059964622,16.672068071226292,396.13297148696523,4.5995278560711546,3,15.010946080969953,53.567358759232555,64.51951608323938,3,49.7797569179059,2,64.45630231612026,3.41867786828623 +57,58,77,18.72649425,17.58406365,7.978996755,81.20176515,chickpea,11.480270533958736,2,8.983284085491416,6.388832413443726,436.5363175124824,1.3076643952658884,5,12.527163233589338,63.317287181715955,101.3488154533488,2,34.82675934639328,3,89.18276045752143,1.0702891185252104 +49,55,78,18.65580107,16.17772668,7.863113671,81.70769297,chickpea,27.452257018012133,3,8.888531866010904,14.39143305071456,418.85534874282257,5.062467373820615,5,8.694667989288202,57.74020376998982,115.37056598476032,1,6.4189904008074175,1,33.508788569727535,3.8552569435433797 +46,76,77,18.2356751,19.68538502,6.967843048,83.74879344,chickpea,13.26077622047605,2,8.617777166746158,3.83402629385754,402.0965807659653,9.022721388055322,3,18.517849301719927,52.28593901846396,199.66489031281287,1,9.876464874029601,2,84.68987868234443,1.4904177729925872 +54,61,77,18.81198127,15.21618225,6.206582193,77.5429424,chickpea,24.21622413859022,2,6.709050051320754,17.374977039241777,414.3662135720034,5.12771682691161,1,17.202816202051224,57.26210228964287,156.58038863375015,2,32.81091640804333,3,77.2448468258024,1.8303128510112563 +38,60,76,18.65054116,17.80852431,8.868741443,77.92798682,chickpea,29.994929058198117,1,11.580893894371329,8.083998947591962,381.4319783584996,3.970762390066988,6,17.31392837257514,19.28984363934344,189.12405210963172,2,19.855658392753533,2,34.910097682929894,3.951398292021683 +59,55,79,20.36720401,16.89574311,8.766128654,82.2545577,chickpea,27.082363240405464,3,9.78106760211675,10.683573288033912,412.4025684073879,4.068531778380055,6,19.781030483746207,94.17638438111074,142.9774075782269,3,11.978986265868796,2,83.35936421613933,4.662246593692281 +36,76,75,18.38120357,16.63805158,8.736337905,70.52056697,chickpea,20.394642863959994,1,10.543237515375498,2.6340826916566296,427.44508984548213,4.618156634145274,4,12.253886695549047,28.592842017516396,59.36474248096073,2,4.523720464597847,2,4.330072294778153,1.0527331983028447 +57,68,81,17.17012591,17.30457712,8.081095263,72.78624223,chickpea,22.790242154768656,3,7.622832633130679,13.277217812653122,370.41757936505854,8.342449188934314,1,5.9478421724018,40.11809546844381,71.38914955068967,1,34.272257114039775,2,47.479253011405916,4.555196851556014 +35,66,81,19.37101121,15.77458129,6.138243973,85.24819851,chickpea,18.080024986404176,3,11.885275629711519,3.2854104016273955,432.5100928196487,7.338427891294119,5,9.945728890602242,23.07550038345002,194.7114229527087,3,36.803754601197554,3,98.951339317668,2.656517606693801 +35,64,78,17.92845928,14.27327988,7.496645259,85.37378769,chickpea,22.78968891335932,3,8.819669834648794,12.09481971203267,386.61326036121034,6.9422815465434295,6,18.49712625938062,85.52825243705855,139.34980036646454,1,6.893873320551536,2,2.0558238807251272,3.3481021864237444 +52,60,79,19.45339934,18.23490739,8.380185271,75.6317566,chickpea,14.998251504511178,3,5.1044124309370424,10.161872166177151,360.4512676755268,4.937593045300533,1,8.021766292066356,87.1750003899084,54.331370934796176,2,39.49411297448684,2,93.85456717991644,1.616745132642412 +27,76,83,19.12829388,14.92241479,6.289614016,89.61857826,chickpea,21.541871460057635,3,9.293249155490695,14.585715958846176,384.25766453051637,5.067315382080652,5,19.16398949622991,56.0094689089333,89.09424079466251,2,32.06278950828626,2,30.338527964256834,4.430763756113667 +57,60,84,19.1034283,17.26184541,6.586777189,75.49101167,chickpea,26.772502737496232,2,10.74922772371636,19.822042342362703,408.3103542305387,4.7958284134777225,1,11.52580421177343,6.868434212639296,96.524692677145,1,12.114823267848374,1,63.088957441034566,4.574147963115351 +52,68,78,17.48504075,16.96070581,6.89655198,86.05078037,chickpea,24.623966249910847,2,8.109775352355642,9.509722211659684,424.1117933015513,7.346606373702374,3,6.1778462020034,0.9029841632646973,109.26400411388622,3,40.950019979554895,3,35.022759272092884,2.6365560044084133 +43,79,79,19.40751744,18.98030507,7.806747656,80.25064637,chickpea,26.38601641316534,3,6.450882076451939,1.1549250180758164,442.7906871686305,7.273742513464672,6,17.188720352450822,30.841273427081596,86.06635344157867,2,17.987291880327476,1,85.10389333549568,3.8818378743160453 +44,74,85,20.18649426,19.63719995,7.150681303,78.26039559,chickpea,19.628462685667692,1,11.976878669190004,17.700936230060265,414.3377381232037,7.636493633259607,5,8.572042500914904,22.691659831675693,73.09052931264073,2,24.663364657435803,2,4.442606007810479,2.9406317173914562 +24,55,78,17.30287885,15.15405941,6.64919573,75.57790384,chickpea,18.531786438595475,1,6.809877242010023,4.945121310852776,430.11625966861294,6.108061461286077,2,8.156431144484284,48.72939718403902,189.5251187999463,1,34.726262939185936,1,4.51277406671724,1.7132365773489244 +29,77,75,17.50361137,15.48083156,7.778591618,72.9446671,chickpea,21.73801419770495,1,6.0010352531995945,13.047696607894215,396.7579187759338,9.068331969319035,3,16.136981326205238,73.04698140420949,150.56251841857284,1,15.717363309940907,1,94.3862133483835,1.233230609621645 +20,60,78,18.17234999,14.70085967,6.358740355,90.7760707,chickpea,22.03614471986157,3,11.560346563847705,8.216987459771072,395.71468852391325,5.0304498265967466,6,8.09064454173304,59.805365613884966,197.42921699390277,1,18.879050223833687,3,62.230132218132674,4.9076588257096505 +56,67,78,17.57445618,16.71826572,8.255450758,77.81891424,chickpea,21.059925818094012,2,9.25351685429045,3.1646692989827807,423.9537522666036,4.359266400005853,2,10.437174184557133,39.973371102215474,84.70099219371035,2,22.934935262785388,2,20.173265540442088,3.177390512037714 +37,66,85,20.93175255,18.91295403,6.456148474,78.06910795,chickpea,24.822017865932946,2,6.279108826428956,13.937731639229218,402.3813582419315,4.40382497948817,1,6.998926575830079,44.61665719266937,80.68377651171865,2,14.80201568672489,3,72.36117605525541,2.2865142751285603 +49,71,76,19.71098332,17.63879418,6.613072145,85.57925437,chickpea,29.943211443255056,1,5.552101442312158,15.357823322293207,404.2258598551211,3.590525303563381,3,6.247451740513538,19.667948104339295,59.70007420660832,2,29.683939677412525,1,86.22439988452804,2.9245945426638635 +59,69,80,19.07937684,17.86754927,8.165359297,69.40619137,chickpea,29.0241727125213,1,9.49797946975448,5.1699533029442435,387.4626467983097,1.2967160895827083,6,14.53689580252676,75.79448728289555,119.74433700694674,1,38.53400849279675,2,72.59603353048021,2.9765829537138937 +20,79,77,18.54988627,16.02542689,7.64867466,76.32565249,chickpea,11.667579744366584,3,8.362777405376711,13.021733879508693,399.64971030590135,7.2086334904446545,5,12.056074007669194,24.32184327179201,123.37947873472778,3,17.14592330665935,2,39.243362091305066,3.381918499902772 +24,56,85,18.19903647,17.41333199,6.545888558,80.6405403,chickpea,21.800074137833455,2,9.975782300155924,6.182987465415881,400.3889374474538,3.6579556972777723,4,17.908771661114983,77.86972155525973,54.30901306223228,1,31.181639561501207,3,45.34785000785653,3.7107508224793837 +51,72,75,18.88852533,14.99451145,7.104224797,80.1113384,chickpea,18.067476026225933,1,9.113804472904881,16.189198537386485,388.0564188988351,5.797137148817807,5,19.032606613544026,37.86435988844825,175.87754398196898,1,15.524798271401712,3,95.35553531909812,4.73147263985435 +57,73,85,18.49311205,14.72115044,7.358099622,91.94595352,chickpea,24.037268835277033,3,7.3850957630104395,15.690749108817098,353.548727718767,1.0715680112887727,5,15.045096317237084,33.676846936167216,117.32934165021186,3,44.44932814010678,2,39.925280914533005,2.56867121154816 +22,64,82,19.48974337,17.17260319,6.4740245,87.51312796,chickpea,14.23907603538938,2,7.166337739065072,9.035475584384248,364.1550525156102,8.82953222632951,1,9.284375156753711,31.635749428267868,190.95066355422324,3,16.85821321468648,3,72.85928563426684,1.9816934582464287 +52,73,79,17.25769499,18.74943955,7.840339389,94.00287214,chickpea,11.8285937485254,3,10.377411334696063,4.345740564364067,439.90367424049504,2.6967908007056756,6,15.066569698868319,56.10461154252149,93.50844436839998,1,24.028511899407256,2,17.89926919969985,4.935058237322844 +29,75,75,19.62416326,18.71483156,7.064790365,88.4585692,chickpea,15.897071974750855,3,10.692051704546593,2.5148352147937936,444.8943044003912,2.1716721951593843,4,13.800550372816897,1.381714488530561,98.16274814973391,3,32.50785587732725,1,79.35040092523936,1.536636292259733 +44,59,78,20.67526473,19.85388984,7.599033472,84.78344008,chickpea,19.947621318367467,1,5.06580304569626,12.103503343547743,444.81532222968514,8.745087331215563,5,6.385149119274406,44.32343257649044,162.31865569220687,2,3.716450900550805,2,12.423317014506342,4.244842165443473 +41,69,82,20.02381489,16.63294455,6.715587232,68.97806542,chickpea,28.250741125292,2,6.918300610057977,7.1339305723449975,371.7952683514761,9.644479873220309,2,11.276553952503424,39.822183980201906,143.98815300871024,2,35.647373133767516,1,23.948880802495054,3.3085353644119397 +52,56,85,20.1187446,14.44228303,6.81712422,88.68168643,chickpea,11.656547185356438,2,10.4822757686118,15.711767524414558,364.31288270478854,3.9234126298946763,5,14.953278737396838,79.01579291080839,197.01967181900739,2,45.74301912923368,3,48.79919369825747,4.46227898298532 +34,76,80,20.65691793,15.84572566,7.985417393,65.23811143,chickpea,29.979965983368174,3,5.4670364657330985,14.213162516176608,394.8182140165517,1.1038859824349625,3,16.95776187219184,62.38827838095349,126.45468590001254,3,43.39608271486477,1,94.73458253173692,1.375606727669584 +42,74,83,19.2582557,14.2804191,7.545258424,65.78042032,chickpea,27.393446589245826,1,9.602328608798254,12.273020487978396,415.5909006385842,2.3968835784240206,3,7.188379502947527,71.19900806439094,53.935291120351756,3,37.32316481398094,1,56.84241005350885,1.3948924059825334 +34,71,79,17.927806,15.85622899,7.728998197,74.63872762,chickpea,26.478581969419828,2,8.966390791268909,5.328551803833593,392.708684942077,6.682422762463133,5,9.211589209886675,47.37313922383727,116.55764357221682,3,23.826744858387872,3,29.534592474233257,4.895861790931178 +27,73,79,19.16288268,15.83500655,7.354973451,82.69766829,chickpea,26.67850111240156,3,8.181205351753285,19.147162490050594,353.8579363574177,7.07900220686989,4,11.416063437046578,51.2371668641043,160.1714048413134,2,32.62732094034127,2,61.1928752093302,3.2962393628241973 +30,70,79,20.26942271,19.96978871,7.313122235,69.64449182,chickpea,19.738529986535294,1,10.81564759099324,0.038107266674445306,378.4954209806302,8.556879435010451,2,19.380378214359432,43.66419712766961,82.32626339444275,2,22.19490124653591,2,88.28464864146562,3.166157637840389 +57,57,75,17.09104223,18.25142068,7.785039076,87.27444866,chickpea,28.383699425159037,3,10.277683329522995,14.727418553203044,389.14465669767526,5.528464580441527,5,12.911504026779463,29.07413498105963,171.68021322194417,2,37.3084693850182,2,15.831080804176045,3.0490942624768955 +27,79,82,17.06579293,17.54024066,6.307004923,70.87150577,chickpea,13.357899296620008,1,6.742360603458354,10.577598122345044,400.56072957554636,6.218240962356194,4,7.798792878871041,68.90982568302124,111.93346662648949,3,20.32570088026412,1,9.496897998917964,3.3623404822215175 +32,71,85,20.62767492,14.44008871,6.403982316,92.06630306,chickpea,26.94225128596082,2,5.60519369766758,19.356695033037187,441.32254461885776,4.011070489191036,2,14.578949129705629,28.675876886304152,63.16795283211216,2,28.45687245436266,3,97.54006089629037,2.1973555173970656 +31,76,82,20.8248451,17.85057083,7.599279991,79.20509212,chickpea,12.77954673106138,3,9.99751534602387,4.7095459202198775,421.8805448358208,3.9352307160006337,4,12.575815285817331,61.59077512151969,135.55733176017026,1,46.837568688207064,1,53.07589838369149,4.160453588353617 +33,75,84,19.46210401,18.72831993,7.217018459,68.81405149,chickpea,27.096972090022707,2,11.139780759827515,4.29079893966585,372.89853208340304,4.729604373858946,2,19.622742721327086,67.0398326585035,161.1763131651524,3,47.10939508790569,3,95.0298103848115,2.7379722699329965 +47,80,77,17.18248372,16.42891834,7.561108006,72.85017344,chickpea,28.901180479380105,3,6.194122276807066,7.28659022012176,407.5843319159114,1.734952506599186,5,9.867624322237404,44.71999901031345,72.60430392587723,2,19.914144095388707,2,71.01680187825711,3.2574921904080605 +54,62,80,17.48911699,16.39055394,7.489545074,79.45758333,chickpea,13.320788058022774,3,8.28679838193019,17.709909465858225,365.2646258124796,4.593647019650798,5,13.142181692018003,96.80566279680207,174.13136626885782,1,12.411896697916841,3,44.19784943857603,3.5850183919945056 +47,79,78,17.48395377,14.76014523,6.609696734,65.11365631,chickpea,27.295604427348373,2,9.313509896804556,5.512746680377834,441.1628244809847,4.040683196425894,5,19.0899986927827,21.188772063302576,136.6628479939916,2,49.25603412813157,3,69.32193499628833,1.4752834240243193 +35,57,83,19.48316794,17.44534641,7.476800943,80.4986291,chickpea,12.970284474954587,1,7.748139292188885,9.13463713950448,436.44334394010855,7.870700526982329,6,19.383521302963025,81.95542228250365,50.954417880407,3,21.239167833863743,3,16.360922408510092,1.6731246778214448 +53,73,77,19.71359733,18.09665739,7.325451279,73.64476535,chickpea,28.483221209014367,3,10.16746638856756,18.910283740944738,383.31553500517555,5.746119021266241,3,8.756709673821002,21.636779426444242,78.15760186642788,3,28.895495531649168,1,65.18168545370106,2.7846347274877954 +45,61,78,19.48649305,16.06240074,6.489389282,81.5284269,chickpea,22.84477059004342,3,8.671086425745088,15.886427846506027,421.39652925228285,2.2161530325290575,5,18.066052688847243,24.98202532059447,72.44552108567827,3,19.88342270384856,3,9.489527802817188,2.3725706962308113 +37,78,79,19.95264829,14.82633099,7.786366322,88.6810311,chickpea,14.90137643538507,1,8.870745425614995,16.311546794430157,353.41805003064263,2.17605794976177,2,13.7367972884811,64.77856576813026,127.87082240192697,3,13.662100147944123,2,27.078443637245396,4.539683666397325 +30,75,81,19.41789736,16.80472243,6.408437886,68.4951189,chickpea,11.284287837910371,2,6.692229384622463,15.073056700417588,416.2721851459184,7.725692891917048,3,6.7737566418159485,14.865382099531022,117.70349254221927,1,18.472479201934906,2,75.26848304306128,4.295481286566759 +37,55,82,19.45591848,18.02235902,8.423873703,78.44910564,chickpea,16.865705857598286,2,5.453685934157896,14.22018801676639,366.7393557548683,1.7952618037915802,5,10.331295895379824,85.77149912167982,55.61326172153046,3,13.148044024296695,2,82.91241152596598,3.024963024012242 +53,65,76,20.19137759,16.41998269,8.719960893,77.33795356,chickpea,13.24948353816419,2,6.27764189651712,16.129956469425334,449.2043745512914,5.148700455292939,5,8.814358560087827,88.8643409824953,108.9088756920103,3,18.8893402706219,3,34.64980437045393,3.2243447722358236 +22,60,85,18.8392908,14.74071856,7.811997977,94.78189594,chickpea,11.531019543181118,2,8.719907736566812,10.95381276885077,385.68783350805097,9.357082804345456,6,15.466187795103231,55.69802681900854,169.08993056819628,1,4.12780122100111,3,45.01060714218049,1.9311992547581731 +60,61,78,20.71219282,19.83643308,6.317153205,94.03659867,chickpea,11.107673771458952,3,11.329621805341105,13.37876285161759,377.5738891198305,7.662949766429506,3,16.09784138955446,85.66741800375823,95.44522793873355,2,19.848735896650698,1,51.764659087844244,2.9986718985926384 +42,67,77,18.99424448,15.9362937,7.114405288,78.69707199,chickpea,15.17605479052179,1,6.507387069401224,12.092064585428178,372.1267453975246,1.0857357981999942,3,5.572470770081403,67.64023822917551,188.77076259945255,2,47.45741402286928,3,30.920927474762582,3.1895135713595177 +39,76,76,19.96837462,15.57324389,8.135900726,69.15759062,chickpea,22.35165870933058,2,6.705466281529003,6.854969554048944,379.66509306607804,9.950583145807352,1,6.083519936165048,78.48669415073385,168.93573909544585,3,35.96179276276575,2,95.71311968311385,2.411262909731253 +35,63,76,17.81564548,17.60756635,7.714153038,90.82097601,chickpea,22.86506219543244,2,8.562131836120892,12.37534192699465,445.5145991858032,7.242635841534607,5,19.386381920212447,41.908834460254354,132.28741642007822,1,8.148822504837966,1,44.14971996991248,2.165712295561095 +30,65,82,20.71424384,15.27824066,7.103798069,76.77888672,chickpea,10.755513474047405,3,5.103020707188653,6.329701225232349,417.7462599925155,2.5486233514651806,3,11.994878715822388,93.44835440267599,152.22147269372917,1,27.044044326745155,1,37.58653143248838,1.6682542867978989 +57,56,78,17.34150229,18.75626255,8.861479668,67.9545435,chickpea,24.775945985932008,3,8.89286478561668,6.6709417640984,406.9590440688067,6.803641433200147,3,5.884098929968645,58.06263400753764,79.10303559723077,2,32.54091331001322,2,24.148037816860757,3.517111668376066 +48,65,78,17.43732714,14.33847406,7.861128148,73.0926704,chickpea,26.67336072523073,2,7.532076310431481,14.435938323228658,416.76434959592757,3.8317839119759354,6,13.83142161773423,55.81753262004493,95.00626343603635,3,39.65051744082343,1,57.718758648235244,3.9843006645407133 +36,56,83,18.89780215,19.76182946,7.4526709,69.09512477,chickpea,13.838533161313347,2,11.090525679529073,9.135163915924757,385.37795310339203,1.0370879489545077,5,7.109190760200254,15.466718504380594,87.8917147529627,2,23.040592225554747,1,97.91862091978543,4.887664829804454 +40,58,75,18.59190771,14.77959596,7.168096055,89.60982451,chickpea,16.847427570162445,3,8.95693572801621,13.813972834958118,386.4246741195086,9.736556900052769,2,16.236014291055145,38.96876783618588,170.19914880503742,2,33.17205453163092,2,47.78046586734811,3.0548790014330405 +49,69,82,18.31561493,15.36143547,7.26311855,81.78710463,chickpea,11.817008069604121,2,9.983741398127764,3.72551643090566,351.00278533495595,6.853570360196736,4,8.056156524932218,0.42233403530306246,97.46521532639953,2,37.93690685575367,1,40.623162030357584,2.9743256010311767 +13,60,25,17.13692774,20.59541693,5.68597166,128.256862,kidneybeans,27.512468581866077,1,9.51763865893584,5.079438167852535,426.6788016098191,8.67648678749785,2,12.053498579678289,69.17307590236025,111.48571649144034,1,22.48715713675571,3,0.8898600636219167,3.6430758467098956 +25,70,16,19.63474332,18.90705639,5.759237003,106.3598183,kidneybeans,24.96345299589259,2,6.231957785769042,19.755397300613357,352.227210564526,8.216576162766309,5,8.925164988687051,61.752278833210305,196.43177752092893,2,43.89646255403537,2,97.72894797864235,2.6654565724375163 +31,55,22,22.91350245,21.33953114,5.873171894,109.225556,kidneybeans,17.819731660719924,2,9.820474927772086,4.074698874551961,409.8125315782006,8.212299479078375,2,7.166826458341379,39.75861810136381,144.96347686198936,2,1.4951797795991206,2,76.76771874469912,2.501529534706752 +40,64,16,16.43340342,24.24045875,5.926676985,140.3717815,kidneybeans,25.651885974975315,2,7.690638875975539,9.566861977978707,396.43427029396656,9.52141924306254,1,8.559228963108206,12.564163735655033,148.3329136887229,2,9.757608685232338,1,93.89634676888019,4.3244265535518505 +2,61,20,22.13974653,23.02251117,5.95561668,76.64128258,kidneybeans,26.504125760169867,1,6.882398075708998,5.003979276946405,353.2220330655377,8.099101928366357,5,19.513415420315063,77.16387143755196,129.83547481040168,2,48.81378737722203,3,71.97717208510502,3.745214112505995 +26,65,22,17.84806561,18.77621951,5.949949081,143.0984171,kidneybeans,10.02426042785048,2,7.985858574754115,2.334916088242671,426.15200265550516,7.491014159146264,2,7.416228745136397,79.62204538303598,72.4624461797907,3,40.62007937803436,2,44.00618661019382,3.246431419554556 +17,57,21,19.88394011,20.31564139,5.789214289,60.91974792,kidneybeans,16.541197372535187,2,9.801117924182222,7.021644099906704,350.23251557818054,7.0002545963959095,4,9.439151631851555,59.98134784433788,77.50585042481956,1,5.96468717447281,2,42.747793877435534,3.306133142094209 +26,80,18,19.32509638,23.3334788,5.581021521,104.7783947,kidneybeans,20.603631039841375,3,8.10385608287084,18.202389887865394,428.9131703124481,9.016377741120369,2,13.225161982995917,5.3734039376570175,93.15715494443177,1,19.110674185156682,1,96.61824407150141,4.514289968223743 +17,59,17,18.4167001,23.42829938,5.689858133,132.9801054,kidneybeans,26.774617745085003,3,8.691696276342428,16.658179973693628,423.05915011134505,5.527089099084947,1,15.600299101848439,88.8448622957402,187.89880840317812,3,29.071251220413853,3,32.915348262842905,2.097447946257074 +27,59,22,21.81167649,23.20591245,5.794158504,130.0608093,kidneybeans,18.425932510115622,1,10.147254544800163,14.871133659263414,377.45792000289845,8.151395236938576,4,5.710080187805279,42.16774630447002,54.301752600177146,3,49.608117307012435,2,8.095278372948478,3.4583032103799356 +28,58,24,19.72702528,18.28173015,5.748190463,143.7630894,kidneybeans,24.964031578852484,2,6.647306472633382,3.5852700220927747,391.48191593438673,8.313992460509617,4,15.097344293602976,94.57030281686973,121.03178339074496,3,46.65186634596271,1,91.68471637793031,2.7420847301608986 +25,57,19,17.15432954,19.87070659,5.566522896,87.99669731,kidneybeans,13.665492297792216,3,6.863102874634539,14.360847004647791,436.6565185777047,1.5731094840819644,1,19.18412348205738,57.365928519711176,58.408861770979684,3,49.518164909454086,2,4.432372028135855,1.4123090099577538 +28,80,17,19.62207826,18.67170854,5.809419584,144.1567454,kidneybeans,28.832605636216627,3,7.29836308144607,19.37008974873278,387.735687781983,9.656784904016089,4,6.792739945280974,5.951117689842144,158.52983957528153,3,24.958203789485207,3,23.278540959691295,1.8968957370157251 +25,60,22,21.63149148,21.17919701,5.887263027,134.3649948,kidneybeans,16.42780693223697,3,11.425776339303894,0.7916443983891464,441.78424862329877,5.855922741927069,5,15.6555585516641,94.73907299687858,156.23339403126585,2,9.673273360125595,3,24.383215188259122,3.6788000179508273 +12,78,23,16.06522754,18.72479695,5.99812453,88.06638775,kidneybeans,14.433044818296704,1,6.712534135317046,14.866213728011783,398.26558862864454,6.248739520014743,3,19.314035332101977,11.947188569120959,98.62938788443682,2,37.65504860400382,2,18.36143055853744,2.0519498471598614 +6,77,25,20.61162204,24.36314135,5.792744849,69.63833855,kidneybeans,10.805769979411888,2,6.061166628155409,8.841607170248885,389.2394648345179,2.7059173765699454,3,7.374789279495772,57.81940592659522,50.515415908905894,1,9.03447217479702,1,40.423921328160496,4.932472004767453 +22,79,17,21.42451099,20.39659714,5.912289889,116.5206923,kidneybeans,14.151682493405776,2,10.600961754558448,10.000207584868823,377.14977679742304,6.221452094595055,2,8.411626508001552,75.99078301754139,95.35114280105623,1,21.194788657693593,3,5.399964479848263,1.2255621921110782 +27,80,15,19.07096165,21.21092266,5.788386951,86.21917578,kidneybeans,28.75197472595325,2,5.356433858095645,5.549910977295733,376.7975400024399,3.6643912617873555,4,14.212065037215803,16.411257491631627,118.48322037922809,1,37.323905260776236,3,25.75968849566118,4.241539636932313 +10,55,23,21.18853178,19.63438599,5.728233081,137.1948633,kidneybeans,16.017119950826903,3,8.115663155614389,2.531698604005108,354.05840249070866,4.863938863798946,5,11.871470998866153,18.626052095979485,131.59770333561914,1,8.496116953152983,1,36.688059027811036,2.9989138583474153 +23,65,20,23.0429097,22.42610972,5.833940084,108.3684316,kidneybeans,10.080568134375081,2,6.738603884521762,4.940401335009799,430.5793671322704,3.5709955694792055,2,9.560659067789214,83.51429876538478,186.9055514633362,3,18.53726217511485,2,15.639886946926119,4.439428815447364 +19,78,16,20.65375833,23.10538637,5.967533236,67.71768947,kidneybeans,13.94424908706781,1,9.270579755233387,10.270693106869755,365.23049751585535,2.4339983989857856,5,11.863002920794692,92.6823007422312,154.4853158709322,2,37.19654155776618,2,13.310989529387662,1.5112111381733788 +19,65,25,18.09551014,18.29318436,5.625096446,144.7902323,kidneybeans,22.913729556605283,2,11.927801913708018,1.5346433068650955,431.9775546455339,7.715514386878776,6,17.08054552055806,85.899408088922,128.89512448422803,3,6.190504312857447,3,82.77388663332894,4.0267064384994455 +22,70,19,18.23775702,21.07643273,5.515615023,69.44951585,kidneybeans,16.616754025087637,3,6.240834048179343,7.235828929446784,415.3513486583844,2.8243276018225867,3,15.813808058464408,12.254951181438955,184.06290632078964,2,12.264436171628285,2,78.95900113990369,3.643342239305256 +37,64,22,17.48189735,18.8251973,5.954665349,121.9401369,kidneybeans,20.707059976696492,1,9.764585715432307,12.934515143375682,366.0321174549998,9.457081057289917,2,16.359284555590477,40.60090224835089,133.62653435192448,3,22.8115884369572,1,18.702851334627525,1.7485326177693472 +11,71,17,19.9191786,21.47324158,5.74644777,82.68554379,kidneybeans,26.759698719054498,3,10.907205820924217,8.139994230300463,401.235705432997,1.1819446803398446,5,8.618731230429862,50.396694908526605,179.133721699248,3,22.482809968992463,1,17.956282883717634,3.9406779454059317 +18,79,20,20.27514686,23.2353604,5.877347515,139.7521543,kidneybeans,14.442414784704404,3,5.105040889933038,5.096240153482126,441.0449488566787,4.082531108788829,2,5.181299304564206,81.10348433700665,96.02627961201023,3,49.367963802748974,2,68.89636947831053,2.2596444420468567 +21,63,17,15.77370214,19.2303162,5.979973965,108.3441414,kidneybeans,28.342690862912963,2,6.085685308283894,16.77901980162781,385.1189359205954,2.1408435114124127,5,16.839072419253437,43.559413613401965,188.2341126004427,3,29.9238499644166,3,69.2043942410171,2.87299513222695 +24,80,22,16.71170642,19.17651433,5.635993966,96.77285817,kidneybeans,28.269491819788726,2,8.055430218961114,11.862198674733776,444.16253713527817,3.5882861117206803,3,7.805995280349702,58.07847311485015,60.38948498563418,1,47.10502676905361,2,75.44260769888038,3.676829917311401 +34,60,22,17.66148158,18.15302753,5.635231778,100.6711761,kidneybeans,12.453555536647421,2,10.28226541678447,15.087531056652978,442.1475950829363,5.623478932786681,4,13.02975300041788,96.1328131608065,137.96694805741024,1,27.24226209630482,2,90.87213735548153,1.0461858704080402 +16,75,21,18.50692825,23.61670065,5.679224346,87.0513289,kidneybeans,26.089262704590052,3,10.895625782799513,11.639765432489167,412.5373434104419,7.420667728414605,2,8.478721409080494,23.38495369648017,71.51163528374308,2,30.054726175321644,2,45.09125890593118,3.3366405545116526 +17,77,23,24.51324787,20.81527638,5.670062975,64.19497947,kidneybeans,29.59891522667612,3,6.881596136998308,5.829400501126997,396.7110919595346,4.747525614127909,4,8.556008376552274,3.3658861848583688,183.56553503648823,1,25.52351568110463,2,55.23343305698195,3.971081543496998 +37,72,18,18.87614998,24.54038287,5.724242065,105.4120514,kidneybeans,16.071083939229037,2,6.714515918607777,4.48156033975417,389.5834665160461,5.407496563621576,2,8.804789560002252,22.211874805638477,187.63634955184077,3,31.724008517560854,2,71.67767282108495,2.8514146818898793 +40,73,20,21.59343016,20.31871249,5.811314232,61.13872036,kidneybeans,21.10292381552731,1,6.669653225795148,10.08302622842595,391.1300221480855,6.451714999786911,2,5.88138290212627,31.651001931689905,126.3408696871014,2,33.00920481317985,1,2.004239979225042,3.832683679290506 +9,77,17,20.12373284,24.45202552,5.783425416,106.158201,kidneybeans,25.961731636860627,1,6.425579999300101,11.985915798707246,441.10994482999047,7.202816295930529,1,6.146769357555797,68.81630537634918,197.95740865981153,1,24.328602403715742,2,93.73064470111461,1.648076659747952 +1,62,23,15.43546065,18.37477907,5.607808432,139.0302034,kidneybeans,22.59439814917546,1,9.322978023907536,18.01900333294743,416.10350550420264,6.815284278482407,3,11.453718416591212,29.420409469046895,56.04815367115822,1,24.202406867462017,2,83.84656799400054,4.65990656822642 +33,59,22,22.64236876,21.59396123,5.946999529,122.3886015,kidneybeans,11.968096013573625,2,8.692602329140563,17.740115393218737,407.67702215315853,5.972307285777767,6,16.840705606211028,48.70216635798267,197.85529415491678,1,31.09104827543325,3,90.1951639349122,1.6579424927215287 +23,59,19,21.98560799,24.87304788,5.852046999,129.5650601,kidneybeans,29.11433231879194,3,11.431351510873498,2.717596056797844,396.05084688819414,7.2402019480030075,4,17.28044392319866,23.51005810995619,146.87665706891502,1,47.586735810474714,1,84.6166227041781,2.1117362819285264 +6,62,22,20.53052663,18.09224048,5.824090984,120.4509288,kidneybeans,15.118545496371134,1,10.690606846019747,12.314004532827248,406.71985990295485,8.548523896934508,1,15.164022803636726,11.375687885418595,89.51464708720701,1,45.2743383074135,1,80.07291499408315,3.2279843128492924 +25,63,20,15.78601387,21.14544088,5.502999119,95.17028129,kidneybeans,24.583990699880474,3,5.358735290398668,3.3944447821310075,443.66468812859944,9.249272351087152,3,12.901841361786635,54.90289908324527,57.4368363577773,2,15.076130068407096,2,69.72955767330372,4.010324404174373 +7,79,23,19.6365349,19.68751084,5.821649914,96.65888933,kidneybeans,16.475552973043193,2,11.603880994249158,10.582564556514315,387.9772769234488,5.378594805130065,4,10.914143727925147,16.97532337852016,137.4364929841551,3,14.19923380612394,1,8.720474941506895,1.2348328463976554 +8,72,17,20.57341244,19.7520218,5.711439256,87.87869161,kidneybeans,16.247993900123042,3,6.986280007841963,19.831162465397224,412.1377493919506,9.579262264464043,3,18.72389696365461,65.56989324619167,104.9010500758297,3,7.180117472143505,2,73.0591027911533,2.0174111864995075 +27,64,15,20.16080524,24.84207559,5.514927264,138.2362122,kidneybeans,29.87351889473596,2,7.703079497809844,4.785757731167129,354.3321177945103,5.274333424001637,3,7.504839696234772,62.8978922310107,79.82206258496839,3,44.283197846937945,1,72.95802840371158,3.0254208741628363 +28,66,23,21.53989176,24.25386207,5.99616119,120.6913038,kidneybeans,19.825321903378196,3,11.744196656530406,14.534320826123587,400.61991106646667,7.097685324427192,5,17.41933497260741,95.82016815133184,52.911512518917974,3,19.296164090344803,3,33.81372900755658,4.839321921772445 +32,57,18,15.53834801,23.75560241,5.695422863,107.3850593,kidneybeans,26.760166422245693,3,9.445503943479054,11.954373418771802,442.9431444410603,7.564046966222799,3,10.715206281214044,38.82550121377331,176.805006972234,3,47.79051041700166,2,75.29313209920417,1.0523971783484702 +27,56,22,19.91853092,20.70099804,5.833010958,108.6434544,kidneybeans,27.762002719534,3,9.717147211681972,3.1421353116979134,445.15764537349696,5.036793551793523,3,18.77016609480195,50.563510147349,171.5160235737656,3,19.444039032078763,1,29.56478874541294,2.5575673532700502 +17,77,24,20.76952209,18.93146941,5.568456899,109.0193712,kidneybeans,26.19833580529604,2,11.375126126669134,12.809806399520713,350.0013849350592,6.44599870645958,4,18.562612146315303,77.09097344279195,88.04408120752419,3,18.63601255972646,1,0.4831164426460144,3.027068012867183 +0,65,15,23.46168338,23.22197648,5.645435626,95.84253438,kidneybeans,20.472434517738375,2,9.20042010966154,0.9658739835947983,356.9808787859431,6.595151563930153,3,18.35964206670819,77.76104344095715,67.7232105892359,2,45.89245799828209,1,8.416721483964562,2.938920604708963 +13,72,21,24.32116642,21.0278674,5.821194486,60.27552528,kidneybeans,16.07083494731713,2,8.022550311769695,17.413288349219194,381.77534065605784,7.4633591120692735,6,14.100064728811102,78.77068236299164,180.01543751970692,1,12.45522379982284,1,10.102844384145527,2.785245165206519 +34,60,23,20.12574053,24.96969858,5.659254981,100.0497183,kidneybeans,15.691226228148736,1,7.049113752307047,3.4139689070875012,415.672638312396,1.4675158454856896,4,11.178304280691236,89.59746357536899,168.04943957666626,2,47.32805580812385,1,45.22754995092894,2.287396791333346 +9,80,19,21.80619564,18.57086554,5.945465949,125.0972687,kidneybeans,21.35430384488906,1,5.267650819517367,6.949317734578752,351.8146417893083,2.203888833790767,3,9.01374591323204,4.572489845082927,160.57205317923018,3,38.33140444602739,2,84.96883342732872,1.4003088916288635 +11,72,20,19.52226241,24.92607153,5.951177452,113.334026,kidneybeans,12.028981062864597,3,6.435084233850315,3.492145542937428,352.64004784354523,4.645756932635414,4,8.05097137814689,17.686227862486003,110.54581969752701,3,30.279356766834265,3,74.12291396698114,1.0114976704307521 +3,67,24,17.00067625,19.90790546,5.520880014,103.2926407,kidneybeans,22.44296685810167,3,9.048272961709976,5.497398880365458,445.49376041465916,3.0673513539678803,5,16.81385190100513,98.77575323835335,59.881271064007976,2,16.026777621528666,1,60.66309937631712,3.0036341556557775 +35,69,23,16.78791503,24.96881755,5.578410206,75.45328039,kidneybeans,18.43342367784564,1,8.160651296465543,6.47679244513343,412.06765807503984,2.854228612996849,1,19.40137663852155,20.224677558739323,175.89477094436978,2,40.16055340946384,2,12.578947344743629,3.348624425576983 +3,77,25,24.84906168,22.89464642,5.608165195,62.21292186,kidneybeans,13.942790975596775,3,7.763281390362714,17.689362511002997,386.2634944140779,6.720948590811908,5,8.75103530592644,70.09988989452744,82.90189296034364,2,0.7782541988605507,1,79.252279971044,4.722406545361399 +23,62,19,16.51783455,20.4555596,5.609435128,98.77794225,kidneybeans,17.946604234832,3,11.837854966891822,13.398142341938552,419.6882018006906,8.643223232367767,1,16.14970421084289,29.736994967634654,138.02610245594045,1,13.148375072726848,1,80.72789538020935,3.7715164059409947 +22,71,17,18.15300153,19.38602098,5.509295379,107.6907964,kidneybeans,18.922862175409264,1,6.7599504219788695,15.797934809524543,399.7032078540656,2.452806052527011,1,18.572712697507516,4.001347049031201,68.99984517438678,1,43.932144340009884,2,36.57274253048194,3.97612537240174 +31,79,25,23.18864385,22.3104551,5.902033406,63.38208822,kidneybeans,10.971919704895674,2,5.285055627683736,11.373709564668504,435.43268551919294,7.072003085363502,6,11.554714840801967,6.779701711148323,147.26429655008639,3,47.74448602994492,2,17.00948274452522,4.360350780667897 +34,59,18,23.38002569,21.98879437,5.744117663,87.66898664,kidneybeans,26.566190855190783,3,5.0334836976033825,10.918413419379977,383.90896346854214,2.6518020059753207,1,16.37905881130497,27.494888037222744,104.78459305413753,3,2.8673555452168706,2,56.499965792646144,4.83469925557122 +12,63,17,18.358923,19.37703396,5.717143397,138.414764,kidneybeans,17.618195055144753,1,8.27509103137341,1.5843899009709372,398.7879534502884,1.9597093087420334,3,10.8124817090585,25.393216768000425,80.36856868382287,1,26.79289810693883,2,88.12283542803235,4.439533903101626 +27,56,20,19.25975367,20.51346956,5.542690119,94.9533526,kidneybeans,21.357883024408736,1,10.562753713053196,13.063349479867332,438.0898894077818,9.114575768465874,6,5.630244658003872,4.357310345089138,101.67694295009825,3,26.490806597263955,3,93.19343936986819,3.168137457139605 +7,63,24,22.95458237,24.03553105,5.858617867,107.7315386,kidneybeans,23.469396541671507,1,7.931817148431566,2.33222581890844,371.47719054216145,4.37062844026503,2,9.5062520021397,69.21266795930427,72.6506806558766,2,19.99749579955548,2,96.18029441319787,4.70471159185284 +24,67,22,20.120043,22.89845607,5.618844277,104.6252153,kidneybeans,15.453491204089453,1,11.132326674919137,1.9230573394385209,449.17927631874073,7.603244919449663,5,13.787007625259815,47.694445873545114,138.30996178062065,2,10.055123204346161,2,80.40645151796045,4.506973483327021 +11,71,24,21.14011423,22.7182355,5.606620346,141.6056722,kidneybeans,11.895561105974524,1,10.697693358897613,5.960178799407753,405.9554037454089,5.167021690906626,1,8.611566612612085,92.29896433024781,75.31009670732925,3,10.020322211924288,1,59.79990542444554,3.5817709609315482 +37,74,15,24.92360104,18.22590825,5.582178402,62.7089169,kidneybeans,24.542975289438644,3,5.853525152135354,5.588424938449474,404.9567011101793,4.748465461046923,2,14.269320540813183,78.44089014797953,159.124328827684,2,8.306636352988994,3,73.22274651076542,4.646217535156312 +25,76,24,15.33042636,24.91506728,5.56503533,135.3315583,kidneybeans,24.83407740527442,1,5.968727018086067,14.397991486433137,403.14446353786127,3.2039414465264375,5,7.906735945985541,37.930902755544324,58.227375897868285,3,46.49031613814027,3,6.607224692221047,3.0595804666286126 +34,66,17,18.81097271,21.27833035,5.889614577,125.084915,kidneybeans,21.176332352021156,1,7.2642973931918124,1.1293190705373624,433.016893024206,1.4149376823810327,6,12.201239962790456,74.82528068512129,176.0496171642922,1,41.606188129780584,2,41.40991446208571,1.656397914563951 +20,69,15,23.44260668,22.77255917,5.934136378,107.4137246,kidneybeans,17.773925549896163,1,8.685607542022456,17.343762495977863,406.4316161124042,8.006953864635022,4,15.679991354213312,41.045785239140805,129.26515867212646,3,6.912874168257998,1,82.92744834894685,2.081737847568799 +37,65,16,22.8352024,18.97267518,5.683548308,63.59276673,kidneybeans,29.080227525230203,1,11.014997565466466,1.4463870187915084,389.3519046276468,1.5807999947027942,3,8.201883700109864,39.655889756130506,158.4992311323428,2,40.31552348030828,1,74.94155737855436,4.109441367781832 +18,74,15,24.9035819,22.27512704,5.70836603,146.4727237,kidneybeans,20.43591067668373,1,8.338879033346226,8.516548164422177,389.361521438987,1.6981848557850816,1,15.99855131578282,71.60335532776163,146.770955929652,1,19.3079181428013,1,98.44962547170866,1.7489216461742902 +4,67,25,23.78709569,24.35679348,5.948164454,119.6404412,kidneybeans,29.676942328809936,3,10.509278509472324,3.8787404725077335,389.02435820707836,1.0135505223405297,6,11.474576084093766,77.29511142154314,162.8530689133645,1,43.602770616752764,3,13.95272581532926,1.3349461335052468 +37,56,25,22.05592283,19.60379304,5.774755144,126.7265372,kidneybeans,23.90900735942316,2,5.428770197081969,3.6180762253200682,447.21548140747075,4.879931019660453,1,6.7620859445853245,64.12721170918346,99.7459900077683,2,49.01607983396606,1,66.68808550176198,4.914414585360598 +5,59,15,18.87492997,20.18238348,5.97229163,134.1811718,kidneybeans,19.895010310467995,2,11.6946613214166,5.1647461795277305,439.32038435214804,6.819827787827926,2,6.294081900032555,79.91657299699317,107.1857264023904,2,12.074966430745354,3,41.55648973293344,1.556984740100693 +11,61,21,18.62328774,23.02410338,5.532100554,135.3378033,kidneybeans,16.61647633429648,2,8.375584900252374,9.636081326372107,403.95165539027016,3.9835159651068155,2,14.620061090521169,76.47276369138355,172.03400089093364,1,1.7143222452754014,1,86.75590314217587,4.749139633370902 +22,80,20,23.00884744,18.86880997,5.669560726,100.118612,kidneybeans,28.899088824927887,2,11.154081719571295,14.46081753888895,393.27395618303916,6.081203706903107,1,12.926192563211444,12.043661665399707,129.19948973563672,3,47.939695487588814,3,47.4563482959665,1.2825986904022488 +12,61,19,19.33162606,24.13995025,5.655726817,68.51253427,kidneybeans,26.16962610505743,1,6.982162159023001,19.247599469574197,409.59307893018126,5.3615686838808925,3,16.91393983978083,45.38817595937845,112.14150979645015,3,15.859537839345606,1,87.1132603855511,4.822698153434703 +5,74,21,16.24469193,21.35793891,5.591704014,66.97053257,kidneybeans,18.355384003808155,3,5.000725850236799,15.055427935662674,355.4159957619112,2.1553313603573248,3,10.873326455519212,34.30874484948383,173.95436604816672,2,46.90156379109069,3,99.75567664097788,1.1698906169496444 +27,69,22,17.91652287,24.90814655,5.932323085,69.14681022,kidneybeans,29.2824667028517,3,6.854594622738157,13.506307890508804,427.0074417677744,4.759096931684615,6,6.340360695231755,7.128296960017211,189.1480747453213,1,36.5113734662266,1,66.49305536786451,1.8514384214421185 +31,75,18,15.46789263,21.43780702,5.824208309,88.88796102,kidneybeans,11.983118511669431,2,9.988099591802975,14.35285215735943,374.1204491289247,6.485329273851615,1,10.251884962150505,29.48813406533667,91.87792496729833,3,14.30571068121353,1,73.65782956760837,3.7572478636543485 +36,68,20,17.06104474,23.77201471,5.86442953,81.83420522,kidneybeans,17.906917426643606,2,5.329953219128357,5.658148571421286,418.1176724431083,6.638963800079786,2,5.450547799159866,18.582171647334565,101.15413851521564,1,26.427502976983362,1,55.06134486163885,1.5611786785707222 +5,65,16,21.32776028,18.48522915,5.866744372,109.1013261,kidneybeans,21.161898388489256,2,7.246797566094891,14.64186372444648,443.5440481589047,4.549911215008955,5,19.212214052172943,47.85163722224974,122.69279795628792,3,25.38821718785312,1,32.73359236832509,3.925440588826343 +32,79,15,23.90910104,20.74619325,5.706198621,81.60211243,kidneybeans,15.353073018423906,3,10.440015464412266,7.027301726426465,369.13558617232957,1.1727777058013777,6,17.041749674219773,53.31245666476552,181.44932599857034,1,34.56300370597923,2,98.19956898094607,4.213788332985267 +11,78,22,23.89756791,22.74378977,5.940546818,112.6616435,kidneybeans,13.788061540765955,2,5.632876165931489,11.630294914966253,354.36124844902946,9.569986385359094,4,17.73022931203207,85.09178423681351,120.7772764451618,1,23.78805872925786,2,17.322648469327383,1.0402899641251597 +0,55,22,22.98666928,20.57940608,5.916779289,143.8584938,kidneybeans,13.23421114798425,3,9.760573082298034,12.638096848495337,411.4834894324374,1.4730410377284466,5,9.34183498398556,76.62537786474877,144.47395894144205,2,26.676636757298517,2,0.612337234127891,4.648640697100481 +14,59,15,21.35135729,22.91244883,5.779090476,146.4548645,kidneybeans,21.737838617637927,3,7.649346895739051,2.8637608644166623,448.37513233511953,4.601718065136092,3,16.289113950657825,82.47892105508534,89.89334344973517,2,26.131549290800198,2,59.35995424611284,4.110168261719495 +29,68,23,24.1638445,19.27907819,5.82738029,116.7324324,kidneybeans,27.837638914081946,2,7.4304370938729765,5.281232217596992,373.55857408771715,9.896542001003837,6,8.829067521805195,93.26372329595117,142.11954544072432,1,8.128292136048126,2,63.79884929046368,4.963950522114844 +32,68,19,24.62835037,18.18325169,5.514234138,149.7441028,kidneybeans,18.011115897496307,1,9.720050882247104,18.734104278816705,357.82446247788414,3.026468411111301,1,11.282366519872767,60.632709640735825,176.05158674655735,1,30.151309536018857,3,91.31826410600164,1.08953703769266 +17,64,17,21.02213209,24.93896255,5.662699104,124.6118471,kidneybeans,20.7434885365372,3,9.696834018376462,18.54707422969308,393.6097922469893,3.8530730811625773,4,18.17690295985238,59.22137252517938,175.9838657764697,2,8.785741397922047,1,88.06156804992304,4.776668259849876 +13,69,19,17.30844532,20.01730914,5.86390397,115.199245,kidneybeans,28.133786676387416,1,9.227475552676326,17.0261057723399,449.40552768902785,1.5568310895493447,6,14.700492063805418,57.58339565124082,173.96416813631942,3,3.069025657830421,2,78.28115052750786,1.9614577682870213 +14,67,22,23.82576704,24.75485098,5.624690248,84.64143632,kidneybeans,12.722751429431252,2,10.8598614900021,4.411364744465427,425.52968341222504,8.048016731838128,3,8.698478608437254,53.95645729278493,126.71016693926957,3,25.755343668025212,2,34.38465248860507,4.602850525310867 +9,69,20,19.30607278,23.96362799,5.591560999,129.3449326,kidneybeans,12.531124637144437,2,8.334571782263083,2.54302796395921,404.3312315270977,6.595306769687665,2,5.899801342925024,0.9705246730235317,147.12779155679016,3,1.016687733033772,2,40.70191178109409,4.292105924382222 +20,73,22,16.03768615,22.33195853,5.976312538,130.3900798,kidneybeans,14.977243556855885,2,7.323366715199722,10.853044055455772,368.38581329988375,5.591420214879972,6,19.19381032008985,35.904759395639275,78.0997368708477,2,37.09422070429346,1,67.49091165657603,2.3290332264746034 +40,78,20,19.18572809,20.83398341,5.669236258,80.15293435,kidneybeans,26.038360544951527,2,8.069194135841734,14.68859661918306,398.2563054665093,9.3520986282443,3,17.64626014503473,4.342274444716232,167.21439823918263,1,48.26408267157923,1,7.083514310218897,3.9275961534017676 +27,72,23,19.92889503,21.79992115,5.961934481,64.02640797,kidneybeans,29.00728658906845,3,5.42506449360833,16.233653377779248,378.2673886036657,4.3157693757291655,2,13.942214232591114,66.32758467602986,74.1463654486272,2,39.52265052248517,1,84.74650386257538,3.521859537716317 +14,67,15,19.56376468,24.67385131,5.690065688,139.2921004,kidneybeans,11.316366737501761,2,9.18836543832641,2.318661248744014,385.6304607509318,4.13418039626902,5,18.51273832612462,6.246486557294828,88.78776333617608,1,18.10532844300554,1,48.02184632660337,4.241862125373659 +7,56,18,18.31357543,24.32991649,5.698371311,76.14153904,kidneybeans,23.291332348908178,1,5.975012823243761,18.602537348829486,420.96259210666835,6.880011552668389,3,19.8188008399775,77.5561200793116,87.18404174212755,2,7.330710201161517,2,66.73513113586725,2.319390123827271 +27,65,18,20.10993761,23.22323766,5.59503163,73.36386477,kidneybeans,17.946714843631362,2,11.575180659985921,13.58353809171186,351.4397463546144,2.3410663943182817,6,11.130512570962232,86.33861906258188,121.69200601433182,2,36.2031883908621,2,76.36693963708645,3.408384399884358 +30,63,16,23.60506572,21.90539577,5.525904526,100.5978728,kidneybeans,19.490939662428342,2,9.767113769969189,3.9400134935414055,350.5283921341889,2.4811536288473786,1,13.96529364473711,27.778012203589732,80.73659140489772,3,42.96442053501875,3,49.7198841237377,3.5033412983840684 +37,70,25,19.73136909,24.89487354,5.819403771,84.06354115,kidneybeans,11.653781873535564,3,5.739923237503676,0.6941571304388505,381.7360655651895,5.982346450055076,1,19.18676189867034,22.343136772083383,126.76347682996811,1,31.925982141161914,1,28.616658063607737,1.6314936158736382 +27,63,19,20.93409877,21.1893007,5.562201934,133.1914419,kidneybeans,24.230694654991012,3,8.44226665788819,17.122584967274516,367.69538638535363,7.984621115435975,6,8.361600321296338,25.699094022192092,95.96285687867028,3,7.0184602173603725,1,86.3256713601544,2.254813111723486 +22,60,24,18.78226261,20.24768314,5.630664753,104.2570723,kidneybeans,13.799752224936125,2,8.61189739966737,0.8378713950750472,413.45329994220447,6.063635782609917,6,18.817036846960388,56.97130643330688,148.88355226995742,1,31.785218060311955,1,1.4820518232845248,2.5423063669422707 +3,72,24,36.51268371,57.92887167,6.03160778,122.6539694,pigeonpeas,12.300796996218914,1,5.072280482284679,2.659512966080706,413.16670948071965,5.483977498461362,3,13.235678121410562,97.17837194001999,120.16398859301589,1,48.71899841386766,3,0.41189132428613995,2.1496913420145503 +40,59,23,36.89163721,62.73178224,5.269084669,163.7266551,pigeonpeas,21.690469967780327,2,6.163348097011641,8.34682011670828,412.90178212033385,1.5244348057249075,5,5.140515336551182,18.34559698798426,64.91873963859064,2,25.440294381307783,3,13.313148957864108,4.117013945719067 +33,73,23,29.23540524,59.38967583,5.985792703,103.3301803,pigeonpeas,27.20553611295348,1,8.038033585965547,2.4515935611989037,414.23830180088663,4.317063119118515,5,12.060118750313956,28.44797223233897,185.91206953466622,3,6.886015294294895,1,10.26302906189539,4.6495955556186 +27,57,24,27.33534897,43.35795962,6.09186275,142.3303677,pigeonpeas,11.836692557738575,1,8.876881648355596,9.850233002657415,361.7105540134821,8.276828900517847,5,15.236328991418173,70.15409225544983,52.31099106975566,1,16.97113869873069,3,8.27716646724056,2.1474045145462966 +10,79,18,21.0643684,55.46985938,5.624731338,184.6226709,pigeonpeas,11.474456310853924,1,7.730261399553752,2.9450354718127603,449.3885939127531,2.1318046566072923,6,5.878450604102734,10.395513720240036,52.41205256248171,1,4.861199048302972,2,73.210630391249,4.519996524081275 +30,75,25,30.33276599,42.35249879,6.446091759,149.299952,pigeonpeas,19.681494496747476,3,11.34773021909681,12.72710136458599,448.44665949010806,1.310700463359314,4,19.780737692967666,77.8945482891502,187.30077377825896,2,15.490396191410976,1,87.66939276800923,1.7075408499207363 +40,70,20,31.80130272,45.03186173,5.623490043,147.0361442,pigeonpeas,29.61732802823861,2,5.995560155416908,15.704537450868761,413.9293889846838,8.100617380177466,1,9.611654468814745,80.12034151154982,97.90348653238536,1,24.041173423751623,3,79.95442886654696,2.357028590264465 +38,55,19,33.18184225,38.23184742,5.864623352,198.8298806,pigeonpeas,28.149057634325928,2,5.62112730643268,3.663909856913674,443.9609740825335,4.631461339409707,3,8.100185418690428,10.888280121314708,114.96499227927268,3,7.359083751337575,2,96.4754676198541,3.538008426745571 +35,58,20,29.38538562,63.47742011,5.761702519,90.05422663,pigeonpeas,20.519347500263585,3,6.077312315215153,7.809820325771961,372.2651173075719,2.6176980025806276,1,16.61623398660741,8.904757695427346,94.53547347950274,2,36.55597204748761,2,87.23955669756691,2.4147997789373328 +38,61,21,30.27374995,67.38680755,4.696518678,127.7767134,pigeonpeas,13.226911105408599,2,8.188188959517042,11.324020218155717,425.2274442646335,8.662691299114757,2,11.935930253141262,39.27257354479104,153.1361601927435,1,1.7743125354371225,3,91.48932095551018,3.36359767193341 +33,58,24,35.45790488,68.75810535,5.269504214,108.6333046,pigeonpeas,17.039146933469503,1,5.530338685780148,9.503660301602304,370.2057266169742,2.3416562938024716,1,19.86099004315194,86.50986440270209,133.1536054131301,1,8.126186964400468,3,76.27668208192968,3.4294185966076105 +16,56,17,33.80020039,40.03262418,7.445444883,176.6165894,pigeonpeas,17.316644609244523,3,9.201795373500994,18.40535993748196,383.70481440793264,4.804550490024798,3,10.17226863052781,54.73951940882075,155.05788916752206,2,4.995733608304359,3,5.308778924174639,3.963376983028888 +31,72,17,28.69180475,49.47225353,5.833031708,96.36222901,pigeonpeas,18.977267616760244,2,11.370115045005678,3.307362590781109,378.28710444374553,1.4123175373500523,3,10.013186934416403,45.21671170869025,116.73738171867416,2,18.880781320723678,2,72.35378543457223,2.535596847973458 +16,80,20,31.24021696,56.67369054,7.339320929,122.0146733,pigeonpeas,24.374317867416302,3,8.735461617204365,2.8315727434383398,411.6613736257922,6.670853814245111,3,10.546189785730615,17.968902100386998,134.62327371890927,1,23.84427908894929,3,69.72657334370606,4.40380454655578 +27,72,17,28.98039357,57.23265151,6.347929353,120.7435664,pigeonpeas,10.111576199163583,3,11.753818102239538,7.109740549153887,411.71548563925785,1.9913254038628068,4,10.05307601794615,31.724552549025333,94.68018892581722,3,31.66732342307509,3,83.07433132610342,2.5582569374392405 +40,62,19,27.32198928,34.13737127,4.697750704,96.51524028,pigeonpeas,23.844591844386525,3,11.438588318263863,17.255331676702507,408.9138087464971,8.601798474665546,5,5.932516771174222,56.08911013858695,172.92472883249303,2,21.5153548614977,1,64.89035655476414,3.250517506845388 +18,58,16,21.47607807,38.80023714,4.962661422,180.382234,pigeonpeas,22.34487535165203,3,5.168262963127516,17.59422221825715,357.0436191605813,7.809306664385367,3,9.520180638504346,21.378697493509748,53.67693480360279,2,14.966757594915459,1,11.293445145295477,3.093144725683383 +3,68,16,18.31910448,34.69776639,4.964887857,107.4721605,pigeonpeas,10.839354431342027,2,11.422137829218888,13.888238606828725,397.62468140443957,1.961767823821801,3,5.378781001946435,71.78760706149521,82.24074584436177,2,42.442439739775025,3,18.50400233818218,4.851682854767699 +26,67,24,36.97794384,37.73992903,5.642813116,161.4812963,pigeonpeas,19.804437899305963,2,7.156592165400795,16.710165326170618,370.3365544913563,7.485819466577953,2,6.174961410389539,19.956112935827807,175.59666308434575,3,49.91377799311904,1,35.91753981796405,2.6608782397173907 +16,70,20,24.80467592,40.1242747,5.6093956,121.5639121,pigeonpeas,11.925177213490823,3,9.527850976326082,3.7572139290019346,427.8957422164799,7.204210577236569,5,16.767951697113823,84.24498480283349,60.906004522160714,3,1.368782417851766,2,83.25191997588729,3.168463851337414 +24,63,19,19.3479443,55.96805489,4.681576043,194.5921148,pigeonpeas,28.00339268681403,2,10.004633732433332,15.897510555527086,350.94953865817297,4.360636825269207,6,5.815317245626514,8.527237301545565,109.74874634292209,2,38.5010703107897,1,39.35298665706474,3.395942307588764 +9,76,25,28.88302142,50.12323801,5.70951224,179.2155874,pigeonpeas,20.609035288675578,1,5.319241146575369,6.206983710762577,404.49385777773483,8.025651540584414,4,12.789747683035639,88.72074093780833,81.37968953619989,2,35.63461842158595,2,42.13124771871316,3.3282030391228474 +16,55,19,19.54314136,47.19188279,6.413543781,192.4372194,pigeonpeas,14.99847935562,1,9.27555930354837,1.5251866173448847,377.71192766408916,8.435835337019693,3,11.989082443820147,85.12975746457009,183.49783908910868,2,3.2649478670135346,1,14.316911284432642,2.281112115420574 +28,75,21,24.7741949,50.54621094,6.007508163,114.2821387,pigeonpeas,29.463761862908953,2,11.506963685652265,12.392273778633154,371.9672260576834,3.026658083918364,4,18.076204639668944,6.6853148920502425,60.91366339051772,2,1.780732152807124,1,77.19050703617971,3.1527112472026957 +16,71,24,18.33124824,38.40975482,4.946369874,139.6483317,pigeonpeas,19.443914261989892,2,7.529945537998149,1.5557834235688661,358.4602276146244,1.8381674603942597,1,16.189187170871826,10.855966717103849,50.75259534219755,1,5.923400342060265,1,54.053633232994414,3.149294542734077 +24,70,21,19.14729038,45.3733757,5.517208078,132.7748215,pigeonpeas,29.57541025793995,3,8.783325774824105,18.31970408849659,439.3641422672258,1.5471113692062564,3,12.970047432419507,4.367640379548998,119.46996553954534,2,8.66123043183331,3,25.20127038312533,1.5711820862484251 +38,72,21,28.23416057,49.4421345,5.902103172,186.5008581,pigeonpeas,25.240901136770265,2,10.867076022950577,10.492769370697143,400.12472136951146,9.46906696224253,6,16.865596940910073,78.33861177790578,198.3465759008838,3,37.47477621622803,3,90.57576170429331,3.2757742217841948 +9,66,21,30.11812084,34.13307843,5.719889876,157.0858232,pigeonpeas,26.39936213598953,2,9.339718440640421,12.99272072519333,421.853941992299,7.786182018755541,3,7.404729205775199,82.30129443371555,65.69228814761644,2,32.19032887819103,3,95.71294713580234,1.8594587371505042 +34,56,17,33.4126864,35.42910045,4.548202098,139.6702541,pigeonpeas,28.960571249626923,2,5.279736296523776,9.947231008364717,429.47480661781594,8.679242146316861,1,13.496605589077785,0.7068301657443321,194.8081603016334,1,0.9650302384279719,3,57.48047774101077,3.5068726859579082 +1,76,19,24.18553163,46.68746847,6.669529416,177.3377996,pigeonpeas,12.42564520814211,1,8.331382035209916,1.6188481905875185,354.4122650440536,4.604244357318449,1,19.445782112222997,44.709602929505074,174.1325321968036,1,17.878096579777054,1,28.66960751257035,4.670057744410638 +6,69,19,26.88630675,41.69617915,4.750929218,94.46748008,pigeonpeas,23.85391520298036,3,10.91420755325355,6.582210559600574,383.6558901266551,5.141911660547071,1,12.013021075576333,55.28812326117611,71.14635494827195,1,13.838677336257653,1,82.1843321215376,4.846333105606398 +26,73,21,31.33170829,57.97429171,4.946263888,161.7820226,pigeonpeas,18.17458205825503,1,5.033648254900967,12.626350332662742,359.5798391529733,4.774294233544552,4,7.408240969762502,20.49833041058693,197.01038588191165,1,47.99200068890505,1,76.63054423243177,2.1472757581855224 +27,61,18,33.30711818,67.07780816,5.266227032,108.5090168,pigeonpeas,27.239878784955625,3,7.713445487347235,17.957789480430502,424.56985927529104,2.640648368077306,3,11.077811208845704,1.3882783884024752,190.9958625616827,1,32.70715018540253,2,29.943009023240997,2.9319944311222508 +27,71,23,23.45379018,46.48714759,7.10959773,150.8712202,pigeonpeas,10.823293035205078,2,11.591083633008159,13.642393198423452,435.68704004957453,7.075779050429993,5,7.260945902198273,96.47567243717313,169.96024929949516,3,0.3017229802193766,3,93.134871817826,1.3093597087944175 +36,61,21,34.53823889,39.04468913,5.617008201,168.5948318,pigeonpeas,17.175231257937416,2,5.17609379355275,15.794662985790032,381.14090935232247,4.64800613414549,3,12.858562908312667,64.13971403172899,140.8788773030001,3,14.957078827033865,2,69.05478630822344,1.8626027002045924 +17,73,18,19.50112224,34.51086611,5.632353113,197.3752649,pigeonpeas,12.198484002327145,2,9.375924752895653,5.314282487556521,431.4329239524566,9.257502766645407,2,11.43564662120366,85.03701968702,77.22720703090002,2,9.915977615153459,3,23.202766540167563,1.5661748855474125 +26,72,22,28.76794904,37.57792132,4.674941549,91.72084869,pigeonpeas,21.93074253051045,2,9.338218913966742,15.894693175649234,417.82015129290187,2.9999146111389887,1,18.83050325218484,95.76192763091353,143.38875839244855,2,30.88120133036454,1,17.357185947998268,3.6086044752922533 +17,64,16,30.97758716,32.24914235,7.161797643,180.716828,pigeonpeas,25.401393509519277,2,9.84026577450224,11.0558953227463,421.37878404022405,4.467365023408394,4,7.775680962849811,22.122644926632905,62.69417794902066,1,38.97688611275558,1,37.44007681099788,4.429394728511054 +14,74,19,18.39759147,36.82639309,6.624966131,93.12330644,pigeonpeas,17.550465980619173,2,6.314859914012473,14.217256882778148,445.6284929002302,4.812143582589716,5,6.683822556925545,36.933918934960964,63.76699357254688,3,47.88056179107724,2,24.826013565545313,4.434869732725675 +39,60,15,35.09357419,30.98685456,5.004074624,116.9106908,pigeonpeas,22.273735686005956,3,5.166187890567446,9.00335836068982,430.1271861166221,2.1967021658099943,4,15.861791557723802,23.66893910420017,133.03865745152572,1,39.64311406859103,2,33.71741488288642,2.24516546428673 +6,66,15,34.93174223,30.40046769,6.345806011,159.2649827,pigeonpeas,13.104249501878993,3,5.219498400600568,7.472620708452613,412.6576256157811,5.922931530035967,5,15.820638927473995,12.793221672129961,114.03875781083809,3,21.259370770858197,3,3.148601577688337,2.849916652467987 +8,59,18,29.50523036,35.72032498,6.216814453,187.8961851,pigeonpeas,28.543974679479618,1,5.749932659220722,14.844315656536777,419.2572572546425,7.723628885147478,4,7.293167876359295,20.997756096630393,122.5474627948831,1,6.753621989008024,3,21.735943778693933,1.2429482971234576 +2,67,18,34.51934775,47.52980027,5.921666758,129.0064612,pigeonpeas,24.124161352916854,3,8.713087617708426,7.927732702496828,380.78784264011296,2.135078374743898,2,13.113106191625395,10.783436678327462,178.01525136395938,1,6.265937847329239,1,67.93701249328508,1.6069438208698967 +1,76,17,28.43430726,52.10010827,6.012719118,147.0414824,pigeonpeas,10.322210948495435,3,7.059986014422503,13.331968197923521,400.16958727229434,4.776103425362379,2,11.832189626695703,93.52162764747895,151.3597257104543,3,12.17537629126369,1,33.422666661530634,1.7682827063529336 +16,73,19,18.41645629,34.80541039,4.684079249,163.2747473,pigeonpeas,17.63277578044524,2,11.686471231031726,17.271656472868653,429.1787060464478,8.252554374619912,1,13.382907333907259,50.241013265608416,130.2794538965753,3,27.940574978709492,2,31.461077411969065,3.3914565384880424 +23,75,25,31.07508973,47.19847683,7.077170002,91.31256412,pigeonpeas,21.787629446120242,2,7.203048917853369,16.364504322227166,438.7993854986912,4.605787243763082,1,17.111194461001013,92.44687083929341,89.82692143137432,2,4.83770581399714,3,66.79908576974834,3.498975266547826 +32,70,20,20.89342749,46.24856523,6.208843215,195.5697875,pigeonpeas,16.034651062413694,2,7.597466618249813,18.09252224578785,444.4346372200027,7.4887836337328935,4,17.358501503782144,54.96703957988509,56.246533803522006,2,30.48635653692184,1,7.968255405879954,4.676194814949666 +28,59,22,30.90607799,52.79913039,7.05181629,170.9919828,pigeonpeas,18.134095039370962,2,8.048709457538177,18.90537272130784,389.1588407432497,3.605138801450135,2,12.660742406619894,74.66357885263196,108.75597526214341,3,20.76251157572252,2,7.519390059380438,1.8207839209388963 +5,62,23,27.9348279,66.45457122,4.722222454,145.3728801,pigeonpeas,20.869700420788323,1,11.27873115329217,11.289289540689381,413.7417849851939,1.6066560905842264,4,9.84355084929923,76.06371706901697,72.79471468140349,3,16.019638661857034,1,29.794629948175956,1.2732306639202835 +36,67,25,35.95176642,36.52780776,6.418062652,136.0456753,pigeonpeas,17.446672583396285,1,6.58572281301667,15.862044290844464,430.8558823579834,1.2531178287238056,1,12.954481426575164,46.106829434966926,191.0977066975583,1,42.038291533204465,3,50.03302997550292,4.472393623152479 +1,66,23,19.54317155,56.92831399,4.803564468,173.1686574,pigeonpeas,20.8171208872756,1,7.202590149079941,19.57669843664285,431.8073292562947,9.127032084325588,4,8.615274134613157,19.48018064140652,126.89525398683388,3,25.428614916510373,2,32.5183729591195,1.6908366413663511 +24,73,20,19.63736208,32.31528909,4.608695247,176.4134092,pigeonpeas,22.31779011810577,2,9.738809435603242,5.283260675237964,416.87115733929676,8.618869860363159,1,17.582753674460637,14.167041069206675,142.36918200156327,2,11.960797835605552,1,87.35628684273347,4.7744718468283285 +17,67,18,31.2192752,56.46868874,5.611510977,129.2028653,pigeonpeas,28.584563960000423,1,10.665537972205673,7.90816136550053,449.9733068383365,7.890303208779609,6,11.063283449677371,88.478642925792,158.88715414329522,3,27.7935243892657,2,18.574318282464553,1.9606372188415873 +5,55,18,33.50876355,45.70976142,7.322097972,126.6738117,pigeonpeas,26.79830410905741,2,8.064716971424067,2.894781351395226,389.7996774800895,4.357972695646012,3,16.179015307276543,63.002023901417836,196.08957382808669,2,18.919240268302406,1,83.59295911078948,3.785333202508487 +5,56,24,24.80710166,45.01110015,5.023115055,188.4928637,pigeonpeas,14.076418862577299,2,9.586080669881008,4.541020922155594,434.6159287482174,1.180529729765198,4,7.194281438495965,92.07260258801698,81.20702457685772,3,46.2466479291619,2,47.1327434243671,2.9088834859272543 +37,77,17,36.20970524,31.94550613,5.617122801,191.0658531,pigeonpeas,23.447386608015286,1,6.942108449147423,12.744894614392637,440.6651640687926,8.530714437243448,5,8.554081150991994,72.18538639341287,77.03901767446263,2,35.63747536716709,1,87.11738363584031,3.0798116927372012 +13,73,20,30.50420876,35.48885969,5.391560418,162.5927723,pigeonpeas,24.55603787299254,3,7.928709300776811,17.28371263208555,430.55113994573094,4.7101012390866,4,15.877132841039673,36.83027364192885,79.52207931110493,2,38.34990097381099,1,37.78509354317292,3.574421346075017 +6,63,23,26.01630259,49.94704718,5.906596905,160.3337447,pigeonpeas,23.026259523357034,3,11.97776724743307,3.047720747184952,358.1394668153906,4.953195631470677,1,16.953700794551743,11.35763771398871,185.09557951286303,3,44.26430368781096,2,58.31548680823973,4.696484639886428 +16,77,22,31.48469278,35.6395615,6.574209678,100.546816,pigeonpeas,13.503443492069124,2,8.956100256436343,9.960515797690979,399.2849832833393,7.6608800139634,4,8.910023849945752,24.05886596717296,156.7035367746551,3,47.0830384392201,1,46.47125693275819,1.0819200898178427 +25,64,20,33.15122581,32.45974539,4.807776749,105.0380275,pigeonpeas,11.14442455105382,2,7.7323394286812315,12.418490607253945,371.64069736070303,8.471443713043529,4,7.370538842074502,96.82422518944526,177.58432507769277,2,30.505387034970067,2,80.38203716999492,1.5086974737716083 +34,75,24,23.50222822,51.29019509,4.760038039,192.3023991,pigeonpeas,10.861290327140908,1,7.987065028235585,4.231017063218074,412.5181946462031,3.3636210970702605,1,15.18864854160398,64.34425166024353,193.08952705065823,3,42.794490340410974,1,11.337315969987538,3.5986807495202764 +20,77,23,34.87248659,38.83786012,5.180271502,148.2502786,pigeonpeas,19.13500094602272,3,7.570002919107106,0.07464850035170612,419.39940794905766,8.411286778320967,4,11.465456695130468,13.776235633911782,73.0235131968403,1,43.02771042008818,3,94.36784942325365,4.306364157586762 +35,80,25,28.09269012,44.93322042,4.895927306,197.1144011,pigeonpeas,21.01508297367535,3,6.768425907113386,9.176459109960591,449.69232699515175,7.076989479594747,6,17.75523553220434,19.468981782557083,103.31310870234513,2,44.83432482297855,3,59.968533694020245,4.000572971014442 +14,75,24,24.54757829,57.3414485,6.436160044,118.3606557,pigeonpeas,16.436343351656625,3,6.522948074585463,16.700265064937216,411.36756159413864,5.54518954498311,4,7.815381712558966,2.2772659267670026,190.3058554769988,2,48.64671607512711,2,75.10612756838738,2.7556172750462204 +36,80,21,33.64769646,48.41490082,7.066087261,100.4673278,pigeonpeas,20.27335682184806,3,10.909934358866781,2.838905560239753,354.6416284368445,9.65905136443818,5,17.967089587263132,26.90585215624942,140.79218594993338,3,47.38884126310429,1,39.18615519181036,1.699460049023381 +7,77,18,20.5591255,60.54880693,6.655918078,191.0895109,pigeonpeas,18.77264635408146,1,6.184451405558974,1.6341812203629846,436.4578901391969,9.835521220839937,4,5.575001624572456,20.416296545625023,87.47150540588083,1,0.2636957763911929,2,43.23573745712835,1.7124943520961442 +29,78,25,19.95991719,59.33157782,5.982854523,195.787103,pigeonpeas,29.57944517574392,2,11.51368180516924,19.164810724013684,360.36853014280257,5.197559446172972,2,8.641981225225265,45.60880989017204,189.57989571154047,2,17.280263379839646,1,51.97521725244837,2.4190333319256627 +30,60,21,28.87667593,62.4901206,5.457871273,182.2688175,pigeonpeas,15.639245965487605,1,9.900521595873464,7.846074262170555,369.9352933596119,6.501549941814209,3,16.21358945528566,18.80517726955916,141.99022831768212,3,4.307914033775656,3,36.17365135573761,4.159844989748362 +20,74,16,36.04353699,43.61444121,4.759490199,159.8938645,pigeonpeas,16.830723829140045,2,8.064456103666004,14.995786516977063,402.82825699768955,2.988959952750694,2,13.25523101909122,51.96817848874,90.76441502130152,2,45.35493942118649,2,43.351612328802126,4.092615972442488 +19,57,23,23.6734328,47.2879691,7.342409555,141.1250722,pigeonpeas,24.70216545099768,3,5.038039970731716,6.990036156706598,355.5609497016683,8.029519026462346,5,13.501224634817897,13.998744811462306,143.4011115741675,2,18.446681063608082,2,47.07600971836663,2.6667848452473466 +3,60,19,25.74679443,40.7192594,4.820788186,100.7791633,pigeonpeas,22.86168392964897,2,8.234082262241294,9.89160977209436,382.7144182317677,7.9811985892574935,2,5.949172376027609,54.144344513121844,139.15722806348896,3,36.29989532340239,2,71.78533469383129,4.075655368623673 +5,77,19,31.08564994,66.68832981,6.242052013,175.9303271,pigeonpeas,28.352043418526467,2,6.335672726847935,10.356971283633287,432.4522558730647,6.7628158819666435,2,12.225756717426703,7.194244291984974,198.9523482048007,1,25.692980204792782,2,43.14961762999783,2.9985365943971094 +5,68,20,18.72987676,61.33186249,5.001038726,139.8710041,pigeonpeas,17.990852893646625,1,11.778976062056287,14.043927356604819,446.24268330153006,2.289837129881322,3,8.797810485292596,45.8892218288413,56.359224984718416,1,24.52575928744191,3,14.782124127234065,1.03974778899105 +37,73,21,29.50304807,63.46513414,5.560224583,189.5208915,pigeonpeas,16.244048200372607,1,5.22703179719752,18.672271589428895,406.2782932567569,3.959330534853915,5,17.36801145167252,77.78734720869814,110.35238385794995,1,39.60405652491491,1,5.988386194081663,3.492891658754013 +9,59,24,20.43517772,39.37252634,4.747352458,137.2279662,pigeonpeas,24.11837034334575,3,7.102052572222014,0.5626128041366352,430.9584213685999,8.345998421077478,6,9.078079062885616,52.150800484097715,139.96582279338912,1,22.076318465169432,1,26.803205066647994,2.608225250711364 +20,72,15,36.00415838,56.01334416,7.313517308,134.8596466,pigeonpeas,22.7039820593924,3,10.274899268364786,16.330875299203804,433.6716738575575,1.8557467592590502,2,7.703686401967665,6.418773604403083,82.76020645587563,1,9.879507351223287,1,92.15642855353462,4.943149790517502 +31,56,23,31.46846241,35.39454002,5.661826398,174.5723999,pigeonpeas,15.507253104127388,2,7.2470653756572005,10.040611032245842,390.4098123071718,9.65620240561114,5,19.95366295593367,4.524194101428525,186.50269846897785,1,36.47228811737156,2,33.079290146763306,3.6378899378599066 +0,70,21,36.30049702,56.03021253,4.672437054,101.6073988,pigeonpeas,17.245111131009196,1,8.165665517651437,8.146610467999624,419.5958786727825,6.55541426532938,5,14.58621345194688,56.25917128842318,169.9866401927882,3,12.784041583855805,2,18.494558229034354,2.422677311419732 +21,74,15,29.49096726,67.10604388,6.471862118,153.2504506,pigeonpeas,14.239629393797188,2,5.930180890564917,0.32484887205410873,365.33510349048817,9.241203703454989,4,14.985989553566508,7.08997334441298,152.61891423472179,2,30.12338303044293,3,69.92512908037837,4.359632779679584 +13,67,18,30.5753044,34.75591197,5.384762927,177.5764304,pigeonpeas,22.880546308604025,2,10.942878186363426,11.94264163464223,377.9078667834244,4.319519021245091,1,5.808709625178518,10.447275840691262,164.89638779262273,2,47.36814043081315,1,54.498712379347936,4.155442907950057 +27,74,20,24.69487673,59.96669215,5.859813416,91.95792434,pigeonpeas,13.651598067000839,2,5.6186189868997465,13.443017448466225,417.7584060263794,2.3631330629197542,5,10.684600526814688,22.685509955830096,100.93672057870154,1,19.134547378924243,1,92.25768325692162,1.0725564482426457 +29,72,24,23.17409556,36.67847052,6.962386495,162.5931264,pigeonpeas,27.40851176067615,1,8.759670980455972,11.85254992025726,375.45628263398305,9.07797299695461,6,19.27734323436708,32.49741574149822,182.59012436242682,1,49.99204584876098,2,57.48010280073774,2.839896353124249 +5,68,20,19.04380471,33.10695144,6.12166671,155.3705624,pigeonpeas,10.044198588370408,2,6.377857918574554,3.218027096446643,446.6280797311101,2.637351437067509,4,11.72858308485769,36.900872725195924,135.44257158631348,2,7.623885438312506,3,99.47926374120914,4.0160719335812285 +39,57,19,29.32379604,45.93248374,6.421748487,165.4113371,pigeonpeas,22.228344391082473,2,11.059069571589205,0.8361363364353247,434.4837652511268,4.856109122076338,6,5.835193338661812,95.82393263241204,129.86446070526964,1,27.985681979488046,2,26.736174420734415,3.5114350324178925 +22,62,16,34.6455408,54.32342534,4.828936119,180.9009998,pigeonpeas,14.745659861885434,1,7.799059646503258,16.392485963873817,383.4980328160074,2.885249036019889,4,19.017045553917093,94.78785917646299,70.48868534896987,3,46.021443014509984,1,92.10351653688119,3.2821039336808853 +18,55,23,21.9989826,56.31006755,6.98571967,136.8274312,pigeonpeas,14.667143323270043,1,7.980615549339858,19.375729406783698,399.1437576006446,5.367992171646964,3,14.535549974353454,29.90132929887428,90.34594853642264,1,11.247155565094152,2,37.70033450106457,4.291225983403853 +39,77,21,22.99774444,60.24218572,4.603563116,159.689339,pigeonpeas,29.525719559895617,3,7.761819565680114,13.384859539509122,394.2821505911681,2.0453140144429676,1,6.455915043750408,44.78452809132213,77.95457635828373,2,15.685464265680038,2,81.01769731017642,4.6962984511536 +13,75,20,30.55992394,35.29006485,6.979540061,178.8998611,pigeonpeas,11.99450787833964,2,9.72952489364852,10.082901755397781,378.2325483923352,4.519431539600638,2,13.24111529061029,76.03519712258641,104.24454508478487,3,46.416719927214785,1,31.291135751010167,2.0864030250017445 +27,71,24,31.46417866,48.17631461,7.064973419,165.405354,pigeonpeas,16.833088612854667,1,9.719712245085152,9.207350550530762,386.8054235053354,6.415453096399448,2,10.810086805519596,7.8634616682689895,79.9700454526329,1,16.15480066483172,2,44.317114820190284,2.104668243475422 +26,64,22,25.95058595,40.58227261,5.16516459,109.1821183,pigeonpeas,24.337247163015938,2,10.219375584757374,15.565935564870815,433.5296174197556,1.3092660917145955,4,8.62793459163246,16.60671766059567,138.38235925906838,2,22.816533471634827,3,85.12215475327716,2.5847038450746616 +23,55,16,21.01142393,69.69141302,5.111488821,185.2039114,pigeonpeas,16.046434214997014,3,7.916642599503468,4.124644356944147,419.7175571666999,8.183942202302532,6,11.023266218165512,10.228139129492131,74.41770991767802,3,24.758953684551503,2,60.62679941083644,1.5024540826707704 +4,69,19,19.25100056,47.70351758,5.374358869,149.063196,pigeonpeas,25.892655539610168,1,9.452703344933475,18.47207847725167,447.0072833307881,7.461516330190708,5,18.598596923480102,13.034343145125737,148.0183145382528,1,35.58946778266804,1,56.25389543240924,1.2198366650308894 +20,67,19,19.24462755,50.54495302,5.671419084,180.6465282,pigeonpeas,14.831856249968645,1,7.841199551983227,14.694132912947095,422.6342923059257,6.620653760993394,5,16.097174434644288,15.733637607200935,114.74736875201229,2,13.659378242123916,3,79.09846833731524,1.4890945348193356 +7,74,17,22.47253208,62.56532471,5.667419697,96.74706956,pigeonpeas,29.64027199573485,1,11.529502570846837,1.8110511924700123,422.38957962672725,4.903695084345505,5,16.58398431298572,38.58947571034705,74.88790175286982,1,29.066355612598223,2,83.29311073762507,3.2489262863574075 +17,64,18,36.75087487,58.25799145,6.07938452,124.6028153,pigeonpeas,13.452076748075058,2,10.431872591375136,9.009049731989796,362.067983606727,5.062420687881043,5,15.528392114933308,97.4830464490651,109.06767149358619,3,8.510428342517063,3,89.59571333514829,2.867079515996433 +35,71,17,29.89286629,66.35375127,6.931924963,198.1403003,pigeonpeas,19.729388147239106,1,7.796798422648668,19.78073427327339,372.69953369554355,2.580833583452166,3,17.939963291311606,82.34839540424883,167.53660060263786,1,26.267010197691675,1,7.047671970007851,3.158771072163165 +11,72,22,29.37735586,44.82294584,6.842744374,172.40168,pigeonpeas,11.165825145073999,1,6.4850700400982495,2.729360502483018,413.44592099441434,2.0423619297680156,3,8.332613524276688,85.29931509617745,140.4591296447267,3,43.30828590240738,1,8.4472925798584,4.18667506021106 +20,60,22,29.65052947,42.89833235,6.876572503,186.9226052,pigeonpeas,28.194653190695508,1,10.291790920882903,15.893567200147881,427.7824063629527,2.03832234107195,3,10.744098444982082,36.073363415089176,87.06233455607287,3,26.990178094741662,1,83.289455549128,3.872240998797074 +10,71,18,19.54284889,66.34777265,6.151029296,173.1106982,pigeonpeas,27.86323769357574,3,7.377153245928853,17.50576299965807,379.6630131111904,9.312911851010073,4,11.671045945311208,0.14174937034495683,94.12727903547056,3,48.37757608848062,2,71.37189488648616,3.75470248949503 +33,61,24,20.04611791,48.93905624,4.567446499,122.4564203,pigeonpeas,27.798513633552275,3,8.392958698758978,17.853521968635,379.1836424720473,2.892880014908596,1,10.909462399410742,82.96361104411935,93.98281928793581,3,2.334894659604736,2,4.0827511909582,3.3577250720615424 +3,49,18,27.91095209,64.70930606,3.692863601,32.67891866,mothbeans,17.573282397210757,2,5.100458492857327,11.15411359530355,430.48643418154285,2.3409097832477466,4,17.554036447785077,72.33332389924287,195.60733541490322,3,7.289069138381132,2,56.52394268103913,1.0041728913278836 +22,59,23,27.32220619,51.27868781,4.371745575,36.5037914,mothbeans,26.01405623923391,1,8.263482514111107,18.151300671961287,404.87018952624555,8.791931589783106,6,5.727882794717728,69.08412850817253,150.2109306161392,2,8.486648988490703,3,29.337332671099546,2.548836781386438 +36,58,25,28.66024187,59.3189118,8.399135958,36.92629678,mothbeans,17.12723207972995,2,9.417667784971666,10.874538512061804,390.1517519482007,2.802286344947758,2,11.969687372725883,25.72878538527653,113.97905735875419,1,40.09185576982474,3,49.66884655041955,1.7731919365727995 +4,43,18,29.02955344,61.09387478,8.840656256,72.98016599,mothbeans,12.606522573557639,3,11.897958796269345,18.416210459047264,391.6395984632415,7.170747632275008,6,17.616900358745152,51.60750952933475,64.9602113199584,1,33.01257553599826,3,8.328396702255592,2.3145428733280866 +29,54,16,27.78031515,54.65030015,8.153022903,32.05025323,mothbeans,11.615198698087507,1,10.079705165529358,8.35703555840556,420.83048848028176,2.221138935208067,2,9.654483998218366,29.88387503818357,85.5268868908761,1,4.095248605924878,3,53.0151193876688,3.9389088536600694 +32,43,22,31.99928579,54.1077461,5.270749441,71.6266696,mothbeans,24.83904661075843,2,8.708023125843345,5.205510666994682,367.3763248550832,7.568199070044663,4,5.890130803357259,99.98384949755518,94.41574136216603,3,11.064872913830525,3,7.0491093325495635,3.437298942377883 +14,55,15,27.33580911,55.27755933,8.050304395,73.44775287,mothbeans,11.544266452178187,3,9.932868173350284,11.378222792285902,351.38413662193847,2.3693736209211034,5,17.150529007031782,21.51548190093091,134.85684466631554,1,18.277132306151984,3,46.19984620355084,1.6239782461563799 +5,35,20,28.92952635,53.57014709,9.679240873,66.35634104,mothbeans,26.908255648224603,1,8.359809256671983,11.163971075003092,430.0530549212115,7.571832587639098,2,15.72303360949804,21.287054494381074,121.75759784343404,1,49.006532909798636,2,9.004401448160804,2.8596900172266317 +25,57,24,27.65472156,58.59986279,6.974978386,36.94255012,mothbeans,21.07517038249238,1,7.07358176142308,1.2832465192822728,351.04684736989157,1.0663580567255357,1,10.705276851302322,98.89310716277177,192.64825248339085,1,20.21146731720027,3,35.23664690550723,1.2659463906501132 +11,53,24,28.52396666,55.77264351,7.39389918,61.32935611,mothbeans,18.250433434583204,3,8.108059038188838,11.803329126516642,360.0404156804542,6.708921807696146,6,17.42723603484213,87.84479566748094,66.36210819149159,3,49.97218409725636,2,20.13563572535022,2.869790596189484 +40,49,17,31.02215872,45.89239456,6.68727523,53.56783314,mothbeans,17.048585962513297,3,10.457466239240961,9.826256579033817,389.8110347339485,2.1359791206643526,6,18.856408257009438,20.75972524963624,148.17511127804087,2,7.776370569015578,2,25.65163721490693,4.72144708002484 +38,56,25,25.74095321,45.38497051,7.88118645,67.43488235,mothbeans,24.86856130849398,2,7.748566972417489,2.4189211863841553,403.1371435476497,5.111341365333063,3,10.98509066560553,64.76523037957898,103.2289550857089,3,1.833968966270244,3,80.77708642281046,3.0397958781830168 +27,43,23,31.70447482,56.85420099,5.875333778,44.94317432,mothbeans,27.806659233164098,1,6.806429734997205,12.728098015724791,417.4658934608411,2.230219092953507,6,16.878948078837034,83.00983292599187,64.04657307232945,1,48.44172253704666,3,14.033001317224658,3.0178178831134757 +24,38,22,24.47876451,58.51663927,8.202706015,34.96933295,mothbeans,23.257199051088868,2,11.925440595643387,16.63706870324745,436.7602167316314,5.584262872685959,6,18.5611307663104,15.756452795115816,73.34907317305174,2,9.879132063479679,1,0.19860598790878425,3.6967140828682776 +23,45,21,31.46511256,51.79939437,8.985348193,74.44330654,mothbeans,18.118653454203823,1,10.105891297208963,16.353869999939285,419.7411383060934,1.386371826519444,2,13.963984470745284,15.77207477645276,75.54763806100813,2,25.2781903703591,1,21.95350472597738,3.4616155314618786 +29,57,20,25.60973447,50.7330069,5.87707519,53.39249517,mothbeans,29.169742921783513,2,6.9757150372072125,6.323442573401032,392.053457650311,3.6516115462137133,5,11.292955818708386,6.343705855618975,53.501604773783804,2,10.945680895513998,2,88.44335936984376,2.358239129325595 +31,35,23,30.30260453,47.18283631,7.707595055,68.04039813,mothbeans,25.352515979929674,2,6.214647539275902,12.568578522885785,392.9769192949655,7.613266290138664,2,19.228413774271587,56.85547128625378,87.89114469318281,1,18.593788525755823,3,68.86566465514994,2.9557487234328796 +0,55,25,28.17489437,43.6672299,4.524171562,45.78172762,mothbeans,28.095512411051537,2,6.0274706818872446,7.0286695283335305,431.9242703409816,9.636186259991717,5,15.564519479022396,43.605291317769044,150.05138747500177,1,29.944929924524637,1,9.007733755308378,2.721114664884179 +7,45,22,25.50634557,44.8302551,9.926212291,74.32635105,mothbeans,10.926084651987418,3,7.77694540534142,3.0999190140538935,420.0316152391806,9.33482433215969,2,5.413359727042585,57.97611468762815,103.58740173572481,3,1.6246786537615687,3,39.941586058805555,1.5166004477830044 +17,58,25,31.12896766,43.58788762,6.455592696,32.76742894,mothbeans,24.125689888251497,2,7.324768177641001,1.2827930135375865,359.04148202237513,7.8060160305005475,4,8.174547729689085,41.96842317375565,62.77071764540508,3,6.187008780024467,3,37.505849605641586,2.855860833156846 +11,44,17,26.34043268,55.59160391,8.016210782,35.1051197,mothbeans,11.573780353481702,1,10.293850266691656,4.911955140908153,437.3320463764,6.123651614868954,2,5.1249251282197275,85.8472732342378,171.97510511970557,2,31.892995885411512,2,91.36980617485028,3.2526238568614763 +22,49,22,28.23494706,61.5620517,3.71105919,72.66666443,mothbeans,11.265376754776792,3,10.511776176755053,0.46316321001582006,420.296959627764,3.885654009085365,6,18.779876016925407,3.9592397402004154,135.8560201864664,2,11.201699148310606,3,7.454777151682301,4.424937921347251 +9,51,19,27.04453473,49.32609633,5.49091063,48.25207759,mothbeans,26.027072914641117,3,11.75673661873803,5.4303329158951685,381.33213636614124,9.901878199868321,6,17.242728883824885,58.89888208111651,80.1082714377829,2,5.802295833218373,3,17.794526587390635,2.5315101021249875 +28,48,15,25.16125354,55.25435777,9.254089438,40.89732789,mothbeans,13.525332836338746,1,7.442243451515088,8.21897640721923,355.24233711908437,1.0103587987767262,6,9.234221762442726,27.906047528164535,159.27389151368942,1,13.669812043066331,2,78.18973453295385,3.1617821912077426 +26,50,19,27.3179125,51.66921088,6.005242945,32.55919573,mothbeans,19.018076572832353,1,7.933340132247877,18.42835587199551,422.60729324840133,9.261260354457281,1,13.44969647878644,52.39873541599045,70.07485656858276,3,33.94955012853916,3,66.3630567879059,2.0856150709311345 +36,56,20,25.4123765,49.66474269,7.437078236,31.87416982,mothbeans,19.393259064605587,3,8.765863699907554,14.727728728378931,384.04013458836425,9.770668477688606,2,17.684822873699073,66.09380200029064,55.34380260488707,1,6.79093563889327,2,0.7280946602128302,1.2605293273887348 +8,60,18,31.21629982,46.01868196,3.808429173,53.1205277,mothbeans,13.46856493102478,1,8.402865778594041,12.981089351329178,362.81672630061126,9.695467732223031,4,9.643269153563654,93.79909141966884,65.69833986214387,1,21.328050402116855,1,81.39506209494223,2.6847596999925734 +24,37,21,30.573999,58.22686794,5.818219385,62.74803826,mothbeans,25.303038280668773,2,6.462734607821044,9.123552992143704,388.27063177431825,2.0028904941047276,6,5.201428788189604,8.8648315856092,193.50009456192606,1,8.245701996937205,2,58.607770465678335,4.591634792174229 +22,43,24,25.42517036,53.2208266,4.52363558,46.19374559,mothbeans,10.384722746040309,1,5.152436642254749,4.127735518442794,364.06424413236863,4.428483981852416,2,7.713901087386951,36.081525421016046,104.4500431856076,1,29.292794786311948,1,66.58225813490337,1.9051116824687737 +36,43,24,27.09400578,43.65305437,3.510404312,41.53749535,mothbeans,29.5591348015403,2,8.345169287671482,9.928861498601332,351.17310417679784,6.646557313664252,6,16.001342304717742,14.368457070070416,192.13503225182203,2,27.75195358578649,2,7.377297652827364,3.717284958526059 +22,44,24,24.30935081,56.32938343,6.030447288,58.99536268,mothbeans,27.88877649247166,2,9.097579365623632,9.44038129615925,437.6713263004832,1.175085103251879,4,10.179467325429528,70.48314313755496,169.3729531513219,3,41.95254529341952,2,42.32199988983304,2.5341215700713353 +17,43,22,30.06142622,45.90067655,5.498340808,41.0550915,mothbeans,11.445407233793619,1,10.380958989041844,14.150382441612681,414.6402456940078,1.0482189720748183,4,13.108703325371783,94.10709396744404,133.98597976313476,3,33.375535746664255,2,67.53866777645185,3.7857122472684845 +8,45,15,28.09568993,60.9835384,4.61136408,33.84110759,mothbeans,24.504910635508402,3,7.11329500181969,1.1143357012464472,417.95193169259676,7.5492789463865435,1,5.9612437187659015,3.653843785149502,92.52929790018439,3,13.03319791861331,2,76.9798757571411,4.44995117029985 +7,56,23,26.33908791,40.00933429,5.545219232,55.50429227,mothbeans,20.44935177539218,1,7.707057416892088,8.59736011517884,428.3681702350199,4.154018483836744,1,16.872167145116116,62.103506744888435,97.96817829051771,2,3.6839144098085255,1,26.30196540979203,3.0540979160295216 +36,57,16,28.61409059,57.14218792,8.292875734,57.02891698,mothbeans,18.46843246408917,3,11.887092101877393,4.1763188447803845,433.4767292280572,9.501114988650997,1,14.494483330958834,72.94605548142832,144.12419207177956,1,1.7580950527365913,2,43.60769997944701,2.5855622341698066 +11,45,19,28.70012137,44.359648,3.828031463,44.11622138,mothbeans,19.20084965700184,2,10.18595844508863,2.864367921471036,431.9444276746626,5.445899020895337,3,19.284462888624027,2.253652286860619,76.50941211221219,3,29.711834607273385,3,11.07045571800629,4.561022255165051 +6,36,22,24.21610338,59.79236306,8.869532817,42.24783476,mothbeans,24.409597837236525,1,7.8509425494344125,1.590128333107852,368.04832867634144,6.2501029305235605,5,7.148719312243441,72.28773596599592,63.019140869967686,2,49.82560446808189,3,95.79393039327874,2.7431530677730804 +17,57,20,28.50677929,45.20094476,3.793575185,66.1761456,mothbeans,27.57426251263818,2,10.755912510707798,7.463916927219911,404.4048518098975,9.144692149246444,6,19.441137990364034,10.368168190678972,186.93661598195618,3,37.181942755236044,3,14.765475560231977,4.498958863951639 +4,47,20,25.97948991,64.95585424,4.193189124,72.19245835,mothbeans,12.48625697800941,1,6.888997965955616,4.055158152103395,377.1448462822919,4.11430305005092,3,14.388854835968619,63.21820813301468,70.98280936652947,3,16.42266572446785,1,32.173329493429435,1.1341619918524493 +9,49,16,30.88482722,41.36561835,7.661537348,55.053805,mothbeans,24.93190490364716,1,11.023257661847211,5.506013210643623,401.77160438701816,1.7119211169722952,5,7.0026889049812135,50.62880046296776,136.72146349939436,1,0.6054499468604202,3,10.401699466046676,1.7235228422721587 +25,51,24,25.5042419,61.66852372,9.392694614,65.07981523,mothbeans,23.430910355174166,3,5.4761975243937995,4.202151055088914,382.680430336379,4.223864135743031,5,14.698367051670084,71.51866914165048,72.11757015862318,1,18.17645519276208,1,93.58967974215268,2.801284532121414 +36,44,21,25.12528913,51.33189406,4.516154055,38.48678973,mothbeans,14.113912935636186,2,11.688404534076582,0.8382972158191615,432.37075333914214,9.152760416666991,2,6.36204527369882,94.55457597577266,134.54231677349554,3,1.7830325527044888,3,71.2902252473865,1.6608205131010876 +21,38,20,27.10508014,63.56791363,5.794289715,62.20279647,mothbeans,21.213342045202147,1,7.565801683677528,15.997406020985059,446.36414313890407,2.647659583408006,5,14.774861131802611,66.14267710057045,109.27265748940968,3,4.020877711638598,2,13.876496959401807,2.7449521485987938 +37,57,20,31.1006247,44.82069159,7.354286985,70.79934452,mothbeans,14.26583797542585,3,7.118495163740955,5.476392028317223,430.1141064496881,9.98903778133353,1,19.14292163946303,35.597760018114485,156.266629819775,2,22.82035408784942,2,10.498451236084104,3.3087370385685486 +32,48,18,26.45707778,56.40226277,5.993513566,64.16167699,mothbeans,25.227846210353015,3,9.181847866561107,3.1267821585822198,362.4001194434914,9.134486155608695,5,8.092848671021642,79.73240359564373,69.916176229547,1,46.28593905428339,2,32.31527518586568,4.524122583910098 +29,44,20,30.04132304,63.56222995,8.620107545,31.83192392,mothbeans,19.356322071679983,1,10.538869813624853,19.427562759684747,433.08163265632635,3.6507575910304486,2,12.35642361704728,97.55643544605765,81.88184770662065,2,30.229827589306318,3,83.76342830669155,1.7699315091087597 +25,51,18,27.77799528,54.82130787,9.45949344,50.28438729,mothbeans,12.141741565541352,1,6.08755579213822,8.926745765566771,421.63354182281824,9.685597516954667,3,7.17052391030479,41.786773298656655,109.12063987880083,3,14.593600827653669,2,18.82304361846331,1.3803628798257823 +10,44,24,30.99256944,43.02151392,8.0344125,58.27600682,mothbeans,20.60356975186675,3,5.198721371433642,6.734449417842585,421.466521744887,2.3733056267770674,6,15.793631203378162,94.01252710877459,179.5541255861399,1,41.08195017079291,2,96.01972839772706,2.342447833046625 +23,35,18,26.4908332,47.36534833,5.414492777,36.99362831,mothbeans,10.17966022997495,2,7.41990657347036,0.3520412928731842,374.77182950436526,2.865425093617887,3,17.280469009255008,66.95747692121297,70.72553267929365,2,39.519403067992705,1,86.2072700690447,1.7814119769699106 +9,60,23,31.96987867,57.17377029,6.276004336,64.25520357,mothbeans,13.79313101036887,3,9.58564940116704,3.72048370173369,359.8508739022864,2.7268210496492054,1,11.444869033740234,28.929101204641782,169.0991222838419,2,12.543930656570279,3,51.18254203249173,4.652752716874536 +3,58,21,25.36140526,46.82652785,9.160691747,55.60523179,mothbeans,16.426411491519072,1,8.094934602230964,10.607060194366191,410.7766900630419,5.506651969878731,3,12.073077050973263,86.77419076748542,120.10039387658452,3,26.43983204921637,2,36.55805684308399,2.7232063881458193 +22,42,22,25.54249137,56.96640758,7.887658711,48.46797044,mothbeans,26.033191890241227,1,8.233225800182886,9.302279965579409,368.44765859667945,5.821035325996104,2,14.716642409607793,55.17728597685514,183.8644627091056,1,37.183745032062156,3,95.52024469602831,4.26017790347494 +12,39,21,28.99319096,62.85948245,8.183844843,70.4713043,mothbeans,19.10396294030041,2,5.519090137193511,9.176797866627528,401.04183079711186,6.99922502348349,6,15.048268716724387,60.21005850141776,108.0819451111833,3,27.729661652341253,2,65.1256786870742,4.768111346853152 +39,36,22,29.34317422,60.50320928,9.072011412,34.03335472,mothbeans,23.7481119491935,2,6.942578069837338,18.188073487694695,373.1951168256593,6.522792197681198,3,13.748538803232183,63.325220592495526,196.73922781149773,3,14.052703827279894,2,30.08389196604795,4.97176643447683 +32,41,16,28.63618921,61.39451307,7.702287236,68.54877876,mothbeans,25.256174771256287,3,11.350258278013676,8.99448400086142,417.202182771941,5.1479472015672,6,14.537185600385692,35.891957087380156,161.97563041305943,2,39.75883986656911,1,98.0636716590419,1.9752259311759577 +30,41,15,24.83206631,44.17085032,5.88509677,52.0810886,mothbeans,15.573493072138046,2,7.46173487080748,0.24224056835894237,361.0616733642571,2.688074360685721,6,16.114580894617685,50.84103894466,183.4788858193433,2,22.39112816062599,2,28.31343965322757,1.500033649118056 +19,36,22,25.44689075,58.55363573,6.16496284,57.04826619,mothbeans,12.472760939716386,2,5.0865336027157,12.715974185103157,354.359080894786,6.43622664059352,3,11.482433909632238,51.29112054684109,166.81359138339207,1,48.68269014388886,3,61.58924421605306,2.917055250346679 +4,46,15,31.01274943,62.40392519,3.504752314,63.77192383,mothbeans,15.321627455529681,1,6.743617593126242,10.630288587974457,395.68833379405237,8.967959669057652,6,11.55860070150078,91.42943478850735,170.48577827146218,1,1.9145197915639622,3,45.595205861225864,3.8101407317778846 +21,39,20,27.06179658,52.3003173,7.388007483,60.74583498,mothbeans,19.13251841260769,3,5.6354975044578,7.464662211915554,413.41454684217604,5.412942836205235,3,5.28313780163118,42.218035025222676,140.37095176599914,3,28.0869786332413,2,52.915298472868685,3.545946938263496 +35,57,25,27.0956288,42.26206161,8.340398059,71.1271039,mothbeans,23.56947337146422,2,9.033133507510382,5.90323133755575,441.9474144587645,9.757850622065066,3,12.566995439532201,99.03135775148685,170.27179532513938,3,23.52476165963116,2,55.96148132094464,2.5024541391159563 +22,55,24,28.56800579,57.30636014,8.66077954,64.53027638,mothbeans,22.12598950239208,1,7.827600064598506,18.74376192640716,444.2638825265699,8.978364104809648,6,10.361250367717158,70.95905140423659,113.80370464769744,3,18.061600558928575,2,53.41398364952218,2.893315053847993 +35,51,17,28.79929247,49.84213387,3.558822825,40.85534718,mothbeans,27.58410011038416,2,6.76856305182862,9.298069441303314,439.21675617804203,3.0231065186472112,3,15.977537266532568,91.9824794406158,173.20601767433058,3,1.0669375779377877,1,81.25760071668577,4.227681612575604 +17,56,17,27.94293692,45.41393636,5.9565851,69.66289997,mothbeans,13.351745529739055,1,7.160445134450459,19.010130309102703,424.595452083842,7.951117757939804,2,16.721682918005015,46.59621541754919,197.86721849164235,1,20.44994482077998,2,31.727080435722186,3.2075655691378375 +28,57,17,30.47757686,61.58245338,9.416003106,61.86633917,mothbeans,11.737787437312106,1,11.121880747799414,6.969204722991755,418.64734462121606,8.571635824099062,4,8.622915328121376,40.14150557522558,159.72780159724152,2,20.443456199193633,2,20.286454207902903,3.6536123231119455 +22,36,16,30.58139475,50.77148138,8.18422855,64.58559639,mothbeans,16.499630482975018,1,6.7845779851311985,0.04434389045487208,373.8366819183951,2.9827260301467606,2,16.30010543808924,65.63109649177721,66.68024230037793,1,17.231306164293734,2,65.28986182078896,3.983436048324634 +11,41,19,26.85911286,41.81420849,5.131779302,44.13827124,mothbeans,29.17208879732577,2,6.7232256902208425,1.055976623356858,446.67846155749515,4.613717835618829,4,12.003692901279635,90.96242189660954,131.77956199612916,2,26.46554464155918,2,4.506975405767277,3.6548503113166952 +38,38,18,26.31051759,61.18749126,6.294130313,35.73403813,mothbeans,23.306985941513478,2,11.190040088456186,1.5101350407348346,374.084498936934,5.300930895718105,3,17.211387004749415,24.382088361289544,167.58019555749286,3,2.8292550355828503,1,49.52509258039326,4.894155611601693 +23,37,24,28.77833449,44.2252605,7.991902443,33.95825723,mothbeans,23.609538094119905,3,8.163641409624773,11.864364287784184,382.642480433989,1.7831109394897955,3,7.4292105941398034,85.56782899705304,87.5650796516407,1,29.094534872773963,3,31.46687187248417,4.603689600456786 +25,35,20,28.90245417,43.35365671,8.923095695,71.90018566,mothbeans,16.33277227942275,1,7.181924260227771,8.882671494141158,439.54834674652045,9.084841755288542,4,10.482235667986723,9.120261008995934,120.62498938190116,2,38.59900510129025,1,14.26571987382178,1.9946041239517305 +40,45,20,29.37687468,57.69622912,6.878498176,38.34303462,mothbeans,26.6498226819409,2,8.716148169141324,3.0422280078013997,382.0353649013901,8.642639870580592,3,9.847961214232349,52.11400095137779,180.83173704297073,2,35.538894391052054,1,52.15619989734045,1.9822284053132555 +23,58,19,24.17093241,58.25204566,5.243634849,59.18953429,mothbeans,29.471734752490722,2,8.928022244467222,15.795774316266826,362.0508627409824,6.121328423947078,6,19.127774952209766,86.21805374639372,117.80240379451624,2,30.84832746837372,2,23.34502627561551,2.719254884659376 +2,56,23,26.65333029,59.79023382,7.550090941,36.91852635,mothbeans,21.905366371713423,3,11.34175364351724,14.617363768866827,409.7672770565939,5.350897653031954,2,17.492823543939195,99.86540536898106,151.11576396874545,3,47.815470337081116,1,89.68950029693,4.718480531299372 +3,56,17,28.19912143,53.50567601,8.709291688,52.13580529,mothbeans,19.47215996496614,2,6.292275716190937,3.2088426309882445,359.45750187260586,5.799886960720507,4,10.191922205821024,74.98404631778031,148.88882305980925,3,6.669174268873595,1,12.130973613426866,1.559721103489307 +26,51,25,28.76488954,52.62741529,7.792508068,55.21606732,mothbeans,10.12783374743764,3,6.090710998443355,19.61449260009768,421.9934207024994,1.0547983975989674,2,12.80984755073947,18.17862636547416,184.20178071374147,2,16.726443286442542,3,11.441041217834302,4.114247028694821 +39,42,20,29.3499706,61.25353851,8.055908858,40.82840673,mothbeans,23.75561074964405,2,8.956207885682948,19.229078077496162,442.07756872320806,7.776503450647754,3,18.115890936466954,55.74187844546535,117.35881103358966,1,44.29208440798587,2,97.50584753088542,4.747570543314918 +27,59,20,28.00937423,52.60950014,4.397698806,36.01203025,mothbeans,19.800941871362454,1,7.5421254276168295,10.531910061996665,352.02010659378413,5.387893738578066,1,7.566411846588919,68.53483893330966,109.6933759824006,3,18.131837395046325,2,17.912191934681076,3.6498401030859715 +24,45,19,26.85851927,48.8246387,5.952384957,34.7426459,mothbeans,17.931702557056486,3,7.949974595168191,14.739774601959727,440.9807227665831,3.0715451539152316,5,6.817708312442658,32.07310474632433,101.59075383220369,3,41.0319500554062,3,75.55631868520798,1.147114139708056 +7,40,17,31.2123945,40.92604945,8.532078733,53.78769958,mothbeans,17.69495169682302,2,10.086239746582745,11.552296876402464,405.94158883996624,2.5917640833510633,1,17.976091007509233,49.7830802187736,100.9275705422738,1,31.09959910432035,2,60.587880462409686,1.9355313013043793 +15,45,23,24.20422636,61.43378674,7.224193642,46.0203959,mothbeans,17.042527658699484,2,6.717720909784783,15.82100629896809,367.82676836859576,7.877867227609075,3,9.83573360069213,83.72802182258143,184.20765856674902,3,18.14979430194726,2,66.1657712765903,4.721977067820061 +26,52,23,29.98835437,49.60384796,4.931890506,52.92929636,mothbeans,29.206922038473262,1,5.31334104987321,7.187470199539783,374.9419587184337,6.391610485124865,3,12.982212953762922,46.98706044200727,135.78244246209266,1,28.118690713561207,1,98.00355393347631,4.333622504210846 +20,45,16,29.93964907,54.61813464,4.626212446,45.43669946,mothbeans,26.74203318602375,2,11.563319189513368,7.045532781697674,436.06270984534893,4.828964833623635,4,14.486563402533129,57.06282847095188,111.58437942852538,2,41.17808626492882,1,45.713400726745114,4.908421322722964 +34,54,24,31.2119298,41.55934359,5.026003659,68.80141783,mothbeans,16.997986847340627,1,11.209331467418256,5.468808989125929,380.88402553408497,9.87852468370304,3,7.635770248975149,8.84601342805068,143.71975808146686,2,12.7539628395388,1,19.57365632992686,4.800636017704436 +19,51,25,26.80474415,48.23991436,3.5253661,43.87801983,mothbeans,15.86987574961903,1,10.717175118935199,8.995700363949508,376.2277612213674,7.581773117719541,1,18.850929499924778,81.64308005371697,149.18123915120128,1,11.872132615699005,3,49.67048177136802,3.3468590536246583 +29,41,21,31.49398069,62.84916863,8.86979671,64.56807592,mothbeans,19.320533704879914,2,6.777285228228739,3.5776155072399307,374.30339710781766,7.363523664420048,3,10.184035609423443,18.321796658126665,106.23942590188344,2,40.534922585949204,3,7.688647976454521,2.7451606252428746 +20,50,22,30.99694676,46.42693735,9.406887533,38.31597852,mothbeans,13.716790171536932,1,10.035565017218836,2.515736010238594,362.9002321598973,3.4735865852206858,5,7.928117288694386,17.92540415695585,199.88157956474925,1,23.022589736537295,3,47.616348824871245,3.466980435204032 +11,40,23,29.61253065,63.04749127,5.80428611,50.1978269,mothbeans,19.412205965510825,1,6.985668316688893,3.2118740050442818,368.1737350305575,9.855034058441223,1,10.788851790042859,99.9544740812911,167.92490009510308,1,29.210513154939292,1,32.81785985592629,3.628752267563799 +15,54,15,29.97604322,57.03184356,8.35495812,44.86052932,mothbeans,17.703594353008477,3,6.443115865208169,0.8687224153222006,423.19690754624264,6.2513514609607554,5,6.707538382234297,91.08972100916837,69.92985117485986,3,29.583167016363728,3,0.10604033766569154,1.4392762964837358 +35,55,22,30.88883074,52.62696801,8.634929739,55.51932414,mothbeans,13.85272937259489,3,6.961355247745702,12.48794711201346,396.4955374918728,6.034790470180624,2,18.687169545982364,21.356160194469275,123.44539325149393,3,31.24001691476784,3,38.33126182604909,2.7523582242649245 +9,59,25,30.39321309,60.16299493,7.699200949,35.37493212,mothbeans,26.940246492019146,3,7.955322528467683,15.886315502984408,408.4719043023054,4.391306634185138,1,18.201428992834984,32.26800792901487,197.40290968781514,1,10.903785003218125,1,78.21228661788498,2.985123950276039 +40,45,18,30.43683729,55.20522037,5.261285926,30.92014047,mothbeans,19.393894373235998,1,10.022925324337127,9.285440757433179,398.1252042508194,9.4481696822315,5,15.057927248184331,9.31917070587529,124.14983543912705,2,12.474276575836225,1,38.91909890284446,4.586244829588393 +35,38,19,25.32688786,63.18180319,9.112771682,32.71129281,mothbeans,22.96719134004259,1,9.625613039190146,6.689293060449448,438.3475448255558,5.45619173586532,2,15.517459772085239,22.00828139351647,129.53025310412784,2,48.62739037929152,2,96.26738094262012,1.9746241554929198 +14,58,17,30.53684308,59.96664731,4.605700542,33.48919022,mothbeans,15.968682046160998,2,9.119783473363455,11.688890105473508,374.62305277735123,3.7148188149785217,6,11.925584636842029,87.58754643150517,50.77746022122046,3,13.028816628360683,1,34.59828724315891,4.172763065451807 +40,55,18,30.38257873,40.5926071,7.115994051,47.95406479,mothbeans,17.320335490978174,3,6.946792724968129,1.7140021728947397,406.8029869464216,1.4443338642609844,4,6.952435041653713,66.3833974926405,82.82202308488746,1,7.181177761786339,2,72.26627503808855,4.020712576187473 +18,36,23,24.01825377,53.76623369,7.214078621,35.03404425,mothbeans,29.819795908558532,3,10.387338933188126,4.57958565761611,434.83295003546436,7.2817122156223055,4,15.317472025375803,41.30414879586007,88.772189629152,3,16.015876191914636,3,23.92179139305052,3.5972557182502425 +35,52,15,28.69841277,61.14754363,9.93509073,65.67591794,mothbeans,19.303856543481743,1,11.363374010473088,2.049268639463313,366.5838567268781,2.812278359766552,5,9.13770678300827,65.06141840971337,79.4819471700776,2,47.58167127477713,1,89.6266359440786,2.7501506781657574 +4,59,22,29.33743412,49.00323081,8.914074888,42.44054315,mothbeans,21.830246552536494,1,5.653256833364102,19.512036881582947,410.01866619549253,8.617960888934881,6,15.869387861210383,47.79923600498152,75.93574860573187,2,46.019758702744916,3,43.861868275188996,1.7007394457423732 +22,51,16,27.96583691,61.34900107,8.639586199,70.10472076,mothbeans,21.695397466069636,3,9.137806338165984,3.2261642681137204,373.25938438534473,9.183517130861276,2,9.979624257400147,71.06205915353328,63.24601529947475,2,38.38877450895896,1,19.43897553475873,3.584369341588516 +33,47,17,24.86803974,48.27531965,8.621514073,63.9187654,mothbeans,20.206656248325164,1,5.121638277919372,10.915303456085038,428.3757230373896,5.731886623813362,1,17.677743582821243,10.627325286040712,116.41148859303775,2,26.349401318020753,2,25.45985028123895,4.453402796249366 +2,51,17,25.87682261,45.96341933,5.838508699,38.53254678,mothbeans,26.22282843549394,1,10.992233592727008,1.1336075354859876,405.5673852414353,8.666025086148043,4,5.617356749166893,39.62000388653322,128.1851338244163,3,36.751678995778754,1,25.863336292175752,3.9885793804588556 +16,51,21,31.01963639,49.9767522,3.532008668,32.81296548,mothbeans,16.28911808895467,2,10.107060232501807,12.44540366209665,432.64891541117294,9.792096309984684,6,8.75227758613493,24.32815581113552,178.82944878902032,3,6.2384998604379955,1,23.54312163673624,3.4213674350145737 +19,55,20,27.43329405,87.80507732,7.18530147,54.73367631,mungbean,18.500345321383342,3,10.063063870844449,13.423075517640351,386.28011862144,3.9603879007253537,6,12.057229630946686,13.634623686314573,194.12622174805483,1,44.801307412690036,3,64.98613402529895,3.784947840954921 +8,54,20,28.3340432,80.77275974,7.034214276,38.7976407,mungbean,25.994381319405548,3,6.134352317376838,9.482870918275214,447.4761784027138,4.641116979602568,1,17.76495474325639,56.79527288515983,55.191308811407545,1,9.717869411293345,2,16.52377092647399,2.9255415520570587 +36,55,20,27.01470397,84.34262707,6.635968698,55.296354,mungbean,20.33728970498565,3,6.256440245190361,11.097232518118165,377.6764343117186,8.438937607669763,6,14.039189900563237,10.905778617738381,90.32702492013723,2,12.604456611684256,3,48.11109997150289,4.976944122869359 +10,56,16,28.17432665,81.04554836,6.828187499,36.35720652,mungbean,16.400390779250376,2,7.976017540364233,13.042821709949655,441.19486725614286,9.592362938734425,5,15.847544662418427,55.31077916779801,104.174370102864,2,36.22287462048497,3,10.199504255258185,4.447998306232603 +22,56,17,29.87888063,87.32761241,6.89077995,44.75215854,mungbean,14.824138686204268,1,8.482151947927859,19.409273166159956,385.6941516327896,1.1567672278962604,1,10.171353094929994,66.43121073134148,197.5500081937698,1,39.209461826653836,1,76.56486017669998,2.8916095363475294 +9,57,24,29.89232778,89.71503316,7.165121109,42.99498978,mungbean,14.933378149473928,1,10.853306693383168,5.482904342379231,368.1231600081201,7.487938255205838,5,19.165326424697987,7.663946366588448,60.20959851793643,2,9.909156547937014,2,63.21849750374864,4.451575322231065 +34,59,23,28.56212158,83.24855855,6.935804256,56.48265193,mungbean,23.467826869882202,1,10.874354105994058,4.259003457389081,401.97749336218465,2.75805852779861,1,18.887037508553426,95.82161749524474,166.60087881602348,1,25.69269867132838,2,99.93927158624639,4.598664157540444 +31,51,25,27.53592929,85.5701901,7.196774236,53.01899249,mungbean,22.266488664004527,1,5.250812780274657,12.170761842794985,358.55102376603145,1.1365635331451938,3,16.834600896161973,87.34646676279631,103.1965213955612,1,9.310323033646007,3,38.54262535099685,2.2009101934158286 +0,49,18,29.68361658,87.93598094,6.990095452,41.82490236,mungbean,15.221686107949463,1,10.04335523058245,11.297550322606277,403.1459617278921,8.119511928465847,6,14.738475118870456,35.71652959075706,191.4140284485437,2,3.1538141216433893,2,91.52597075144597,2.509883975598188 +21,39,20,28.14448546,82.1193047,7.064782138,46.75690086,mungbean,27.039414754591323,3,8.596122918340084,15.597733798303079,352.8317846282003,5.157938844329821,1,6.190927683243376,93.8264169466639,105.89513780179215,1,7.007456561076847,1,79.29263335641022,2.3655095041128216 +28,35,22,29.53037621,86.73346018,7.156563094,59.87232071,mungbean,25.769266961700097,3,5.099860624713038,7.181087210080939,369.51975520466436,3.507004954965545,3,10.869309117453891,53.75771670227362,112.85844642073681,2,40.849611142149016,2,61.49783307300958,4.5615315437302035 +17,52,17,27.88352946,86.45147631,6.364967184,44.64407105,mungbean,29.020144582765425,3,10.949254216586194,10.985005635274659,417.2333981027039,7.343624061381707,5,16.353134375170036,97.9101957951615,184.85533095768466,2,13.502848696573421,3,61.550834432575954,4.23218000990313 +24,42,23,28.22471276,82.35916228,6.428054409,44.01206619,mungbean,10.327596406110278,3,7.252080915746541,10.228046766815686,393.7271182430995,3.722230189667338,2,15.406300199156826,37.23787209507024,186.01945319442794,3,39.712442369939595,3,6.297676106232297,4.075168781608625 +28,46,16,29.008124,84.96089355,6.664187809,45.91011391,mungbean,16.482409020425827,1,7.8627145975910535,10.84826662550043,441.40606670730034,7.894192161073087,2,7.401107980363115,68.3048787679651,185.6652874634005,2,45.09895493657137,2,18.241900210310302,3.99507882740395 +21,38,21,29.75538903,86.45193297,6.637677489,37.54602719,mungbean,25.53461711457743,1,8.101776362903312,9.213701642397398,368.037007278404,2.328197402775652,2,15.868741651347541,64.9713998489903,167.30579177622758,3,40.99007073280062,2,75.59416996361217,1.413953970959795 +34,60,25,29.78416743,85.16906976,6.79385576,40.77872823,mungbean,27.268251662364392,3,11.663386611961513,5.41262583218397,382.99152548704126,7.249114539791023,3,18.32874131866759,62.72165409250524,72.91822191097563,3,2.534770053091484,1,27.330365283942694,4.975011394221128 +19,53,22,27.8640132,80.4513142,6.852884643,42.83053902,mungbean,19.957376005563702,2,9.633042533123344,6.55950191062761,425.03862650937754,1.7263230114163943,3,8.362095885260166,16.392675947257363,164.09135101732903,1,6.759563076506714,3,14.883474121746353,3.310672610506021 +31,58,15,27.11026483,84.96771717,7.121571293,51.52617423,mungbean,22.276990477441192,3,10.13112763951517,12.958154284784808,407.1910650329088,9.30588822745528,5,18.197886415887275,57.65089423464222,184.31341879493667,3,15.658971277531252,2,25.694598106583467,1.2558002851781311 +19,35,24,27.11030369,83.64274107,6.883308033,49.11964582,mungbean,21.75351818873004,2,7.555508947956852,16.376118997552325,413.5326487511478,9.17880646380281,5,8.365489928178285,18.212925427423,64.58607772384984,1,12.662472158242055,3,25.282714292784714,2.1613209757500207 +24,53,17,28.95451232,89.07866095,6.421271178,57.65901369,mungbean,26.871985878654193,2,6.158158582843827,13.170955756833639,397.9251953960482,3.5790124732432576,6,14.838176766504517,31.804907974028986,158.01638692250532,3,8.730742092257831,3,49.150871986453936,1.8909756012423364 +13,47,20,29.21780035,87.93724219,6.54450214,43.1386631,mungbean,29.172405963292835,1,6.526118384957407,2.129719468842377,408.5862296026756,4.714071798484921,5,17.62368755325734,80.99674876743215,100.16792920777236,3,26.2299804405692,2,98.75075790639319,2.4793388249741577 +31,53,16,28.7420098,85.81675947,6.452006451,48.54598575,mungbean,25.25480283780734,3,9.250816834225386,12.976777852789763,438.29359167295695,5.5875800897059476,5,6.3792584986495005,33.79512887063417,75.4008561756873,3,13.055427705211798,3,99.01226344721353,3.913538209968443 +28,45,23,29.65021184,80.29868321,6.489259136,56.76278363,mungbean,13.895573037979101,1,7.3562304741102436,18.161495904516446,445.84657970344284,8.070870695467832,3,10.393496010443384,83.88977965524886,50.97205047104805,2,41.999036393771156,3,82.18647318299853,2.4511041197084946 +31,37,21,27.23924995,86.404241,6.713410626,37.31236904,mungbean,24.38779630113764,1,5.365232708100329,7.539806865431437,410.4670240035616,3.9413545681230846,5,8.624616941496363,21.987847829984332,160.4323649637663,1,2.190211023804012,2,86.96029693351545,3.0737056419770705 +33,60,15,28.95172351,81.67085323,6.510840928,56.51103293,mungbean,23.06373632739978,1,7.312838473724796,15.997895726222646,396.765204180705,8.13338610409476,1,7.534527378946198,82.26439213562536,178.8524442643163,1,8.308775439438577,3,35.25407485096024,3.1265811018397076 +34,45,21,28.18837136,82.60629652,6.287380117,37.01110438,mungbean,10.77252582021304,3,11.567611690122881,8.152309371772192,372.08028447089674,7.696929150821766,2,14.345131576386109,35.37943431984395,54.50653012316506,2,22.910995786199408,1,58.735612789192736,3.3673702638166607 +13,57,25,28.30041493,86.20681554,6.86308576,50.47333854,mungbean,12.35207772263321,2,7.838039082366695,19.02126377829011,376.24138181821274,7.03801243440927,1,15.43034851608366,70.77372230442333,154.2956162824938,1,31.84578020050019,3,37.42692228739166,3.753834460429225 +33,57,17,27.89636126,88.71782287,6.78415271,57.79863368,mungbean,24.226219456379617,3,9.028249261393054,5.093254843308943,406.37923290224285,8.43296945919506,6,8.576779103512461,47.00759237518491,90.36997909549743,3,3.635956750207081,3,14.179006534016326,4.761542857710241 +32,57,22,28.6899851,87.50436797,6.769415888,44.56598352,mungbean,13.945082022622453,1,8.239420615667893,12.593550389313021,395.4618699462327,3.0793326027742793,5,8.697352706596046,2.3998457990038413,103.59936007380685,3,14.420076994353781,3,67.32921722076702,3.3903924787398094 +23,59,25,27.8262623,88.73100226,6.320768488,56.68833819,mungbean,23.679371355101424,1,9.505779897911111,0.8767517812283465,442.60632769514064,7.009360662542389,6,15.584459489122171,90.98484779672437,126.18548013083372,1,46.09156993192308,3,4.89307519662373,3.115523335925235 +35,41,18,28.70562673,81.59200689,6.705008504,59.87065439,mungbean,10.806164626739998,1,11.945840787338916,5.472917621791797,431.8522263570635,6.421474607999091,6,10.989572203564148,81.43505856200527,98.43863613543245,2,33.83200512188769,1,21.05220992386935,3.6513214462752837 +6,48,24,28.6362812,84.61431076,6.790736339,48.48319335,mungbean,23.88330695629805,2,6.171385077639604,1.823532636950047,441.69055164982615,2.9014634468780036,1,14.3805066954009,34.62350964471283,133.12688793792137,3,20.22361528589853,2,90.09583642536232,3.9925361169539855 +29,36,25,28.28511547,88.4393979,7.130278657,48.56690235,mungbean,25.720469344916552,3,7.582908792029447,12.027501768132112,428.1406827787274,9.826969284904811,4,9.552308763181077,32.815172445879234,162.69555002293913,1,12.553191192971369,1,78.47661169737316,4.406168467194886 +4,36,22,27.60887393,86.13316408,7.012740397,43.80041104,mungbean,16.45921434868169,1,6.2076552045069615,0.9253124507556221,407.31598174320334,8.891255748517732,6,7.476394966668346,59.306181273523116,183.32867367820438,1,16.388658966697694,2,12.966578148841034,3.077457453669723 +10,59,22,28.60901145,86.99495766,7.155685016,36.94616965,mungbean,23.568710994370687,2,10.3484262869818,4.380225304884271,379.59080681946796,5.40692368180777,6,14.756259768138417,59.42479727163396,182.7848544321559,1,24.10088448475548,1,65.72745549332076,4.52680920571774 +14,48,21,29.24598976,84.80084105,6.991242362,53.43228915,mungbean,19.57807918709891,1,10.914421376849694,19.65807546294472,357.8642892123429,4.632358651620411,1,5.741850218873673,36.62305958108599,153.95309928980754,1,43.47985417292708,2,72.74584219996268,4.082057289838618 +8,50,21,28.62911222,89.1148059,6.218923893,50.49913241,mungbean,27.901971035126266,3,7.3170820904911755,9.307720648884494,432.61987456955086,4.980442967575685,6,14.574604330265416,3.5892979557944016,68.27353251553252,2,20.96397205186617,1,0.10110417863760102,1.4081027159381767 +20,40,15,29.57329479,88.07505524,7.199495368,45.04467075,mungbean,24.79759330670052,2,5.523702989529167,17.43083548490106,448.6540013717253,3.1207596661021193,1,13.95196419363388,49.51991816670791,112.96638527262525,3,9.44212132999388,2,19.691295885084948,4.000570550685719 +36,43,22,27.82684262,87.16679147,6.389882166,58.37249772,mungbean,21.60431480630391,2,11.401596661617024,4.378503750797417,403.0980153059028,6.243353215931169,4,8.975357742971749,98.08252395892698,101.81546469652605,3,27.210340981600567,3,66.63057951671674,3.302660309180787 +14,57,15,29.8757015,83.14796296,6.623438282,40.12044158,mungbean,22.5801603262697,2,5.025547869988459,8.100873807047721,416.7675209413258,5.522106363309658,2,15.987857721154445,59.03383567581408,157.988831977907,2,26.67278554558684,1,76.25061192971717,4.673019202613975 +11,60,23,27.33684386,88.50229102,7.033012777,51.09802625,mungbean,16.993429954823718,2,9.895989418026428,12.357501399148472,366.05096457463696,7.105378195397041,6,13.303504772930207,62.516098268209085,178.86914690043935,3,41.95858344665193,3,23.42928900309569,2.954418827793757 +10,59,15,29.83040388,89.30428305,6.32400451,58.86687093,mungbean,18.34078535944638,2,5.134806685277974,10.82294034301275,399.47427214786944,2.426137392646901,1,5.352734210300366,17.985366253138167,56.42574427700407,2,23.4552584868176,2,64.07716683225043,4.904711635175548 +7,60,25,28.2753171,82.76020821,6.397636709,56.04995423,mungbean,15.504278100088829,1,10.436229739205057,14.929279588177707,444.5969776083614,8.626819416077272,4,7.072675977795985,92.84157233221461,194.70705710667463,1,23.68048377266269,2,26.234548066505514,2.9348547523692776 +2,47,15,29.86860065,85.99127934,6.401455706,58.41394143,mungbean,28.466005448070174,3,10.872042143082611,3.8401013109308013,423.6103633595251,2.142934390529188,6,17.03666641706841,61.559491888741945,76.97382344171658,3,39.773042948784784,2,18.705148926966665,2.248472219643614 +20,45,22,29.5888162,89.9939693,6.904587016,54.96121262,mungbean,26.972461723523185,1,6.063556432511746,2.7840333900816994,428.93059514246585,3.5918656231442765,4,11.936660450557929,11.920920206413854,50.42472403517268,2,27.120854464832316,2,44.772869528343065,1.705449382025931 +2,39,15,28.07219563,82.9116472,6.478557136,49.61865305,mungbean,23.263032045833988,2,10.114437607355836,6.1213007925658065,394.58503889237136,9.917378998527436,1,17.668136974908577,58.61933544616019,84.44065673348354,2,27.243453932592082,3,12.359811077369809,2.4810491707349294 +27,40,24,27.84026517,89.99615558,7.063022095,52.84626009,mungbean,27.779577050348035,1,8.026831395105756,6.146631510110295,436.299443944315,1.8548389583460048,1,7.168881959560198,33.08342578107543,53.97530155063081,2,16.583109723706784,2,46.700413258832455,2.6734670274434427 +35,48,15,27.10818093,87.4512669,6.981758362,55.03723979,mungbean,10.898518512426204,2,11.298400392382495,0.8664790556160606,391.11092849988313,3.860637799914421,6,19.925955611981546,58.698851525095975,100.68136184336748,2,6.158774755379448,2,41.42199040525014,1.7461563264743805 +4,59,25,27.68515114,81.94268594,6.227134139,54.62243308,mungbean,23.293865898642444,2,7.987059038012418,8.505165616009776,414.72842334402077,7.926112389978731,5,19.067890134532277,71.12434142606327,78.241680550386,3,19.06835774200267,1,83.06274285044593,3.6623129025872605 +1,48,24,29.34594634,85.60472562,6.232836962,59.03629954,mungbean,20.34366154221962,1,10.225356610738617,11.412656638306649,437.4206367078022,7.336082733543491,1,11.226824645245827,96.16504191722007,59.76557434005949,1,1.3359412336788945,3,7.671267364170797,2.3502390538878126 +36,43,21,28.36319404,84.8593608,7.140437859,52.93031105,mungbean,22.685406022692995,1,10.81159905558441,13.593329496571258,398.2784843873901,7.938921330620739,4,16.525094492489245,81.15299291975569,113.07828278245151,3,10.680369806268935,3,26.294333993845775,4.950568938645809 +11,46,24,27.65280218,89.80650642,6.459252023,56.52558045,mungbean,21.884680624706466,1,8.416456178994569,6.898478040163449,433.750841732157,3.0795177966550678,4,9.108311989771785,87.99000818331743,94.59863293422805,1,25.836296186041707,2,65.66749088926352,1.8510770664296765 +34,47,19,27.31372793,85.44815232,6.568795404,53.15223123,mungbean,23.200537277837306,3,10.467041548754,3.6152265108094483,363.92079258180985,6.24735977626606,2,15.389462584959894,82.5825368119248,156.46814223935624,3,39.02472488589031,1,29.02594628493478,3.559221918606901 +21,44,18,27.06909959,86.89934108,7.12851089,50.46746116,mungbean,19.1248653473817,2,6.358961617572832,16.406411186796976,406.8840093840905,4.6970565098277515,3,17.4933012696765,19.04615805088483,65.76244156394868,2,1.7186714186944507,3,14.46580349418628,2.3312390105968923 +17,58,20,28.06642822,85.91625451,6.42937879,39.23831035,mungbean,20.51109414201811,2,10.142010747452062,14.756608327990406,392.71257604477006,2.9001200135356258,4,15.94759287197125,90.21528272029573,149.31122732827652,3,15.388351975067954,1,35.52413389529308,4.844738223643443 +25,40,21,27.73329078,81.13903037,6.248900919,44.17580911,mungbean,22.222386708197135,1,10.679585790922411,6.872896393209798,350.92491197662963,9.40556477988647,4,18.889230715194124,79.53418458051524,160.31776082560293,1,20.646927756586848,1,9.000340723490607,2.437512876784151 +2,38,18,27.53632932,89.92908171,6.619891498,45.48591922,mungbean,22.575228491367987,1,7.443793519035861,8.666745176496258,400.77043779962077,5.221483058041268,1,14.854601615602014,37.480953526467644,154.56542616742786,1,7.129856308733756,2,29.235572164910593,3.298679928538759 +9,48,20,29.66461594,84.28187572,6.377568542,56.09542002,mungbean,24.936893411311598,3,7.50171975498957,4.064010903653966,443.7708184395928,6.231629077284575,5,5.367593787991231,68.08824220566248,148.21394882632706,3,47.4019837847058,1,50.09446897877239,2.7545356943792236 +37,49,25,29.9145443,85.85384444,6.415459592,41.39081525,mungbean,19.162143277756837,1,10.73964675235138,14.960746236835682,416.9776633168782,9.346502114619847,3,10.3701312040501,51.666657553156114,120.18236790224748,2,33.89136361203388,1,25.843721635757653,3.8113346263089785 +36,38,15,28.36363858,87.59810657,6.320662012,57.99524359,mungbean,10.419889465405209,2,6.884132627238562,13.266390532950725,383.6512922648041,2.7280096516060537,3,14.828588800967479,84.23016970287026,152.79123219406466,1,20.259843710324333,3,71.05751756121816,4.128275988692238 +40,58,15,29.46416042,87.60890009,6.978400282,43.15411472,mungbean,13.532386053203107,2,11.990356848583282,7.136626679973044,389.53527721157224,8.13439464369839,4,16.707119630014994,1.814341481660553,195.5193840649691,3,14.192814050823682,3,29.20576037489552,4.62812297046926 +30,44,16,29.73013036,82.89166381,6.442335593,50.91511275,mungbean,14.701166856771303,1,8.214284695587256,1.4504565578124295,435.73329802316005,7.961942493640746,4,18.202671102131582,54.38874549445904,131.92867480533897,3,4.995897681185657,3,80.5788544351989,3.6111676327306474 +1,59,23,27.46852989,87.17649,7.184398832,43.78420984,mungbean,26.905932728273402,3,11.226191732257401,8.599382339575566,354.0976472218162,5.188935254713487,6,13.930657876331313,67.09443765171088,75.37124859615275,2,28.55642697960447,2,42.17533794655307,1.002106323932241 +9,48,22,27.77076285,87.09979549,6.402926221,49.50812624,mungbean,25.49440692183775,1,9.742414481339951,7.826374104067371,406.5249893272392,7.3174679087956305,3,8.058816799601235,35.68583590696458,78.32041495933046,2,17.460285209265574,3,20.31212812983124,1.3884052850005073 +14,41,17,29.12939524,88.48312598,7.085982325,36.45012824,mungbean,24.7320869147292,2,5.899465873826669,16.224448134473654,440.84165289687746,5.5608183970397915,1,11.903803860926331,97.62030142459099,124.35302289956334,2,40.256400288139695,1,9.315582959203006,1.5241443388292537 +35,52,19,27.10606808,89.89593328,6.698574085,37.45680611,mungbean,22.484433924046137,3,7.578688814992074,16.89303352234699,390.3559587884441,1.9813016477587713,1,19.800345571464,83.43551365499751,89.8798796510059,2,18.14165939754372,3,12.949566920544587,3.8947093771980272 +31,48,17,28.88078945,86.94206817,6.594739424,53.79732545,mungbean,11.402539356998794,3,11.355889337197498,8.03394412668489,420.58124228902943,3.1194214845378494,4,6.629019063650249,69.56757292010822,162.2115142975116,2,47.660237812236915,2,69.76822623845096,3.177945054129654 +4,41,20,28.14720892,83.8001509,6.647965508,37.44800463,mungbean,22.08148010791099,2,7.607656468366766,7.500012894699619,417.97009189654216,2.6718209508368536,4,11.492481457265804,74.36620510948036,156.27800171407574,2,25.33488584850645,3,77.70959981791007,3.4422690291564737 +30,37,25,29.89129144,80.14487166,7.120032489,54.7960127,mungbean,26.23302146648161,3,5.788779725272575,6.978795568065697,400.88392117006043,6.123245865448629,6,13.347020969872116,13.018482086353222,179.90972750152187,2,19.810814951772688,1,56.64198625918559,2.0620392609098546 +9,35,20,27.41503453,80.98004661,6.91380932,40.53173216,mungbean,18.871151704650686,1,5.576141179640385,6.399982121585717,390.4579538812476,7.678046358918431,3,17.269830118669212,39.50075116604344,72.1137879378363,1,39.35917653338595,2,24.396029438174494,3.247032048103397 +20,41,20,29.27308605,89.4875022,7.073048264,50.9246554,mungbean,28.665088563274722,3,8.17095365937584,14.457716861200986,383.58961854031287,4.693911681664237,4,5.6694268404667545,87.5219839422208,179.09836786234132,2,37.82974880609078,2,17.46964102767873,4.115820278043751 +37,50,23,29.65296893,88.48587386,6.5304707,56.01913159,mungbean,17.12070475802118,1,7.827264304211853,0.6640737862093449,356.5887368100393,5.944345804867277,5,5.981973777214476,40.68538241032837,136.37629868408635,1,29.530348486189073,2,63.90464640008714,2.6982724138544323 +34,35,21,28.44524991,82.67639542,6.684381357,58.18713162,mungbean,24.94473910818272,2,9.65907455744158,11.340469340089967,380.6628519737677,1.1226346124607014,2,17.088616038557497,62.36845018467784,181.37260550499826,2,5.428551936316007,2,44.429849528196954,3.8240825600409885 +14,37,15,27.96235681,83.97586797,6.581351374,48.9366954,mungbean,24.404918822352524,3,7.121041597292145,18.21332158201602,384.344735927687,2.878720402127975,4,8.180664789164245,67.24167478095501,109.80672690326148,2,41.336118664569035,3,6.580660894974011,4.800520508152797 +23,39,22,29.25649321,81.97952224,6.86483915,42.02483277,mungbean,23.550068739343324,2,8.95437910855697,19.41290050266285,446.0539623082047,1.6968176693227155,3,17.446376455638,80.31078401220695,72.08074640097868,3,29.903513615604183,1,88.19131559366141,4.506149649585761 +5,45,21,28.36291385,88.00989267,6.487124217,43.05130077,mungbean,27.361352207263565,3,9.510164739539354,1.0869656059702848,360.6948344284867,4.4055428515449035,1,14.58284157754409,10.165241920978518,136.87788570547661,2,38.0633226592188,2,6.158653855694862,1.4956908918261074 +22,37,20,27.62749466,86.49366929,6.605733068,39.26137642,mungbean,23.959931245701316,2,7.069340577545434,0.5024926251262563,393.11236674053134,2.2295612394182265,3,17.99531880882654,22.770902481251156,108.40347174624213,1,31.773629362778777,3,63.88369419627394,1.3384841759576198 +40,51,17,28.66086349,86.12194568,6.860602782,50.01534317,mungbean,29.66750757185857,3,5.668087676421662,3.8554870115677375,358.116839944377,9.731649307706565,2,5.120032545129725,57.9808071471353,136.67465270054322,2,4.472954345654462,1,11.738676227159683,3.932479788599695 +27,56,20,29.2114218,87.11497805,6.41874299,51.53848218,mungbean,15.13176545749669,1,6.628777207590542,14.365636813455547,426.6984771305092,4.909971703262595,3,9.146224006178745,17.305225248719825,184.44222143919737,3,29.452261325630047,3,82.51507701289633,3.8425618867109925 +31,40,22,29.40889385,86.16063492,6.365513634,53.35486977,mungbean,29.609828086557236,3,9.142173471891418,2.644329589550991,432.0031917172272,2.102557598817891,1,12.351444940587857,74.00184641274879,83.59398800199217,1,19.78896068199664,3,68.48798617979502,3.178233401378305 +38,36,21,28.02952623,84.8845732,6.556372966,36.12042927,mungbean,10.50661295510137,3,11.577044996407075,2.8271035116278176,434.0139525335403,2.8088038386097525,3,7.456285996613666,57.44318648744772,107.43748095346953,3,4.371497947856751,1,3.7754254483178373,1.6857164721718711 +6,37,17,28.08657178,80.35005927,6.760694228,38.14476781,mungbean,29.354737294445616,3,11.087007992721738,9.664957262331917,415.82741129992985,1.3176951734269078,1,8.173172837667805,54.55277276325282,133.9913064665919,2,40.87672355012186,3,85.86355701417385,4.369280660848471 +6,47,18,29.16174608,80.28038146,6.715276663,40.16545979,mungbean,27.609144629312254,1,11.177068350624445,15.470662880917192,384.72509406532134,6.5774494095024965,6,13.182658035537527,31.629052933350955,108.88304447587058,2,48.110109093871074,1,4.398369653856282,1.0098793158669315 +24,44,17,29.8596912,80.03499648,6.666380512,50.66487502,mungbean,29.146111076779214,1,11.150097755717367,16.73248106535018,376.885585353395,5.69317678348879,3,5.493859357593379,31.02748591627984,95.19039742906912,2,46.217059729561946,1,67.08881550406021,4.696934795491084 +25,59,19,29.06631494,83.6869203,6.626629798,43.95183726,mungbean,14.239136974517791,1,8.79932911515087,0.3331277200397187,418.6148778863097,6.95444629128521,1,6.013455775344818,58.65137527891166,155.6591529376839,1,16.3777903965291,2,72.42722090829294,2.096714133819357 +32,56,21,27.38538997,88.66663953,6.702772465,58.29933073,mungbean,23.440529568659336,2,10.838772958420417,15.604833561379559,419.51603557459754,4.090801775363449,4,13.937258424004748,34.74370718983179,120.83857556141179,1,3.4529150989251525,1,69.57312618613682,2.629191817435851 +8,45,18,27.93034941,85.42058715,7.011030515,43.25095608,mungbean,11.661358078018417,2,8.46482226627843,8.342840531929983,355.91937784908026,3.985567798864174,4,13.10106079882591,72.9129551464377,63.50617900278353,1,30.331131678550477,1,52.684374306350904,2.0271950430709293 +19,39,17,29.2808618,81.8009244,6.890156495,44.47427436,mungbean,22.98077834388595,1,5.494881706440911,18.12106918722083,428.29799611559343,2.3792889546190534,4,8.860870006789508,68.01657074272896,183.9586213001541,3,12.921559522761761,2,47.20460820659053,4.9492891053459385 +39,37,15,28.9973145,83.78911515,6.821747052,59.84499208,mungbean,29.033398635116285,2,10.29776739900633,13.880357530818639,367.2667739677519,5.893271831813116,2,11.824000668712628,90.9847461249218,150.9985254746033,2,8.660981948697316,3,28.939611908199335,4.068456607549175 +33,37,19,27.92678579,86.5543196,7.183189922,43.4826194,mungbean,15.314463414163031,2,9.144012168297472,8.212375813589968,433.6612806133312,2.7744470428400043,6,10.816641749979667,0.06568998616534039,146.98071359501517,3,19.26838285506016,1,69.68357850833551,3.1040338209845038 +26,54,17,28.5474135,88.9570454,6.27258822,49.4897245,mungbean,20.287319496496032,2,5.207686992138332,6.123845778343291,370.51473408627623,2.7017666772937403,1,7.572633422768209,5.993289318553108,188.60076191114655,3,11.965474155610966,1,12.561058257546964,1.7644157574681802 +21,51,15,29.36488409,89.1886954,6.679127482,48.30159325,mungbean,17.7157768054615,3,10.568648970003217,17.734310667496317,355.7771604475385,7.686820021283655,3,9.160521424211055,27.81764204367283,126.42838700104328,2,31.99566210277028,1,40.336887588204384,2.6723407591101647 +22,54,20,28.56149805,83.63802195,6.689825155,41.013132,mungbean,25.66663034078251,3,11.261724208850609,1.0439915076258832,413.6910122503096,7.053076400775706,1,13.672741208290503,56.80896239661492,193.65551558083132,1,30.396229943253577,2,45.8749524534074,4.84095195822511 +29,45,16,28.43683487,87.91332682,6.583381939,43.12063289,mungbean,14.257501799405535,1,10.872232836095066,8.842491454079886,366.8034163839278,6.0595358504597785,1,13.266938023573456,45.21026160524742,195.9146550892993,2,47.253156414833,1,49.40789866524785,1.9132490032789486 +4,40,21,28.79728147,80.45744422,6.725551062,44.30070517,mungbean,11.924927051281731,2,7.458857033604382,0.33769495846467823,389.25602897098446,5.678531006017132,2,8.246649742764491,20.16684905938988,187.46655981055525,3,28.134555127802486,3,72.68841219983062,4.125197586424832 +10,37,22,28.7275267,89.12760359,7.069747814,58.52974279,mungbean,17.84930633635421,2,5.7746495699956455,13.134142324486206,368.9867389589185,3.6755056138436597,4,7.422017192863086,57.2374599523225,160.44535006661815,3,2.9697062309190447,2,44.60379052623678,4.55253872477149 +4,44,19,27.95639663,83.52706038,6.921993878,43.25726752,mungbean,28.69797587119391,3,5.203966330062462,13.644098701343179,409.00298624952575,9.684244699772307,5,18.256759712218937,53.15579662752336,88.43146522804989,2,16.618845892799193,3,44.483237210317995,2.7430766915677096 +20,45,17,28.17458662,83.69659318,6.770955317,37.2464655,mungbean,12.364684035734651,1,5.0805159091736085,6.892584945428717,395.2920185066278,1.3090863802542554,2,13.059993440389267,90.86756132157745,101.72538685372791,2,21.35276716450583,2,28.564027550934547,2.252382713795479 +23,45,23,28.77653519,86.69133979,6.983130466,56.12443206,mungbean,26.963498847263175,3,6.358876678063082,8.737910097302361,397.7465295272182,1.8892128620285542,6,17.070357510114743,7.996987706132952,160.16413606534178,1,0.5905335264858425,2,12.960066683772197,1.1692504775718313 +25,48,21,28.438097,83.48991368,6.267684328,52.55469976,mungbean,18.612922244354976,2,10.030710261274812,11.36428209901161,406.8325454466031,4.205619608621003,1,11.273380826937686,53.505984706106865,113.34548329158793,1,11.700079038499617,1,80.96126386478558,2.5448077798919084 +56,79,15,29.48439992,63.19915325,7.454532137,71.89090748,blackgram,18.922578028891927,1,9.191656129359494,19.270504094265544,385.63827938346793,8.036079677972126,4,11.014390246584512,19.4391788287283,104.53137484517814,3,8.054422566324938,1,71.5197743887691,4.647243657494869 +25,62,21,26.73433965,68.13999721,7.040056094,67.15096376,blackgram,26.467277437375525,1,7.756742701577019,13.51039294794251,367.0861571014446,5.945108745690581,6,14.332923662893455,54.390863460683,65.71082491127477,2,47.63017708599011,2,40.60210142402953,2.476589509542112 +42,61,22,26.27274407,62.28814857,7.418650668,70.23207557,blackgram,17.847875103559108,1,5.606756913903757,6.503058374370316,423.6303973199391,7.0983445547414945,2,11.783802400828563,17.83712989970797,73.98434354427917,1,46.225342853627836,2,90.85330432584583,3.788237438066084 +42,73,25,34.03679184,67.21113844,6.501869314,73.23573601,blackgram,20.578134570744396,2,10.983093574913802,10.894159173350124,385.38856870926645,3.585402074437731,4,10.571228796753875,11.011768602955541,85.18682216924896,3,11.901989767291699,2,10.255749061908448,2.934417933282647 +44,58,18,28.03644051,65.06601664,6.814410928,72.49507741,blackgram,26.779319044777264,2,8.359980431894265,17.83873384580256,434.33865685516537,8.73997196714627,6,14.869845294232885,43.65510480127962,163.2946577927375,3,8.583264440551192,3,16.000826665074964,1.143063319549087 +50,55,16,28.81460716,65.33538112,7.581442888,62.26242533,blackgram,23.292722904671827,1,7.902764878274361,8.76272601854196,433.5408732228823,9.98112927450739,3,6.5630738097496915,58.80673471963901,120.56714336454503,3,39.20003347338712,3,17.67328714554499,4.916220737424393 +35,72,21,34.03619494,64.28791388,7.741418772,66.85510868,blackgram,16.532717945506896,2,10.586124760950323,11.907816206457477,373.3767105932916,1.5814922959725026,1,16.602604870814893,61.289488462023556,105.42243131698444,1,16.534011649124146,3,49.176820732934026,1.255564253971042 +30,64,20,33.8642935,61.57072498,6.573531614,68.02199825,blackgram,15.649079354772613,3,8.951445002897177,10.835165555130764,423.2790686481394,1.3143019150317239,4,17.943273125636075,22.003132379392387,156.5596174595184,2,20.356786302791757,3,37.118052401570125,3.036800573401638 +27,64,21,32.84213012,68.68401492,7.543804223,73.67166182,blackgram,11.107117322247353,2,5.839071196449529,16.872362276403955,368.5777097526811,3.440820108626049,6,14.13166551941277,8.948641311090311,185.96738434884242,2,7.932424734322319,3,68.68180563429213,2.613095409144007 +50,74,17,27.10053268,63.36085585,6.5408208,73.84949872,blackgram,15.369051103041095,1,9.591871830187703,2.631042772258354,399.3824738253012,1.5399400895889173,6,11.229805851617144,30.086582644205507,144.0582826092894,1,46.14562639181224,2,44.09536957821069,1.6797021112964434 +39,73,24,25.65842532,61.18235808,7.22405917,69.28607828,blackgram,27.244386378816042,2,5.094887532590085,11.072496779059122,381.59436520298124,7.667592901456899,2,18.487593495454583,46.90151239749854,102.29354150771127,3,24.074377811001085,3,68.08082239262372,2.7601284371660624 +57,67,25,32.34744009,66.61452812,7.551364319,64.55882254,blackgram,24.558808594651648,3,10.482074099653111,6.115096971387535,444.31519047686163,7.017896366273345,1,5.067141847034413,59.84297395396661,60.67619902218952,3,20.589680906323228,2,70.72101713838917,1.5939381511223925 +52,63,19,29.58949031,68.32176769,6.928898659,67.53021213,blackgram,27.22840461276248,1,10.106772690724112,3.8284566241290996,412.1433191624531,3.5672895611202486,2,13.02598944030411,8.112501635995928,50.63697165072813,1,21.884875797250185,1,67.07301448895791,4.665151071808281 +55,66,22,30.91219459,68.79427388,7.747775263,66.63830637,blackgram,29.868738998853313,2,8.323945459513116,12.443540151971284,404.89619555145595,5.456156936257829,6,11.933347329514683,13.289574023746143,182.27065004196652,2,8.207947153728574,2,93.97683731601482,4.76246135702345 +51,56,18,28.12787838,64.2097765,6.706505915,70.86340755,blackgram,14.431461476970997,1,11.879486791349617,15.638596817760888,373.88087632035445,3.9893332770158203,3,6.396930807088294,46.97023043664984,106.28959531394025,2,24.042336638436883,2,89.80774063003423,2.1026904531596187 +36,66,15,30.08545364,69.34811988,6.668238556,67.1367443,blackgram,19.13805170446003,1,9.151017234400271,19.650616903898136,390.2999405655276,3.8384874454582665,4,18.075295812636618,29.06502517451164,183.16726339663998,3,46.38267302789619,2,13.557431288892897,4.041723822909636 +59,55,19,31.74379487,62.51007687,7.332375138,68.97097538,blackgram,18.436854100084386,2,7.677021777254691,11.882356108654575,397.8454410520954,9.667150129250674,1,16.54380697601012,93.15954948785581,198.0416735829436,1,45.58404609641952,2,80.3856593430933,3.3845534485317645 +50,58,23,27.81326852,62.50460464,7.596802025,69.75555541,blackgram,25.602886753243176,1,8.610037248469956,17.03407316788102,425.0895161388173,5.8220460614289475,2,15.131312056873576,84.30791684304533,89.94207353374462,1,39.22212709465782,1,18.100603140909378,3.644557505548892 +30,65,25,32.88733849,64.59457409,7.70650895,71.50569456,blackgram,10.0829206182682,3,5.230110114746603,17.250480152451843,446.05177456412235,5.564829835684749,5,16.88894961002032,61.06565122082299,83.35247686197148,1,21.690136791549712,2,35.75136815052045,3.2505096470318056 +20,62,18,29.36358721,64.98742947,7.366542647,61.91208707,blackgram,13.646635296163916,3,7.659623155515862,13.315719808468895,372.2885485015453,7.800092388246922,6,15.313625395428563,12.908864585610969,67.37642970029091,2,38.12598646279954,1,16.758696167969656,2.805022294190884 +58,71,15,27.82592799,67.5861883,6.919243702,74.01229707,blackgram,16.51250342830847,2,9.884864880985255,10.49667546625081,432.54919675328813,9.957045678380066,4,11.530076813913775,30.137838389737936,116.53077378215482,2,28.72234941497001,1,52.420987351669226,2.358748143984829 +25,71,24,28.49538735,60.44848407,7.187721818,74.91559514,blackgram,13.610894518514336,1,8.142420579924092,13.920700459505486,426.0770330202066,1.4652667351086182,6,5.580136797569292,79.20061359973344,159.8400644661903,2,18.913827270592716,3,87.74772588296493,4.239861925813785 +52,71,16,27.74274761,68.53997144,7.075886472,71.78615328,blackgram,18.31880842937396,2,6.22900597212474,0.8646567970618957,384.9804843474358,4.661984963631134,3,18.832002649101177,10.037123760801181,163.2871170261023,2,32.69033921414921,2,21.154919470070965,3.988968507760309 +40,63,18,30.41588462,67.66323804,6.74441168,63.02473185,blackgram,17.61211365012349,3,7.091492010589074,1.6163234138594196,388.32486446821684,1.554756978652595,5,15.291371419512183,86.88072197332394,74.62377489951865,1,23.130979425338023,3,36.95263177708023,2.3836151916073516 +20,60,25,27.3254209,69.09047809,6.726469088,61.19250859,blackgram,18.27169062729123,2,10.698454693740754,17.240716658312486,362.7167176916475,1.6624323928132727,4,9.354547601254986,72.41392762200877,174.7781740882922,3,28.15696283327158,1,51.977556842911035,4.655028844108863 +48,61,21,30.28496619,61.69295127,6.628264883,65.62859526,blackgram,16.04396540629365,1,8.604601630588135,8.921026970440927,430.29829044669066,7.254118372763572,3,9.295367708738567,19.5638592658686,150.66977606930715,2,36.702852988603105,3,99.46941011799687,1.9130487705279302 +49,68,22,28.56840626,61.53278622,7.127064207,63.49726331,blackgram,25.999899765748083,1,7.348115112245079,15.547477955538733,396.8493593775919,2.545208556174425,2,19.07183959024545,51.77765954214306,127.91789266095192,2,29.844599029812287,2,62.36810919499791,2.9514103102568146 +48,62,15,25.36586097,66.6379724,7.538631462,65.81655892,blackgram,26.835018838945256,3,6.891373786623266,11.187818181563959,421.795439658548,7.7892620144999585,2,15.319779973347385,48.16151629502522,180.60572692020085,3,27.16903152774283,1,14.902698534094982,2.171385332459931 +32,66,17,34.9466155,65.26774011,7.162357641,70.1415139,blackgram,22.106012685551185,3,8.208097998451727,5.439752819005874,387.0913481904231,8.055024166004337,6,11.03150677420312,67.17379583940122,73.37568349208142,3,40.71251087468033,3,18.11935145051259,2.3992154977540703 +21,63,22,25.09737391,67.72837887,6.859409487,74.61649888,blackgram,28.138703794532923,1,9.700013822286145,5.114374244334914,381.8898108578714,5.820814209109331,2,11.79263096702799,31.54671766558558,194.63639907381616,2,40.94074662667639,2,17.142840297218733,3.7638233813163082 +20,72,19,32.47648301,64.34848735,7.397190844,65.820457,blackgram,27.019000199708113,3,5.58106823009286,13.86790981177418,370.02839397746476,8.670186216139424,1,14.971766342736522,42.169377601415384,190.61647353307185,3,30.3075911296129,3,64.69790666351692,4.346772568122207 +25,65,21,33.86351172,68.59232289,6.880245789,69.24464096,blackgram,27.00848098599995,1,10.010991701609614,3.514824827075702,393.6301309896302,9.99280222597621,3,5.445247487247115,71.78558733971589,62.41911573572013,1,3.448945717649571,3,15.402950131366588,1.7805124782882427 +41,78,21,25.19857725,60.37332688,6.581313137,70.88787207,blackgram,21.523216255559326,2,9.2803712536085,6.976037703455125,356.64838192542436,9.33059421892278,4,12.19956712937601,54.21980040702646,94.75979929375829,3,34.21305422496039,2,65.79232810285924,3.2645393629008788 +53,67,17,31.77681682,69.01852894,7.296972161,61.46892873,blackgram,29.874288781108582,2,6.719868002891305,9.319220434306745,424.0250764453127,9.706732538324761,1,9.40767573320064,98.98309142043887,146.37218810389942,1,45.35882501911984,1,69.85981230898142,1.1966515983905053 +39,60,21,34.89814946,63.59948557,6.97297656,64.72797143,blackgram,28.892264485924578,2,9.138024139989016,14.134481884888437,390.1494257319296,2.2608344849229685,6,7.733598997635678,12.941719439253784,134.77764423891185,3,22.29846422108111,2,11.91074642503126,4.456775851376639 +25,76,17,31.74105409,68.63525428,7.241148507,62.3061735,blackgram,20.256072078972384,2,8.513747305566397,0.16976103228671713,394.31611513682884,9.814070819436676,3,6.156999801710866,94.84780304150358,88.03898210198298,3,47.31342086158274,1,91.11396800842421,4.974024833956182 +21,78,19,27.16159076,66.76017239,6.92009048,69.85112265,blackgram,24.231881594151655,2,10.906575074255421,8.1231291839304,382.24545212753367,2.1797677223210026,6,15.389327730105213,59.660783177695635,95.12893874147937,3,11.711689038415585,3,67.19822226664515,2.953830870591314 +57,60,17,26.23773129,67.88521396,7.504608385,73.58663968,blackgram,22.36151771380224,2,5.511941232022479,1.1031463431507516,408.9912229992451,6.970765885562602,1,10.177759178982638,5.958802156942133,187.95604143250077,1,16.94040727265625,3,89.32009609411783,3.408121227158564 +56,75,15,30.20157245,60.06534859,7.152272256,66.37171179,blackgram,26.451794927584427,1,11.553600695920732,12.85503646964965,430.7252040753533,1.7367079316017526,2,15.317315947509522,22.80717886938336,151.8923373571551,2,36.881734388226555,3,83.3247121944187,1.3429864802452842 +49,72,15,31.55846339,67.83563765,7.137004749,74.86960831,blackgram,10.992659009788897,1,11.5249214718563,3.0194590402103083,439.2044213693608,3.0701632443847835,3,16.098441137695083,34.54561499679749,72.6382077363155,3,10.40247184547572,3,63.5229989096202,1.1827672489687373 +24,80,19,29.67892453,69.0854554,6.808041722,65.66436565,blackgram,27.821200046958005,1,10.81188343557905,19.591857181532546,388.7133963215799,2.5841719012644924,6,6.510280534957897,59.20285623028199,64.29046029603937,3,9.183066681075214,1,63.81996924079986,3.250484355243185 +49,76,18,27.05365239,67.7017527,7.393631868,60.4693835,blackgram,14.855370604313354,1,11.210130935672609,3.6532890740787427,436.00999734239286,1.2065562640553598,6,12.832847243513505,25.243916832515755,153.79970077928374,3,4.8878977777853825,1,34.997315130536286,4.8990724616807695 +28,68,19,34.63880966,61.38597868,7.69950698,72.43169115,blackgram,24.909939462273396,2,10.347051184277149,12.124319587535714,376.8338152346977,3.4137835856805663,5,12.83650918642269,56.880961018634544,154.0690890069302,1,47.52729839925541,3,84.11374911034714,1.6574569658489704 +55,78,21,33.39438752,62.93692886,6.602888249,63.57445989,blackgram,28.736961880967943,1,5.341972385235391,0.9046086986397794,408.0909955635647,9.07679090438356,6,18.189473191011295,66.80565816912744,89.11704199613982,3,28.06850860333114,3,37.404781748553084,1.8455052345589027 +50,64,25,28.84079155,63.37230676,6.734447425,70.25496749,blackgram,29.887280240029916,1,5.297057520782795,10.263674146180069,428.3073343843985,5.613081019408456,2,16.860066715683935,79.95587222935924,129.9587894841057,2,13.340950518327638,1,39.23377227967955,3.8748187219622015 +34,80,19,31.49338309,63.0563645,6.521217963,71.48327008,blackgram,27.53323428378236,2,8.051484276634657,6.650263710223907,356.6769822529189,8.78874097701579,5,15.87346912943228,44.09882530660242,111.88469883792585,2,12.145016671015107,1,33.03312049852708,4.506014112938477 +20,68,23,25.54960633,63.95425534,7.707332484,63.1830529,blackgram,17.8144256944369,1,11.260861092009911,16.025316198575187,374.7967661847132,2.571329901487574,5,5.121105741765044,40.38815006835029,86.03300245354444,2,49.030418678745654,1,61.730052872688645,1.9981416705778612 +55,67,16,34.37329112,69.69366426,6.596719015,70.27184748,blackgram,25.06094807513915,2,10.62770979144067,7.663871227056315,409.055467938469,6.003239505117515,4,9.90236847942932,17.370140302455006,156.08010202144814,2,13.469121041077543,2,95.79365135392925,4.84773923508363 +23,70,15,34.6008247,63.11296779,7.403623355,60.41790253,blackgram,29.620836117410114,3,8.574941324379031,8.587600156410272,368.09462700637414,2.335926174600433,5,16.64985238429464,41.88805544084497,194.61257684940682,3,41.34875031190699,1,62.05724224044224,3.9470631659220303 +53,74,15,29.43463808,64.94329356,7.517097,72.17818157,blackgram,14.841228761081371,3,7.844482610827926,19.983758249450595,406.50058218602555,3.4349949448147683,4,10.360370877233603,22.125455925882065,172.06074618132618,1,26.51671296879651,2,69.8060518326831,2.791499520284755 +26,67,16,29.10713092,67.90577375,7.17620823,67.83345933,blackgram,18.16695378725479,3,6.511672498757356,14.016686824307751,427.5098872002775,9.783091025739253,6,8.136597354541186,37.22295437522365,117.74159336186368,1,39.999592899020946,2,30.389216009717323,4.556876122173283 +33,80,22,28.57006111,65.71765781,6.593961761,70.0866434,blackgram,12.108626796795905,1,7.399729392584067,5.143324051794529,395.39340251821477,6.81792384978133,2,9.03377900695986,52.28123252866618,146.0076261603397,1,49.236285008666435,1,53.67983877633936,4.819772402201583 +37,79,19,27.54384835,69.3478631,7.143942758,69.40878198,blackgram,12.920400303176496,1,10.147753883442158,16.973603743548352,421.64726987301185,8.890250732296316,3,10.971969631797958,16.169267952289445,55.509855090416806,3,38.40741565870805,2,78.73427324139745,1.3385393565269643 +33,75,21,33.04687968,68.93875631,6.690655045,62.30278274,blackgram,12.644838590168145,1,10.770374927879171,19.344046055152127,363.7065451285466,7.978737149951488,5,15.221322943397007,6.363183448273757,145.85744220412107,2,19.011270952029307,2,78.06274051764883,2.1861297971506404 +22,55,20,33.95309131,69.96100028,7.423530351,61.16350463,blackgram,10.796257502339472,1,11.594514260569767,12.020205106268536,399.95630657729066,5.686842209432004,2,11.105251884576656,76.34389733946388,140.2290349013113,2,48.06501708621049,3,34.32365238280769,1.6279709547624002 +20,68,17,30.11873003,60.11680815,6.578714843,71.72980375,blackgram,10.818319611405244,3,8.928754651783434,17.29532928267083,408.9807492558236,6.796996860963593,2,6.302929277219317,0.4659333647148767,127.69643411605914,3,2.5253346198019444,2,49.923812485473654,3.8905839032977774 +43,68,20,29.57812712,66.17587668,7.497469256,69.43895491,blackgram,10.092302479295224,1,5.3769081564223535,15.77020990141536,405.9589692194056,3.000780450470375,2,18.091770263572528,66.65577871446716,99.63635367225574,1,30.54158151462662,2,63.27436801635835,2.9829235719861202 +44,76,22,27.26458947,68.01232937,7.775306272,68.91754359,blackgram,17.977476906476454,2,8.69791843703997,6.177301643398218,369.20628365146297,3.346744793363857,6,14.959958457850945,8.325817911822197,194.73841426068023,1,21.329861066557505,3,5.921960171661289,4.781444598333863 +34,60,16,31.35730791,64.24992106,7.322555223,63.85668948,blackgram,22.9996416547392,2,7.824448877768872,7.424200339667539,389.1864655376613,7.471551355793187,4,8.378453168178808,95.00709364141632,191.11147527632264,1,10.150818325071553,2,46.31510329987676,3.099273055923887 +21,72,17,31.52104732,66.55723677,7.580527339,61.71111448,blackgram,27.7827388314278,1,11.764151140927705,6.314121103564023,370.91323616286013,5.158967527913977,6,15.623239732805603,5.973358138236895,124.28650757682946,3,30.53018111668426,2,88.43142782628448,2.7074430870741106 +25,68,19,29.39982732,64.25510719,7.108450121,67.47677295,blackgram,18.26070172110823,1,6.781280908845157,9.559016403089839,444.36665929263415,2.8410066857905307,5,12.104943995977468,33.11682171143,75.06525515108467,3,12.644526969690217,2,65.8144512106195,2.476153478214783 +41,62,15,29.38400259,64.14928485,7.358974541,65.24194361,blackgram,26.2709344099806,3,5.694865036669737,0.18072600661302785,379.2132127928223,4.698816359205969,6,7.334254571900827,83.76175987488237,110.30103782164042,3,3.9809682508280653,1,30.389776745561626,4.778841273241292 +28,65,23,28.38686534,61.88871127,7.405176138,74.24459122,blackgram,22.94600596496226,1,7.378597506846579,18.590630674987306,384.3919174963336,8.825775089728321,4,19.474085239815928,39.80771504628736,50.209990366842156,2,18.73129367804544,1,62.234785767363654,1.2005054953406193 +35,64,15,28.47442276,63.53604453,6.500144962,69.5274407,blackgram,20.113293079539623,1,5.063887239785832,3.8789626237204122,371.29923172842973,7.598530598745731,6,12.643175681344019,24.72035450108878,135.49984369999714,3,46.23666004424658,2,57.241222571620156,2.7098829578931527 +52,58,16,30.64095781,61.14508627,7.167435834,71.36947525,blackgram,29.209071571315924,1,7.051620689415581,1.872791280049364,431.3003551939999,4.146056028363027,1,19.520129228580032,95.35511296721987,130.10517342022558,1,16.878671291779668,1,85.4888846980033,4.723537303246935 +58,75,25,25.25596239,61.36669662,7.261791753,68.64685069,blackgram,23.699623410142078,3,11.799487714054543,1.3828388462876484,361.84700841356874,1.7304186406525564,4,16.329316861528618,10.423496231446949,144.72862041740888,3,21.424004593279072,2,64.21834523537355,2.9964404045364597 +34,66,19,32.97030511,60.18122078,7.586642101,73.44678678,blackgram,29.784502645239744,1,9.530569812064545,4.669588993648468,434.9618214861572,3.164770027185559,1,6.5260159269638995,0.6390887070379714,127.71224823901218,1,18.885532720592273,3,48.236477578324376,3.1804356774603937 +52,70,16,33.66855394,66.60416867,7.534811833,67.32520551,blackgram,17.29969334991618,3,6.448743837863282,16.57287258546842,377.2860584857802,1.4736359362421823,4,7.909298531878886,35.74725669250686,71.14582367300697,1,12.31703372260466,1,1.6128539119043217,1.433932147750339 +23,57,19,32.83963757,67.99803573,7.251000789,73.40452716,blackgram,25.145010558349448,3,5.1068419596869905,16.379127462388666,418.99747816661477,1.5203979373611851,2,17.442167991559455,20.13914721550394,114.68805617232346,3,26.78917988695879,3,46.4971940770443,2.3569248468643305 +42,58,25,27.45853567,62.90020977,6.513620918,69.46020927,blackgram,11.114582318756037,2,5.837217962483831,11.389073514300998,395.26621984641685,4.670532084776915,4,14.067756854706236,17.707709113916504,53.69999432609255,2,23.43365999859892,1,96.75594181928477,4.412940503819076 +37,62,17,25.68576704,69.84354028,7.121254928,74.62068748,blackgram,20.951075117301148,1,9.075074750605662,14.961016299953304,433.3421811734861,8.012997123792033,4,9.340832653703146,79.46337659585481,199.25068069960747,1,22.367383168793044,3,82.755005274053,2.1699689484594473 +44,75,22,30.0328403,64.14800537,7.574561547,71.21006868,blackgram,22.07938122392231,3,5.413115962476269,2.5056741486178713,404.4674685265983,6.751890788767515,6,14.497523356095062,92.4287460284115,119.50298824045485,1,5.665279434640796,3,42.81375323562152,2.4265028096320984 +21,80,20,28.20667264,68.27085245,7.350869792,64.32887142,blackgram,21.490728974630017,2,11.164256760020859,14.68219879459089,410.84202350449766,4.452900535511214,2,7.558927444694694,16.211175587809613,97.32467280795538,3,39.55789724380659,3,92.55059076302004,1.012690556010968 +56,76,16,28.27265858,61.18956161,7.513151076,63.29900785,blackgram,23.69212809400546,1,11.494125703307342,4.132629667025885,406.61388084726906,3.7528111029693316,4,11.642250662882411,31.41998747971848,155.85935528792442,2,45.90979430963916,1,49.25854292423458,1.0399064943927474 +29,76,15,28.5417236,64.2020154,7.025607706,69.68862306,blackgram,15.509799224738156,1,6.926161579754062,13.406375032226086,367.58822048679724,7.82880080217106,3,12.157540370032384,16.66223058973879,123.06528174361713,1,5.423462007267404,3,98.77054885161797,1.3171530104024685 +43,61,20,26.87187036,61.61367264,6.804253866,63.51822045,blackgram,16.694120968506276,2,5.832731402971343,10.774680800355716,442.4324842912114,3.1009939844083334,5,12.251138053974115,88.59112310691435,67.9321834390274,1,17.21659492700586,2,93.49342720795008,3.996515781327503 +55,60,15,32.79766751,68.77994074,7.163043872,64.11411069,blackgram,23.34752238847974,3,9.744000018907236,0.16289928088857097,427.2086500596179,6.5876954379564205,1,12.451458546683178,87.62649462582064,70.58021496314488,1,28.767564527650986,2,45.649973270195346,1.9836150212274015 +44,63,15,26.42333018,64.51136845,7.338929556,63.46546487,blackgram,26.689688838112808,1,11.659486229061596,12.649925297790478,434.08156528776925,1.9380396799265824,5,19.28704101401827,81.96716991785846,154.85654775776175,3,41.01847324395841,1,26.6225757924494,4.04620561178568 +29,67,21,29.79181107,63.38789228,6.621323612,63.02169909,blackgram,18.995601685542827,2,7.1343653346836655,2.107717191849432,382.9038238817102,3.0121941005710884,1,5.88551590633209,20.902777569587915,58.89684581873291,3,30.318446748089894,3,94.30269801082511,1.4602024382062906 +47,63,16,27.44003279,67.10464369,6.661870999,72.50669768,blackgram,25.808793266961022,2,10.51790295984596,17.650773074589207,421.3095140869937,4.805309922062655,5,17.96337479595145,60.24211492581443,73.18220250888163,2,31.087077769093476,3,23.71197042376647,2.45412717182445 +40,68,17,34.1262979,65.14877461,7.733149554,70.40795007,blackgram,20.17573526040703,2,7.247727998529076,17.469223904990855,419.6920891571865,3.6659894001058353,5,6.570140418756661,10.66701848482059,133.50837263268656,1,30.43659098033905,2,75.69386453735095,4.0556940899494105 +58,61,15,30.94908189,64.23364112,7.402891666,62.78730907,blackgram,18.00302441264779,1,8.123899715709292,19.303742988110283,383.79171057066,5.575175369809854,5,15.155013476277489,35.5879515083131,151.60388893327962,2,34.564512034583686,1,89.40534726268645,2.2912961047360763 +41,74,18,28.75751783,61.02701476,6.599147298,73.37686831,blackgram,26.56135993667578,2,7.415097265202078,13.83666246027648,403.76676405671844,1.298272119330001,3,5.205741193830994,37.31328583105372,162.5997835211911,2,46.353364632526315,3,6.661267958280048,4.089198495647053 +58,79,17,27.24766491,66.10123083,7.04174124,62.31842057,blackgram,13.283022799234775,2,11.502263054774286,14.777581418333607,356.91933436668324,9.528817072091545,1,12.406994067188442,87.44810857361375,54.14399328550846,3,10.334829300308085,1,62.557409225875396,4.166732718480864 +27,62,24,28.63005477,66.770943,7.353876754,62.2737345,blackgram,22.58134340424461,3,7.599815801414351,9.691422887456005,422.9710236570852,3.293886166008625,5,6.233908928608468,64.07349102064501,83.3625975461365,2,39.52727734043711,1,4.2681271028335965,3.942374802650465 +27,60,17,26.41768321,63.64698302,7.026795359,64.42177127,blackgram,21.53987789309604,2,7.070355788341972,6.784330891449217,388.8233796148441,4.939745401220387,5,12.208292793144125,21.361820984209402,118.89215353500873,1,1.2989234870194155,3,42.26914454241661,4.487605389826317 +52,65,20,32.81705216,66.15665137,6.814301033,68.83924882,blackgram,11.259541097797426,1,7.187574479802873,19.20381219893769,350.45905509785274,1.003337922269687,2,17.829485071760562,56.31352757581802,186.88821455381273,2,37.47350177678026,1,65.5643894813797,4.1454971257333355 +44,55,25,29.6321052,65.91359954,7.42160832,71.16331975,blackgram,14.58953920099858,1,9.115713463016295,19.082734298553653,388.5958613109717,5.984215910537722,5,18.62057429983328,9.055361942485629,156.83633674580958,3,45.366545344227035,3,29.30011154983575,1.9330139211139716 +21,62,24,33.49077065,62.73317204,6.847382891,65.45328463,blackgram,25.62278487311488,1,8.323090798171133,1.1723024031364515,444.31972665309075,9.06822527203353,5,12.805859274442241,38.3637985495601,80.5046332109429,2,24.44809592514939,1,97.96254470424319,1.279736957580777 +60,59,22,31.86847286,66.74217464,7.191522601,74.22238583,blackgram,10.437046480821193,1,7.795197220412568,13.185676615846226,369.4455646119876,5.723811028374692,2,14.939623841101811,79.81820739939762,72.995371326739,2,34.609086210934194,3,32.06297543101091,1.1827082142205625 +33,77,21,30.32992227,65.62971858,7.01285529,71.64631281,blackgram,27.96460644982269,1,5.034601212324363,18.652597432915154,410.1282057967608,3.444600193721331,2,17.66666405214337,98.71621334583291,71.4078706574277,2,27.108369144656542,1,14.527354618976718,1.0053175385519495 +59,58,17,28.546224,66.31394098,7.368318809,62.83469851,blackgram,27.280009662050613,2,8.145861163325478,16.12653639295363,361.8935983115696,1.2612962159319563,2,17.997585864689952,20.902829287318326,146.76750770069083,3,37.10629113887449,1,22.596087517644225,1.6268254069890529 +29,63,17,30.02629908,67.88811637,7.26154329,66.47264636,blackgram,22.4997111867399,2,9.23367905065711,14.454980874029841,359.10811662351466,9.332691474228236,2,8.114087235392383,73.4088616475051,93.71657364838555,3,5.631798173528252,3,65.31320578863922,2.0584458424187217 +59,63,18,31.65531175,60.13263713,6.52669158,66.69096751,blackgram,13.425150343119519,2,9.16521886885483,17.39233958985225,446.92776572592686,1.3036294844823653,6,6.273989964143322,5.856722915637147,99.75117476329007,3,11.358650862743374,1,43.239706864786406,1.481283471991123 +29,70,15,30.33499674,63.54718862,6.872594461,74.16699119,blackgram,12.641802593529977,2,9.08596974341158,9.036001343794222,426.1124007747379,9.725134715758282,3,8.106026994288186,91.09218086984188,147.3650968859624,3,49.68972998528319,2,53.12913532685073,2.285841856923615 +58,73,16,33.36984395,65.67718163,6.874142175,64.89517488,blackgram,27.92743382889407,1,8.045813170613412,4.596843033736104,437.71713193430276,7.118478301673552,1,10.064723772412728,6.702505032170791,98.8434458998906,3,20.386840440942837,3,95.4328586284362,4.7000041677112385 +55,77,22,31.4345059,62.99303471,7.76061831,64.77651469,blackgram,21.925867768493475,3,8.591762668504018,0.5018563557163569,404.6724396796241,7.114980253269617,2,9.559749753370513,61.07592178820267,152.8117827755376,3,29.4882690666738,3,13.225173500543496,3.0299246218877687 +42,79,23,27.71678273,63.29103387,6.781841984,68.56507978,blackgram,22.86476639253589,1,7.823688077135046,10.042828630982964,400.4204948323379,7.178226476036309,6,9.110924137466132,84.85260998467434,87.89464619059336,2,30.85724309238017,3,75.28169110914124,3.340848900190254 +44,77,21,32.63918668,61.3009051,7.326980454,61.83876146,blackgram,28.13724493810058,3,8.226536460412332,3.226825568773477,433.26961572140266,5.912718579305849,1,10.038085614160039,50.087253630099745,156.38451463515426,1,49.11793312758296,2,88.48464557276321,4.394297137277207 +38,62,25,32.7477393,67.77954584,7.453975408,63.37784443,blackgram,17.498770371575933,3,10.08220063485057,12.385414792962127,427.7359311169656,9.77061463527822,1,8.258629274564207,82.44182352938914,80.3320131567748,1,48.162697429234015,1,75.61933765785858,2.822553828415736 +32,76,15,28.05153602,63.49802189,7.604110177,43.35795377,lentil,19.27678302496176,3,11.33885373357628,15.008295632342442,429.2802551955656,6.46232154001593,3,13.496643537670344,52.362972805916606,182.4505183242789,3,45.95448423139486,1,24.884930026718887,3.5042943703293523 +13,61,22,19.44084326,63.27771461,7.728832424,46.83130119,lentil,18.438227419464383,2,9.937594279335745,9.129900283092717,424.60994377089946,5.018341503913297,1,6.099576085751946,82.13979551698755,109.50067254914393,1,47.24587657730934,3,97.75552054259823,2.310804589700542 +38,60,20,29.84823072,60.63872613,7.491217102,46.80452595,lentil,25.04034799814584,1,11.328454084375423,7.20789948844581,360.0309193619573,2.9231737408738847,2,15.73311624579651,4.739794714120071,69.97518015217105,3,40.223670775374785,3,50.15182236368132,4.0685573249955045 +11,74,17,21.36383757,69.92375891,6.633864582,46.6352865,lentil,23.18270053667158,1,7.455691856518834,6.629620933102245,368.76991631661934,5.991943934763735,2,13.686589460527586,77.77151779511888,91.90201542793761,3,44.72237053114457,2,65.41840102109762,1.122066032189974 +37,71,16,26.28663931,68.51966729,7.324863481,46.13833007,lentil,14.45817986156172,3,9.626370641021136,17.55861119160324,411.58704287651585,4.772328614297107,5,5.886138622888223,68.65614250772512,95.91380501974997,1,22.547897579239063,1,62.37843674736327,1.2991179532158545 +29,71,18,22.17499963,62.13873825,6.410441476,53.46622584,lentil,16.837691677132028,3,9.620154653125123,7.910947278204485,449.6635804608351,7.612303978595492,3,8.69086732756665,68.81675954423258,146.3702009082898,2,0.8843807577655294,1,86.68687056283959,3.001104050965304 +2,72,18,26.57597546,60.97876599,7.836719564,50.89110726,lentil,17.894337990722356,3,6.487111245714061,9.764371961158762,412.58377696237983,6.48257464676545,4,19.609272427998484,90.36287087809495,152.41668215557087,3,41.68530793287651,2,34.663865847429044,3.478139945834692 +6,59,21,26.58972517,66.14007674,6.139215944,50.90994463,lentil,10.236719226201533,2,8.656703397912683,15.430076214408611,393.51725429745863,9.906356957512898,4,12.537050497598052,80.3887350055627,54.023510964660375,2,3.5021582396895843,1,63.899945608529976,2.8012177933299593 +13,64,20,19.1345771,62.57526895,6.590571088,36.46946971,lentil,26.287518898382206,3,8.816027960722252,17.29904297523991,402.1956578346373,7.681152597712535,2,12.803219739282223,9.388962427804026,187.89553618285592,1,44.01972478632954,1,58.94788076396742,3.5048195924739747 +8,58,17,28.75273118,69.15640149,7.286049978,35.15426171,lentil,20.957280266813378,2,8.378705176732355,9.031564218333648,430.85572840377216,1.230903918036667,1,8.415045614090296,81.95126369016734,105.91613314063801,1,2.5264928579438406,2,19.265041023560237,2.157693672907198 +6,77,20,25.78746268,60.2816298,6.058306161,49.14337177,lentil,20.748192638913565,2,11.276729438981565,1.2865924206754698,361.3512048600494,3.60616371774761,6,8.659463024683705,31.576805770407436,112.13425567540412,3,3.8393354365707344,1,94.69239078103128,1.9851376230357078 +2,75,22,23.89271875,61.78779413,6.658605362,52.55730112,lentil,28.726548183184818,2,7.00398495931532,0.5866523481491925,410.979891204902,3.42341749073929,1,13.835772955356319,87.63887499618836,179.03630022435976,2,20.638041099528504,2,49.17617357712925,1.6250570190764937 +3,69,23,28.67408774,63.18832976,7.299360767,42.96018627,lentil,12.730847055470505,1,9.619756849717362,11.365544549286348,425.0166592382505,6.14063048592668,5,9.152563191495503,55.716383062708,176.05472949422534,2,37.91878465080736,3,38.57234137226557,2.9817288877634285 +27,80,24,28.42062847,61.77336343,7.815210661,49.02366803,lentil,13.462866142379825,3,8.125371069066718,9.898828072028572,441.50801141535413,9.623038817054168,5,12.16263917451695,66.92070845786286,142.07962565693092,2,7.253230278776035,2,37.02352784079864,2.396001717247454 +39,78,15,21.35499456,62.60136323,5.925391795,41.78219834,lentil,13.663389299811406,1,7.679533826019791,7.854718512313699,433.75249989747294,1.220703542092924,6,15.671333037601716,97.75591542346523,196.65335693721997,2,34.636055457826174,2,85.6977911304507,1.5358365932272755 +40,79,17,21.12695586,63.18738532,6.403683619,38.71834464,lentil,18.49230232182243,1,10.00278861587699,13.56269634844844,423.73460285777196,7.387340726592608,4,13.095067675199239,76.77591920593886,164.9275724718721,2,26.712135205444334,1,35.32776859431765,2.947503898953036 +37,62,22,24.02037872,61.62313345,7.397546271,49.78102578,lentil,16.884037140910706,2,11.900976572377242,10.542452075709933,366.9985005732283,9.56837597585988,4,9.631474456386652,5.023994159038569,180.65125468786547,3,16.17752875029704,1,52.78029645572961,2.796238204118609 +31,60,24,25.40474421,65.8567539,7.722335992,51.92057267,lentil,22.002019183940455,1,7.766478696570674,13.929083422155097,358.53661260329045,9.415375957931225,6,11.410312902456944,91.85756925589224,80.53423027896366,1,34.070265613356874,3,23.988822249400943,3.0808566533582473 +22,67,22,29.03017561,64.49166566,7.475926645,54.9393771,lentil,28.95694615481085,1,11.451804426249694,14.683642448418725,388.0698280728713,5.045427694048343,1,8.731687773360413,97.83375635640893,139.55052821065215,3,16.28086075941118,3,82.96005324564412,2.146180083019831 +3,78,18,20.21368219,68.65257685,6.887130053,50.89732989,lentil,27.85663558691468,1,9.436186763520624,2.6631401841352775,401.3579812573595,3.9440249650749495,4,15.993041878439268,68.24932342435335,192.71602737962226,1,22.243249723939336,2,66.78094398730433,4.269529976277018 +4,80,16,29.19585548,68.01965728,7.441976825,44.93261911,lentil,12.825285071494225,1,6.55384272637031,1.8631396836044911,380.96784299648306,2.8821566691499907,3,12.591736408355327,90.02019656713342,193.00337185971227,3,33.04401510236746,3,67.73461567337063,1.6967040137215377 +13,61,24,18.29783597,69.6897615,7.629910253,49.39111479,lentil,15.167263794397696,3,11.941165303381856,16.626420400079418,358.25627282760365,2.5792250599552817,3,18.337205549786685,87.71966777358324,121.60021550260196,1,41.82999785970061,2,9.295081044467102,1.5401563097373465 +12,66,20,27.41434987,63.41785982,7.336117221,44.43177543,lentil,26.188213083498283,1,9.862899428897524,14.22386350166076,396.5281637554764,3.832925210937656,2,13.539747660732495,36.836968536178375,51.05472640846486,1,48.83186979892773,1,39.56579738002974,2.0901914698106303 +4,61,21,24.84063998,60.09116626,6.75020529,48.77790371,lentil,22.399836623372344,3,9.916116978825903,7.087892294011757,389.66563298406436,3.375452176327615,1,13.762275594990523,26.537487609830855,118.82675084403276,1,16.802287876322925,3,18.229750827767788,2.230370003989106 +9,60,21,29.94413861,67.31323084,7.52178027,40.37113729,lentil,22.94051785690319,1,5.690374644978047,2.9660956689907403,404.3270446066069,8.54902951580384,5,5.61381978366978,21.652407132402153,102.30853153222665,3,10.729445852207236,2,34.0569832797955,3.739215696412038 +18,66,22,25.87990287,67.55109024,6.347379185,47.89645224,lentil,20.91114093424476,2,7.744262254749348,16.732080641813322,359.25528858152137,9.605752222904433,2,11.911153453403193,95.40023529375566,120.62724331766951,1,39.21285245968853,2,78.1171023446337,4.252812624022077 +32,56,18,20.0467711,65.84395319,7.135251532,46.05333124,lentil,20.359243797782497,2,6.3192162267518786,12.60041912253296,370.0136231255031,6.503828377311431,5,19.386535334945343,59.13109437631534,86.89098958851878,3,21.039614409417922,2,69.11455827605741,2.7539423294032126 +6,72,15,22.99451999,66.70897237,7.670178119,54.49044154,lentil,24.864879055825213,3,5.807752675700151,14.74449809137582,415.46808257544694,2.7167636081389457,3,11.311253001262102,15.224678626164811,94.98826067190492,2,16.061782553470888,3,82.81533838713784,1.6707014397067965 +15,77,20,25.13163619,66.92642362,7.399749291,49.04015558,lentil,21.47557123265734,1,10.040287015448866,15.45898234200077,400.87796778506515,9.438511266559809,4,17.243312537519028,43.82413772019298,175.73687771843703,1,16.919145065631696,1,71.45152025547326,2.981503190174741 +0,65,24,28.49584395,62.44616219,7.841496029,53.14531023,lentil,20.545202213880586,3,11.247289334267847,3.6327324507315417,448.8130409912659,1.4646507931219666,4,7.222196032575084,3.7007045470598743,159.44951613947921,3,29.98514498569301,3,50.218513129547674,2.7482267033091916 +30,79,22,18.28766124,69.48515056,6.254216611,48.60449438,lentil,16.20968329584451,2,8.436000761536494,12.340733181986396,361.27339326022417,9.090789724056382,3,11.872016789150674,19.57497777052133,118.1412619908518,2,8.084026512388931,2,79.71969105200336,1.9455433789884853 +3,63,16,24.38041875,61.18458224,6.868881708,53.13946695,lentil,29.04553646469292,2,7.115118395999351,3.2624980560728734,448.91234136571495,6.209932244437648,3,18.862295897439125,6.994867744293476,95.34627692635584,2,41.64568702145529,1,40.189520858328706,4.6850636108381565 +2,78,23,21.31852148,66.43934593,7.320514721,45.42616802,lentil,21.199323146876043,1,10.213355052607668,6.911864212126977,377.0020688629428,7.5321686236661165,6,12.097382189624634,51.562079227875444,181.99635360652044,3,32.411865423952854,1,9.685291886637737,3.7866438662796593 +10,78,18,18.54141834,62.70637578,6.296976913,44.07819743,lentil,11.619596564199043,2,6.901399450119246,1.2319557546458237,427.20438676187115,7.343510048139393,5,17.357632210629852,35.99340273357855,92.2156358266072,2,13.91828346241214,1,14.064688357293075,1.3959887328666332 +14,67,25,25.28710601,60.85993533,7.241151936,49.37369982,lentil,10.936884055517533,3,11.836996552694592,17.590336730852762,376.0742126187407,2.3748477860454753,1,9.095816997348496,18.250282662502492,148.34399646506313,3,40.02143710031867,3,29.06435336871851,2.8610551131141415 +39,65,23,25.43459777,69.12613376,7.685959305,41.02682925,lentil,10.668559319782968,3,6.391909834526754,7.958084854078242,420.24264650324176,1.743894977004242,6,10.232154749531146,95.80187666395142,97.79828414472311,3,39.00088379504362,1,45.98289268382856,1.673029754855631 +19,72,15,28.83600962,69.76112921,6.890760124,44.08562546,lentil,10.332752976245239,3,8.294363761536525,4.027588885127853,404.2713496280691,5.110229548141154,4,19.178325353203356,38.254918699692475,93.99043405695397,2,9.009656450559195,3,80.40516323082772,3.1771731611179157 +18,57,21,27.37659643,63.93927841,6.155915975,49.47371773,lentil,26.331670808980753,1,8.3511594270818,11.609303734369401,388.10945127226273,1.586407904011544,5,12.301091477402878,19.469523617499995,148.70915148085135,1,25.106384489821327,2,67.77301236402458,1.4793824668129307 +31,58,15,28.31886863,60.19461399,6.167855382,45.36521251,lentil,27.595820067402826,3,10.25567147508117,13.926134424198091,384.7273064495869,6.493201871906988,2,15.930731191698463,61.205595350981554,87.4501510324937,1,37.1792411011761,3,29.18394508171467,4.192587246829369 +28,58,25,27.4818649,62.04814951,6.861640036,37.81123974,lentil,17.6698208866501,1,6.889631896279932,3.1065174494573955,421.1294979696148,4.124305131155101,5,6.749504431581244,5.311328221908296,125.24542034036091,2,22.217327334339114,2,95.26081494538393,3.00261012746715 +5,65,19,18.28072173,68.10365387,6.978361689,48.80253285,lentil,19.90072248328306,1,7.761889469687644,14.67160153890947,378.0569602031333,1.0564141783000287,5,7.384101129670398,28.929658408895754,191.64101104801014,1,15.031763808923998,3,37.976421509693544,4.4180338407896675 +16,65,19,27.61204997,69.29786244,7.043160241,42.72374404,lentil,29.119181021801737,3,9.062038550207928,14.02925739398908,439.84937544736624,6.99520526570196,3,14.37338581561564,46.688699728067384,117.30729812273795,2,47.66305801347107,1,8.226413311805258,2.8466493418336225 +34,65,19,23.43974653,63.22011726,5.94239222,45.40277297,lentil,10.785040291831237,2,7.357999257745636,1.4465349876511358,424.57373771280726,3.6976918985047096,6,10.649972759834867,2.6070591207796423,88.36887333658018,2,6.754367051941496,3,0.629364367600993,1.4033804348983039 +14,69,19,20.95628486,63.68128841,7.239455147,52.39881209,lentil,14.95942230328832,1,10.682835114312631,17.312042169596047,373.12798609879303,8.298089778649647,5,5.402425966924702,37.37428476715756,101.07652864776117,2,28.967885602940406,1,14.654915478788766,4.492107231229698 +22,55,16,23.7937153,68.03209183,6.516317561,49.73922097,lentil,27.073735859090377,3,5.054747724762896,11.431874066738546,400.88081487476285,2.200394323565268,6,17.816464903062418,4.447605238355445,159.7020166179442,1,32.4601671800041,3,64.22073678411876,3.0317056621894505 +24,61,17,22.6371424,65.44544859,6.233269045,38.30411077,lentil,18.081055205437472,2,6.304728401782241,3.7354977625526953,393.9346484297704,6.3716461428768785,4,5.300027266792263,88.72919194341164,120.0753439618402,2,28.407064801676,2,29.970289700460164,3.671251275863861 +2,79,15,21.53577883,65.47227704,7.505283615,35.75107592,lentil,19.227672453883155,2,7.820464503866872,5.530977828947972,382.47923203170006,7.122700167611304,4,8.76288709788691,70.70320479592385,91.74046195047956,2,0.38507576285265466,3,52.84407589467291,4.9251846020066985 +26,63,17,29.87854588,65.73085206,6.950300686,44.95654782,lentil,24.736942539024028,2,7.498385966808504,18.7866728339011,396.9546742232212,7.023729731116315,1,13.345261125097057,8.04590438527878,55.69251692863946,1,48.354878668016696,3,97.5191859054308,3.464898415794676 +27,61,15,25.2653291,67.10004577,6.958054839,48.33941188,lentil,28.64486522684999,3,6.137018525462221,15.974127827031843,367.67017660078216,2.8954213388843226,2,11.819256917378803,36.9554874980362,75.0219092753126,2,32.902542709848504,3,74.07777773893307,3.5284204272470885 +24,70,16,25.17885316,68.93307305,6.54803469,35.03484812,lentil,14.345952234830875,1,9.77899953280404,10.596461673613467,415.4995492412156,4.09934833693325,6,16.789980656020912,2.2743437479968764,164.83459344987793,3,25.266777642718523,1,38.48476774483601,2.1151785850653724 +13,74,25,24.12192608,61.09533545,6.461618577,44.23629285,lentil,26.962441006957377,1,10.712694067392874,14.066382631068109,361.3229095724389,3.7407758494007712,2,5.051375583343425,72.97108931382394,53.2330702961511,1,5.039669238199762,1,59.493879877469034,1.880089209091727 +6,64,23,23.33565221,67.40460704,7.065264073,36.18678721,lentil,22.03811390384275,2,5.669890526083501,13.156988843374398,431.522831368868,1.1670529693550002,6,9.092995030783623,62.982786275086866,97.9659991903941,2,38.02906610726529,2,72.1528698741107,4.681176112910903 +12,58,23,21.74600081,63.39503184,6.765091462,50.43306085,lentil,24.89054517424656,1,5.69655648384773,4.059310743829371,449.12414548903587,6.118478342895687,4,8.441222802212597,44.583280550703776,190.56710446391864,2,28.258862798617685,2,61.3173822263886,4.680997266645102 +32,79,22,27.60195453,63.46170674,5.91645379,54.37814199,lentil,15.708845879449388,3,5.945760731909819,17.61623053743278,447.89180038916084,6.266976310580807,6,8.332525369685673,18.008088330203176,110.47108043166624,1,25.48596351364297,3,51.43406180629421,2.3564520313836734 +6,68,18,24.388717,62.50453062,6.711341147,47.26052494,lentil,28.348238943330266,2,7.124887638232765,17.759181717624518,402.6386194379623,9.377079677854862,5,12.211252769786213,38.55566511027472,117.30724455676926,1,22.4678921273072,3,63.376262585810586,4.178196401125927 +10,79,20,24.98287462,66.895409,6.379881442,38.21370568,lentil,23.202399721730934,3,11.861477380099046,6.781859808946191,406.4933693505076,6.182380090370091,1,16.015969346411424,26.47516825013311,163.27810036766954,2,2.5770707415240626,2,13.73378312935577,2.188480191495183 +38,77,22,28.234829,69.3159965,6.313284268,35.36831423,lentil,27.962340127888282,1,7.148028603347068,14.55011107983852,448.4794083381553,7.270209975982189,3,10.978467893336326,20.975602105443603,153.25370991857812,1,15.239527814175824,3,7.569973547662256,2.7964471477769233 +17,74,17,26.03026959,69.55863145,7.393210848,37.11395801,lentil,27.880726849596336,2,6.871373522409962,8.715293172561339,394.63279722001573,1.6623864127392145,3,16.974563530723856,85.53687742505383,140.3822080773886,3,9.931552525473652,2,74.66630263870651,4.411342686800024 +26,68,24,28.04849594,64.07691942,7.504930973,37.15824966,lentil,14.641443659577863,2,9.237731698910501,0.15506563320055733,374.07235697004734,6.265930772398644,4,6.885387349924699,12.53051187257549,166.63596843628682,2,41.081097126263636,2,29.05474926870926,3.5463757274504446 +23,75,17,24.87425505,64.00213929,7.198076286,48.28137482,lentil,27.017716233737296,3,5.513668358304629,4.213603198624785,396.4343685685683,6.431280058274633,3,11.451320274116753,34.63207907787281,134.5919794451369,1,6.604137137384475,1,59.513878043347,1.326306285743469 +32,78,22,23.97081395,62.35557553,7.007037515,53.40906048,lentil,22.618112514277907,1,9.304473950112495,13.06476411981668,398.43322172548886,2.886499308278208,1,8.339240910824124,38.70730457812781,91.361134262089,3,18.322703542686092,1,36.936743581037234,1.1829075053158067 +19,79,19,20.06003985,67.76252583,6.677262562,42.89509057,lentil,18.329149855486456,1,9.880709175279915,8.480549175136563,417.29174767279846,3.983581654526077,5,12.37093831520438,24.758554240807207,75.32923724020878,2,47.70789376799374,3,48.30141247495157,1.543963804763277 +22,60,18,19.59221047,61.28633405,6.74398035,41.7704893,lentil,11.908288521970295,3,11.87892454932975,3.160515995604345,409.5818997164872,1.1813482525994417,3,15.15959043217057,43.510860473123714,113.72324073670846,3,3.7967571561129674,2,25.390709257806254,4.56975029805194 +28,69,16,29.77013109,66.29327012,6.547361618,35.69674138,lentil,27.093874300113924,2,9.875342098314057,1.335826471093513,437.1446180832128,8.832227883615587,3,16.706238759488983,18.660956862827728,123.08791682710324,3,42.341301366570676,1,84.2370450757781,3.321662161799542 +1,67,21,27.52135365,60.53657684,6.551577598,48.06491307,lentil,20.10713785979723,1,11.79303773006647,16.064069697831023,371.88394650003477,5.6856838406539625,4,16.783901863612854,16.594426910489744,174.76189519589886,3,34.818104138956926,2,82.43951231269595,3.9031547477059214 +12,67,23,25.62896213,63.14909763,6.585020303,45.49683991,lentil,14.617376998249076,1,5.875797263042631,1.180002656854564,399.0970044237872,4.749962158465656,2,15.239111513465748,78.64516984607066,196.58796532887774,1,0.7816359790176253,2,44.2051744851492,4.174632323836173 +36,67,20,20.39078312,60.47528931,6.924042372,53.31508572,lentil,20.09578928612813,1,7.897613057558718,9.858719108757159,364.60267090105225,2.279296351926116,1,19.804129363872715,30.04899292872899,168.1958611998441,2,30.913615285413997,2,25.598524550507108,4.17440372679706 +28,70,21,25.39038396,60.4989659,7.437373666,39.18374505,lentil,29.618639154480586,3,10.262345909301697,3.870646132242692,365.4839651736186,2.1107097813758906,5,10.713264914906455,53.05652802309554,167.55163719360405,1,22.439777024035767,1,70.04856309766483,1.771372524515718 +12,71,19,24.91079638,60.71367427,7.142611056,42.19740397,lentil,23.367817290240918,1,10.167946684827523,17.433949753435222,359.81608835945536,3.19349397168311,3,10.987763765003198,88.56496673304649,132.09347303471415,1,12.621257373056732,3,63.933575183434755,4.197837154451969 +22,68,16,27.70496805,63.20915034,7.74672376,37.46160727,lentil,14.568782426502285,3,6.691179995148137,16.585933712118273,376.33384459989674,3.0761146445909877,5,15.38626120664413,65.51434926241406,144.8199745948752,3,49.966588688193,3,54.757311191407354,4.040052790930984 +26,66,22,18.06486101,65.1034354,6.300479414,51.54922825,lentil,16.70909070992215,2,6.329312056580471,13.77580785489586,409.99273939245677,1.4536083030102742,4,14.82536904733736,71.4881409713671,89.31210914127567,2,42.96153583612492,1,58.73846493687744,2.7015081822804365 +16,65,16,18.13027797,62.45851612,6.078724107,50.6128521,lentil,19.347354404761397,1,7.594171588144832,8.702869440392933,404.8072014034098,1.7275573518216776,1,7.542420166716921,5.335543427749556,163.6520288102647,1,45.086950263044784,1,57.63335150363893,2.3615979720169147 +14,59,22,23.82723528,67.89815262,6.76660668,46.90725077,lentil,12.580143977034561,3,5.640233975690396,9.272559366739133,396.2863056159336,1.995847090284123,1,7.270986292908939,12.116362071525998,59.967085749731545,1,36.059081687586186,2,68.03778561239437,3.940962900095362 +33,59,19,23.19305333,62.74710773,7.641024177,49.55213308,lentil,25.503149422228482,2,11.572300653496455,12.227823773458757,384.79135919037645,7.893359729994192,4,15.839673043583383,29.35492705782943,176.17768692245426,3,40.15672639081654,2,68.34540989360012,2.3194648057620255 +21,63,17,25.08966129,68.17543102,6.559681838,41.45486619,lentil,24.35983833596474,1,11.676934803634056,19.07973181633846,371.37627765656043,3.187829296101506,4,17.730521057733213,24.76429292631056,100.63237033264744,3,43.07950506433505,1,28.008464560688928,1.637794069390457 +0,69,21,25.86928193,61.88321072,7.072923306,36.68284038,lentil,20.5287131393018,2,8.290499811772033,3.989482862201663,405.1792635422988,3.980204474980871,4,5.368694876503923,18.296648745872012,60.037905735770174,3,22.092876505252956,1,7.232765455408597,2.0156475683399493 +10,75,17,18.43966037,68.05394959,7.732194788,39.00992137,lentil,10.273651368619861,2,8.474102929447048,2.715976683101875,372.44306423985284,6.013797166523013,2,15.723457893185449,82.07842976595529,117.79350826302277,1,36.501071460514304,1,73.93491461744216,3.270412719818772 +30,61,18,27.14911056,67.02664337,6.157782589,52.50812701,lentil,16.86115814260713,3,9.636014556966886,7.536925250971455,440.0246880479634,8.73373455899779,5,5.309411245547835,87.167564959173,187.0996702339672,3,13.319585444892345,1,58.780479549872034,1.5954873621414327 +0,74,17,23.33375853,64.50515776,7.240988401,47.01510708,lentil,11.911879522102387,2,7.914023130622338,11.94318729709434,354.50570134084677,7.680509515846121,2,12.683016127147482,42.68945995710794,73.73495527144723,1,47.131885286021685,2,16.07976342466276,3.276836937163461 +35,74,22,26.7230014,62.96841833,6.898905799,42.87274897,lentil,18.27494295425739,2,11.613911921738687,4.11477578824557,443.24173022827716,9.96243899295224,4,7.503654771329578,97.22310741572548,176.4944206941241,2,37.84536070969103,1,98.0792279815589,2.8737564558815176 +7,63,24,19.55750776,64.45268309,6.818681086,53.04669416,lentil,28.703481602002075,1,6.989636769606996,18.27946227694952,384.88349336278617,2.670690451669968,6,6.679976796214723,79.41792450521413,157.9013663391306,1,44.861678989915674,3,30.402394503279695,1.0848313235463038 +9,56,17,26.13708256,66.7729209,6.261937875,46.48280681,lentil,26.640420031657005,1,9.827287079356793,17.44561099467664,387.25084103019685,5.226436901308074,5,17.355183462200653,77.46893186117966,108.44596160421976,1,5.258451529725644,3,44.42394872558376,4.55553690488122 +14,74,15,27.99990346,65.57653373,6.493036868,49.94043064,lentil,24.29132104227174,3,6.848778050243904,17.753936946189427,409.0844997064032,6.765523037418564,3,6.1899160302164,75.759091363561,198.45304675823485,1,31.911345169979004,1,40.62044426107527,4.814154330682637 +14,76,20,29.05941162,62.10652364,7.042474679,36.5011366,lentil,14.965284817188142,1,7.196692431707119,9.415952248757637,426.0575308213743,6.936689181790468,6,9.994170882994322,88.78100281668921,89.60474845952584,2,28.917184423742853,1,9.356713934737105,4.46005079839197 +36,65,16,25.71269843,64.1123333,7.692013657,50.17067771,lentil,23.127137141911934,3,7.325446429469261,4.232665860292803,432.3041374084792,7.211427927417553,4,8.604609848622736,67.0785943863858,154.934642056815,3,25.365652625450824,2,4.663569390422406,2.529130508862883 +28,67,21,21.79792649,63.73086065,6.250994223,46.62370222,lentil,19.052286849157873,2,6.067945755608558,13.90833760268739,447.1535936419785,5.561190995086831,5,18.013855696487443,85.86586274487388,126.09323181397222,1,30.48507407714437,1,78.87429136630153,4.6583495318507495 +28,79,16,24.70626432,60.26854183,6.052184881,53.12442925,lentil,27.421001812254996,1,7.097979566016889,4.367215994237026,400.38171340982797,5.5307925257107176,6,6.510458165350672,92.51355976215574,113.18112791386608,1,30.71919138969269,1,85.89521782365122,1.6431166956141783 +40,61,22,20.94981756,65.8108757,7.002216044,44.23913012,lentil,26.567203951506727,2,10.410089478318362,18.30405744613611,414.01934528835034,5.083791828018195,6,9.702007236333653,89.86790352292105,199.98216584521697,2,19.299275594049192,1,99.4983286850011,3.6189134109851104 +10,70,19,24.84918386,68.98088448,7.272427638,41.61080544,lentil,16.224592754313896,3,7.7345059863568,12.45897742131061,359.6796474264641,3.181755889680254,1,13.263157700100418,30.021212620449933,106.57211441033523,2,28.731652182002055,2,80.96799379261705,1.6652679053172594 +12,80,19,21.91041045,65.21662467,5.962001484,36.10211371,lentil,15.741550620497598,2,10.023150441252197,6.4792907144254785,399.08402621682814,7.062958727067285,4,7.833186790196846,28.590868323658093,81.81545760654961,2,11.093004080134271,2,92.01907247463677,3.503932626653242 +37,77,20,25.93381964,68.70533022,7.080506001,51.02372773,lentil,27.843123662888544,1,10.02309930107644,13.088094293245415,410.0845438938006,2.977444233364575,2,14.23491008657956,74.80553720234431,145.3109905266887,2,48.028206038465164,1,83.20455667238541,4.844698022416765 +0,67,22,29.82112112,69.4073209,6.593798387,51.56461082,lentil,23.621614234869263,1,8.094505143791043,7.279192672821262,393.7340646290788,7.419163032875788,3,11.68965317624134,39.82833238735511,104.98692431810102,3,49.02557208181798,1,89.18026765214034,4.1537795111306455 +7,73,25,27.52185591,63.13215259,7.28805662,45.20841071,lentil,19.08616281257064,2,11.067205293135189,0.36647455041529664,392.6489769162514,7.374535241385539,2,15.807682408544945,71.18245871306873,187.44019395595274,2,19.72114292744743,3,11.448965436396975,2.885738777246149 +10,56,18,27.99627907,68.6428593,7.32710972,46.10585191,lentil,28.963953824655274,3,11.980394329176537,16.637197652092986,409.5988106177418,7.090399879050168,3,11.02172474329563,33.56750120687965,121.27967822228166,1,23.514765182000385,1,39.090588689564775,1.1143340797659702 +39,70,15,20.76774783,63.90164154,6.366355781,47.9271552,lentil,14.622800795816357,2,11.783512368771714,15.159914943360171,364.41869639648803,8.374056204502335,5,6.431010647859985,94.2030909202964,69.5250543637903,3,41.14122916828145,2,51.916412055031614,3.171390200893986 +26,56,22,23.05276444,60.424786,7.011121216,52.60285259,lentil,23.139438594218518,3,11.118981359251332,3.254042724435857,423.95901923397565,3.5616146697169304,3,18.173097313156013,96.93239327895921,147.45909433889156,1,29.138211158644765,1,39.280768621221476,4.620146393687778 +9,77,17,21.65845777,63.58337146,6.280725549,38.07659414,lentil,14.924898117590468,1,6.712965179303653,15.559355250182247,350.623168984658,8.830728960574737,2,18.932097039352662,40.35355917750029,135.40246335772966,1,34.72027668961286,2,5.626124369170904,4.790774302077372 +4,59,19,26.25070298,67.62779652,7.621494566,40.8106299,lentil,23.98906903439705,1,11.839940350240234,19.77469078527322,429.82565359421,2.575532115910115,2,19.514213831710844,78.7989173527513,70.29259414435741,3,15.752540477982556,2,75.48529089324909,2.7661226508034376 +34,73,15,20.97195263,63.83179889,7.630424083,53.10207889,lentil,20.245398484872055,3,10.175526795219522,6.452467603018146,432.01195193665023,8.504917297828268,2,19.457186779090673,90.0395984795438,151.69375527092274,1,12.444479126511048,2,92.38531724048363,1.6044679422712096 +33,77,15,23.89736406,66.32102048,7.802212437,40.74536757,lentil,16.843666120790946,3,11.634115816262156,4.964019382638066,426.9932412151866,4.104704264912611,5,9.372103560946316,76.54885794351736,109.46735374335651,3,25.16698073628958,2,38.23503718670073,2.129619540343404 +2,24,38,24.55981624,91.63536236,5.922935513,111.9684622,pomegranate,29.82073358067431,1,5.229433445769335,11.642977760924872,358.9806938418626,2.056651290642166,6,6.456733572143127,74.80624577650414,182.63103067288174,3,1.257897127552754,2,88.36356341169642,4.978824136865915 +6,18,37,19.65690085,89.93701023,5.937649578,108.0458926,pomegranate,26.191276152311584,3,6.708153128475681,4.668144003312782,381.05759021427787,8.955802673708687,3,17.723249768831252,62.16619975440017,198.18831931961984,3,49.176200538614,3,11.883734881760466,2.7365924903723675 +8,26,36,18.78359608,87.4024767,6.804781106,102.5184759,pomegranate,16.176486379938588,3,10.550730709631335,2.435822334766331,415.5251824017887,1.4933273454210279,6,9.599499002296529,88.58182814068985,133.8168623728943,2,30.038182661245955,3,75.05481149204861,1.6887381853661445 +37,18,39,24.1469628,94.5110662,6.424670614,110.2316633,pomegranate,29.69958033164991,1,10.044658469828775,17.15953709817065,411.6391028501411,6.352370792037157,4,8.879352206050186,72.68772870707213,187.58838882556282,1,31.20721700325994,2,65.0172479281433,2.91295167821925 +0,27,38,22.44581266,89.90147027,6.738016221,109.3905998,pomegranate,22.676006405774526,1,5.4308831263030815,17.38533549339872,398.1071022352494,2.506324756457042,5,10.812225299099897,90.37057458127191,195.59519923855683,2,21.61443209411101,3,35.7233904175992,3.1051786868366325 +31,25,38,24.96273236,92.40501423,6.497366677,109.4169192,pomegranate,12.177924300529003,1,5.56924058366493,1.07657231560766,353.5336932534243,7.138210997826638,2,19.39904803255098,14.743381338790574,92.09802794361752,1,12.479189491000808,3,56.178089490775626,1.872612412709247 +21,21,38,22.5526059,89.3259486,6.327673765,104.8955643,pomegranate,26.280214762278696,2,5.59983721105651,9.046386825360333,414.8612719277506,2.939434078672868,3,9.14566342417358,99.90675719545892,171.23370171870343,3,45.53685171482309,2,74.9722448178763,1.7638096985495046 +6,30,40,22.77035608,91.45498527,6.36137446,106.9659201,pomegranate,23.707311279600493,1,6.816272993056142,2.6982620417635705,423.713665115704,9.349070545091745,4,15.413516080924513,25.097291847709823,103.63097631738657,3,21.914961023549136,1,32.98964904938344,2.686251157761785 +25,27,41,19.20090378,94.27659596,6.923509371,108.0423555,pomegranate,10.806668033177386,2,11.307580854722428,5.7003841919991505,430.7276096887616,1.4463218319134223,4,15.288416974091387,70.99911747321036,158.98328408349303,3,47.66348831544285,3,71.93431802691461,1.4421661877375143 +15,11,38,23.12808226,92.68328358,6.630646083,109.3930157,pomegranate,24.403204180156607,3,9.799509196981468,3.709087459930225,350.1055289700729,5.433465769545952,4,10.770290321930183,83.31810965619054,112.44735959212738,3,1.023681288866768,2,60.06218848385101,2.497284417749725 +14,5,36,24.92639065,85.19098079,5.832525853,104.7693804,pomegranate,16.854470545846816,2,11.167383123987264,11.028140519927302,385.6321643750148,7.127845484957718,3,10.384903996642228,87.21853212357598,171.12560398866833,2,39.50639505756428,1,89.95940094542547,1.1043151026415963 +16,10,41,24.77464458,85.63608688,6.738993954,105.7595811,pomegranate,25.1540447810393,1,9.373111386124847,9.193857154094324,357.9660609025812,9.908473573708982,5,16.537403171444417,78.8109377959032,142.4198261231162,3,49.13710266867884,2,18.982348429753195,3.106634561334538 +36,7,37,19.8671184,86.35590206,5.782435567,108.3168858,pomegranate,27.753003309689646,2,5.472438994375602,1.0479295912050746,390.97699839213504,9.041922104418413,4,12.88164060530626,83.36006389015498,58.519688137933905,2,6.499175693601201,2,3.694218979187691,1.9307413107201703 +4,20,41,24.26601316,93.7974061,6.537042717,104.5375109,pomegranate,15.01205584314594,1,6.6216509286468215,19.78311657292187,395.14610009109407,7.056214001901947,4,9.503020333741148,88.06765418632077,132.32370121208743,3,2.7518908751832774,1,93.94034782626714,2.9639190330408467 +29,22,40,23.62600218,89.73266695,6.145104401,107.6836871,pomegranate,26.838771128325185,2,9.53382864057151,14.73810601866404,438.21700997949915,2.888362245534815,2,11.03547491070291,53.77803970521513,168.47286975292036,2,2.9172959776212926,3,10.722033358024408,3.9456994372554406 +16,15,42,19.67832052,89.08935702,6.890784045,108.5468633,pomegranate,15.443780035055942,1,7.429194036074655,5.020634964148374,449.24408988062424,4.170761220736455,5,13.771334380840875,63.46835412030083,169.73686727741472,3,24.927888255362213,2,12.033072647245625,4.650349448050031 +18,27,41,22.36509395,92.30882391,7.175344328,104.8216333,pomegranate,24.322284268605454,2,9.285023044041834,0.39083125830263077,388.71216009790413,6.426294756440868,4,14.01919524875444,64.92722665699185,149.01800314887296,3,1.048151784356105,1,77.43179254354618,4.69277734106482 +11,18,42,21.57936934,94.88267728,5.938528744,102.8593382,pomegranate,18.96694772570151,1,11.292019764770568,0.13398758194617333,438.84541434540364,5.10418823591812,1,13.239059233340388,4.163888210012834,134.98556772081304,3,33.67471150955671,1,91.02142201659989,1.4670729532676092 +5,15,38,18.26233221,88.16779129,5.709380472,108.0756727,pomegranate,28.76925310088035,1,8.863933726865742,6.841955039179679,390.32692975903734,8.552554423415442,6,11.923768395299893,95.70099383744692,94.0555619916651,2,20.60353807311775,3,58.77072467084334,2.113860098063629 +18,23,44,23.71028128,89.61794165,6.184400085,105.6499907,pomegranate,13.125473693909079,3,7.0116316854513805,14.512991656540423,411.49213648002575,6.0740190360412045,2,12.669168385554382,86.56522641623332,112.38070935767104,1,4.33634603185149,3,15.229306369325057,1.6476738826772217 +9,8,40,22.48720144,89.9224883,6.553509673,111.6631582,pomegranate,11.74268614956109,3,5.265638783236493,5.687718549226686,364.69659923546897,9.773097176584038,4,18.96030604185022,61.06804863785102,187.56723975298007,3,40.33473055869141,2,38.26410271219438,1.639145330735337 +40,27,45,21.6602498,94.79397419,5.885638185,112.4349689,pomegranate,25.62410154531993,2,10.121272660655007,4.5289396189883435,387.9696001982271,8.697286392109516,3,12.274019298673615,4.249559593759578,98.4246107750133,1,38.42244388288514,2,39.52935306914658,1.4227309705744444 +22,23,44,20.13037175,89.31505137,6.143874691,107.3416913,pomegranate,16.552014653998782,2,10.155982672641443,4.816263169059458,411.9858223870076,8.04043282762039,2,13.501408400663353,27.49589642025012,148.31944962193256,2,45.09289149012348,3,12.069954006087524,2.6927007455307472 +9,16,39,18.41164435,91.11927248,6.101198974,105.1834976,pomegranate,28.93421486775686,3,6.322785560610438,14.414364340577674,435.18749857137516,3.8321816795298393,2,9.14679203104739,40.420992573585714,87.58039411789743,2,26.256622509427146,1,19.687302114786597,2.4599989144117607 +12,29,40,19.68291173,89.75272999,6.594037135,111.2818551,pomegranate,12.737816445976943,1,8.4689973210167,13.924068805665616,384.9787071312857,2.0247917903587362,3,16.481452964060203,6.453921700720411,113.05110286089462,2,17.604713622365814,1,9.965941695605618,1.736492664774103 +0,17,42,23.20242586,91.19442671,6.859840821,109.0946323,pomegranate,10.607368638078409,2,7.824450744898426,8.166221517062999,396.78962817190177,6.950395145057668,2,14.935347197786873,45.763668056646566,74.89969577858119,1,14.496601118974878,3,70.17285506049159,3.0049957297612515 +2,21,44,18.92157197,87.31290342,6.56893406,102.8013275,pomegranate,26.339483590651508,2,10.929224976254497,8.221959908193151,355.32018779994036,9.269256213550257,6,15.750148904222502,55.339318108100635,108.28549828102351,1,8.124372002402808,2,23.338099182564985,1.5362183834227205 +28,6,40,22.10621387,91.34039616,6.769855664,106.8704803,pomegranate,18.54841639924345,3,6.9996712996742225,2.3406667127598335,398.07216830653715,2.434757805727848,3,5.828097621736742,43.690074722144146,78.50648007611984,2,24.17602788764796,3,27.987555544343902,2.669138773880433 +8,23,44,18.47412402,89.68919664,7.130837931,108.4758509,pomegranate,28.376525535257734,2,10.60103736978654,10.38742760956574,389.4663649409971,7.861572589606427,3,13.978932879893112,2.8975525752417797,118.97258350847514,1,38.42402743665473,3,32.233184252300454,2.2966041233209102 +29,16,36,19.81069447,88.92944254,5.740338002,102.860084,pomegranate,29.068625112908336,3,7.1254342516536635,0.8406887632323112,382.093102278883,8.665868569799166,3,9.664440731854631,52.18932540905763,78.51227358684415,2,33.4805760801006,1,84.31627510362809,3.2034352631764222 +17,18,43,24.4880844,90.83687246,5.843005428,103.1969341,pomegranate,16.997505665694476,1,8.786358178221835,6.646637452485431,437.3837976929168,2.7024158948205397,4,9.118294856898508,74.98763731620348,97.23084059358806,2,8.426748307004118,1,27.698265945996514,2.4365727769827767 +34,21,42,18.75927679,89.93457597,6.648687274,111.0196744,pomegranate,16.968321508148083,2,11.19892371271047,6.055952837532312,360.76012639635314,5.408698897826524,4,11.86889178793739,6.949340298689643,106.16638401385879,2,2.119843779269359,2,5.737829186309873,3.806563948343017 +21,23,42,19.54128063,90.29751796,6.902751061,104.3739878,pomegranate,27.213230446197805,3,10.34634338527339,3.4699578687445243,430.23898899386785,2.697424405634942,4,14.461817529043229,99.08578762433316,198.24606166941552,1,24.912561852531578,3,69.3568714144565,1.488973699832692 +25,17,40,18.91251245,87.74938524,6.608023872,111.2800516,pomegranate,19.277553432727444,3,10.455379519800749,19.909062478913597,367.43522021863686,9.273580107102832,5,11.046264832933225,75.951484160126,102.30621896907198,3,15.639574763437986,1,82.93454702234945,4.328754772513261 +8,25,36,19.91330523,94.95031368,6.828522375,104.0277061,pomegranate,19.605703128350104,3,6.23364843631577,3.4504469408929395,396.3132428209828,5.4150591831721995,4,7.416357107502619,66.20191569959634,88.5096312055496,2,17.777664681518505,1,63.863728165609814,4.763350611791864 +26,18,42,19.72620525,89.64934166,6.910374919,108.2287276,pomegranate,21.13809726435067,2,7.451686902975808,14.23040341233432,409.4623218476303,3.184524687448451,4,18.396618151539286,34.08218891391923,171.28128766778366,3,27.03707640099599,3,1.6954239386019476,1.5300638866434206 +4,19,42,23.83185873,87.84034604,6.306605528,111.2232716,pomegranate,26.392065523250196,1,11.16461244479341,12.380545028997679,362.1367930300196,6.182512638054062,5,10.436827130332048,77.31043944604944,191.92938931615672,1,19.48620561188595,1,40.83775354148334,4.981574245277226 +36,24,41,24.94467632,94.25702672,7.009180374,103.8799347,pomegranate,12.673266312138056,3,11.04213085356158,15.34863623382023,370.15837925534436,4.284580359126355,5,13.513503607348419,78.08255510386356,182.88175156487472,2,34.591465818118174,2,54.628725429295265,1.6320339978378238 +5,24,40,24.692258,93.87030088,6.297907579,104.6735454,pomegranate,16.004622811291533,1,5.25695217713196,16.31151716937383,425.2094635588337,9.508041913375557,1,5.4682902558123185,95.7241103523145,74.85659030924579,2,31.00262565966419,3,42.53895010478236,1.4749541756129805 +19,17,39,24.72485577,85.56083187,6.728599215,111.2787584,pomegranate,27.332540299795355,2,10.660257780277465,5.07017514774553,382.2356271411589,8.041345495744707,3,14.060831694863086,97.94910784594936,177.89276826503374,2,43.16793559458734,2,8.510561665648419,4.218148001181021 +39,30,38,20.12644921,87.59629625,6.965156738,108.065579,pomegranate,11.195846847011126,2,5.163682757185528,13.654797832479018,404.00710134170856,1.2457596488975902,4,16.105745475948513,60.22777152888254,173.2837914673207,1,25.861313796731316,1,28.010829549208992,1.1193121968793514 +5,29,44,21.02432943,93.0569505,5.578095745,104.7847006,pomegranate,15.817099084796723,3,8.648771053138201,16.246556136034844,363.5694695561259,6.70681604919974,1,7.264586235692451,34.88139033110579,163.19587042124698,1,3.343288758961638,1,40.09420152014509,1.388146564628066 +4,24,43,22.40423537,88.1508343,7.199504273,109.8695196,pomegranate,12.88791272173219,1,5.655148991198279,6.615044793374394,350.39816964245364,1.4591231395037292,3,5.084484877617874,21.898949829189117,191.56001905721564,1,3.9417950763689538,1,25.488753888616433,4.850005903892116 +38,21,35,20.33691147,89.38003827,5.841367187,110.9653137,pomegranate,21.02573587462075,3,8.430523937575625,17.196682550178167,401.64241475363696,2.41081046728446,2,17.19873043012587,83.18572594263303,143.66925639260648,3,27.065267197973707,2,26.82033907290321,4.501351571183664 +37,11,36,24.24779615,85.56033312,6.710143266,106.9216033,pomegranate,19.65705878605352,1,8.560609316815263,8.27440938467526,368.51337763714287,8.14828106310998,2,6.694865066391495,38.262427115976806,160.510896792121,2,33.68967707029298,2,36.17516052530153,4.693488953455045 +9,25,41,24.81530144,91.90842992,5.972714857,109.2853418,pomegranate,22.060361526689416,2,7.356054698209346,10.162682657682517,369.3762766719512,8.223448560837326,1,14.95329352497929,58.45779471096733,91.77330336082127,1,31.749801819655083,3,21.035330176216526,4.44599635007159 +29,22,43,19.66329768,87.95158129,5.561851831,106.0380805,pomegranate,29.19763352450463,1,5.264431242061461,8.323869112918743,408.0781043227347,4.195284082618885,3,7.400757690572969,66.8140846231147,83.04145140397361,2,21.06778293898841,2,61.06000211952305,3.5623000204991913 +5,21,38,22.43377991,90.3396556,6.107054808,112.459697,pomegranate,16.154835652418242,3,6.936039666888001,2.52393210400929,421.9432921767643,7.251225433788187,3,13.037458987800013,60.990876333896196,143.7552328149077,2,17.606156744293166,2,40.96530341642731,1.9114200062269084 +22,26,38,22.92052307,85.12912161,6.988035315,110.2437841,pomegranate,19.36511424713501,1,7.511500458718205,15.384845287667591,410.94528279095033,5.863683741506208,3,7.193982875010971,56.39115145938959,122.6251744623977,1,42.83504395917244,1,0.6711863464431533,1.6575200647197739 +4,18,37,22.91843172,85.40695044,7.13147457,106.2817706,pomegranate,29.614070801998576,2,5.790408853180449,18.0672892283866,449.3679214774031,2.621242218992858,3,16.42105576408848,98.93380607385063,169.74581834789421,1,15.959487781162801,1,99.03723392530051,4.807359963877918 +21,6,41,24.88244467,89.39686219,7.086947687,107.1951707,pomegranate,20.976618138030744,3,9.69337756637413,14.837554144064136,423.1374788575144,2.0598431181630055,4,19.58063400694489,95.15637861517483,142.360576497977,3,30.207205190688246,3,22.684571074479464,4.314911993790776 +29,21,45,23.40981539,93.13277,6.749260456,105.2240743,pomegranate,15.94913001881664,1,5.174529735836245,9.564344165850613,420.701310276496,5.431874500655034,3,11.509850948424722,53.31855314689234,165.72081081707876,3,36.497562598657076,2,20.031144033719407,2.350646622702507 +23,5,44,21.20725375,94.26304717,7.16300467,107.5660804,pomegranate,28.910970975883966,2,6.502625260398327,10.785676658922046,444.6682499234743,6.4337634759114035,4,5.9270930847093,26.626827961338982,79.84532811480098,2,10.709648306397796,3,13.09087172735033,4.596748480002578 +13,7,43,18.20230419,91.12282162,7.013481515,109.6623974,pomegranate,28.24014920359668,3,8.9506654612049,19.82310106507206,386.23734884669756,9.8347622556956,6,14.60904638505167,2.5936299700129006,199.38919346177227,3,29.338067546017893,1,28.92887181123228,2.5198454263672923 +5,13,37,22.34375696,89.7870345,5.648243649,103.3183074,pomegranate,11.442336684911844,1,7.3601797071140425,10.427818577162293,353.53652362048416,4.927833957471779,5,9.258559204402381,99.94570244673973,159.78335427298515,2,15.502997018505093,3,81.73559359754438,4.724939119339368 +27,24,41,24.32770134,90.88292835,6.610251186,110.4606459,pomegranate,20.375739681026285,2,11.563781374089327,6.437053249595399,369.51112145817143,4.9618986618781795,6,19.478730822827657,1.488883089532167,131.12639719611798,1,44.024537937792246,2,98.4608641902676,2.8839897959638328 +7,23,35,19.75088482,88.71691157,7.054313823,102.5538035,pomegranate,18.843079670742284,1,11.092604125141976,6.947505305185954,396.9251895746729,8.942793901246446,5,9.662298263096144,88.90534364943925,123.44943016901335,1,43.715078787708116,3,75.40749005777026,3.0558927471892328 +12,20,39,19.86173586,86.19740917,6.026999326,111.0217929,pomegranate,19.679920824644235,2,6.3233267358718965,16.442019652120322,405.1330777653061,6.858904794367301,3,10.278835704867513,92.28329676089052,161.2377189490994,3,12.681232810623777,3,31.95788225627343,2.4113815120326536 +4,19,43,18.07132963,93.14554876,5.779427402,106.3602023,pomegranate,26.15873615091607,1,10.319647646111868,2.373590925495974,360.78748704074684,8.229213652997597,4,17.856948997319897,18.658139911746517,186.13765990389976,2,8.689984493995805,2,29.307550303091812,3.6502765520118534 +3,9,45,23.89162561,89.61850203,6.535244251,104.617522,pomegranate,19.872746998326967,1,9.61421688801782,11.418204127586389,419.27810595569315,3.3321579655159055,3,9.155164483401924,29.47993716060585,176.2155671139116,1,28.69233807347351,1,58.0879042749414,2.7476449977089237 +1,27,36,23.98598756,93.34236582,5.684995235,104.991282,pomegranate,28.65626092875943,2,9.142501828732783,15.391525782167086,437.9228767298965,9.924951777926776,4,13.773738903765036,6.171702411391133,63.98318110736047,1,46.047228006244794,1,27.973358277861593,3.0338913306873785 +23,30,44,20.93892916,85.42912869,6.12476108,103.0295938,pomegranate,25.649589032234665,3,5.811108729394996,7.604685167603586,421.5269151472393,1.9383719004123459,1,16.344591827856142,35.52647463854295,75.75725763658251,3,24.922898993069055,2,59.02002762709249,4.064790866794923 +24,21,42,20.82210727,87.22815682,6.999014379,109.4429934,pomegranate,29.851948868154214,1,6.279204436884408,6.41919561159029,424.061301017349,2.7561361267997526,1,19.984020697709134,38.81481129230203,104.46759744959553,2,10.61193878653674,1,65.97583651562601,2.6789792180585374 +13,30,37,20.86474944,91.61793636,6.277148771,106.8685636,pomegranate,25.6333268407937,2,6.101170766980701,11.59386730442649,372.94096849107393,9.223834826876638,5,8.350423748825772,5.4761833915802915,53.2835793139323,1,13.200756384985768,1,75.29389404931351,1.2112763199817511 +40,11,44,24.45840036,86.10874614,6.322396027,111.3779693,pomegranate,29.707505847967465,2,11.528118763165931,19.883380790228696,419.79513875794976,6.831286017551865,1,5.070293203318788,90.50050298716059,72.53311159524988,2,32.52537313293238,1,33.427486907500025,1.0431216299534607 +21,9,40,24.51147697,90.64498715,5.956401828,105.6209954,pomegranate,22.522662700012916,2,5.2520025247478275,12.8178881100399,362.59605666334664,2.074794672021953,2,16.736078169058114,84.84877789414753,192.859235646241,3,42.62932595995023,1,6.60456410189939,1.502133853096094 +3,27,44,24.56811204,92.03009222,6.591302797,110.9633894,pomegranate,25.945411146318563,3,5.154807859453156,14.12323121660728,371.3988125150133,9.839765672676334,5,17.25378196566404,95.56669581782653,191.37822611046167,2,43.14189227355051,3,87.0300022819573,1.1810451494905632 +40,29,42,24.63228709,89.01574455,7.104094929,110.6956184,pomegranate,29.04661627663448,3,6.579925721150853,18.260451055040406,391.40359502420404,2.537508284503658,5,5.273845335687605,8.604832777843407,62.00159685734603,1,10.010534401084842,1,53.09516717409305,1.106465328121585 +14,25,40,20.07386547,90.97819712,6.407872061,103.7084055,pomegranate,21.925453917561953,2,11.12948331431135,3.6382443681219723,429.83778197385993,2.966346475883461,3,11.109942443819286,24.673872986830002,148.7010535358363,1,10.220379410290198,1,87.65264422201172,1.922033703874702 +38,14,37,21.80523051,94.63612858,6.658402594,102.6488846,pomegranate,18.328247890624652,1,6.840531290274212,6.236316021398888,376.9000822177975,6.064131615259199,1,9.089793102028162,1.8523847945511651,172.94818176868122,1,26.444989842964727,3,14.78670304904821,2.3906918821461205 +34,9,36,22.8122645,86.34233767,6.276038961,110.4432293,pomegranate,12.558982611490364,3,9.578071246714503,15.401444041233802,378.19356445309614,6.930698556045506,6,18.92551932826589,43.84489119090678,144.29637134442208,1,36.28078659687494,3,26.204863864523954,1.6120979025998525 +32,14,37,22.73031253,88.48567856,6.825256236,104.6843243,pomegranate,17.099713366928857,2,8.62107815709467,1.2867164656501662,412.2146229615919,1.5786048400228232,1,18.04950857157175,22.340219290259878,146.48202857459376,2,2.2960246088567504,1,69.49096922280833,1.8356024922443286 +18,21,35,23.2801227,94.94330457,6.368560522,111.1382096,pomegranate,22.5984525465593,2,6.971033442504762,14.012332504150828,430.8181701393216,1.3804447930054118,4,18.229010346185262,73.96699686754609,89.53170937255086,2,33.81461205325007,1,23.24873575888352,1.743232615753267 +8,23,38,19.30106297,87.1775172,7.005410734,105.4766591,pomegranate,24.990104035192857,3,10.614046595086132,9.936062852646259,432.0440730311121,9.354324482009925,3,15.65937814162079,82.40748785389783,191.92344392817174,1,19.018081369481166,2,82.0986942080776,3.581248272750314 +15,6,41,19.0087067,88.83768149,6.897368477,108.6793978,pomegranate,14.519284558567707,3,6.449944713407401,8.595677737237118,360.84384279381334,1.061488285898968,4,16.16790636602586,23.849539716591273,184.11407044715554,1,14.432614971069702,1,63.76780162940261,2.8009396727579086 +0,5,36,24.35193812,90.88612388,6.152906502,105.529185,pomegranate,11.56419431173771,2,9.735640518588152,7.702485639858178,371.1079938008926,4.267526339095509,6,7.629371158931173,69.02426740678733,179.47937353077538,1,43.94395777445992,1,45.841605737953884,3.2376751878933607 +22,9,44,24.72235539,88.87651295,5.744361602,112.1926517,pomegranate,10.792855126483271,2,6.7016076185346805,12.076782215101051,358.3628533565575,3.6813012731424504,4,7.33727334924099,75.30656884785947,66.37212078439187,1,43.065202766816974,2,48.062903328967444,1.6630433761270407 +14,8,43,21.92513945,94.46485312,7.051654924,111.7162016,pomegranate,24.560330833216412,2,5.449703854999377,15.00652577093619,425.03480919866854,1.3646291908135129,4,13.71347913221117,26.847734468288476,123.85575672362899,3,48.51869626951365,2,45.05589178223855,2.646806424163198 +31,11,45,24.83954414,86.88738076,6.034612928,107.6435771,pomegranate,15.581675035547706,2,11.135635758447401,1.5401783753993614,359.8985605479082,6.178929217705724,3,18.803502326636277,39.74296817049174,96.04580067095773,1,0.7264491056705347,1,73.21625426649929,2.3269298276801593 +39,17,45,18.09691127,90.42177379,6.924490731,104.88189,pomegranate,24.7255773805313,2,9.713818943400861,14.599876188340502,437.75714463324897,3.1239727335283933,6,19.184757463800903,21.280650759763,87.47214750948181,3,30.054004296764074,1,86.03219848576765,1.0626256928772828 +10,5,42,20.24104904,91.08706822,6.887005997,109.2537734,pomegranate,22.169247906066875,3,8.149083032265233,3.815287447205351,434.81812933671006,7.191391591935223,2,18.330253058217046,59.5555455410242,137.22047165016235,1,44.43138353018881,3,28.534513479081813,2.5808837098755864 +8,28,38,23.22594,94.42971362,6.8444019,105.6917856,pomegranate,24.305979237070282,1,5.736067602335522,3.022388521648167,422.4382097037606,4.3780414407331065,4,5.7868017740297635,80.90943605466082,155.75860658256528,1,32.964133183880286,2,89.90464182807905,2.388040277390816 +32,13,42,23.50128217,92.97527546,5.786058032,106.61905,pomegranate,29.978702331721447,1,6.406945447321342,19.702727270258883,363.09522458065027,2.941953890435432,2,11.002591840545193,43.71246986512895,193.90270283857663,3,23.806421990245052,2,98.4758512044287,2.438682411825818 +18,9,40,19.44623085,89.02127045,5.627186257,106.1606833,pomegranate,24.11949009499051,2,9.498764379065737,14.043222586820189,416.4018595298999,9.769024484976974,4,19.81844174298983,95.71343617191745,154.53103676689994,1,9.236759992220255,2,68.34581561351727,3.667144303701168 +20,27,41,20.51343484,92.51675903,5.700088663,110.5764023,pomegranate,28.944356438977977,3,10.033740227501823,18.859839558296446,404.80343824147894,5.827118590714913,5,10.650083990046834,13.048374516159356,198.73414152438573,1,16.86816936963768,1,46.52652613722735,4.3467356422876975 +39,25,36,18.90223032,94.99897537,5.567805185,107.6103211,pomegranate,10.925763227969833,3,10.709548863952525,18.341960538106875,424.716501150514,4.598171295251497,4,14.300004655362109,13.731712070600043,175.19968712731446,1,15.660287801545314,1,26.107885887734874,3.354237223619951 +20,7,45,18.90592319,89.24126808,6.077886012,112.4750941,pomegranate,26.91403297348575,1,11.264828453137671,10.455824425120461,445.9992597473871,5.020543554857289,1,5.877847599425198,38.36357473227289,164.7096716691027,3,46.36573660092475,3,69.07568175678655,3.4326289238361802 +11,10,45,22.63045168,88.45577158,6.397995609,109.0357597,pomegranate,24.16718845143553,3,6.3518987252534975,4.214766994323103,379.952452126036,4.090074620843957,6,18.50155519787145,51.78617405301475,84.75000582500161,3,16.531062631179605,3,41.536774673184816,2.0631736456242393 +40,18,43,19.38603815,86.79058496,5.767372539,109.9130984,pomegranate,26.693873208227995,1,10.568684008478677,6.502643765512854,423.19564172804564,2.980341530813273,2,11.585158774892342,34.83612659692766,81.73447500534702,1,10.379766122699808,1,80.17128038911436,1.862196183757928 +3,26,39,24.38318965,91.19431555,7.079973241,103.6012114,pomegranate,17.852454053949444,1,10.093531912900424,10.297795128371925,441.2744866745445,1.8357442620812883,2,5.291855032584359,92.79665883219313,186.70206817100558,2,24.922197984666028,1,85.13783650658317,2.9856169769062486 +9,16,36,23.77989026,92.93386903,5.893332378,106.977723,pomegranate,19.468467810458545,1,6.937846510971145,15.633098516806479,376.352780540804,8.533257433131435,3,12.102652069067265,40.75739482899292,166.95904670957788,2,26.500729956660628,2,63.50618664784429,4.929816009450487 +30,20,38,22.59890174,93.16343942,7.058222596,110.0932899,pomegranate,28.200468303177807,2,7.949219313735513,4.478250958611425,360.5996920027907,7.962657276686905,4,10.119523525457872,36.81586793492953,90.98925686543431,2,17.192061472415315,3,37.970707119990564,2.3206819533459075 +40,9,41,24.37766782,85.4017118,5.78270695,106.128338,pomegranate,10.429540377920024,3,10.01602388609432,15.993900320416385,389.2446168753697,8.2230605000939,3,13.444873018602962,62.96852601184318,91.48132388761478,3,43.519935225455704,2,37.741890848545,2.8277917527110135 +40,30,35,20.89273273,91.07776977,6.269663963,104.4407083,pomegranate,11.45359823331303,2,9.448771812000277,15.85535137982409,354.81145772349373,2.2243540688799346,4,9.420084035140118,80.87369167918159,114.81555461561844,1,24.599623835710478,2,25.33262373780517,3.6811299113879272 +32,25,35,18.09903225,85.70786282,5.892913826,107.0050976,pomegranate,12.358802990493395,3,5.3371336555318605,9.115429650457536,375.7433179226366,6.693698181731581,4,12.597770960172092,33.57254711309762,189.50176376909363,1,2.9083262135413293,1,39.3054086431507,3.8381575107869 +33,23,45,20.00218987,85.83618191,7.116538883,112.337046,pomegranate,28.83400218978235,3,11.378909982042412,18.742899809578304,405.89842378246186,5.599919189589377,1,14.536901818208296,24.445940854869875,77.79460668512309,2,10.76132110859755,1,44.505996897925485,4.330322544034911 +4,14,41,19.85139326,89.80732335,6.430163481,102.8186358,pomegranate,20.662950993546588,2,7.037216999939602,16.196263854123487,401.4095761669354,4.483171717945397,2,17.173627332291556,82.52167444268808,161.20814345370042,2,4.452168951636715,3,67.85394957840779,2.098904310554943 +13,17,45,21.25433607,92.65058936,7.159520979,106.2784673,pomegranate,24.475654701845393,3,5.7372457146868925,11.839194211757407,408.2012911348687,3.568954441082209,3,13.761880164848712,54.27662274503401,188.88897787321326,2,17.98378726176476,2,68.0414926115095,2.6553980712940715 +39,24,39,23.65374106,93.32657504,6.431265737,109.8076178,pomegranate,13.01299691329733,2,5.6967943680021165,19.012762426657048,441.0813902074959,4.458731055602319,5,19.78620215228434,29.67879324016599,152.9955973904517,1,0.9862809751114143,1,62.47553565117836,1.9396273488883122 +8,28,37,23.88404783,86.20613842,6.082571701,108.3121789,pomegranate,28.495770395351386,2,10.837918687585873,7.216049492131933,421.99608322961467,6.21325366863047,2,11.227101569907948,41.993814912312544,92.79412051831957,2,16.536691195498797,3,35.62414988897257,2.127919890949294 +91,94,46,29.36792366,76.24900101,6.149934034,92.82840911,banana,12.926622977414898,3,5.0406616673263125,13.003000836200593,420.34157412462525,5.333925296728927,3,19.920234449727026,51.08689910025191,137.38599652401876,2,23.36132557756145,2,59.483626823407334,4.792121736179208 +105,95,50,27.33368994,83.67675197,5.849076099,101.0494791,banana,23.035940691958224,2,10.569577736800543,0.8001305000049364,430.1163170600121,7.271328271850457,1,7.144636176223656,73.3728708044459,77.51230591761626,1,31.90308866982247,3,15.533399106499402,1.3234981798957648 +108,92,53,27.40053601,82.96221306,6.276800323,104.9378,banana,12.524361694819397,1,9.881711108679161,15.012190790508622,431.56556248737644,2.38034516432312,4,12.757510911841779,3.8580496404138853,129.51005311597203,2,47.3800394053659,1,86.85914379132925,4.25626916327424 +86,76,54,29.3159075,80.11585705,5.926824754,90.10978128,banana,22.513203908267013,3,5.188812393683553,16.73651997504425,408.5252416814535,7.918439594485177,2,13.951293198325324,61.00939867666849,187.5306309203067,3,0.6973995760514939,2,5.0265539995445145,4.64182116701836 +80,77,49,26.05433004,79.39654531,5.519088423,113.2297373,banana,29.777288344130767,3,7.790229825246227,18.39579482003864,388.424864898947,7.241953737271924,6,5.813607725040022,71.36484852226477,97.705825018495,1,2.4031058474914877,1,77.2293203446676,3.0408959428707867 +93,94,53,25.86632408,84.42379269,6.079178788,114.5357503,banana,16.775126349262365,2,7.070961032190277,14.857725675668416,414.9204459945005,7.386698908572576,6,18.52970350683104,87.48582567366311,94.29559554033244,3,30.68086425097022,2,3.4519973331019527,1.374842861209046 +90,92,55,27.00932084,80.18546798,6.13465588,97.32531705,banana,18.149825091205173,2,5.619849328226572,8.409839638544403,389.3993462924736,1.272330613332756,2,11.73113688063436,4.589745420585711,110.69171816953703,1,18.49202368582154,3,72.24625534452422,2.249791116913073 +108,89,53,29.55054817,78.06762846,5.808497604,99.34482238,banana,10.052477463262905,3,5.709020799271084,13.446311241670351,394.9186016581508,9.408521934996944,6,16.106318931704585,64.22810995738644,89.61214012869137,3,2.4508080296692047,3,61.18810570261915,2.9567270958826595 +108,88,55,26.28845991,83.390039,5.891458107,113.8729798,banana,13.938658325440155,2,6.202810781836392,17.513247589983102,350.7000511818338,5.116649990091593,5,12.69127655699791,62.566772860138386,127.77659063440505,3,23.25453909578706,1,48.78311561511849,1.889993430885759 +105,77,52,29.16226551,76.16151562,5.816622479,100.0075679,banana,11.707138610625297,2,10.162597273699845,6.358750306238625,406.59845745360366,4.384394726502066,1,15.536159990450109,70.29119766505491,51.11617165795382,2,19.017799687761368,1,21.61345939390672,2.7955586937937813 +118,88,52,28.65003945,82.68752542,5.843163161,98.75084366,banana,13.357367325075874,3,11.51857078403277,19.991293973622305,388.4971371790509,4.633586885293936,6,14.419306349821642,61.71534225074568,197.21994231502353,1,3.5278297409456263,2,59.903897466094705,3.2776381566383743 +101,87,54,29.07311132,76.50045221,6.376756633,100.1692639,banana,24.934679426813197,1,9.31215859929856,1.6604839494408052,388.27358624603266,7.463935004455667,1,19.3983908371509,43.749918756964554,177.8561845062281,1,16.370637088146157,1,21.46145844514107,3.7561652648311794 +95,75,50,28.08166093,75.26429821,5.623615687,118.2761894,banana,16.534598306057312,1,9.463407442063787,5.4011667664317,435.9291904710045,2.399819997925447,2,17.95836540713945,60.92001050806009,123.60563924023643,3,13.675491050790223,3,34.1684314009776,3.8345997594243864 +106,85,53,27.1994597,78.8086068,5.91505509,99.72430835,banana,25.53862575487019,3,9.596316490921488,11.00243255962782,421.13145643211203,1.6891462610302876,1,14.51239382164546,32.34660641571684,58.587838804379984,2,14.1150550402927,3,20.756182841633407,2.191669318183087 +86,95,49,28.05484146,78.04602887,6.458714879,108.3957179,banana,11.7319678187691,3,6.926496409845076,12.013230472741533,403.56956437933445,9.439411608783812,6,13.570464146983959,10.058564514541835,51.753279586206,3,33.68659979188914,1,74.13086855297291,3.877184822707213 +83,79,55,25.14748006,83.34688193,5.565028635,98.66679427,banana,14.839741894819547,3,8.229272115930936,8.447847791888147,398.40373300846505,5.865943543700374,4,12.451451190437556,33.02576539189007,89.3992975284932,1,32.199225982384434,1,96.26877197099833,2.7865279125394364 +85,95,47,25.94019018,78.3422098,6.211833161,119.84797,banana,27.518603511688376,3,6.531625356232604,2.425486443119098,407.34526179314685,7.459031045433743,1,12.322578874135328,69.3040563424938,153.76497951273336,2,12.708021714042022,1,62.55921928632674,4.725838354795833 +109,79,45,27.66752761,79.68542782,6.490074429,108.66464,banana,15.314945120564765,1,10.389157922342278,1.1702085374226012,398.4342604765606,2.4866857239335314,3,17.098896523829143,35.15057049279255,65.83655439898922,2,41.17340335222284,2,89.90241919427632,2.3182148208546227 +100,76,45,25.56703012,75.94067692,5.590236025,102.7867717,banana,28.759852898029557,3,7.151245689840065,15.539511953758305,388.72366135996816,5.5022715245822615,5,16.459640461889858,93.74981890355659,145.9956272682502,1,23.712924510219572,1,35.785934383999965,3.137749568514842 +117,86,48,28.6956201,82.54195839,6.225225239,116.1616839,banana,15.90121818968138,3,8.10181375136421,18.493177746157844,382.4990238811133,9.418732172400192,2,17.009024862016155,16.464097955123457,142.8194488043616,3,32.97119291197221,3,20.171545617663277,3.887821955847501 +114,94,53,26.33544853,76.8532006,6.190757459,118.6858263,banana,23.49247275808871,3,6.042544369567898,10.411307597505461,399.3878558253665,1.0421816862274453,6,14.08824937603403,53.533726878739984,122.57117304040804,3,4.4529646545517725,2,10.03879291701294,2.8998168638567923 +110,78,50,25.93730186,78.89864446,5.915568968,98.21747528,banana,29.30871124087917,3,10.236894497591763,14.84169747055332,381.8517062073429,2.7209767054791123,6,19.405844805993553,46.255193140033846,189.58415085726557,2,7.528830971671247,1,17.976945599728676,4.6141049698722085 +94,70,48,25.13686519,84.88394407,6.195152442,91.46442491,banana,20.718189891761554,3,5.4039778673392345,4.439165022842795,419.028804761657,9.99243875952363,3,11.777634328395207,6.000846969202611,60.27905269154682,1,6.628054492140628,2,6.261926757082536,2.1307500858274655 +80,71,47,27.50527651,80.79783998,6.156373499,105.0776992,banana,29.88301411162114,2,9.155012521456666,15.213543306289587,370.8265777936193,8.60510468170322,2,5.960392000092261,86.77981987178508,170.48394930956465,1,20.251109855632322,1,84.30355687720149,1.6142163582606246 +114,79,51,26.21009246,82.34429458,6.313197204,112.0700033,banana,18.23010693960116,3,8.001007690797934,2.2972149025593036,405.8521747583883,2.7849340760724783,5,6.201233702386217,25.72708576135403,161.4683243095217,1,44.45908410887545,3,66.68340147618859,4.784206755930409 +88,78,45,29.10403455,79.19588629,6.324270089,92.07835761,banana,18.730584097929267,1,9.515198722166835,14.707289994626322,408.587384129495,9.159651491370374,1,9.679122862166155,85.42152823409276,67.18167385925389,1,46.21744778931283,2,64.8955726943374,1.2313007616153961 +112,73,48,29.2440638,77.32017166,5.707488987,90.66727868,banana,10.236418935932338,1,11.06332737021467,14.93096207982888,367.80701326673454,8.188351286579607,4,11.348316403484628,99.1353612383801,138.78509223918883,3,39.41541846436922,1,40.61837706274816,4.016287638450098 +117,76,47,25.56202173,77.38229006,6.119216009,93.10247183,banana,20.533254436551665,2,9.366870371875624,14.79246763838754,382.0212936727345,7.048745204077591,6,5.940711317773674,78.25916555892715,104.97742665561066,1,19.50374793132465,1,32.21590855996318,3.619073085882926 +111,87,48,26.3985515,81.36028902,5.571401169,98.16752001,banana,13.1171819705369,2,11.66973852884568,9.358851369750337,359.7621466453938,4.073651432951074,1,14.675004970702995,33.32442009921718,58.29583581205883,1,26.377474033021205,1,64.45809371881758,2.276637738972317 +89,83,47,28.09577643,77.79586769,5.63127166,109.5408614,banana,22.404137971812943,3,11.162966421969886,14.042622902248658,424.3621921058967,1.928670127110461,4,19.948179060053484,93.89194979994564,155.75930047011695,3,8.741243458732907,1,46.83597984205164,3.4428619953049786 +93,91,47,27.84767901,83.31110751,6.101241579,117.2878912,banana,27.55667555080722,2,9.65186092426331,6.228119792540808,360.79649312961635,9.652687966776272,3,11.196988522390381,60.12708950158849,69.02751453205184,1,9.720119190201581,2,37.89242378369736,4.870448705635736 +92,81,52,27.39341554,81.4654833,6.438137279,94.31102057,banana,19.94864011567039,1,8.497703567129708,6.65699537058927,381.46199320459,1.7404658169429401,1,8.17947994116069,73.39638903916502,120.36272247815921,2,27.729916219222627,3,96.15110465454121,4.450381961402927 +105,74,45,25.14517635,81.38204104,6.098369122,119.218154,banana,14.208862521680636,2,10.437493087241696,0.17753555078378946,393.4598923649828,3.600119913862779,6,11.18422785676651,36.49148742374373,70.36439034657317,1,4.038049027266882,1,97.68876639385662,3.5016801292888635 +102,71,48,28.65456263,79.28693687,5.695267822,102.4633775,banana,26.47893498336275,3,11.157198458354944,13.065167305790448,394.87538767194013,1.3401273681365597,2,15.625517541363694,47.62165825902667,188.24374717444857,2,40.58674828785608,3,13.157158428654192,1.767977344317866 +94,91,51,29.16093424,76.67484233,5.618094446,109.575944,banana,16.34199929243283,2,9.513756849931628,8.036963188455942,352.4826186466844,8.67295390140297,2,16.041854748545262,8.131603863538173,157.16962318939147,2,44.59125466288734,1,16.215571792178185,4.255247128476416 +116,71,47,27.57278064,82.0638878,6.435785799,91.34276507,banana,19.183170637023395,2,11.852868351514662,1.5064254716894188,390.93297037384815,8.68193854197115,6,7.607388011577837,14.708915813658418,136.55047740249117,2,7.342904567684727,3,60.95080721214987,4.955820722219015 +117,79,49,25.40909896,82.36208097,6.176644228,112.9794804,banana,28.66033763504648,3,10.11195673530127,14.839807680278717,437.9486793716481,7.240395766392195,2,7.927144611983932,48.36269347040309,52.61145259552561,2,5.854667226981047,1,36.12329597223955,2.45265234721584 +119,72,55,25.99069521,83.33983116,6.220643671,112.0777152,banana,24.81120905960601,2,10.473805003358208,0.17118767070442242,374.5830735355215,7.011804520697462,4,9.829334079120807,79.25996443420644,111.32606950802665,2,9.71499517998351,3,83.81268817415865,1.1991542277074045 +99,73,53,26.29039046,81.06003778,5.871702211,118.6730366,banana,12.082360233995129,3,9.685712911894925,0.8883129337517204,410.9714503350251,6.992504467903647,5,7.808888292184088,90.97542950929282,146.9055825803878,1,34.10184531493009,1,19.406438509940806,3.42009562238864 +91,84,52,29.14827211,78.71024836,6.390741836,117.536781,banana,28.197370529551716,3,8.609898406463024,1.3321022219409606,353.03623323103847,5.542140943988395,1,19.049409467205116,89.94779957026088,108.0102982610089,1,1.9967054616352842,2,79.00648719562187,1.9192940643557752 +80,90,47,26.59743595,79.35898915,6.21084479,107.3944717,banana,11.506309430984572,1,10.026817744631089,16.736676140716305,394.4843365063117,9.818073923291145,1,7.990033712528942,44.371007636262995,189.198798563846,2,0.5622011977599706,3,92.42429639209494,1.2423241106596983 +101,70,48,25.36059237,75.03193255,6.012696655,116.5531455,banana,15.530924907457651,2,9.19425107938234,2.822569042629799,427.37233879319604,1.9495754101805653,5,10.188852586051226,27.641614193290287,133.66784955192446,2,19.044473974260757,3,72.775425717233,3.3041794434877234 +108,89,53,29.12036889,80.18080728,5.908770059,112.3982055,banana,23.119344861006976,1,8.968669023837858,19.4954167796932,399.42807084178395,9.2362819167225,1,12.901696061055818,10.103690434488621,82.37363432397902,1,24.834122760153264,3,67.89817123217507,3.536399676784218 +100,80,52,27.53911354,77.25629897,6.049801781,110.3262123,banana,25.524463437340614,2,9.505243056648847,5.505448399810198,379.26589962721647,9.93485357605481,1,6.11973546419548,42.7029233793639,126.96855250845971,1,44.20209350674671,3,85.23222682577698,1.928106238332992 +109,91,53,29.66727337,83.51014178,6.010095853,110.2511102,banana,15.794511260178512,1,7.274274653398306,17.80426077963508,368.2652385737153,5.703315712445146,5,10.472275735486088,23.414214163657853,164.87351229655803,2,27.622077241005545,3,33.01715348727906,2.5218952267946086 +82,78,46,25.05802193,84.97323747,5.738678895,110.4408803,banana,25.338599941106793,1,11.716617080477041,2.2904373355138308,442.9742594775492,7.9696037959651,6,17.697653333120083,66.45150425361285,125.26639085731209,2,4.65409925376663,3,38.85078133130989,1.1616141766776957 +106,70,55,25.86824781,78.52399914,5.74055541,116.3019555,banana,22.320775102816395,2,11.572974056296742,2.217029291420216,372.5952800163124,5.737878708329387,2,11.82165254491427,54.04695648205616,66.63436130821667,1,12.05824978234838,1,15.965303558043864,3.528893445857993 +90,86,52,25.85036988,81.95580471,5.793260262,119.0856171,banana,14.378057037948288,3,11.045736040525444,16.382971250404317,363.50603963494933,9.757099236524025,3,11.390763729836642,96.80722519006736,188.4896958564439,1,44.2026588415261,1,3.8610374475257614,4.194544600126358 +83,95,50,26.51682337,77.79913575,5.50947065,108.8547508,banana,28.194907437768567,3,7.240389374499693,1.3032657141797244,387.7821770451483,9.44856613376591,6,6.545454307196434,43.10119809759644,144.91924297014918,2,9.534724133943529,3,27.266663957955462,1.9720388412483474 +119,90,48,28.66725136,79.59242542,5.986442306,118.2583441,banana,29.75237262700032,2,9.012164867989878,2.8610418396072324,362.0648898410088,1.5530257142303436,4,13.073921718709265,71.37334549172665,83.13118127617372,2,23.74337882510828,1,12.587226817870501,1.9430193718097173 +107,72,45,28.14938935,81.54448882,5.790768046,91.40508414,banana,25.708471739896826,3,11.176903458383743,9.275402661681799,423.0544034841603,7.091390424607422,3,15.91580939596522,73.02743129535219,191.49752176821633,1,11.407584172130548,2,27.265259587985636,1.1648958556263516 +116,81,55,26.42313317,83.6995044,5.915546415,95.12322062,banana,11.295995627972912,1,7.012549120672988,15.689063888491654,354.81605892779373,7.999526207771023,4,6.012772304163915,92.99352217404262,180.70842699158234,3,48.19911711989498,3,52.85291915538481,3.9393246604491825 +101,75,50,26.59386409,81.40740301,6.242528278,109.9825551,banana,24.416039912164425,1,7.721136906644496,2.8422454232988126,441.5562016554199,2.1360334567069508,5,16.791209497467776,67.76734051558391,94.91751510641296,2,17.773724028664127,2,56.77409542686293,3.9610903428076347 +93,81,50,27.71822477,76.57853189,6.036079266,102.2099836,banana,25.781208025339247,3,7.65146830848793,10.577300247022052,399.67973196761625,2.218286163969639,6,12.006145190471608,36.56025071736326,54.920615001959334,1,22.664930959984776,3,58.60590473033106,2.670371141188816 +95,75,45,28.98333432,82.95958244,5.829898502,109.022564,banana,21.272419181042864,2,11.989832995090515,7.004003638388669,367.162042665388,4.144689014658733,2,5.616544199771487,5.942648096659675,110.606988854816,1,42.57072930114567,1,60.972056547494645,4.751879562298598 +107,71,55,29.42017919,83.96754496,6.088064451,117.229079,banana,20.61753669055696,2,6.803161315606813,6.168546217532997,371.63252826367625,6.747449450188395,4,11.276854747987482,63.57346960176522,183.9641309870998,2,0.581889380204198,2,6.9353102569503555,3.3234091023459347 +83,94,47,27.39872329,81.10523402,6.469370954,112.1355384,banana,28.52796280991322,1,8.918789970563157,13.075805259333881,424.55211942120786,3.1621610283400194,3,15.675761511243179,56.634505921721654,142.2998374544171,3,26.55786463052841,1,9.015567455702156,3.963342736401389 +102,73,54,26.4020227,84.41007614,5.720726906,111.0162259,banana,17.87408834951183,3,11.796320158543242,8.823167204814848,380.06526238636144,3.860645576018089,4,11.065346387414639,46.96743547423195,94.89607315602036,3,0.9989367787162362,1,1.8055876548962901,2.619658795498201 +86,79,45,27.81251452,82.69285419,5.80766417,99.20961514,banana,18.857476570663636,3,9.866232992109666,9.282577545118096,429.34007672388157,7.276744767592439,2,14.038757827961106,35.929992422073965,54.34566149308729,2,40.595627058758346,3,33.86236931876241,4.290405313809866 +117,86,53,25.19640218,83.55829874,5.703381728,115.8586081,banana,28.325902456647423,1,7.199617397932129,6.013541915655642,407.0695846633312,9.626488030478281,2,6.16603146531418,29.08195864131342,75.60097227870389,2,10.439229790470478,1,35.134174584043954,3.4410718154910986 +111,79,53,28.31193338,75.77363772,6.165001278,119.695765,banana,12.565318535077452,3,9.114565337533465,18.21709815672202,401.88530786264056,3.5428000871505034,5,10.980951553966701,36.841457540188905,190.8154419389171,2,5.77573688980903,1,59.34889982136383,2.8477146321339237 +95,74,50,25.90113128,80.47152737,6.002481605,110.10323,banana,12.140492059410413,1,6.307752498523949,18.48856976330031,375.25517488682283,1.6912936629694861,4,11.127168251244033,50.40849678858177,103.64972568576056,1,45.605074365805535,2,39.81081939622676,3.480746028419332 +91,75,55,27.48612983,76.11239849,6.212369363,109.2768851,banana,29.175327495168705,2,8.582610836664788,9.045567356060776,388.03068494349077,6.712049293063654,4,13.870994410820844,97.47333162437359,108.27932768196311,1,44.70250201361512,2,91.99749314681281,1.103839794752655 +93,83,46,29.38254012,83.50423735,5.765308943,109.2486647,banana,13.227263136868348,3,7.789179650193862,4.871629044430499,410.3954382702209,3.7594865979379044,2,9.457105336915925,29.124322468889318,108.77805833149617,2,39.50050937925871,3,53.407385523523274,3.7516214734589264 +92,85,51,29.22118628,81.08183635,5.740764682,108.8616474,banana,26.61634477390673,3,9.615613982055766,2.0949554733417153,427.7203059917793,6.702638485310747,5,18.138112469407908,72.4047794926328,66.20559784957513,1,14.007089730189875,2,11.29779820542579,2.0772940102187754 +104,80,54,27.09062164,81.33506906,5.879119455,110.1331182,banana,23.64088673750455,3,5.213924341872948,12.548312640716432,435.86171801584385,9.345113453773006,2,9.235689660273529,48.053784295785285,118.1578343495433,2,31.799609604221207,3,36.188101877108714,3.8255524381274726 +103,72,51,26.12643374,81.81365007,6.099478745,104.4812858,banana,17.411591381515123,1,8.370201790706803,1.4533845395745915,425.47707050784663,6.620878441116255,2,9.750358665345214,30.038942423895097,50.479500982936706,3,1.2198301621376029,2,98.30492670992926,1.1492373572882335 +92,75,45,29.01207743,77.95192527,5.674403359,90.43495443,banana,16.63685599608202,2,9.163123097571123,8.52912015825506,367.7380147536476,1.7845633388422986,5,12.437864722089468,22.386006224317345,170.79019417937394,2,13.728279190087445,3,17.944154994015605,1.8298510525115126 +93,85,49,27.96799119,79.28625709,5.694243847,119.4765557,banana,27.631991593462327,1,5.4551748869713395,6.973011253361423,402.14785400186264,5.553103255923066,3,11.223180484940467,53.18450433172036,104.1995601673656,2,8.68508673196821,3,99.28882643431969,2.7125461425612185 +120,87,52,28.0764455,76.05522115,5.905494703,118.9923573,banana,18.369332701314157,1,10.090547545606887,11.786457985210959,449.88834668983736,9.441879465338177,1,12.896648792314545,19.245261551592428,98.77029643768734,2,10.247071453295604,2,42.483014926553174,1.5227509843265343 +108,72,46,25.16278237,84.97849241,6.110844721,90.94554618,banana,13.083566603279369,3,9.75783462713622,17.395255043352574,387.26377555280664,2.276128470741809,5,10.087101236041395,61.95347221370556,96.73329433689935,3,3.548702259054781,2,47.83334750817044,2.135296558993779 +105,88,54,25.78749808,84.51194224,6.020445317,114.2005455,banana,20.526831018972622,2,5.934902047903253,14.282760918873432,438.13424937769014,8.563093804014317,4,8.500440310339712,89.49947766095691,77.03014587497077,3,39.41433595341513,2,64.30920035138445,4.939304045818896 +98,79,50,25.34119774,84.47321314,6.435917308,91.06493353,banana,12.53884387484913,2,7.665334151977271,14.103438574597018,392.1614221201162,9.996560281828213,4,7.028795148293019,83.86635456389267,82.17341704399402,3,12.041754877495087,2,2.8990064081105715,1.797201398619395 +111,88,55,29.44795403,78.34971537,5.505393833,96.45042585,banana,21.184210718597853,3,5.343629340775273,17.565681931325898,368.73405109612725,5.022820291476323,6,11.273220756993787,29.498011187845375,106.72827996056486,2,27.08617082826313,1,0.7375168927973297,2.563942594054962 +97,74,45,26.47522633,78.51833782,5.677719902,113.1161095,banana,22.82054916657342,1,7.968238869826388,17.6920836748308,400.09446783476466,8.542185369526404,3,11.090204942669999,2.1615029794269436,181.5013610913242,2,29.57056886470391,3,24.756620855330546,1.6062707722305336 +95,82,48,27.39489579,83.31172003,5.719014989,92.78133617,banana,21.757814358798004,1,9.888220059166665,13.855376984633732,389.3526303422334,4.29936277317319,5,8.198105230559541,31.16284718684752,193.74679060695505,2,0.8411888947684965,1,85.73100066563661,4.615583926374544 +89,91,55,25.08347445,80.261731,6.275572298,94.32961456,banana,19.839832895229613,1,10.95310827300801,15.301841934099446,380.8661048749861,1.413298621923211,1,9.040540649593346,61.514992963163614,177.5520904594253,3,2.75202209842233,3,94.9865095839756,4.086551780180214 +89,85,55,26.6719835,76.48541655,6.275384607,91.73358569,banana,23.961921889132327,2,9.489963537005455,6.471867507081958,434.6634446272299,7.1326634437187115,5,14.230284349836808,14.554253532748984,169.99995393891058,3,3.0680263490406534,3,1.1244899065431468,1.8627753862482055 +118,88,51,25.44926208,79.49221962,6.201911642,100.6619171,banana,21.259516464292563,3,8.258927778693504,9.307068231972561,367.74966611312004,2.541833340080773,2,9.671773994604049,34.09854996877014,125.61092857081643,1,46.45291777348057,3,93.58146307852427,3.5239763202604344 +101,92,45,28.22776705,80.6430384,5.758054257,98.00403016,banana,23.41065976745307,2,7.941116070376768,14.759443307494266,405.3345338723524,4.495260445117842,6,6.619226210823543,84.5213502137492,137.68668492743717,2,12.271642594236582,1,71.0343959275815,3.13309647162218 +99,92,47,28.1279509,77.48247073,6.323933647,103.5045395,banana,25.78484558924632,3,7.100583657424489,13.345319577737413,421.4405726447123,8.14045261132587,3,19.0129528667178,69.29188020915988,160.6337250087647,3,19.5677348591201,2,0.2649266444043952,1.279394712114605 +82,77,46,28.9470467,82.1888998,5.901100841,95.83016448,banana,15.141719267175484,2,8.403345806940486,14.538392437524521,387.63327926352514,6.605467721607468,6,12.972721837493198,46.54558529644929,192.47553191750362,2,31.90734794186067,3,83.60219831303561,3.510788251320101 +90,86,55,27.96236771,84.15403614,5.644486582,97.55986676,banana,11.229387089027805,2,11.767631229377928,3.2862505131953057,448.33745283342625,1.4111762562105594,2,9.407386730695462,22.477539321637185,96.5890597807709,3,3.6734182046660346,3,97.15802378676287,4.306572500486806 +95,88,52,28.00316034,78.90085998,6.235461772,94.68180316,banana,22.668378327418573,2,11.566289830371755,19.14650522802172,394.67034402278824,5.418596982803845,4,11.492222560284997,96.05688832395303,166.0479016258637,2,38.93190560681779,1,72.45010605397746,1.1969757132453158 +104,73,46,29.1400919,80.1190228,6.28236237,90.45142867,banana,27.146732440317972,2,11.82325042889975,15.270772831802939,417.2027712335599,9.435198362659824,6,9.537251193425599,50.28482956993214,164.66024351651748,3,24.14714764734704,3,1.1467196158396864,2.2622379798867165 +102,73,52,27.9122104,83.36307683,6.356090905,90.24211529,banana,12.797878443899851,1,5.489952429084051,14.912085680072622,354.8722961835253,2.1423696708813216,2,11.80440390094221,2.757849056082684,192.38838170532205,2,43.75918987676439,2,21.28481707244533,4.114900234474259 +100,74,52,25.43480512,81.53977797,5.837258235,96.47800391,banana,19.80368362939287,2,8.194500831556658,3.747601866796051,444.85611113805203,2.2617419179173197,2,12.67432505012567,16.51228678371367,192.67623820392848,1,47.46367142309605,1,49.83559953630441,3.76524632387363 +94,89,48,28.55980972,84.51602322,5.653437902,111.0843029,banana,28.957814237953468,3,11.7251915784244,18.13070113574214,415.1980318039025,3.237881082393708,3,13.805482905712408,57.80412125490687,91.79399187121398,3,42.71540477169628,2,87.84249410859692,3.061635017089876 +99,70,46,26.59580783,82.99556744,5.727469947,100.5123341,banana,21.354007052882906,3,6.883608512588595,7.189377098668681,383.98045703867695,1.8638775523783686,6,6.768483954129709,43.30837564855556,53.21213083999676,2,20.554686929645392,2,69.78811099907699,1.4002508249433543 +112,87,48,27.19711623,77.3970629,6.200111068,99.46950465,banana,29.78439711820344,1,10.138441159300394,8.973130658678128,366.77118782956245,7.81983630892144,4,17.53474765202055,70.28689618451757,197.87012983697647,1,12.36274066943716,3,55.985479590730726,4.342805791973679 +117,82,45,25.29391516,79.29122198,5.614471478,105.4220251,banana,15.584397075121004,2,7.7683814421636175,5.925945166568489,410.8027948954184,8.628985493296632,1,18.49521224581808,32.3741023922442,64.5454667859827,2,36.52924646195382,3,92.07414843236037,3.0468218287785436 +96,86,51,29.90888522,76.98740841,6.257369799,91.99964712,banana,28.77461744817684,1,7.694074293233861,16.18337528886357,364.2448230618667,5.740019822028858,5,15.337410382062473,96.21100982977501,149.89228123683662,3,0.16255248710489445,1,52.862909334112054,4.538575530013052 +113,85,45,27.94972463,76.63713353,6.037430836,109.0921631,banana,26.975793549868218,3,11.754585700765285,8.563171198028305,355.354567254719,8.583765283529544,4,19.97037131235514,32.859956069984065,132.5031800088976,2,15.61699181865593,3,94.6260442009146,2.9685500696425047 +105,93,46,25.01018457,78.76260938,5.760457558,108.3690513,banana,25.985397521550126,3,10.000456677671941,13.105259390723221,370.0804597824554,5.510576775151264,3,5.284084232215516,55.37381233840232,86.52833522466042,3,31.861284017307863,1,75.41771130836273,3.890809864919323 +85,89,51,29.21144871,84.70189923,6.158164422,108.5501443,banana,14.482681972169132,1,9.00643611748248,13.475142208259552,427.49678494344823,9.904854957732908,5,9.221954869984524,93.27100319822716,92.48101099932857,2,16.91994897182972,2,9.702331721106184,3.254032519027928 +108,94,47,27.35911627,84.54625006,6.387431383,90.81250457,banana,20.64717442786585,3,7.020558825761817,16.95137583935374,357.4229701866576,8.955251938936522,1,13.301019792094548,78.83700795458655,135.84359887193932,2,42.027669326339925,3,65.27968116971184,2.3332934790040865 +92,81,52,28.0106804,76.52808057,5.891413895,103.7040783,banana,15.283342019033839,1,5.354606925462043,9.573667049338459,427.99106969296804,2.2204136583461875,4,17.30771392430109,54.49950529943253,73.01433469753412,1,3.5225913617418723,2,96.17759935923752,3.3488505695340103 +110,71,54,28.67208915,82.20793613,5.725418961,94.37987496,banana,28.399852346490746,2,9.68956205082103,15.302181367936393,401.8110884217699,7.152591915249997,1,11.841643148359648,34.4137840601472,149.27264479590212,2,13.982788693711768,2,28.777049067158956,3.6424803904330694 +82,75,55,27.34585147,78.4873835,6.281069505,92.15524332,banana,18.555143687744447,3,8.396095163056769,6.650122181188114,405.3878215362177,8.840153815512029,5,6.077036955993403,7.4695975725765145,89.33713674118,2,26.42727502804495,3,86.24720879780006,3.259270844797937 +117,81,53,29.50704598,78.20585613,5.507641778,98.12565829,banana,11.573271067419459,1,6.316263759235625,6.745786041018691,373.67248661154633,4.108874470165744,1,15.94349852495316,80.83270465944659,88.00600335674346,2,10.144253466513081,3,54.991252710229574,3.6466821886643705 +2,40,27,29.73770045,47.54885174,5.954626604,90.09586854,mango,15.153237764318618,2,5.212209318188972,19.665625811403377,368.04096505557686,5.1512683065150116,2,9.004362734187211,61.582773890612295,155.81085615975425,1,12.525378412232453,2,0.767139280584328,1.9146314184876605 +39,24,31,33.55695561,53.72979826,4.757114897,98.67527561,mango,29.815689630254557,2,11.364753654106135,1.7650666453897035,449.45908858087137,6.812937754161568,1,9.888753997742356,17.285648704535006,184.5842361921965,1,35.240113808606274,1,89.11822211662881,4.268309289185184 +21,26,27,27.00315545,47.67525434,5.699586972,95.85118326,mango,27.38565575983308,2,11.513797253414538,3.0529033328737354,406.72560208883715,7.934945897236288,1,15.44637788402324,74.15333531683783,80.259544837464,3,2.2154621439191557,2,35.52674600925372,1.8133938466114374 +25,22,25,33.56150184,45.53556603,5.977413803,95.70525913,mango,18.972881644166975,1,5.854913008923347,10.095067141907538,403.5341508162376,7.610028300297993,3,15.04184898341601,26.18640033130206,131.24198060086317,1,16.436602099372234,1,41.92005730187189,2.8940414695796854 +0,21,32,35.89855625,54.25964196,6.430139436,92.19721736,mango,11.928400658221928,1,5.444153275031569,19.27385801687268,430.87234497122734,7.815785446443787,6,16.940205000176988,19.95420713331374,106.1888904057198,1,24.312857522159398,1,45.9756141593372,4.15812646700556 +20,19,35,34.17719782,50.62161586,6.113935087,98.00687989,mango,14.675872562535845,2,6.407590703985772,18.934049561828317,390.43361696096053,6.204922288756691,1,18.959595045115574,90.31038422405858,131.1759390675589,2,48.872594649911164,3,73.47984397528661,4.3456984654009805 +19,21,34,30.01592643,53.19212381,5.074272692,97.72843182,mango,25.940271838540546,2,10.752462916382324,9.653928815730957,367.31616540398335,7.92593638663964,1,12.175154027214555,63.34061175156555,112.38628828487566,3,44.90203171290382,3,36.25938241071667,2.3447709905550567 +18,17,31,31.74592134,45.16127859,5.667507706,93.75441586,mango,29.05599984484948,1,9.35757378942182,3.738089200227057,358.5920500588174,6.495439897637864,1,19.61080994129024,32.48700928869857,69.79957890183655,3,24.705041719116632,1,44.459408421379244,3.5802305091234685 +11,36,33,35.99009679,52.22780489,5.978634285,95.3713484,mango,12.970253989218888,2,8.326114116889578,12.696458898696978,390.42591406964897,1.104039945335964,3,16.330701248100816,63.51933564693726,182.3865652721956,3,7.721813274953488,1,4.232264029026423,2.222033397867616 +30,28,30,31.86641378,52.19331595,5.064613314,98.46768642,mango,13.17561760067285,3,6.684201672713111,14.797411545199262,379.01845063874157,7.747109266976485,5,9.52571080168973,14.757121558613173,79.34951018095876,3,6.545228761665384,2,78.9684350362596,1.9683796362735189 +18,19,27,27.75518664,52.34605806,4.772385986,94.11213345,mango,24.47669178874236,2,5.6268569664119115,2.004341335515374,435.8932715711568,2.8572932528071178,4,15.435591701627809,49.440327608148,105.97283401491157,2,19.276190455874993,1,25.726948977491926,3.813251231788439 +23,23,27,34.72413192,51.4271781,5.161148592,97.31258083,mango,10.113773482446792,2,7.845182417496389,5.447219990243588,400.03850110171106,9.99999015034425,2,9.118073697250987,34.51815271586389,122.33706548769418,2,4.540941515511832,1,21.238926462321682,4.825716715321727 +37,30,34,27.53907547,53.63549533,6.797779227,99.35408185,mango,14.677124413861492,2,10.938622747814467,8.907052748897419,403.24886131334665,6.529916273747172,2,17.397240556245126,86.82938135676135,59.08923793896368,1,9.590604048772455,1,6.160901887878756,2.1192554524854708 +11,27,30,27.69637763,48.5622488,6.39474303,89.85646496,mango,28.466059968889667,2,7.84347332562691,10.173530822301103,360.72707602617623,3.7988338349649418,1,6.0190532867932784,18.296057935871712,160.46350289723225,1,30.59030288214008,1,61.62433145011802,4.157665978159493 +12,19,31,27.25373364,52.66319725,5.566704378,91.87312479,mango,12.116268860828583,2,6.264368752680308,19.71563690487492,432.75990544308524,5.011290348657217,4,14.574196346125166,11.43298573892315,84.52425171463749,2,7.583673027371796,3,65.84559781668001,3.795940641406826 +3,28,33,30.33723921,48.88704844,5.755049971,94.42850522,mango,10.56729998261652,1,9.880710218674665,13.569236952013902,427.7502731111899,2.2029134070542766,1,13.724713379591968,31.958326477339305,156.11533701288172,1,8.888352949390587,3,40.23011805392112,4.148243441893429 +37,38,32,31.85744939,45.53106268,5.417340525,91.55845821,mango,16.020352572280615,2,7.225482495858943,11.54415593938815,377.5939042471411,3.6388247127360316,1,7.4339173492871495,58.9345448944986,146.0603777102092,3,45.80708166856856,2,92.90913444205383,4.277737682887045 +26,37,30,35.39986338,49.45962621,6.166173834,97.41054011,mango,13.515716639695825,3,11.836348727367918,7.045002557058622,421.4086810437386,3.0362445457527447,1,14.818564820370828,52.02031271400146,51.30104384258071,1,39.12135290133456,1,42.2975115792381,4.364951347651021 +14,18,30,29.80747243,52.13797867,5.191265116,95.74606104,mango,15.83044909575043,1,11.368463265035466,16.090160768062482,408.738558659662,9.744896268021048,1,18.244901642232392,80.78380897006187,100.71169551443022,2,32.538754136434804,1,71.79797018716702,4.442525820495195 +40,16,35,34.16438906,54.16482251,4.954739564,98.33351125,mango,15.520950459783,2,9.684546547948212,4.076746117295156,412.30044246522164,7.87537917555961,6,16.313046278511273,16.380641315099275,188.34717771095097,1,18.586115288438563,2,71.08536366851826,4.5877147763989585 +4,20,25,28.93270187,47.94053996,5.664587011,99.9834242,mango,25.451892160205688,3,11.983307126403108,5.04660514797274,378.21123896501865,2.4887360222467283,6,17.71556008046845,84.7589980566184,150.70526580365572,3,8.15994377886135,2,31.686544928669804,2.4095926986189697 +36,25,33,27.98392787,53.33018851,5.548584852,99.61465679,mango,22.6375026764087,3,5.675524728754592,9.335734865940466,441.5292978790143,6.276372468195617,1,10.046483937919389,29.916675730556353,150.5704491376427,1,3.685060020334535,3,41.750438938962965,2.701899224912262 +30,17,31,31.20478173,54.49960506,6.804437106,94.62954663,mango,18.59303977657086,1,9.908640214566315,18.199889651731894,353.1559946853126,5.333513437293568,3,6.615898559690987,81.87523500271139,115.42511319876529,2,20.44369038769103,1,83.69461211291572,4.32765545321964 +28,37,28,32.13409675,50.52559148,6.097869767,98.63333684,mango,24.53922115912867,2,8.598033003701104,15.273448915025902,430.2249163710166,8.72156033183487,1,11.78792384147933,58.13427645016299,107.75428531092112,1,34.28090472513986,1,93.2132688904447,3.030399108938353 +38,15,30,28.91862016,48.13974548,5.075504537,97.01331604,mango,22.109309180516867,3,8.67149886720926,17.355488002534827,363.20737842101283,2.643244250981022,1,5.394924300745588,60.79984583724315,196.43105045416303,1,25.843225156749273,1,8.575457284665934,2.0853571162404188 +12,37,30,31.09779147,47.41196659,4.546466109,90.28624348,mango,28.97651413578902,1,7.724918699066491,6.696853833508181,353.0281566812762,4.3030334220000075,5,7.008972353893549,37.954475616088686,189.71883058697694,1,40.54873490258497,2,27.509956367105936,3.1563018592487007 +38,19,31,34.73823882,49.08864345,5.855119268,90.65022183,mango,25.96472700919952,2,9.974807388350705,3.619421649099097,426.9053221336568,6.234116023415303,2,17.60026116214953,77.24229548343284,83.20248043139586,2,41.60946104160847,3,79.09408939355859,4.310243370995706 +8,33,29,29.98080499,49.48613279,6.442393461,91.82271568,mango,17.349656767443335,3,5.682681685666704,12.868864381052763,425.50991899004424,2.6434928257237456,6,7.745704552052411,63.51622484773989,138.71353935936656,2,9.909025658725707,2,92.3879545210987,3.716507871560343 +15,27,28,33.80398664,46.12866113,4.507523551,90.82549241,mango,15.634928991882045,2,5.857405506695924,0.3482219813111631,384.14010390479535,4.81013847255665,3,12.171331132368586,25.992772521455354,184.89915236043353,3,19.64006540060329,1,61.48317822225782,3.6066284349943993 +34,16,25,30.07202564,50.96040505,6.10729559,92.09609766,mango,20.715076606168907,3,11.357735837490596,12.868205321779758,411.23663209182325,7.187704113845516,4,16.0936163429313,15.065150342049439,133.2872655603867,3,15.268934476082935,1,24.769198947326053,4.071821805484809 +11,36,31,27.92063282,51.77965917,6.47544932,100.2585673,mango,12.462926012823116,2,6.658189512274884,17.56580147150928,358.47897699870185,7.029844640440521,6,8.366348050685712,85.70078117389967,73.28630513993075,1,25.616852287575348,1,98.87719740523619,2.767887042693355 +33,29,34,31.40948821,49.21729127,6.832979509,92.99739415,mango,15.720683747476397,1,5.002760418077519,6.734116270592827,413.2649981165205,4.849597883631073,4,16.205759308565398,15.011668850394422,71.60545368971304,2,6.334963616786659,2,42.67253287673033,3.552331227102117 +12,31,26,35.7877738,51.94190321,5.395275719,100.2160615,mango,20.57995716136375,1,8.434107945156772,9.019836126445318,413.5096980570487,9.036068298521116,1,17.05799703948668,48.13013130051317,130.00948679223578,3,31.622306581858687,2,42.1011065529623,2.651631894860917 +12,34,28,33.36140093,45.02236377,6.13526938,98.81596545,mango,13.087241229945965,2,8.73759285456476,9.533559194922939,359.35127428046866,8.242842977536945,4,10.131359195181009,85.44757115664035,59.55049462480298,3,44.58669256375109,2,57.22237055131024,2.6488656729355924 +5,16,31,35.96054636,48.69677802,4.555688532,98.00644238,mango,19.69151395508908,2,8.56087782448124,10.55864284089678,410.5396306284175,4.815182598917837,6,16.691024386379986,86.7180603140091,64.07907127470942,1,7.9384539450655875,1,22.282451838926875,2.65123293767258 +1,30,29,28.33333307,51.39586505,6.434197756,91.67241761,mango,13.56465894595029,3,8.648098801104226,19.15640832241305,416.4930891188513,6.778424443821644,3,16.58759929561223,71.64338521415937,92.8247601563515,2,20.490560449062876,3,76.09175261963932,1.3312439024009537 +16,35,31,32.27652024,50.19368841,5.316875978,95.99487068,mango,28.945067869860168,3,8.603667156833987,0.2065913675047537,410.579476517807,8.719099950312973,5,8.22380143738097,4.353965185209818,104.300861506187,2,33.32488885527923,3,0.6804897443117186,2.238705243812902 +35,18,26,31.99490489,50.84881347,5.279388967,97.38741498,mango,26.14085037429558,2,10.223447364698938,14.246003416614561,370.7795764543419,3.6024062936239156,5,16.866580304626183,17.90766495707595,185.7570161701242,2,1.4059166181180305,1,23.696850723523543,1.4507015182404164 +4,40,26,27.58258929,48.56916221,6.720041791,95.8445641,mango,25.392462588985584,3,7.350324999196648,3.8775227141460866,365.5566899373565,2.6744081232528614,1,16.61475644619076,10.424025153246175,125.38439689725244,3,40.73679271746584,1,86.17157484495104,2.755672365283657 +9,29,34,29.38471637,45.88744691,5.72742254,100.8124659,mango,13.32250068553737,3,10.103222279475569,3.76417346751299,376.32439864922185,7.763583681282601,5,18.617006975803307,24.950538210796026,167.65971112990937,3,47.372567878371285,3,70.11381187351641,3.8551816526236893 +2,38,33,32.38697531,53.2328243,4.691396195,90.21633216,mango,25.686458540505363,2,8.538126184601378,8.39717821832998,447.50999155316197,1.9442781448170918,4,6.381454922352157,30.558984450101946,55.49886082300576,2,1.9501967408222243,1,51.95037583821941,3.1687195174705014 +26,32,32,30.91471455,49.92963856,6.810186079,90.14047759,mango,18.13417431721645,2,10.786094478493714,12.165794691961953,391.15060316805,1.7447592480205663,4,12.203680850445497,66.37333138441791,123.88094527863902,1,18.863521126370326,2,17.774300117131304,3.315351680940747 +34,38,31,35.37775595,45.58110023,6.454045329,97.41586402,mango,12.250950933904623,3,8.179810521372492,10.042163274067754,416.97929280665664,1.8812584659740805,4,9.464530003698798,17.18517852972632,183.00886716216237,2,46.34503740025691,1,27.415865802861006,2.3857106510065593 +5,32,33,32.32362177,52.5896771,5.842763773,93.36718816,mango,29.934365822330737,3,8.870927057371205,9.867214172330709,443.27913746807815,6.588507225997976,2,14.645431828199303,6.54027127711303,143.85779771982436,2,31.576169053017523,1,97.63330265842927,4.1886943352146515 +31,29,26,28.22373428,47.40519056,5.024124684,97.76832322,mango,14.383905838249806,3,11.857356630025311,14.32243823889185,375.2131891953593,5.98443737789419,4,14.062227107567086,67.13949267198196,128.76021626961207,3,24.087683208405085,1,17.644583059757903,4.288568117811943 +34,34,35,27.27433181,47.16808054,6.422710539,95.257992,mango,20.95036774058351,2,9.5993931578653,19.36173328136402,387.58164653246746,4.299912494997331,6,9.140673308295046,95.25066434331677,177.47223893405862,2,26.468298338451618,1,84.10268479462717,2.019432164539923 +36,19,32,27.10710832,50.70880979,4.94295037,92.37238878,mango,17.787394502864416,3,11.55443526159478,12.398022895394085,384.3138914253102,5.608619681497929,2,19.937954866060988,90.66065568342695,141.34673528238787,2,40.88574286201217,3,48.440863762199946,4.5669052833921056 +7,17,26,34.89226666,48.75613373,6.414526606,91.63074547,mango,17.71277351180151,1,5.325111925681399,3.4765201330276096,375.25961144540406,6.222592932148673,4,10.00877819645267,70.56134789190057,103.90233721084041,2,6.836079498180775,1,46.58566333826983,2.2673632055422135 +38,15,27,33.7462686,48.50387598,6.777788126,92.26439205,mango,20.50207888167642,1,8.842771318745514,15.283105507044304,432.9567580075293,1.964330878345144,1,5.617528391220695,10.444626282556158,53.8175605751648,2,37.49104231272193,3,61.88928289240951,2.253294618242708 +5,19,25,27.3511056,54.43945147,6.441328044,96.27792547,mango,25.354738784996258,2,5.378872392919558,7.550335106731218,350.93478516205545,7.192034852598315,1,14.395449267801938,60.41607759501204,166.32532771304403,3,34.68685021849828,2,7.938344944155851,2.0655764275603565 +37,36,26,32.89300162,52.61323969,4.650536197,94.49161372,mango,13.856105173905371,3,9.00089293536961,7.1649319543635155,391.51081011861373,5.017660610549315,4,9.046370893924117,51.84114767210608,175.51582967415646,1,48.35737911211418,1,56.69439997342949,4.739084650469469 +21,31,32,35.38598705,51.42664176,5.254532213,90.29643888,mango,18.209124155162264,2,5.151235616995752,8.067064566503827,394.6440813528534,6.820278828977285,1,8.004778168305311,66.28808257406162,178.80655691289454,1,42.17762789718611,3,35.38950014110663,3.570451315458836 +37,36,27,27.5529736,47.90859131,5.910634533,90.40332704,mango,10.783959311288562,3,6.699745250130075,15.849165893429504,380.7558763974026,5.694371470707981,4,15.45580205149215,95.34139445636002,89.17570909603396,3,16.09283478874064,2,96.91403704874678,1.597657975646214 +23,23,30,32.82141065,47.45553843,4.755273631,90.89173106,mango,26.92493135810683,3,10.608561658907384,10.840999312272505,352.91785640617684,9.38755779624438,3,9.912014991984902,14.02201019292596,151.02006458177112,1,30.72540176084994,2,67.14698558757331,3.2050186837580985 +36,26,26,30.17294105,51.0845903,6.814630246,95.23444287,mango,22.368035030771573,3,7.485103077973636,1.8876236472418007,374.9701394223415,3.7599811337865745,6,16.076365350984737,42.01228869217829,148.86119798149065,1,11.667972740686361,3,50.47434443163694,4.059820122907609 +24,33,35,29.26382931,54.82257868,5.342866119,100.7586226,mango,25.629737763625258,2,10.363996173786898,12.357516995339143,356.44205651926274,4.358217045629873,5,17.575738133858227,56.10345896699454,140.54430102643914,2,18.101419809140957,3,49.990453212098096,3.007052048504865 +26,18,30,32.06097197,51.08494181,6.336234624,96.59816497,mango,12.396979879056623,3,11.572200260236826,2.017425182023278,440.4510293030868,4.614830027656963,6,10.840625068097474,79.26453051363697,130.3430383523095,2,37.66371595569794,2,22.52152398937344,2.068097232452359 +22,17,26,28.69818144,47.71875722,4.754435025,99.642454,mango,12.392641104323507,3,6.620135706584731,13.280683971314723,361.0312890274221,1.213871076964352,6,14.121603062378982,93.6580665983726,125.44311060182989,2,18.88569997404496,2,74.86243095133553,4.366930601578961 +11,34,32,29.14305008,49.40983294,6.831706773,97.55155537,mango,15.677192302870697,1,10.214782987424837,10.981876870916414,404.2140329337621,6.103912941643716,6,8.35833418435517,67.61513092498616,109.44631453871511,3,10.523572644714124,1,5.370390900236,1.202665947913653 +29,35,28,28.3471611,53.53903102,6.967417766,90.40260445,mango,28.796271812581608,1,7.4238511034422885,0.27264951853978037,407.6803607004108,3.2594768340407105,4,8.76695637635554,18.685893804904897,88.94216406000888,1,47.34881905795667,1,90.42296835182543,1.9875899454332568 +22,28,26,27.67256197,45.41692012,4.947683034,92.84991507,mango,24.24630233128739,3,6.725833302874016,17.579487517673225,439.98271417578707,9.89933243961977,4,5.149003838578972,44.05801407595852,188.26723330830296,3,8.467878606426215,3,28.119868880864416,1.386674874248285 +23,24,32,28.1218093,46.16888595,5.630619901,93.30247448,mango,10.691681153943303,2,6.690508617925482,13.288392080359444,426.48022847668926,1.7695317893625044,5,7.417074608224206,35.46534555152574,56.271673645187235,1,22.353106525136635,2,49.092626084718326,3.5250369715113274 +1,35,34,30.79375683,46.69536813,6.27339822,92.21318555,mango,28.64394564828418,1,6.33286251838077,2.3240261152923014,435.19734750364245,4.066302104353758,2,11.192610803061935,43.29270424757342,153.4998724982956,2,31.21216102332393,3,7.7517293976692825,2.1896080365222717 +2,24,34,28.89409382,54.80750249,6.472774648,94.76322976,mango,11.349458943073508,2,11.745399852463754,3.176058113591038,380.15245491459143,7.765241570267472,3,13.928006543012541,93.66291853960091,73.0004976028303,3,11.200451297971037,3,14.796929505859612,2.839841894632002 +39,37,25,33.33024826,45.61143594,6.953246506,98.28583013,mango,15.886645977396885,1,6.644275926980583,14.954989160768895,376.7001385911211,5.616012667614688,2,16.05195423045227,22.171801215110033,61.97061959420441,1,33.9141151528893,3,67.97266195829839,1.283070302462654 +15,36,27,27.78912455,53.96886679,5.643710216,91.01152997,mango,12.000506154949411,1,7.011213930015463,9.382618565801458,371.6628651391069,2.880681776101782,1,15.832406686044985,34.27695444529169,199.7696085130453,2,22.193884303513407,1,23.27049112764985,2.160106767729868 +3,18,31,31.65333432,48.20662669,6.392313973,91.09745581,mango,12.045818537223123,2,11.178545757297279,14.462440173864788,382.93352075275385,7.031637447651523,2,8.012454116402086,41.62839697955012,130.46750481159933,3,4.009759647512462,1,64.45514156303686,1.8340978457647612 +8,38,32,29.75150773,46.73723302,4.981816523,91.405983,mango,23.596282614507775,1,5.528111671444128,14.391395119761876,362.6939736398739,9.631558731083732,5,12.255022840224704,28.01460829166904,198.18363124538593,2,24.43941160711069,3,95.35116889648104,3.026772447503423 +33,31,34,31.32995611,50.22287593,5.421265283,89.78216168,mango,27.565137396918484,2,8.30278609110212,9.275386768695682,361.1337793298358,2.540245652344579,3,19.950884386582302,11.422324882026846,120.4462779683401,2,25.848419874035176,2,19.892768163969865,1.426228460647605 +14,29,32,35.63627319,48.97047762,6.942520105,97.51952041,mango,21.83194359588598,1,10.099539527536088,10.494491600347423,394.6145673267954,3.5427864233049835,6,16.259497866333383,2.149918135654738,185.66288279919476,1,37.72937009882267,3,79.55329503836295,4.360022181084265 +18,20,26,31.66524687,51.98594645,5.435840509,89.98024312,mango,13.101788392720406,3,11.737134587248727,4.163657696742861,423.66604028625596,4.9436028646781,1,16.115192997071944,33.520070548634266,122.84109623346083,3,18.35268937869956,3,35.65121566725233,1.872980972186455 +9,21,32,32.26935342,53.56092806,5.870116071,95.94035356,mango,29.34023694955561,1,6.030596401140092,0.12617165400783392,401.7787957179044,9.010799635299275,4,16.549000456414138,85.96235364259128,99.92315719772444,1,11.200639102784733,2,51.71972963944078,1.2121467566126936 +20,30,27,27.81005614,51.59445462,4.74910393,95.89898581,mango,28.427872693199404,2,11.325878339399612,12.361024482338008,387.6850994951384,8.819933673958825,3,16.586404782094807,90.29632802073307,50.89515262658392,3,8.023073098335681,3,43.70538282726187,4.52424981664452 +9,38,25,34.58561471,50.34035336,5.497946899,100.3060719,mango,13.225138142555844,3,10.644924615799095,14.992618175558096,420.6717457655312,3.48250227039773,6,19.848270714519472,93.41659716333359,188.55757541290686,3,41.85548348154198,3,96.97134141668006,3.398294995421895 +26,24,34,31.27180992,52.23810152,6.811291098,89.74409017,mango,27.072188394799525,2,11.078800673122377,4.253194453774749,389.3775983014044,9.218622788424819,6,10.11356139261532,29.71142871510296,174.4805144828922,3,25.962916665797987,3,21.762090085331355,4.9971303231714455 +31,36,29,33.93679864,52.72170281,6.460542749,97.4611918,mango,14.497889151035778,3,7.156714110942934,15.822584004023305,444.28196955779237,8.973983699727167,5,18.48651814501219,36.08879753424412,149.05049501552014,3,7.504453034610559,2,83.3107456116617,2.597192247416237 +14,18,35,31.09154239,47.02058367,4.791146778,91.46664318,mango,13.009933763252793,3,7.008055952107394,6.825324326750925,427.9846736749895,8.951496714930467,5,7.132093255868785,99.25979517070807,127.76690214778165,1,40.76998824238202,3,53.88177029416863,4.219090307535458 +40,16,35,31.89356292,49.02450149,6.4841522,89.59371481,mango,19.078530082322473,3,6.429873161269791,16.45672905508927,410.91048353675785,3.915643952530767,3,13.610872072169691,60.755801590424774,108.40822058204476,1,34.20698385590063,2,3.8175072581735248,2.7747337794052633 +28,27,34,32.45465292,50.69693751,6.526654345,95.04871605,mango,20.16287908097148,1,10.51943841792308,5.0083344367687666,412.97465579847085,1.9562090314633784,3,8.39208592476675,17.626101614922206,156.43312607480055,2,13.19785228797688,1,84.7094727164004,3.4120437769192806 +0,17,30,35.47478322,47.97230503,6.279133738,97.79072474,mango,20.966199351371756,3,8.73626150546118,2.2243681803518722,421.36710829186137,6.917189860104865,3,15.920464020677722,35.787020459290005,154.10427469865033,2,18.921362462531775,3,18.54321193645839,3.7774351776285124 +1,29,29,27.32961444,49.30347234,6.052026047,93.53197359,mango,16.82892056563115,3,7.8698546869647465,6.960873360697624,368.48532778182687,3.300031143922392,4,16.226513427836117,15.74260257996899,79.52362590478475,3,8.179879735331468,3,93.01664336616857,4.858995155757364 +2,36,31,30.90225239,49.95955487,5.73171945,91.77522598,mango,29.618378360382103,2,5.046765418593381,3.1481351291771476,442.9058294380934,4.7237841787535615,4,10.22311781342259,35.15909687899709,122.6433651080593,1,3.5565101873162863,3,81.5265956073904,3.824298999963472 +12,27,26,29.09382275,45.5661059,5.32307197,96.23520043,mango,22.681703927230465,3,8.290872703009377,12.331380330798073,412.9487642581278,4.957044797430302,1,15.546588173227724,94.0276981304365,158.60746395196685,2,15.987959037343257,2,94.95185982204275,2.1844486791119597 +7,28,35,30.02086169,46.78393776,4.66910839,96.63721027,mango,11.55177333359918,1,9.679904877439435,15.596502100812696,368.8228913451877,9.576864099342774,4,16.147271561864763,6.7471469456108935,154.80948645235458,3,21.27736766887064,3,87.2346352307003,2.229721270632325 +0,36,26,34.13072188,51.25786185,5.101206389,96.38808001,mango,16.15189052261151,1,7.25953424692906,9.920568708081433,382.8159231677604,2.7212068862482557,5,19.722095286644254,66.70104494797708,180.78760129111865,2,3.5504123429854872,1,99.34118066774981,1.5630711405574722 +26,35,31,33.44619894,53.05980465,5.339556562,98.05089394,mango,15.595314711780246,1,11.326764999357927,5.93735316584608,397.0707794675381,4.697437676902995,1,5.36289892832429,77.20367964618457,195.22949557130661,1,48.32960540692936,2,62.414838764644365,2.8009902477021953 +27,21,30,35.3915464,52.48823147,5.061081874,91.22881052,mango,28.029991510909177,3,8.72817466692484,12.597886429563104,358.12652889009104,5.88693124031704,5,8.039213788205704,1.3091974620817237,162.0651469017243,3,35.42478704565121,2,61.076782313926884,4.550731876599774 +22,38,31,31.53356352,53.06009323,5.821106036,98.57025046,mango,14.17633378507037,1,9.11360221362467,13.959549894264601,380.271498672713,9.94856375278071,3,5.710802946199851,99.11254849946218,161.2887164678362,3,2.4970433078432497,2,78.00022009949915,1.4494084948270483 +22,18,31,30.7645515,47.93791463,5.956027059,90.38503469,mango,11.108241239335278,1,11.02561183816479,13.471491272099865,352.5506673658353,2.3532226533346066,2,6.5141276884932715,0.2912858162559595,120.93984847031098,1,33.60746779727431,1,17.972231549152806,2.8966156322014616 +28,23,28,30.01821337,50.0983181,5.676032581,96.08745082,mango,22.87051321085243,3,9.81295467959671,15.062966831906472,434.31631355710465,1.773515703439831,2,13.00262331986498,24.70196361464898,150.25666850684064,1,31.779453859214424,2,99.24323670010587,3.463862867438773 +7,31,27,31.32863689,47.59319575,6.524114355,94.67344737,mango,13.933111449479496,1,9.509737446104545,13.50214048892388,356.6473433745257,9.390835482314177,3,10.845098926039574,6.423778544903858,91.720155830778,3,5.448930070731761,2,79.24347323551095,4.176852022414662 +29,34,26,33.88004781,54.39416048,6.273953676,89.29147581,mango,13.912195781612864,1,11.645756294396623,4.297113802966452,425.22336166750404,8.772105460548104,4,16.38686156151737,28.657552445378766,113.78180183756137,3,17.76189656722772,2,6.940524022604977,2.6724517434338284 +8,37,33,28.07802689,54.9640534,6.128167757,97.45373619,mango,17.420923946520322,1,6.568366898894003,16.933095045417886,396.3807808019393,9.278308559951308,6,8.090025076880652,17.947884029348305,112.7979138370323,3,33.83038047280012,3,82.926380779971,4.761396150796193 +39,16,27,35.53845018,52.94641947,4.934964765,91.54560427,mango,25.762900270322305,3,10.56658423712734,7.21459167800667,432.14988663543227,2.686887062448016,6,6.451612430943162,37.33870430589318,61.55656742589002,1,14.536984994086954,1,30.696160483330814,1.1744323791366198 +40,24,25,28.70595247,50.44030129,5.445008416,95.8946444,mango,28.05750322081822,3,11.60368749941923,10.474803363211189,398.33723779343904,9.425137015279759,5,6.571528996003135,26.55143172198844,134.34555973132368,2,18.915887941914427,1,36.298620105842275,4.086661569220803 +19,38,26,31.48451729,48.77926304,4.525722333,93.17221967,mango,29.774124577850348,2,9.107896447562602,5.707683204412735,380.7282732590555,5.183610483230737,4,19.323871537416824,54.82872454357999,122.81308612722137,2,40.00637718200626,2,36.10912076151161,4.97807202978695 +21,21,30,27.69819273,51.41593238,5.403908328,100.7720705,mango,14.823679511169106,3,11.131652589519263,9.7682373179501,433.3556860232486,2.2439757377097695,1,18.23555096377595,74.04818755858318,104.48565759466295,2,35.65197034412009,1,50.613751787905215,2.585278920178274 +22,18,33,30.41235793,52.48100602,6.621623545,93.92375879,mango,29.738825614820385,2,10.760095519982187,1.0866939828767963,419.14858881599434,7.373538347854294,3,15.899700996915952,15.315046707125257,156.25669793594778,3,35.30472775278633,3,77.8334354761513,2.9281337181842244 +31,20,30,32.17752026,54.01352682,6.207495815,91.88766069,mango,11.84767260897906,1,10.705365928232103,13.18635940850684,436.59214595725905,6.770849112113359,2,12.487580336474934,46.67522708632483,109.68732037488228,2,20.48841240135067,2,7.889667498026975,4.167741661378349 +18,26,31,32.6112614,47.74916499,5.418475257,91.10190759,mango,11.754106736230822,2,5.949850465387486,9.317438789853917,371.1037330090304,2.3348694569842157,4,13.192733909653404,13.732498522496606,77.93494848418956,1,45.85009662995419,2,48.000197060480524,1.1752984743342179 +24,130,195,29.99677232,81.54156612,6.112305667,67.12534492,grapes,20.347876568579043,1,11.118319676588353,18.804217234246263,430.5065751819473,1.1951903792254988,1,5.207192961491851,1.4172862509735706,188.95414187482595,2,46.00423391571693,3,3.5891858265399534,2.480357982448325 +13,144,204,30.7280404,82.42614055,6.092241627,68.38135469,grapes,16.496335589793823,2,5.200766039757856,14.564567042235275,429.00086641136176,2.748629066419663,1,18.551789207009158,85.66566178329018,196.7066562998277,1,20.612399034562927,1,14.319492404577428,1.1372937832334022 +22,123,205,32.44577836,83.88504863,5.896343436,68.73932528,grapes,22.168864617679898,3,6.44745130183923,15.52408840876572,432.8964486545,6.439147802351083,4,9.204864450674886,18.967161850471616,172.9760334545142,2,25.753076756276805,3,82.09382635083415,4.9348035870217615 +36,125,196,37.46566825,80.65968681,6.15526103,66.83872293,grapes,15.067755794891626,3,8.672953659538402,17.963515574186715,433.27527271906007,3.710994479420271,2,11.911083066033001,23.516699780553463,98.11425778695009,2,30.73623793106441,2,12.043291217252383,4.450684179741213 +24,131,196,22.03296178,83.74372787,5.732453638,65.34440794,grapes,24.22334231610793,3,6.965756858001104,4.3775452778904,396.0249927295587,2.614872528659109,2,5.664810725997242,2.0503168584691367,161.62913034429772,3,40.114240806877895,1,3.2636751410366593,3.4566573718141176 +2,123,198,39.64851881,82.21079946,6.253034534,70.39906054,grapes,16.705183656458175,1,8.609614686742923,14.829832934461095,380.9832706739987,6.947465462344696,3,11.034044571614098,14.87964432498703,177.8189288474846,1,10.844951792061568,3,76.18186919797185,3.9330895588276267 +35,140,197,16.77557314,82.75241875,6.106190557,66.76285469,grapes,14.62008457354159,2,10.226271704852973,16.78093173452856,413.06353563390576,2.4221680193400434,2,6.301045368228517,5.2496333827894315,103.36562046480961,2,30.74265171952603,2,87.33094314474414,2.501525471502492 +11,122,195,12.14190714,83.56812483,5.647202395,69.63122027,grapes,29.38131835722012,2,11.012704844717478,11.682948956718244,396.5410116170759,2.499690418509302,5,12.016137738590952,96.71358283582275,179.76101771844552,2,45.20095956412633,1,52.34981248380552,4.626408984973151 +6,123,203,12.7567962,81.62497448,6.130310493,66.77844567,grapes,27.627078170920313,1,8.733748359262439,18.73195083123363,396.62192315098247,2.5499244971222854,2,14.646934063915197,83.25699665255941,108.12586318195407,2,0.2134059110439679,2,52.55218301059171,4.177917785740755 +17,134,204,39.04071989,80.18393287,6.499604931,73.88467027,grapes,26.451612556723482,3,10.537050799574882,3.3394496368297433,439.6479376702555,2.272237680830338,6,6.789511070434468,33.94450949227623,134.91927036530026,2,33.74398721820629,2,13.580744827190848,3.369424440814223 +25,130,197,39.70772192,82.68593454,5.554831977,74.91506217,grapes,17.51148645603562,3,10.432567898016591,10.682190462941588,427.0928199364056,4.099136697793677,6,9.516456719930924,53.920307484104576,86.06217357088326,3,38.61620495997322,1,13.361415980280666,4.2136706433330815 +27,145,205,9.467960445,82.29335466,5.800242694,66.02765219,grapes,15.602127425028119,3,10.516704489781905,5.806513986085527,434.2757034578024,2.5643969116508076,3,7.171846852972446,77.10854473591296,195.84908559672579,3,0.5671276899259725,2,48.64512826154168,4.012138945227582 +9,122,201,29.58748357,80.91934392,5.570290539,68.06417307,grapes,15.125943929784174,2,11.68043481447691,10.401621923211646,351.8697653908431,3.2481087816261347,1,19.521609034369746,30.790299768514405,53.28686107745293,3,28.286154706873194,3,91.29478551750368,4.524707521019794 +16,139,203,17.82803682,80.96093443,6.27564088,65.84748763,grapes,25.623572519850256,1,6.512361567994478,0.8541918193835185,448.0668048342269,9.707039205011139,6,5.152325825186536,56.64718722087716,92.74499870877445,3,39.736595880271196,3,30.98365250897006,3.055783516130485 +32,141,204,8.825674745,82.89753705,5.536645599,67.235765,grapes,26.256420146185043,3,5.104906777333026,13.874822911267069,388.8692328642522,6.917806231919146,4,6.279970139168548,22.14687499970892,93.753587828945,2,46.868420934854086,3,52.21090210868722,1.3779595528012223 +22,138,195,27.83487131,83.51444973,6.208196881,73.02882766,grapes,14.947970460124811,3,11.274838151798434,11.861619528005267,366.90603557749364,7.382527541925609,5,13.411083618687115,33.31182540659876,119.91495334365248,2,46.64284780125082,2,96.17276879008661,4.591922307636116 +31,144,202,11.02105378,80.55557235,5.870600622,68.23963161,grapes,24.18920529781756,1,9.660157539001244,2.653339247290085,354.34884892952886,4.252726370982826,5,16.841870992662543,49.603077830365926,178.5027974917523,2,48.55032992861984,3,32.64937918265678,2.8024966896256607 +3,136,205,17.5862944,80.84806564,6.334771461,71.4065452,grapes,24.824483905062962,3,9.168872999954157,16.220509171661,448.12834192875994,2.5386487139602956,6,8.500404640651968,81.10735266935973,124.91581943356606,3,11.722363792144058,3,46.05430237647562,1.5302207968741057 +28,122,197,19.89363946,82.73366439,5.856575335,69.66256816,grapes,21.913312119485408,2,6.598409695714603,13.960380820535976,446.62675043442425,7.506281103953085,6,5.008420799267784,40.0785001460237,139.84672168339898,3,22.08136439551593,3,32.87276082058441,2.4748215350067513 +4,136,204,29.93707596,81.77713468,5.898944282,65.52279323,grapes,12.661393448287384,1,6.272751682423989,11.963985908137785,359.9507870743375,7.854175747491036,2,14.10140289237103,56.88557046207761,77.83189227854476,2,15.654898788290266,2,67.28258087046977,2.299443904511152 +39,145,201,36.73126647,80.58931938,5.775600435,72.24230804,grapes,19.020993788030772,1,5.421545702504631,0.9734116200489251,380.64949897653395,6.646926257444949,4,16.651619383503267,78.06295098668106,185.70645377513867,2,22.714501318257486,1,20.599291587648494,2.626357729763256 +38,132,197,20.42094753,81.54185044,5.931101816,66.93065667,grapes,22.007679841140426,2,7.640145692674437,19.810095075940573,438.62336842778325,9.323514852028191,1,8.57292676286141,21.443359639874014,155.27413571499994,3,18.95569323669271,2,20.012340025124576,4.525111774606778 +36,133,198,25.51939719,83.98351748,6.2286454,69.17281221,grapes,20.301319031545216,3,5.832292298220323,3.509860634463393,439.1272109977449,5.3931871625711105,5,13.89284217112483,23.14839141840055,63.44562444176315,3,1.758717705490398,1,65.55490814145453,1.5645308365154524 +25,121,201,30.50734778,82.71775569,5.594240603,70.08200379,grapes,11.714645667151553,3,9.123124856143942,4.624838205736738,443.52367130345783,9.828679122892133,2,16.988446917781808,77.04206167833692,134.99564651810348,1,44.1853390008047,2,32.40568155287621,2.710896733466158 +15,125,199,18.4269936,80.55625868,5.569230319,69.75734306,grapes,11.402795765612453,3,5.415000927988154,9.058340850052307,415.38574223372353,4.77077048165404,3,13.273344850108584,1.6838691453127108,189.53399071782414,1,27.297270639100272,2,55.12182974738206,2.7444985388383274 +24,140,205,12.087022,83.59398734,5.93202852,68.66813363,grapes,25.638660365926764,2,5.2578812407126305,11.917367652633072,408.2985743728025,9.451805466081062,1,9.127122437919297,40.58738553751561,71.22365474829576,1,40.08171949453727,3,51.233287865809075,4.557430900694884 +13,132,203,23.60115364,82.48336987,6.423216506,73.23901752,grapes,13.037254641214304,2,7.729509764612611,0.1145656097540737,408.5641414297775,4.74545611181106,6,18.399378098605844,51.444853231293116,110.93762484310342,2,39.36563993875235,2,62.698759877454165,4.817665636372601 +5,126,197,12.80000387,81.20876367,6.417500829,67.10439401,grapes,11.388349185657002,1,5.305920704380046,16.811210883275766,439.44387905158175,9.035840212309639,2,7.314279476569183,52.1248138321187,139.6791809291173,2,33.10709879124295,1,79.6415303797557,2.4115284903008396 +30,120,200,38.06099482,82.24729637,6.234904253,65.70148216,grapes,23.02240762427472,2,9.245744833616143,14.568916633004918,391.46138345864927,3.4283825755704314,6,9.574439121427538,63.49071187479104,156.67289734318328,1,43.84831710583838,2,34.720313873210195,3.5638260464258344 +23,142,197,39.06555518,82.03812973,6.000573725,69.30772897,grapes,18.246309706544192,2,8.578819745337046,0.40823999015429546,405.7531416321484,3.0880087384165913,2,9.216907322548213,49.40356417964426,86.13634506083083,1,9.338311709091379,1,91.21933800695501,2.0780742547913538 +26,135,203,33.78372897,81.16314317,5.685102769,74.53557341,grapes,17.225275574266917,3,6.605099881988845,13.320447855768547,442.00542866104206,6.705408933945247,2,8.80017799766222,17.14126267177666,129.1971075875992,3,31.37664190874431,2,58.33450994690874,4.020210665323116 +7,126,203,16.76201707,82.00335557,5.662140095,73.28712806,grapes,27.672479056191204,3,9.752134275893944,6.720288958979676,374.17172069815047,8.321419633984124,3,6.763185328863748,20.504022605895788,102.10683839764002,2,7.437883396966666,1,61.51929802015703,3.5297373394078213 +32,139,198,35.89307536,82.66850729,6.358186848,66.53946559,grapes,18.778872334351057,3,8.317899432902026,17.983821619121127,362.58061077352676,4.563895742321762,6,14.020573036443768,71.56820710479495,176.27241468035012,1,22.233061009059256,1,80.20093987264364,1.4753751027696613 +9,141,202,21.01245395,81.17931863,6.119495295,66.38448261,grapes,10.984919948676247,2,11.083892588240836,1.125775254890482,372.48744177347766,3.2882216470282795,6,13.706144121932423,43.67954866817977,53.91278689911475,3,31.750821620064272,1,78.04498392158962,3.957470130811709 +20,142,196,10.89875873,80.01639435,6.207600783,68.69420397,grapes,19.484099304103143,1,9.024742549931304,4.154619297336506,433.11640596546465,3.7319837735489836,2,16.074808875384193,96.62295868778337,135.7033850373049,2,1.9091230143778948,1,7.559196663725054,2.3186550757608044 +32,129,201,16.36251869,83.00471609,6.48754639,71.55665483,grapes,22.330633948436315,1,11.70067218917779,12.046701925160114,378.476363972584,1.091073418920009,6,15.376265974755675,86.8606693262562,116.16818244352449,2,46.15320406827636,2,42.93254505523705,3.9679546221730253 +3,134,199,20.28370163,81.32235739,5.81717753,71.06611222,grapes,23.788590159525025,1,11.45198497060995,1.4266814185660315,419.2313734357698,9.578323320405744,5,11.658965244237876,41.78807874899145,61.0359298873191,3,48.68084965178243,2,61.11486392126577,3.5208659698284177 +38,138,204,25.11108456,83.25447587,6.325480034,73.01026829,grapes,17.313184729706432,3,7.572945985229856,15.400613783094379,384.6826211859224,7.533527815604889,1,16.695660038546617,42.58665791642373,176.09719037371823,1,0.7349057764742373,1,16.30386230606242,3.964191081353774 +14,131,198,33.4641162,83.86742974,5.562790949,67.92204319,grapes,22.70373097856308,2,11.277909607976035,2.4007763796901793,377.1429785845643,9.446816909302862,1,12.13034679726718,49.4791239109173,189.46950408972734,3,0.6069531234466463,3,83.68687215004906,4.266625817700119 +20,122,204,11.7976469,80.86325389,6.487369687,65.06962486,grapes,15.056620653688066,3,9.771058773683471,6.496278990519164,404.968810079458,1.4483502508596757,2,11.938677056889574,21.933664870228053,171.91035872623956,3,33.70341307436038,1,30.40902279063885,3.770295642292007 +40,126,201,11.36300891,80.03100049,6.116982944,71.18289431,grapes,26.26395315703626,1,10.731730065116317,1.8646909789620914,368.8449687551723,8.610899420893688,1,5.455793986187772,24.048948421460658,182.87266604173357,3,19.081183316420002,1,40.14600762842054,4.3970182868502 +36,128,204,25.23542319,80.68700527,5.695792761,67.03840888,grapes,29.228987087502926,1,7.953328621750047,4.621229657619066,381.44692381605773,8.731969679665411,1,10.37043144179264,15.378505447760027,91.08008173258065,2,49.069225835521664,2,98.49465493055452,4.881125265537981 +11,132,197,15.99050693,81.23966573,5.734317007,74.40198861,grapes,26.830585221895113,3,11.727966320700226,0.09929434017231786,425.99981805100714,3.080484486764996,3,14.205326164246427,50.93582002553754,64.91055509058538,2,28.95764336455795,3,33.106916494267914,2.5022625085058503 +0,137,195,22.4359017,80.18612085,6.329499832,65.3973168,grapes,10.185962454520539,3,6.374898764779577,8.234247249946765,361.47191909719635,4.17996434585633,3,5.736138588625649,12.141341159915864,81.17487461980579,1,49.2775838078464,2,70.49484985269643,2.3781468933416363 +19,123,200,34.76086052,81.03544763,6.167013532,65.70430027,grapes,24.559204486515533,3,9.89029672035699,1.8977599192064143,413.4592038269283,1.645727398080517,5,13.489433419563987,28.264055495345175,183.75570065479465,3,25.51844230794774,2,13.368062567845218,3.319320942042521 +31,136,197,31.11047251,83.34010951,5.653776058,71.43001582,grapes,10.33026980115875,1,8.116303555856828,11.629396371478407,363.5635071504812,2.387687198984419,2,12.25142140852855,14.915508739120797,105.01724328003681,2,31.288994332857545,2,50.20628143766799,4.11328167218692 +4,134,200,28.57828803,80.95628959,5.840256272,73.34232097,grapes,28.71880532328536,2,9.931031915769513,2.131959700114805,350.4774122433321,5.428885364568997,3,12.267307865618344,68.49194070366936,195.25998606350882,1,49.52861751346978,1,51.177923385594845,1.2812860235102677 +39,139,201,41.18664903,81.01783402,5.539980812,68.68895899,grapes,12.65252837641646,1,11.692619282397615,3.9596626389020617,434.84787777127445,7.637829565488534,4,11.720919652266238,2.286993835886464,68.74730964413166,2,5.1317343542841956,2,54.632539929657,1.7357885358308747 +8,127,196,27.02766138,83.17093908,5.833302165,70.95666003,grapes,11.248818123109782,1,10.17051940229179,12.827381171417883,369.38433672624257,6.207797843565917,5,6.438303369448963,39.717974239698414,51.86176667044706,1,13.612639807133359,2,87.82006368671365,4.349723002199182 +39,138,203,21.19339319,82.33098331,6.399433771,74.62834921,grapes,24.271407784450503,2,5.080092933657373,9.543228115771548,365.73895896160366,9.10404923306351,4,5.726596281939259,17.790862069907163,82.42792699356917,1,31.65539161490829,3,3.0431481833970153,2.565814177465535 +32,120,204,10.38004759,83.44518113,6.138958698,67.3917379,grapes,11.099956508299684,3,7.9911382124626655,2.287884836566678,409.07160095456607,4.460667301565087,5,12.015068643615361,70.85331880982056,71.06269247215529,1,38.31314220670094,2,70.52943983971296,4.631603877553342 +12,142,203,31.3115978,82.56407013,5.972850838,65.01095312,grapes,26.56929895755497,3,10.980043622142704,12.495750389982078,398.15881312992946,6.493816722144114,4,19.578171160783512,97.21760002197806,55.09117389599558,2,9.823946162797098,2,30.491511073989397,1.4438023915730076 +8,133,195,20.46657776,80.97598029,6.456079585,71.29813872,grapes,28.33684740894858,1,7.62774362566026,4.0387081180481825,362.99164315482886,7.832591130601337,1,10.376261475908896,20.175634961200505,166.9901505026583,1,0.7892802288247291,2,82.53756676856794,3.6960584939447156 +8,139,199,29.36947679,81.53996362,6.336426667,66.13442813,grapes,24.105264289258383,2,10.653899759801124,13.02778097617447,393.5403994006502,5.455774175167231,1,17.500781133178577,37.773403804841756,166.265297640808,2,5.2127422792745906,3,60.718001062256974,4.503221067785754 +21,134,202,10.72302459,80.02130636,6.425419926,65.2982112,grapes,11.56502365464743,1,5.081437222586004,13.296203541737396,407.5691674130522,3.5424640914603938,6,11.62723947182315,17.85880150818482,193.351124606585,1,45.25056352245708,1,92.80570600008662,1.738080666061014 +40,140,195,14.97846952,80.49979873,6.294395676,71.63437433,grapes,23.21829705289648,1,8.29443769788704,7.32954846737444,388.0038027841902,7.060451134365646,4,10.929934293081658,23.072847383351693,151.39212603584306,2,33.323860896664655,2,51.32731796105507,2.7928653957554683 +39,127,202,15.3246651,81.67215994,6.477768039,71.60102999,grapes,22.6514815456319,3,11.195810784011428,5.1655282620879595,355.80987678243946,4.555670903851331,2,11.796360538237415,88.84990126848457,55.82584035452409,3,26.810838523526158,2,39.7445881297884,1.2938186809577523 +19,120,195,18.73932187,81.12109244,5.931538447,73.55807954,grapes,10.27446969830681,3,6.245597846771007,13.972374523228428,406.93069399013314,4.285511109385019,5,18.020010918914934,48.852104201198365,50.60435372748454,3,39.91705440640124,2,57.15418309356334,3.9277137118464323 +21,139,201,19.3642553,83.36094029,5.980598579,67.15094741,grapes,18.835335118011184,2,5.179628807913219,1.7613430841446243,397.14776289454727,3.846894053487636,5,19.6610653675873,98.93733455295266,147.47028618002685,3,30.26722019556963,3,67.78593240432608,1.5102837087411713 +17,136,195,41.20733624,81.61051026,6.389783283,65.90227462,grapes,24.93024936779359,2,8.363752566912844,8.758555086627437,400.03080966392116,9.515966979663878,5,16.368138782154375,88.60967432713794,138.26944034209043,3,19.06392630277657,1,96.79660512842855,4.974642460911946 +33,139,203,33.34214482,82.51034633,5.693287415,70.68098614,grapes,26.886004500313,1,6.712088787345264,0.20182962784198066,379.10446851795376,2.1311217136044744,5,12.244586002443643,27.107031342348662,124.45694859700934,3,32.44756555297195,2,66.22154842160171,3.736296204920579 +22,133,201,23.81995682,80.12211649,6.00299607,67.2739864,grapes,21.902404473279574,2,7.53242229993586,11.940202221103718,444.33164289383694,9.691940499614397,4,6.173710232033101,17.608914066608406,68.52576369985275,2,3.3247710602555216,3,72.25644961041607,3.2941308384950023 +32,130,196,40.66012294,81.24995984,6.372959542,74.03030056,grapes,16.96396246781107,3,6.787804831538917,12.46516202598829,391.0944074197805,5.992741677348981,3,11.077030190903617,28.09357312305769,187.90639629883864,1,44.20485697002182,2,91.99722468734572,2.8711388334906895 +37,135,205,11.82768186,80.2827185,5.510924849,74.10225057,grapes,16.780189640221295,3,8.666381051328106,0.41188088463707473,360.45634044873464,1.240644815077002,3,13.612812385287597,41.87171720324131,175.5998131314331,3,42.72026171451932,3,33.19890873317745,1.3927675643409123 +15,140,195,13.28504331,83.54193816,5.69945282,65.80006004,grapes,26.202623566777866,2,7.02492089451006,11.441332825410509,429.4283456208621,8.453876561532468,4,19.5874026788381,81.23989918055776,173.89997621003664,1,12.224867392471817,2,84.304563020987,1.1087376252745944 +39,132,196,35.83089092,83.32560104,5.778594403,73.67984885,grapes,28.57864596383116,1,7.734796919962665,5.891193279250933,361.54073734314903,5.8992461137105945,5,14.002928625779308,19.651000375933926,69.88824642409867,2,16.429004244922307,3,35.83890517436713,4.140179768147533 +40,121,199,26.18159716,81.03886263,6.315586313,66.05911698,grapes,18.163780013724548,2,7.921977942093024,5.612752355685182,416.45832486846814,9.322689539791348,5,12.843356663121389,72.83861793260836,163.1320381593003,2,3.6157983874724686,2,64.66001930270714,4.613628182929984 +40,132,202,24.57558351,80.70695797,5.971813006,69.706113,grapes,11.329050465502027,1,11.331680324553712,6.840271860012413,364.4129481860432,5.623231912882943,3,12.471518969288198,34.168194443420965,70.88071344821763,2,8.24992608246513,2,63.2677275393563,2.979517587232062 +29,142,203,29.67229086,83.71498986,5.891195653,66.48490371,grapes,20.76540487878467,2,5.045936530403499,8.286503152503077,391.2862239602749,9.591580621272378,6,11.706696582783232,64.45872913094712,155.47061887207352,3,47.91104239210136,1,7.996957150392459,4.831072607796554 +32,121,199,39.37102553,81.25353895,6.129812716,74.08101744,grapes,10.424958203965502,1,8.03649054811858,0.6047804158632575,400.182834061015,4.14333578131997,4,9.081836627964282,76.09384661279304,183.9097310415527,3,33.62969456103901,1,86.58539132781354,4.9622661494218665 +6,140,205,17.66558428,82.92903419,6.313085601,69.8671263,grapes,17.48389157563706,2,9.318153222448208,19.895476954971233,447.0562512025941,9.821364391687453,5,13.460000946223554,86.36946413396342,165.2600118877042,1,27.329844229914258,2,26.950453295944822,2.1618876272649734 +8,120,196,24.06679352,82.66396666,6.053662544,69.81855775,grapes,25.150931295049116,1,11.091368105418073,5.243991850902381,396.0690072292885,7.218775284520737,1,16.53161218719352,32.664062115052,124.78550780130074,3,35.454439554881056,2,53.31230406022406,3.1876566835827096 +34,133,202,15.31413469,80.09711412,5.804799142,74.82144653,grapes,12.70042217067424,2,7.6195920897077,15.579327217018976,415.3582206190508,1.647927074289674,4,11.766448768038675,57.60959618195541,62.12600925332787,1,23.879019001306727,2,29.55542338810243,4.080248962505253 +35,135,199,21.77466746,80.54942557,6.400719746,69.39630398,grapes,22.03301080540591,1,9.037526174777032,9.811619154845095,431.3343482040615,8.259090493343354,3,18.813378474671214,6.26872119566837,173.64843125729539,1,17.21935439478923,2,93.2405561435586,2.0898611572916845 +16,145,199,26.91624843,80.76838926,5.953966361,69.30927185,grapes,27.55411739335504,2,8.784610860931343,8.244979185675911,408.43138902255106,4.9936764112314656,4,6.320576889865194,98.38571614897114,50.38834294155715,2,37.26368861089372,1,27.60390218574853,4.772762438213158 +8,136,201,41.65602996,82.22118237,5.609255992,74.19664838,grapes,24.439522388269978,2,8.993371822317629,5.693625193255554,350.11290992847375,9.479764546641643,4,17.32232250193894,20.119656300413357,52.964845601130236,3,8.651581224393556,1,95.33081102689184,3.1837057272320513 +25,129,195,17.98667801,81.17712085,5.777271492,72.37127689,grapes,13.372461857545963,3,7.228892156071948,9.79788707305094,409.2209212871498,4.151222912077351,1,12.622412381288225,7.036277248575329,78.76704627423113,2,33.12339265627998,1,57.764446852955565,4.230184152402504 +16,130,201,29.12033769,82.79092939,5.682395429,68.8503047,grapes,29.48187169979946,3,9.236200125773456,10.847404002881937,443.30097904129707,6.762449449647546,1,16.39860447162043,58.63846535812134,137.89720380913064,3,7.641042673429855,1,78.83405081873346,4.092488990882343 +39,129,203,34.38922481,83.18392806,5.863996687,71.03001556,grapes,17.306128817610425,1,11.049779931289248,5.885028683478135,395.4139855587744,5.615047121775154,6,9.833618826213746,43.40807993652115,168.5822199141312,2,46.5926126086011,2,69.6844159768035,3.4939416343831726 +38,135,203,41.36106301,82.79782954,6.444373116,69.92107482,grapes,10.070927185903988,3,5.833881648624252,15.703769383785426,374.821216089624,2.1099155779397702,1,6.169632647772331,12.08803969571186,156.3039986054594,1,6.547539853987533,2,30.569372587717503,2.562292583436751 +33,120,205,35.12158265,82.26890793,5.550832178,69.71518491,grapes,24.796835911314528,2,5.2637964663816765,17.604783978881635,410.74045347777417,3.617901302616278,5,19.088685389896888,3.6638480186973688,191.2988880236906,3,30.666320033995987,3,69.24332695563264,1.5651268195707222 +35,125,204,19.6491772,80.15215777,6.107741788,73.69529586,grapes,14.012818343701646,1,10.77974296358557,13.11953204166409,429.7709641817895,4.256516055037412,1,10.63103895230889,1.8598415610821206,111.76343857772153,1,37.85574606900667,2,62.5210412576062,1.5607359373227516 +1,132,200,16.27852801,82.94270065,5.620745638,66.57462809,grapes,17.186455084550587,2,5.55954068390443,6.707939734563597,441.0045955654954,3.862613625906442,3,10.224465999460472,52.38547729055938,176.2615802492927,3,47.00922795224749,1,51.745418093696394,2.164823646160302 +39,140,203,21.11903604,80.63399198,6.349875906,69.27779761,grapes,19.64843562238713,2,5.596897115103051,10.999640955823729,419.8522368055458,7.659274996695711,1,17.879488669073105,41.025313255597574,61.958756044626675,1,45.84986719889096,1,57.74241995332762,2.6931522440398594 +28,145,202,19.2077707,82.9042841,6.484323189,66.83113717,grapes,11.879864319508734,2,6.259675705099257,6.29392912091302,431.9056557301724,3.926291112489052,2,7.351637762348728,49.63497688724969,94.74695707138625,3,24.70709638130189,3,36.435427767906106,4.044358560196688 +6,128,200,25.96308415,82.57813624,5.838748311,70.31782647,grapes,19.183086182483958,3,11.100008005571947,7.813251814291895,448.18996397936934,1.326478905904735,4,7.093745902612716,66.27066113443594,158.25459172539072,3,20.671769665987988,2,13.626203298986727,2.3356513425782808 +6,139,199,25.67385024,81.6212135,6.29099842,74.10919422,grapes,12.951263015127374,3,9.536327630198851,7.643248929356821,372.0350798310628,8.930473392105004,6,16.28513743787471,86.18134725793892,165.86231913387093,3,48.78535470874981,1,31.974293817378474,3.35951045640167 +29,122,196,41.94865736,81.15595212,5.638328481,73.06862952,grapes,24.30883058797581,3,7.415010139677875,3.8245200311142513,362.7021965388745,5.15464513342752,1,18.876190677800132,6.39297450833769,183.01863299393767,3,6.448507977338696,3,27.158414043274405,3.8722001414603433 +37,144,197,11.18994268,80.8084305,6.415555956,66.34234944,grapes,16.086148154120384,1,6.164542047138376,7.6537148666786265,414.04683183621717,3.484522073842711,4,6.775538323136537,71.09435719648845,165.63728297599863,3,10.403198830652277,3,41.86682700887363,1.099840373463767 +38,120,197,17.5438296,82.94703302,6.323722572,73.77063744,grapes,20.805396719280253,3,8.966821066317959,4.149904103132645,385.2810958091512,4.992222664186304,5,6.304349792346059,96.64266870038215,124.77367930959284,1,28.632466204273367,1,52.73731819830797,4.043878163201191 +38,141,198,13.05809741,80.28297993,5.757009965,70.75633584,grapes,20.22125963985225,2,7.240012660812663,4.136524286274234,382.40785009593696,5.5736279176939805,4,18.794372687325545,53.60714098048548,62.550071797767664,2,13.697315885434774,1,36.315315005422576,4.124759069558038 +14,121,203,9.724457611,83.74765639,6.158689406,74.46411148,grapes,23.497584478464198,1,10.793036903829783,19.833046897012842,405.18074459836185,2.9796519112185127,5,8.16905154879666,26.505881165945567,140.33962000081482,1,44.31777615804294,1,83.82672727648202,3.936387707998109 +6,125,204,27.92004934,82.93262435,5.733539807,69.92092839,grapes,14.387860606427491,2,11.208778285904451,15.674615951677612,388.28358771928123,4.190594180836482,4,13.595601397148558,95.42073627275711,163.4620666185328,1,29.1162936611071,3,67.56984387976344,3.2469504360013137 +32,138,197,9.535585543,80.73112694,5.908724337,69.44115171,grapes,22.546024199984057,3,7.019064616380854,19.9199826107443,354.70671601584985,8.6460300788549,5,6.314869927370623,69.10892334790981,129.61421446574238,3,33.20521376519993,1,17.717004761098686,2.309931003395532 +11,124,204,13.42988625,80.06633966,6.361141107,71.40043037,grapes,13.617083412408157,3,9.313099372779437,8.312904500697812,366.80669690745003,9.218050237827853,6,5.78881231493316,3.259690907889623,107.02740662403299,2,26.966940784868417,3,74.15920454286797,2.9874734314720066 +23,138,200,9.851242629,80.22631717,5.96537863,68.42802444,grapes,20.659253035003697,1,11.5931261600174,5.980267101423409,391.01785525778945,1.278220659232014,5,8.906972853132384,57.90837285963063,89.64248019859518,3,7.600589668165108,3,59.84541813807584,3.4226851937934066 +40,143,201,24.97256132,82.72828653,6.476757723,66.70016285,grapes,25.79774742042684,1,8.493290316580499,18.63055699687838,350.8865050150381,4.790904205690039,4,16.19371250199152,43.61904284821132,97.97807742822201,3,43.41065092752765,1,23.406272164870934,4.608156963527788 +6,142,202,27.23708304,82.94573346,6.224542938,70.42508897,grapes,29.968617484794752,3,10.555048085917589,12.362612093517553,382.68602826550466,2.5818124859462848,4,15.980734383080032,29.77239252138999,69.26566539091439,2,39.95702601380175,3,36.5853936743491,3.041394154068765 +37,124,195,18.70679077,83.4795292,6.209928251,66.5964488,grapes,10.585878969377427,1,7.0327914305877,9.991407035457643,448.83098416211885,2.509372031536241,4,14.250548651478022,78.63178056653503,117.51270786951746,3,8.575076617643147,2,20.510575841670498,3.1768969706708146 +35,134,204,9.949929082,82.55138983,5.841138354,66.00817551,grapes,13.620887594116882,1,8.60428261016723,19.87887962194215,356.2586011043857,1.9347815994509894,2,7.034460884221852,83.61378677730937,180.59551886170507,1,19.0046980683044,3,59.308094178353485,2.584635792261708 +119,25,51,26.47330219,80.92254421,6.283818329,53.65742581,watermelon,15.08057143272421,1,8.027283110067453,14.96982191349759,414.22599700584624,1.344140889128729,6,14.523878245032087,22.364049487884763,161.28518180011326,2,18.5315340878856,3,33.02319610973146,2.08080145911473 +119,19,55,25.18780042,83.44621709,6.818261383,46.87420883,watermelon,24.427121599577383,3,10.101718272355503,8.170348736431368,360.6510451474165,3.2532672950310437,6,13.908764762614,13.710835045428416,168.44399298878298,3,46.65051039730494,3,7.165788120495553,2.990449002019592 +105,30,50,25.29954705,81.77527562,6.37620108,57.04147057,watermelon,27.84067676259624,2,7.579268771166818,8.261587575971678,431.5347959508772,5.734523032741364,3,12.16716920366361,69.97663780354762,92.77118102875127,3,37.01421915647401,3,71.28015676851679,4.313107985607182 +114,8,50,24.74631269,88.30866319,6.581587932,57.95826144,watermelon,21.722580321044305,2,5.772596623130273,19.181056439686063,372.86409118748287,2.921236687365518,3,9.81437262168684,58.11745110331942,79.80731930159591,1,26.132573421882316,2,93.85424063745279,4.577691526922323 +93,22,52,26.58740671,81.32563243,6.932739726,41.87540028,watermelon,21.602344170023034,3,7.231309663750425,14.066736680175262,352.18779364242596,3.449338253540801,1,14.41379909393983,14.856152630265996,134.883311430444,1,1.3028932416761008,2,8.119585085852432,2.5212047514151625 +80,26,55,24.53442564,88.989272,6.140099215,49.11618732,watermelon,25.12719040421041,1,9.361535531115965,6.5253267734110665,398.51599018239193,1.9214001561947591,3,14.633184455423498,94.15471968944591,100.88329992197345,3,3.4643239345160337,2,52.15214520820989,2.6302694441372974 +85,27,45,26.0713757,88.7285657,6.467095849,57.79652846,watermelon,23.830453659792354,3,7.850181282368382,17.03606065616996,379.2687004119619,8.289147416341635,5,6.606951476626887,10.193711226253644,122.21610525792188,1,41.96179721013296,3,70.62777844750879,2.516279113240213 +85,22,53,25.96534238,89.77076659,6.849471704,59.46338556,watermelon,25.314460823671297,2,5.020614937208482,19.921116566851307,405.480224870631,1.9685751029663738,1,5.408733667279752,64.51343391119512,134.5862112896906,1,37.53404159645669,1,90.11547922911858,2.834910565512706 +82,22,45,26.22338015,85.34866045,6.512196212,54.60159289,watermelon,12.911916496105801,3,7.590671245732807,5.333382637164979,439.9962068503652,5.432401316970583,4,6.552262385631607,38.83775059989665,138.68233548882418,1,19.98851493752226,1,71.51384950893846,1.9139448763013949 +118,13,54,24.41311871,89.81574032,6.039584629,44.07843475,watermelon,29.529001844156213,3,7.3296592598999855,8.942970321241727,353.59423286169925,3.6708588720555113,1,12.27144021304244,41.09571996044994,154.20445015719667,2,23.17977192080432,3,6.7463574982636665,2.5287892400488894 +83,25,53,26.49195283,80.04678201,6.057697106,57.72799157,watermelon,18.944428139050196,3,9.774312500937185,14.754508319120168,428.5706645376688,9.684923032163953,5,10.225017442884901,5.248957745208537,104.29173227112187,2,2.4685429446858254,3,7.128698132757338,1.1587747091123592 +86,15,47,24.04355803,84.18406764,6.423898762,53.78929956,watermelon,26.621022154819595,1,8.46397942792327,2.7842410020183106,402.6465894153068,2.7648125900723537,6,8.978576670397572,51.53359901794784,146.74711575342664,2,30.9770688800553,2,12.65170991141672,3.2300132965088393 +101,10,47,25.5421695,83.31883376,6.936997681,57.57343233,watermelon,21.088463278597516,1,7.2436679849194725,9.836275473120022,388.97486235110614,1.1802005614026998,2,5.958716120730546,92.03340144377707,165.42229088622418,3,18.918563426276386,2,94.24733640915572,1.0307288884131989 +119,9,50,26.74550678,83.9195902,6.251286661,40.794305,watermelon,23.37443777472351,3,5.645978957694785,9.490062564344397,449.9843895315361,5.373932968490006,5,8.68954288117213,64.18630578773784,133.96539496961844,2,28.757362333676646,3,68.08270480000127,4.481753214768252 +104,17,46,25.7131428,80.22972777,6.190015912,43.08961827,watermelon,26.077866311517653,2,5.142872354139186,4.335479443790462,365.70973679730776,5.402968184419812,1,11.562772955033987,6.670529897760669,90.76985197987518,3,35.1229878236265,1,75.52895985858721,2.4022547261433167 +95,12,51,25.76484262,84.1726996,6.681606702,44.22066914,watermelon,21.471103993490335,2,5.207475486756047,10.752781667389634,430.5980311977573,5.573705502859079,6,10.154983810553922,89.03383333816734,70.37777676276994,1,25.450505891309806,2,40.46983396646628,3.2675031838461117 +102,14,52,26.79489868,89.64815231,6.51075991,57.74091817,watermelon,27.732078309628573,1,10.919997622339245,17.638958675979968,449.1074899046401,9.133904216544106,1,11.19473276779887,70.83869482143355,53.49338153291603,2,16.728700411814003,3,32.02400037667946,2.7659930888304234 +109,21,55,24.9004602,89.73524177,6.770278088,57.44942094,watermelon,24.415408129749835,3,5.701971325520555,3.76419869938166,427.01190768716117,9.712236543802783,5,17.14764564566257,17.049870293077852,108.49218094873126,2,34.16245591304561,3,9.07253393126699,2.055680592974894 +81,18,50,26.80750629,88.22874955,6.429788073,58.79889057,watermelon,24.026217182541146,3,8.706022169270602,7.244390807975396,423.08737097714845,2.1421991179394952,3,18.44552470032312,68.94815590491868,75.41706822969478,2,42.21221946783022,1,1.4787739190078697,2.051408429628882 +103,17,51,25.11189154,80.02621335,6.209888345,44.20656987,watermelon,16.80893605080174,3,7.639288518986463,5.475668322543932,391.64639733328755,9.667935901249521,4,17.12373830028952,47.17226506077539,79.3620932170511,1,33.148646162923455,2,46.06870132818089,3.8854460868794183 +105,14,50,26.2148837,87.6883982,6.419052193,59.65590798,watermelon,12.322833033084805,3,6.310621094426313,0.7466364312138007,408.4361893285713,1.9999768199040557,3,13.959919723447143,24.99863137888757,168.2725521814658,1,32.32397981360736,3,19.85793442532813,2.1665464442055984 +97,8,52,24.9103226,86.97190046,6.237861736,49.48575692,watermelon,22.4253864099393,1,6.950997185612286,13.733534472239695,391.2269331822146,4.335658249427974,3,5.204122428064978,63.808698603534175,60.86100923451223,1,32.332715711310684,3,60.75139543404752,2.860168528309965 +120,19,49,25.79448878,84.26830701,6.762471629,56.45229202,watermelon,23.61077070207454,3,7.996035175997341,14.987432022263704,387.497385907457,1.4383381802874893,5,19.394208196704923,67.75625932943122,180.18104003405432,1,4.580019100852178,3,41.413424237486005,3.8402128503947788 +95,16,55,25.26931156,87.55055105,6.612847999,40.12650421,watermelon,23.471283449453573,3,9.637369140486765,12.639977059334768,361.9440731272848,6.419037989361491,1,19.988474247705994,87.9054365835576,82.77037833553902,3,1.1960148013349603,1,55.593093607253486,2.710534773314128 +83,29,52,25.76402693,87.5931128,6.704688865,46.05122728,watermelon,17.722522156618602,1,7.62611267881841,15.082652897015977,419.3680018395526,4.166017930877185,4,5.1687481476634805,3.5145788671858247,185.04592558926538,3,15.1288871740911,3,86.27092572521985,1.613740632798009 +83,9,45,25.85483596,89.13163965,6.049609892,46.85176955,watermelon,16.788202945031202,3,9.798498651724604,2.2293186222111006,404.0991999914397,7.913993375279732,6,9.173622273050656,55.802304448060916,169.48296366236775,1,20.568616286704277,2,79.6159480812863,1.1712700919686259 +91,21,50,24.33528185,81.44030363,6.762030215,48.32113628,watermelon,17.437142014543618,1,6.1332952013513085,10.754195814955164,372.15779816203764,2.9407617210917616,3,9.645266632531841,31.006760561550305,60.13001053688698,1,24.41144064512491,1,25.76694512183425,3.4697449659086868 +116,5,54,25.37601283,80.99313508,6.65398725,57.23028471,watermelon,18.08431334081383,1,7.478626433876013,7.120855149425889,394.6132745782334,9.088694148318243,5,9.57679867302351,47.20326685878504,85.98139515218116,1,48.87996712188805,3,75.40355689788854,4.036225568416358 +112,28,54,24.86094646,85.05318563,6.738030547,55.29563514,watermelon,26.378982381060098,3,8.837854311390593,6.130790942492805,446.20107397970855,7.612079525024436,6,8.209344107060833,60.02285262140341,158.94908165448794,3,32.01692963569515,2,37.72303388268431,4.141245066202392 +88,29,51,24.71885473,88.94568335,6.095689937,48.45978627,watermelon,20.02658110656892,3,9.375478185503688,10.31176461654772,411.3653356542639,7.185595573512529,4,12.97604300804192,51.9748868593766,190.4045172655387,3,25.74300943332897,3,92.3988818325925,2.491773188485261 +118,15,45,24.21495706,84.20576992,6.538006356,48.01138482,watermelon,10.684028294412435,3,6.503033105305132,13.665384978017872,437.6056437205155,6.633508415048315,4,15.109093501908598,84.8225720100479,168.11809095514155,2,33.77242930806395,2,17.22963896482319,4.892097740497659 +92,21,48,25.81692236,82.043255,6.377427122,54.82963379,watermelon,19.777561369107946,3,6.3803952441017,16.717455024668855,432.2430466489977,3.494754945175141,4,8.603747389969428,5.923853939317436,74.06432056298912,1,36.43048610228846,2,31.133732070343477,4.819795995897783 +106,14,45,24.47018505,84.16390229,6.417011754,57.26773002,watermelon,19.587695356431166,2,11.654855160601707,10.24880869099337,415.74433066274327,6.805410542117871,1,15.233409063567308,11.139014315828788,75.54402366252853,2,25.919640881137735,3,54.80059981316967,3.681131488679155 +99,5,47,24.13078816,84.84494575,6.649086972,51.19470197,watermelon,25.428318334979572,1,5.864720192248127,4.731745813560176,427.7455354843901,3.3939878174708404,4,16.157157632536098,49.633489716995236,54.50146691508304,3,25.674493898409068,3,55.71600753587609,4.301176939369433 +98,8,51,26.1793464,86.52258079,6.25933595,49.43050977,watermelon,17.270074219558595,1,11.500941093358136,1.0608125948554603,403.21406079412543,5.049645774695224,3,16.93758030418249,7.729502671158606,130.61575212717233,3,39.341566916834076,1,1.710626706526741,4.473416232825268 +108,22,46,26.17668721,86.7295205,6.121168559,53.33484977,watermelon,27.553654543993705,1,5.430485821136482,18.4304325157788,378.38024489632215,4.470175467745232,3,5.80415584514136,50.954655050655674,70.4126351253287,2,4.438089265648642,3,39.05490499986185,2.3915488472516775 +119,7,55,26.03867719,84.6378378,6.031424482,44.3993381,watermelon,13.602948701916556,2,6.548405730132392,2.490517211820258,430.19643799013625,1.2383314070899143,6,18.08993932820846,42.3022071478197,131.3403914582902,3,18.16590755983043,3,84.55119574112895,1.1621800300106275 +117,27,48,26.53259325,82.39053979,6.835268184,54.30660782,watermelon,21.670056742486587,1,10.572155435224595,11.948188854348306,368.94892490957113,2.48576548243765,5,7.253150936184174,96.72595721642679,120.03831653295755,3,42.041712062899116,1,36.12767272603692,2.6172716606592603 +109,10,53,26.81938687,87.8274604,6.551750306,46.06193778,watermelon,21.819016082558637,3,6.486787135067582,5.189578579072713,418.3526924344442,4.15075438326129,4,6.034845277014539,6.90391056486842,129.85806102189594,2,1.5148232427392105,1,53.45224736158283,2.2056479392053614 +80,16,46,25.50405534,81.40297428,6.940236218,48.47833278,watermelon,19.82711147429742,3,6.474150788233125,15.37127343694738,417.4402110706576,1.0051495621635338,3,8.65068974196134,63.152497040236156,145.29000112141526,1,28.93126609277478,1,53.10149382619578,4.448753553084629 +100,18,52,26.20234499,80.38266489,6.87606733,56.47941847,watermelon,22.03001272495083,2,5.95191968486971,19.766122630415225,426.36759663902035,7.622463789576448,4,18.1589728019415,32.0896582550581,197.963726336477,1,28.69589149938217,1,81.04188873872057,1.3724827914472493 +91,7,53,25.13735887,89.28272716,6.457216535,43.52897517,watermelon,19.73592291960547,2,5.5748843117412274,17.448799536726796,361.8487569629669,1.5913425776662184,4,12.387624878198856,69.26865247717579,95.4069949603555,1,22.688458702061876,3,2.444295827576648,2.253836222281105 +86,6,53,25.92030221,83.47202566,6.921847888,42.10681516,watermelon,21.294355699453444,2,7.146288319698157,12.095184585370934,408.1378838657522,5.59472945569696,1,19.73861644960262,67.31367040793347,189.74731309561963,2,16.820273107879324,2,48.24028685070747,4.284445664724507 +107,5,52,26.6634609,89.98405233,6.881425746,57.40847165,watermelon,10.190471188400569,3,11.553719159703515,14.242953260840785,364.2333579662939,8.698565438470917,5,5.861486327553763,45.39981946361213,55.91068111215151,2,38.12713076702009,1,77.75469251395249,4.586737639892364 +103,16,49,24.06731461,81.64075303,6.915717008,51.75212401,watermelon,12.08634046970216,3,9.46828544850203,15.236036334518936,390.5829439425116,9.772643021617917,2,9.28066180407899,9.051909332210705,114.72785694205729,3,3.282094809061742,3,40.48316956220591,2.935638517295788 +101,20,48,24.6774157,82.75411437,6.206247494,57.05709413,watermelon,15.748256057965616,3,7.280528551190927,5.731066747164862,368.52827894814675,1.2007251791652886,5,15.28405738825433,57.26784609985194,90.97254205712233,1,2.093686147517598,2,44.23354577504169,4.763020017880784 +85,25,47,26.11440416,87.64081095,6.29542477,58.48160844,watermelon,23.720900193686727,3,6.525308812483548,16.426194003955853,405.43714661959547,7.319309478237056,3,11.208862013084783,58.58890101322156,185.41246774885494,1,42.93869185152409,3,14.72689880925695,2.2429487551550404 +84,7,51,26.81530456,87.65694462,6.399669044,55.74073582,watermelon,17.74959335643334,2,7.912803561907983,13.111270364581225,427.27850779914024,2.3380663010928076,4,12.126471591694337,69.17302025929001,67.1883483586093,2,21.58758804009032,1,55.72761443633092,3.7901315367148247 +102,28,54,25.15623099,80.27525115,6.862157042,55.49541453,watermelon,27.387744840904876,3,11.276675440303718,17.339316236520553,438.78689552818605,2.0848187480226836,2,15.68029500501044,72.42898304601184,154.60800348787296,3,34.12604122373445,2,37.200254639651256,4.885916153854552 +98,25,52,25.2801372,83.15393658,6.224066378,49.29456609,watermelon,20.072348046242126,3,9.737051827834474,16.448304752417265,406.1159997356679,3.8310898651628174,5,16.992765556353387,92.44517999016082,164.39093274730334,1,49.64350034301409,2,41.3368359142505,4.208976823296947 +97,25,50,26.22005978,80.90127035,6.093814669,49.08553937,watermelon,22.212875054130173,2,5.837542488732717,0.4146394415575516,430.10724127717276,5.555114912936913,2,16.160844188264264,42.960918764381795,80.5101650011845,2,40.170699093341995,2,57.73288490828834,1.1590580255399567 +90,16,45,24.92093261,80.61750795,6.291540278,50.55710813,watermelon,13.337424516681004,2,8.947332632249584,8.682777538989686,417.4226994891049,4.8046897641749,6,10.751337218333912,18.299802458198698,179.83212132332608,2,20.436254122476488,2,91.04808455988116,4.104947135405833 +95,12,46,26.21667586,81.01009354,6.32281728,54.65423596,watermelon,10.048511242971323,1,6.53822273874278,7.956761916813788,415.1446613771631,7.253802562223287,2,18.018385618937355,74.84494790251048,80.5088989621827,2,40.923278092226326,1,35.01971544401445,4.167206911219157 +82,23,49,26.81383586,87.21986949,6.873283991,51.70497792,watermelon,13.800710213243498,3,6.476821898514528,11.274558870972113,404.01856522110864,4.874897410088316,5,11.939953025453915,6.142642961606592,72.2049318713776,3,46.0158749531781,1,10.508562689161383,2.2289030560828893 +82,25,51,24.31334971,87.47409052,6.074209622,48.11248366,watermelon,12.29387392612072,2,5.712298604428518,11.536306544655075,423.2452181368836,5.710090453010161,5,5.738641538938908,0.06644432345032092,65.1315118703144,3,26.7606117073044,3,66.62767715411536,1.1201956677669633 +110,28,46,24.29105004,88.04541346,6.49889585,51.26046418,watermelon,26.31443477323952,2,8.00074455750525,14.256582480513467,434.8112383432898,4.163335207559089,6,16.85498145265477,14.118301723722471,156.54657477867374,3,25.65840572536876,1,88.96734455733026,4.790804024393259 +118,21,51,24.42998931,86.33904774,6.678805092,48.58241822,watermelon,12.834479681896692,2,11.313343614017715,17.9403890755909,423.43681297517674,6.5294532361024515,3,7.051313156336479,47.80262286402876,143.2130660647947,3,30.858656859575394,1,15.392180006377387,2.3759144136563686 +120,20,45,25.66576039,88.6984228,6.114128685,54.22722466,watermelon,27.30344959382624,2,11.073789201023295,15.520626780024738,381.0052523262655,6.142731061098618,1,13.042482716021372,96.1159317583696,97.37811807988484,3,38.835365210997644,3,77.13132561057198,1.1928217202070766 +91,7,52,25.07803672,83.46230461,6.405054243,56.39962921,watermelon,12.398640117408124,1,10.868039920191869,17.316863646839987,419.68002775931654,6.613028074714131,4,6.798963137732608,86.24981188262514,188.82342966762658,1,6.203623365148419,1,80.63467938126219,3.8325183197345942 +81,6,55,24.88910524,85.87059083,6.110142735,51.70699144,watermelon,21.294322434435742,2,8.541762329278692,3.345948683432396,447.99979124252167,9.561597632178763,5,19.566732282565603,20.575425742507413,78.0782379946382,3,25.020298471375135,1,76.4176335920374,2.0013702609116875 +101,13,54,25.42900869,82.91481799,6.828982708,56.34144589,watermelon,11.409634274015048,2,7.949673367535174,13.515732472298206,434.7398421246137,8.113062130141323,3,12.364499052661383,48.57451161045505,129.65755934236736,3,14.694442430631904,1,31.139881487668664,2.821541789728983 +101,17,55,24.37118217,87.1269128,6.451499764,44.63907691,watermelon,29.177952179205207,1,7.873385160358042,3.721520440778603,413.5288749015404,9.73736174094327,2,5.042549847623982,99.90956902704858,62.32583125548061,3,31.91402728172364,2,70.0089266016951,1.9240125087287057 +111,6,53,26.4930645,88.59143088,6.313512999,46.06382209,watermelon,18.86515605418325,2,11.434095037989884,16.041027350622354,357.5630440509389,5.606794550628744,2,12.938604470340767,9.556894506909575,156.39372900121202,3,48.106760424043784,3,52.87620342915918,1.4551256546789522 +107,10,49,25.83202912,89.00481725,6.755192025,45.24690619,watermelon,23.978503476769937,2,8.744513600914036,14.169677783622785,445.0462371426793,1.4063224928628262,3,15.801012853654711,92.25161079977725,168.959420004097,1,32.2988607252579,3,91.40107787698409,4.3797881987250165 +115,11,46,24.41592661,89.39655519,6.623167177,40.32161859,watermelon,20.602902571116914,2,7.408692340048937,16.22589474104741,444.7601765462798,9.453363503341997,3,14.33997001599311,24.93515968943929,85.04684201197321,3,29.167810804111433,2,61.18234545534843,2.9460337372661196 +84,25,52,24.37190239,81.2514818,6.12532356,44.20899581,watermelon,22.47252299621921,3,6.755166592863305,5.956562847637288,419.2256190345847,3.503217213705943,5,5.688996591851321,51.64439112731623,135.9048160568414,1,28.493075299237635,1,47.73851841020611,2.987962756334236 +120,7,47,24.24782473,83.03687902,6.653867608,54.7657624,watermelon,20.05074631056686,1,10.28000278529727,12.077638943522174,378.46355056540256,6.370378060303753,6,7.2290353258230295,79.06473715248815,166.57147874658767,3,7.477924741677466,2,14.159863605161371,3.8483408298452617 +91,12,46,24.64458469,85.49938185,6.343942518,48.31219031,watermelon,13.53062773626413,3,5.8303837928129045,0.7098699023932831,417.38301293565974,9.486207446286786,1,9.064528156230402,26.318748624131207,106.4400803141661,3,49.7999586356249,2,66.73347194425764,4.0594028089210346 +89,22,52,24.89681131,86.10782926,6.217300786,53.14626213,watermelon,22.643901148041618,1,9.612596751880464,6.388531666081776,361.7039794451841,4.977231571593711,5,8.895256210191278,91.20196030473168,160.55476192728185,2,41.12208769203116,2,90.9405498944429,1.0698493506472357 +113,19,46,25.41864024,81.12122989,6.286387658,49.52320689,watermelon,27.825121776386094,3,11.475216516480904,18.4941250256491,446.4255462848621,2.133570547520861,2,10.847954209252826,11.084107717682256,142.36409571467544,3,43.94957927534123,2,40.589980885731556,4.417890694735801 +97,22,50,26.26028739,86.14585891,6.7698938,58.97878791,watermelon,21.37637203655423,2,10.736385791433529,8.965556400384022,444.92165570702525,6.020797971168634,4,16.462154801354853,80.26119635477889,169.23831107647976,3,2.3188803402420524,3,92.06230002312633,4.079271858894021 +117,30,50,24.90123934,87.20772913,6.744966312,46.59207341,watermelon,24.934578042843963,1,9.34925222271637,2.647109255066933,443.41596364018307,1.4873165413688927,5,18.8363769204078,3.3935594910512457,189.12140155181967,3,44.85890711855053,2,99.51596314989966,2.399515218799641 +90,14,52,24.84740848,89.20454622,6.391858432,59.67927244,watermelon,15.7807815033009,2,5.1626474783856535,14.369199694695485,363.7134773251346,4.02439311489173,2,18.621772544236535,58.15916535276676,120.15469891590001,2,1.8940112230016626,3,37.78445683273334,4.1496272153323375 +104,23,47,26.98212846,86.70068316,6.770434148,42.91292205,watermelon,25.602121026510357,2,9.222825937177042,1.028621913334109,417.57955200241395,1.4244773376562077,2,12.580276463161656,37.667349218606304,108.82131199252791,3,41.478187448093436,3,32.79305858068899,3.1980171047629495 +81,16,45,26.90435747,86.25426228,6.727468157,59.75980023,watermelon,15.30228532605343,3,7.852294674719821,17.637267992371328,425.12714124507215,5.940236384627302,6,19.15748078672584,46.00049178137229,56.53822219553542,2,49.40044224355378,2,41.2537642009134,3.0771040052974485 +88,5,47,25.86475496,86.67468041,6.662244646,41.16554802,watermelon,21.43328587577617,2,9.971138818651099,15.92909247011615,371.45923579630806,6.623372603539079,5,16.131097342910724,73.58991039370508,183.99429737436932,3,16.48859903567395,2,96.52872560663911,2.3454508756211947 +92,7,45,26.70607759,81.14149505,6.944640222,51.51033554,watermelon,19.246636812408035,1,9.374930674358435,15.077588040997014,401.1532220207423,2.64256267663425,5,9.539892974351893,27.565351433852825,137.22543962820458,2,6.480086255261853,2,93.26723137404096,2.025332952299277 +81,18,50,26.44019475,80.91934337,6.507110986,47.81847573,watermelon,11.411661021388435,3,6.598225468953286,9.59515224824508,400.6358495213048,4.046969641764573,2,11.650246950045664,94.33511181063325,58.07817506594002,3,45.789022742332556,1,5.265470982022524,2.388237732260697 +111,5,55,26.283443,84.42478917,6.520663422,50.78669728,watermelon,21.777580522616837,3,7.873224439403135,10.79860632298281,360.03625597839107,9.441157267760392,2,6.94290032801983,22.006865046489,73.72876935924288,2,0.38328620014156933,2,99.18513137634633,1.1981476276512595 +108,23,51,26.84366082,83.85039964,6.106500787,40.228644,watermelon,17.695310570992287,3,10.483413883500084,4.9897152965814096,355.25891398283073,6.745932746829426,3,9.715161179167247,70.92186953901066,149.63760803872756,3,14.57346623824201,1,75.19432559758025,4.044716270531646 +113,30,50,26.03967219,83.9862443,6.277484043,43.87712348,watermelon,22.755856239388827,1,5.195165947753184,18.196918542564987,367.3605050429052,5.442825239184866,1,9.097620988369082,97.44530770184998,100.11407641958607,2,45.47041396537419,3,66.80295910961426,1.4008867051839653 +83,10,53,24.92994759,85.00802358,6.195142279,48.75859458,watermelon,12.155894355983186,1,8.847541762684756,5.890630201457389,402.01046102944656,9.153942291836294,6,11.297716720260599,64.9912465509,135.01115606008563,1,5.2609134815877585,3,72.84856220919735,2.4768340206514607 +101,11,51,25.50736962,84.24340241,6.792035575,44.2068997,watermelon,10.198437805362424,2,6.716068709719799,17.681376646519077,414.7569548836301,9.300187427365145,2,14.62947134592754,15.492241343391344,194.00269897218308,2,43.293732222284184,3,64.69503285509865,2.623236581518742 +114,21,55,25.4438391,87.9392312,6.472756256,57.51549686,watermelon,24.150433597295176,1,8.212500387824258,18.805287849018892,392.7810693229812,7.163423050624006,1,6.031491887413884,23.068484545509804,126.73007576950194,3,39.29414511095425,1,31.810278913821044,3.907076471156623 +99,6,45,26.12588914,86.5507939,6.000975617,40.71210074,watermelon,10.562518996422764,3,7.747470079091838,1.8698484637087587,374.0165695127267,3.255113075162396,3,9.185446323510396,25.633188799136143,181.04607156181368,2,19.353635358164293,3,24.471908514252217,1.3173399188494144 +92,20,55,25.10474753,87.5267616,6.587791262,59.26519444,watermelon,16.352388261341748,2,5.381503852817495,15.280478738703733,361.25455635317047,4.2376470155858765,2,12.857608790291494,72.03256358717269,173.04439018246595,3,16.853282571471357,3,86.45282845498659,4.677660546908395 +92,7,48,26.27520631,86.63249555,6.956508826,54.38748495,watermelon,21.60005269676551,3,11.719101101147634,9.23875101424981,398.89318101581324,8.652178274706053,3,14.516534772219089,41.65999981307434,183.49684900802328,3,45.207253532089396,1,44.8090195132886,4.916441309778198 +91,24,55,26.27061608,83.09194521,6.259086583,46.76837499,watermelon,15.66952298386388,1,8.748483826688483,12.310285436588373,357.8008720912217,1.6453643975108208,5,13.946415867705623,37.220353270560445,70.59540478674438,3,10.256480485468927,3,52.33491042501788,3.9579139130307723 +110,21,54,26.73690828,87.82430156,6.747537642,47.46447019,watermelon,16.64570674406493,1,11.633806630421953,9.230450414235772,433.9008984983309,6.500697335901975,1,8.33302766763729,62.45178570570536,154.1485706177147,3,46.672931635868494,2,32.18394673588837,2.8654457081307885 +112,25,51,25.04746944,85.5667282,6.932537231,56.72496677,watermelon,18.296730221674448,2,8.945755218612078,13.52918594316024,428.8993864547364,5.6069255346053115,5,9.046968852765332,12.760387974057785,85.63316804697791,1,3.0451823352083407,3,33.562316809925086,4.19541306894993 +89,25,54,24.69368934,85.56967628,6.353107393,48.99390828,watermelon,22.332135681504607,2,5.078649659286884,11.724172184659526,360.18439569556733,1.8876527078043375,3,14.092267306969013,73.2126689844116,63.11008006764848,1,17.752561594949224,3,90.20202131627771,2.453828107857343 +100,10,53,24.54356968,84.60808277,6.211748957,42.00660251,watermelon,17.99205611461348,1,5.904797107223221,12.022569125625864,424.9870499947723,9.85392327296059,2,18.613574193290763,90.99911368149675,185.3384339003948,3,0.9006575236781167,2,76.14111630980562,3.514958888269382 +83,22,54,25.89762315,81.96664832,6.277245254,54.49960057,watermelon,17.108719469787573,3,7.664980302032863,19.869153634390027,385.1834061389161,7.385627805040875,2,16.572473918345477,23.334166447670825,154.69495012621178,3,29.68970355682759,2,41.16622765456116,3.3616265642091867 +95,14,50,26.6333118,84.31756844,6.560443519,56.31866159,watermelon,10.824013233168412,3,10.019672661321096,14.593999027035053,438.7381022548419,9.670698253417966,5,6.88548851913935,72.25175226791244,81.99505163008746,1,41.78646129734237,1,13.093938432332974,2.861968903336076 +119,30,49,25.35794749,80.45846265,6.903020221,47.72078245,watermelon,19.06123846092757,3,11.20974087892672,9.848231523939736,362.8771381668897,4.876165257231532,6,13.953129266160827,12.86145789610974,149.46146378980507,3,44.86632092205215,1,68.5513079840399,3.9442181636918683 +97,12,47,25.28784623,89.63667876,6.765094964,58.28697664,watermelon,27.37628313135259,1,11.82199184635984,5.768700287537478,380.1775636774393,2.768453551321115,1,8.14314749470486,37.96804067298086,127.71929565888989,3,48.307097653382684,3,75.8208518605932,2.352583119175193 +110,7,45,26.63838589,84.69546874,6.189213927,48.32428609,watermelon,18.397784672917602,1,10.670729021099337,7.670149151051384,425.7781429456622,2.4144018897646973,3,11.810210300985455,30.467506113101816,142.976686436046,2,14.7338583591417,1,45.36693481832528,4.38054750103431 +96,18,50,25.3310446,84.30533791,6.904241707,41.53218699,watermelon,24.817854958358325,1,11.59360203614989,0.42541277744587047,356.01438905058615,6.085384098201592,1,18.9820246473166,92.7446791620957,159.82449310760563,3,18.184353126887963,1,68.96410011074785,4.265260766857184 +83,23,55,26.89750174,83.89241484,6.463271076,43.97193745,watermelon,14.960071918085777,2,11.792706665748858,0.8121473158322079,407.49612494894006,5.442120884537088,1,9.530170846247312,81.15434660510901,90.76772313484383,2,36.20557666983261,1,72.26775396102057,4.2172241217214 +120,24,47,26.98603693,89.4138489,6.260838965,58.54876687,watermelon,24.72318470716917,1,10.00073013800571,9.497846038832185,367.64572186816787,6.207398404685215,5,17.68987543177763,1.6983831463376564,155.3053682808843,3,13.281726441105151,2,58.058789570142544,2.937824559456906 +115,17,55,27.57826922,94.11878202,6.776533055,28.08253201,muskmelon,13.61459930168156,1,8.374898673496014,1.2989541223854006,380.16203378587323,9.032898780909001,5,14.63257214102336,81.29325655350875,64.81147079120959,3,5.658761975512938,1,82.49201546672697,2.411655012860871 +114,27,48,27.82054812,93.03555162,6.528404378,26.32405487,muskmelon,12.282475807709458,2,8.322449518587476,12.72416001397418,445.31989801899334,3.497036636879807,3,19.519511427842122,58.11604841695374,141.19355893864417,2,2.2938714730128487,1,44.547751278230216,3.49188367577355 +101,25,52,29.09910406,94.22237826,6.750145572,22.52497327,muskmelon,25.013074832082783,2,6.447218546966658,16.445610590927703,385.78882145817363,9.525805419895788,6,9.183320704021423,19.869522443870625,53.07939838750246,2,10.952129322817866,3,44.625483743000025,2.2172025020521264 +118,18,52,28.04943594,90.83130708,6.562832807,20.76223014,muskmelon,19.221817751887183,2,8.842752481861451,2.0547285941646876,395.84408373406484,2.337540560172008,4,11.354816482479045,75.20145831009214,143.78770997752196,2,49.83554587622525,2,12.740562489965745,2.0499224631600788 +95,26,45,29.91690582,94.55695552,6.117530021,28.16057247,muskmelon,26.321084325314814,1,6.069536072630834,18.712239343397346,409.0694064121561,8.268540843181574,1,13.68877246138556,22.414300143375975,185.4679255506686,1,26.151367628277182,1,65.9620129893643,1.7927761955915527 +81,25,49,29.86895762,93.25103208,6.076459669,26.26243014,muskmelon,11.93748914705946,2,11.490051608423247,0.5116778763148289,448.4869466332193,7.607617349699039,2,18.683738464018028,20.655526854295225,161.0074708460191,1,5.78590846580278,2,2.8399666099433563,4.254176010883148 +117,24,53,29.17220859,92.21405224,6.293486295,21.30290472,muskmelon,29.195912464852537,3,7.935584277796011,19.677790357316557,353.76720960682195,8.13542911453765,5,14.952482609999327,12.615207139254059,91.24112569778947,3,3.4849194874876757,3,33.524735926173534,2.1418570787146236 +114,30,51,29.24908541,90.06998135,6.069171847,25.93496537,muskmelon,16.861238686994373,2,8.41422431039912,8.817468225809836,406.5387701293301,8.822208409798417,6,17.366403221423944,78.41596566857551,170.4998435771385,2,22.822621590745733,2,4.437683271388604,2.749025522356106 +113,6,52,27.76317235,90.35567642,6.740983646,25.21609113,muskmelon,10.423127337590763,2,9.452678285421902,7.112187164652786,429.2625994355992,1.7435717491639868,2,17.09626896369859,89.99534546775618,132.41378417742283,3,26.962124619137978,3,79.57749911981355,1.1081705417334264 +108,26,52,28.82629037,94.26765349,6.201797639,26.23838511,muskmelon,17.188346404755414,3,6.341198491117228,0.5159105806061071,378.0450413782895,1.6831456188411673,2,8.311690455784124,62.06594461901515,149.5442802314285,3,4.004620039881656,2,53.904543538709504,4.742539627299169 +81,30,48,28.52379742,92.09688432,6.041027474,29.86681385,muskmelon,21.594300295136016,2,9.483979908430225,7.434052449692761,367.8387500949227,7.152925276244309,2,11.249393085946524,99.73985950449837,106.8661270173158,2,24.442875150239296,1,56.508289109103835,3.3432834600249453 +115,9,52,29.06785065,90.97685539,6.019372459,29.1194739,muskmelon,15.827244022290168,2,9.945290075837775,0.9828653424895029,377.76941211522603,1.6646682263047692,2,7.823865234301863,16.202263617753843,67.52705254523843,1,27.91541178675273,3,84.86494918105,3.9743034741796657 +83,7,45,29.08417927,90.73891887,6.704104127,25.33014238,muskmelon,26.640503241817996,3,6.30997468245723,5.2917251790724285,374.09964731224454,1.7757134666763719,1,14.554174891649968,69.53407660068646,158.75207963010973,2,14.86501140258108,1,84.63714671552437,3.710041440435618 +84,21,55,28.47090661,94.79453182,6.494251024,21.08484101,muskmelon,16.098333654928673,3,6.185410746960464,15.5234466679388,421.29752543899053,6.032880342873202,1,8.95170726602932,55.83744386425204,93.77574533653339,2,26.769780531618125,1,8.573927773290457,1.4629081733121665 +109,26,45,28.27973674,90.38971208,6.224535449,21.58992507,muskmelon,12.15128734277424,3,6.416110806846817,9.59734616169162,364.5718087858376,3.748354777866508,4,14.90449185917469,59.21216103656346,114.30601636912215,3,16.267849790021145,1,97.17373789618776,2.5238437312254742 +95,27,55,28.47212559,91.21322065,6.160414414,20.88620369,muskmelon,14.00298223280621,1,5.046102745823626,6.30203864310036,356.0818357392836,4.987546025694768,6,15.10068947163066,45.72398640406542,92.89077549599833,3,33.901855825751824,1,20.867060211728518,1.5127578880135735 +119,5,55,29.68846716,94.30111601,6.168757984,26.83924845,muskmelon,16.062880963574187,3,10.516192328761576,7.923933040671249,416.77274994527374,7.3726710345678415,4,6.326215721268702,33.17820170878281,77.87450358965383,3,26.917565055237162,2,83.0439742163446,2.1862823701170457 +110,14,51,27.02415146,91.66737633,6.085444691,21.26034986,muskmelon,18.077447710991343,2,7.528402266140802,19.415455196330587,392.035563280348,4.826780178001326,2,7.171945701854611,6.713849059724131,77.52136830296298,3,1.4971213125616556,2,50.071661703073275,3.0394326678718087 +82,18,48,29.09588297,94.16748386,6.159050816,26.70581328,muskmelon,23.751735741152366,2,10.660509236615365,19.13995661230026,422.6817066229204,6.2153359496898055,5,12.886109717593605,0.4580682279183179,98.06971732242107,1,7.1480588539304275,3,96.18502455176923,1.36524080197993 +87,14,48,29.69238699,92.58862544,6.606033244,29.1102594,muskmelon,14.094775208355001,2,11.224160360025303,19.07349833021431,391.06909386740375,7.327951956865402,6,19.94158723833417,70.57924991150077,140.08477892322514,1,27.040693203935007,2,35.41839904699421,3.579237597118789 +85,9,53,28.20619412,92.86798698,6.447662945,28.78654515,muskmelon,15.346038548397892,2,5.322739366425422,5.177393120888658,358.314710537237,5.254768635380155,4,13.808031972864287,8.999001777870209,101.24366482888195,1,47.25917635409957,1,62.86431314242111,3.4501090388627094 +100,6,53,29.05248036,93.92217834,6.105909623,23.66620626,muskmelon,12.614882619493192,3,5.590402251665099,7.912835812557811,425.5033174327106,1.108815526798625,5,10.649020581554305,17.88582314683599,63.84226764809659,1,11.221200492010563,1,13.92362409827923,2.1853742747110196 +107,12,46,29.57240298,93.61870344,6.559763394,27.56918621,muskmelon,23.60289859959217,1,6.876731762122566,2.8693593975713827,367.3474249433681,4.227668403247401,6,7.023448573709636,99.21471129924872,84.7752433099965,1,26.808598819308475,2,78.25239109989212,4.910663626847833 +91,13,47,29.10968327,92.43510994,6.14410903,27.95602304,muskmelon,21.561517703949477,1,7.084233589184421,13.084486043227407,445.0686707474853,2.6076940837190796,5,12.98294853336236,75.49864804445686,165.25335565029658,2,16.182823964679997,3,95.97108215960013,3.460467942529418 +102,25,50,28.20480805,92.91440379,6.099662369,20.36001144,muskmelon,19.03718406369557,3,5.614893516890749,10.355729281140817,444.6116215133145,1.4675603472349363,6,12.748623702973699,37.68427719413339,152.24460855187027,3,23.137404080264943,2,54.39639222398293,2.364459034387271 +117,25,53,29.11858526,92.12543021,6.413927319,24.52020164,muskmelon,28.919170091841963,3,7.2582638801874495,11.422098154950607,418.3249870697451,7.688826027694844,1,17.474665318770462,59.93535367880369,179.6803975469958,2,5.98846278748717,3,87.20965453316657,2.884378261900366 +85,21,52,29.62800691,90.10051615,6.075144116,23.69586761,muskmelon,27.999164189782423,1,6.610441686188105,14.014861081189572,443.42293208774487,2.71729765313728,2,19.31250367677011,31.55769292111913,145.03175125241575,1,7.742245433662803,3,18.15089549120531,2.5281143776679804 +104,25,55,29.81196601,90.36881284,6.123802502,22.68766503,muskmelon,10.246730022434704,1,8.39253098365192,17.754810616286125,430.11994957586364,5.3334569602926125,1,6.8291368782096,52.80865531440243,132.1831986863351,2,14.346869267237539,1,4.089198928264814,4.974024234405839 +102,24,54,27.72338349,90.93897939,6.698468621,22.81863447,muskmelon,22.329606839709932,3,6.051017292204926,16.758821273885612,388.67161201894874,3.896573103125083,6,8.081809490950969,88.38305322173792,193.80988853688527,3,0.31080191413654923,2,51.382203393030366,2.544036092751465 +116,25,50,29.26092798,92.92367701,6.088885814,28.70627683,muskmelon,24.937335777390842,1,9.090631606230161,3.763304668535725,352.2523219747634,6.885490978766973,3,15.26306209424044,49.31694333576336,80.38030899614631,1,30.374463758697228,3,44.240441913184405,3.3943413255332793 +100,17,48,29.72791119,94.29753295,6.367800632,26.52364146,muskmelon,27.02378663600873,1,11.267318007921094,3.7300035838883128,397.041253291706,5.312243033679395,3,15.014132878832253,58.25952637087379,90.77778002834698,2,15.566603704900366,3,18.38095775134737,4.868517088857807 +110,25,54,28.91105641,90.78413842,6.425930938,23.44398467,muskmelon,28.229352511634612,3,11.758310869332162,8.138876953800471,409.62440031253317,4.190237263395002,1,14.928614268450298,82.93738386445487,130.98840685460982,2,37.250876270205765,3,89.94346251953608,3.57415132507002 +104,25,51,28.96361426,93.88482153,6.469983276,23.56130173,muskmelon,13.921565654332607,3,10.830059424442556,15.016968283375052,445.57058427524225,3.3627676814647547,6,5.091293663169117,66.2639812785327,163.6580924686328,1,15.010904050812224,3,17.38446471385683,3.947212056703057 +107,11,54,28.59052369,91.33617236,6.094016338,29.44008034,muskmelon,29.204581729847614,2,10.268585058335919,1.6513288809944315,367.8733223969777,5.013319659517506,5,15.63498909357326,83.08000964985163,155.89871386775502,3,14.599739460457345,1,51.7412066014898,4.437243695680028 +98,26,52,27.33897716,90.69759008,6.150090899,28.69113835,muskmelon,17.276004022599764,2,9.381572324379011,12.501276558513158,357.04673209926955,6.418925587904132,3,6.8125564812920585,67.13482843140206,133.24279479028075,2,21.34579603434077,3,60.12253684150461,4.120247848707674 +88,17,52,29.90415889,90.75284363,6.646962425,25.37828397,muskmelon,24.039681761619498,1,5.860185696224211,18.567416125784966,410.2587412233957,7.002538825322334,6,7.679411837716394,75.80461692091531,176.5869346559838,3,19.646914287081152,2,35.40853056485037,2.8543609159269714 +87,25,46,27.42711692,90.02696201,6.379690748,21.7508774,muskmelon,25.787244489943113,2,6.710552695898029,8.476623589121491,434.8141506269436,9.393052040448444,4,18.789105754895573,76.50625388614232,181.33988947599076,2,7.18321087338884,2,66.79306603291107,1.2952186114941142 +120,8,46,29.55657523,90.70937262,6.732834334,28.36535596,muskmelon,13.999825750530306,3,11.488074658344186,7.615308528323919,388.5572881351647,9.136524921505185,6,13.285965606458994,26.957878222834918,154.5213818023066,2,24.82749954431182,2,51.06902342901191,1.398390258292086 +95,13,46,29.84070774,93.76312893,6.126019932,23.28207838,muskmelon,13.581078241216552,1,10.356894887745415,5.584744465888081,427.50322020320533,4.357710182303956,3,18.246982499564147,27.353748570185445,145.11760033124744,2,28.693320911572258,1,71.62385606318146,4.436371646894983 +108,22,47,28.53545677,91.72742702,6.161123579,25.1290048,muskmelon,28.982135482995226,2,9.161602080461755,11.159358022978616,438.81787687651047,7.234406108355488,4,10.814514667268414,80.21306913962447,71.60138048904422,1,48.01634816729275,2,94.93480657257099,2.337972107732756 +82,13,52,27.11535046,94.86907886,6.442810053,26.51924782,muskmelon,12.471833841614878,2,8.822223113474395,16.01375504998059,426.2179151056364,6.281998363559654,1,6.520176680415888,22.707522638224287,103.12848262273967,3,12.90665156736392,2,93.130495011934,2.7054272005527893 +120,23,55,27.84492803,91.60666594,6.732049075,26.47844429,muskmelon,12.067107354463678,3,9.024384973721482,11.024654532481206,437.7318363205695,7.091102750949109,4,12.065337156543853,91.18119461356467,85.974353186178,3,28.518503680168745,2,15.894539405080154,4.115392862148845 +110,22,47,29.03157242,91.82172592,6.243673725,24.93861254,muskmelon,11.521976739715187,2,10.919327056095264,15.599060126569535,353.52891972841724,2.8740937602438335,1,7.7815178439270145,11.915692086780016,136.87559990063562,3,37.844559067833295,3,57.8268863199753,4.159739619346444 +95,23,45,27.82424457,90.56698742,6.266208727,21.19014526,muskmelon,13.658592603368296,3,9.371350580902709,11.675449618541132,419.06948423077495,8.353611243907975,1,19.535852324570804,54.88757526727608,138.2029020239989,3,35.25117231362394,1,52.742064722170845,3.183261428873679 +106,10,49,27.72653142,92.00687531,6.350623739,20.21126747,muskmelon,28.633048466965107,1,9.720801085793632,18.871871697125084,394.97632085581427,4.884042465441955,2,16.503596909011797,15.657468934276231,170.4196141886935,3,41.4463390910407,3,23.91864921892629,1.4968312769782401 +99,12,52,28.69708334,94.30759855,6.002927293,22.21807088,muskmelon,24.191272961560372,3,7.951066458267496,8.524233400098124,359.3369194705196,3.282177951726664,3,5.117686057188358,9.620473115710826,147.22444635798695,2,49.61712706314814,3,30.973238303896412,1.3471739254093213 +106,20,51,29.73019662,90.97015715,6.342573112,20.49035619,muskmelon,14.32349780465393,3,9.560845686007283,13.712211088796646,384.2186602168302,2.287534332759183,3,12.523006958530624,73.71132587176228,144.08870430745085,2,13.778474980183535,1,51.12118482958267,1.2817447264828372 +83,11,53,29.54097171,92.91778307,6.163921248,21.9653077,muskmelon,21.139083686717008,1,8.032324723844882,7.838411804901182,354.6150676665037,5.037375790114838,4,16.897340606553108,81.30622641239317,148.96483184278736,2,33.212425146338745,1,6.8153566187162955,4.5409924687293515 +117,19,55,28.80311922,91.78336933,6.121745389,25.16359891,muskmelon,20.76764583191936,2,8.594881333466711,15.761576920950532,373.28594044295323,1.5563274318628506,3,15.413352677899569,98.05321972831243,164.10915029354345,1,3.6162076204539817,3,13.56106603822086,2.972980337594526 +98,26,49,27.29035669,90.53330091,6.130160473,23.49535234,muskmelon,24.407663527890154,3,8.931478823284344,13.753713275588497,368.3712697124544,6.151484011982912,2,11.28600201691502,87.22418003946129,196.67522663831514,1,17.233838732700608,2,5.552341307839059,1.2230362831951513 +113,20,48,27.46583649,94.87679041,6.440584681,27.27899847,muskmelon,23.39466615387142,2,6.714562149101411,1.039288092209265,444.287352746323,1.8827884498348286,2,7.2088806266331105,81.46311599069445,158.6897048483919,2,12.830121360645396,1,34.73328064904091,2.511059743383969 +101,17,47,29.49401389,94.72981338,6.185053234,26.30820876,muskmelon,10.99905404126643,2,8.220099559076845,17.297856383782495,402.30743149614705,8.624379900298386,6,17.692001113739266,54.011222670516766,79.62804323875835,3,11.769014405205013,1,3.060721347310824,2.8149540066189465 +98,7,45,27.79161808,92.51054946,6.157724816,26.85422624,muskmelon,14.361587083063084,2,8.499269232356653,15.570596165056932,429.12945530826096,4.458821384415738,5,14.867719060339406,60.631115625697305,134.39213187443812,2,40.707568963033495,2,5.2174822068246085,2.6336791416930567 +93,22,48,29.12533739,91.52291141,6.776987974,21.90440445,muskmelon,18.5999558383134,3,11.352790494571833,0.1628716110217976,364.621972126209,9.82331327459374,4,17.0410376145068,66.45094605443475,196.10186297008752,3,3.0447921777839726,3,32.08614917126735,4.762290063128177 +95,21,47,27.93114233,93.56161439,6.431970877,20.66127836,muskmelon,27.954329017137717,2,11.98271409808449,18.91437407243833,423.48121584907415,6.651037657528395,1,14.815793514581074,76.21961990982241,177.5494190482779,3,24.37506379108582,1,80.74378487076855,1.898925541532312 +109,12,48,29.45771748,92.12534736,6.708743843,20.76212031,muskmelon,13.580952195016113,1,8.321081846213994,6.109922535557342,387.63263402353937,2.8338382458735434,1,12.371695539365126,56.085433102483066,83.84224190851623,2,32.60935609150802,2,51.506352633827966,4.805399675831536 +118,12,47,27.96872279,92.17444796,6.010739645,28.94766949,muskmelon,21.20284577988588,1,6.323453695039426,2.749057815774407,390.4367775220989,9.26844929559307,4,10.310394689347717,51.76972308796641,110.33546152077085,3,1.7824553272847488,3,26.615444700662984,2.8684581698376843 +100,14,49,29.48882958,91.07574233,6.365956658,26.01909355,muskmelon,13.574201538317467,1,10.948137438484341,6.691616654732875,435.9387941940288,8.359886673614453,6,10.021269920415033,16.73861352164967,59.7206261876251,2,4.6670802423831645,3,15.573631604486614,4.4928137995731525 +89,9,47,29.47156259,90.77069618,6.668382766,28.75226067,muskmelon,29.669212656578516,3,7.487120703864998,3.389545797280762,418.96455880955546,7.989159229127943,5,17.517238009047684,72.30928032570804,142.0114351619101,2,46.52047286686194,2,65.69596292506465,1.5636855032037769 +95,16,46,27.0767265,90.14362622,6.74669542,24.4514648,muskmelon,22.997797768847203,3,9.28376084257263,5.520228492456434,395.88140280285995,6.217705616398856,3,8.998689027591164,78.03156516099388,186.15246948100108,3,42.076256308551955,3,84.09031519600887,4.778596130762054 +95,7,45,27.30008597,90.80015308,6.031665834,25.09484511,muskmelon,29.017296698549156,1,8.319959880895988,1.2552485190838403,377.77600503190547,9.217875178450036,3,17.15816893906969,54.75705951202513,133.2209310875953,3,25.121042425294288,1,79.46693076110472,2.836914173657686 +87,6,45,29.82729394,90.79007335,6.40077205,22.84203589,muskmelon,26.29904135698009,1,7.926870967930492,2.2483278065860257,426.865433247963,4.076402555504556,6,15.084376899794623,32.367437136581756,52.16698676252896,1,40.94431838677702,3,58.7230603141463,1.2839271602322073 +93,20,50,29.93061247,93.22980899,6.448792689,24.34814338,muskmelon,10.9771659955865,2,6.508541298638852,3.7312496442596976,396.9105688373528,9.125224668885872,3,13.456894545299233,75.55143863128988,72.15140896743843,1,48.9004849077092,3,91.49547212061883,4.680713395977086 +84,29,49,29.94349168,93.90741192,6.251420275,20.39020503,muskmelon,13.96817341455851,1,8.282612709579704,15.756823631393955,367.04841958802615,1.3683365561567173,1,5.63107686813183,60.7671719514601,106.2236939279766,3,22.316570670464213,1,63.57342354166987,4.193732221021415 +111,5,47,28.03306461,91.47355778,6.274452811,21.17924769,muskmelon,29.718802662604123,1,8.52445084631722,18.254370637641447,445.908047381875,9.18733085761202,1,6.61198545041369,78.61176035432237,87.57735236538299,3,0.9490771023170752,2,92.66679474810697,2.007543230326648 +111,5,52,29.8843055,94.0371147,6.135996372,21.0000988,muskmelon,28.76635936673021,3,5.733073394910208,18.525217856821126,444.1096206831903,4.960442397012651,1,6.272567014328746,88.06559262871983,72.30713628663838,3,31.25819816898937,2,65.37252575931586,1.442227284309804 +111,15,54,27.7058373,92.91185695,6.194090172,22.06207161,muskmelon,13.890549774079739,1,5.5855914738742145,7.173246546339069,409.12400945139854,1.9158918075280442,1,7.415229059568722,96.32969504571034,64.98481666077642,1,47.59886798756568,2,28.341054762204575,2.266348626374431 +89,11,47,29.78714005,94.65343534,6.327822962,27.8659442,muskmelon,15.304269463163255,2,8.122379230430038,16.26797555789777,378.93018104722694,3.5405703760967238,4,5.614339780200821,94.49799668881693,86.34403896857418,2,43.94809797010645,2,49.524195229900535,1.0252240572513815 +110,15,48,28.57819995,92.86597437,6.212567211,27.5987178,muskmelon,25.173891411542463,1,6.561525148614008,15.327127635685951,355.8301828469858,2.0728169927355813,3,17.486205606479004,86.35814868791569,145.09261470629886,2,33.26796201508428,3,59.114708625459,2.3073895235378385 +95,30,52,29.48069921,90.33698678,6.640470863,26.0365768,muskmelon,10.18326718307308,1,5.583184597681395,17.183991135339195,386.3280291932479,3.728589974641876,4,9.230047008175958,95.73033372942629,178.72965586230927,3,6.771111386270478,3,34.90753729071474,1.0721915392798267 +115,12,52,27.51492243,94.96218673,6.685553129,21.01796432,muskmelon,15.20174118005942,1,5.256058673700498,16.342121781354454,404.3035439399996,6.276861146759016,6,19.204765782556567,11.75591866793776,187.6483667148349,1,48.10154275920858,1,52.119102347845825,1.8439422002739532 +120,25,50,28.05457761,94.81637388,6.327210469,21.84869328,muskmelon,27.442356918819055,3,11.369397834282392,18.97131468056376,402.6935799865972,4.634358556027325,2,6.998975606212197,71.45364926043996,108.71324040127936,1,3.6799656545810633,2,8.461173114574228,1.744970294486909 +102,11,45,29.03167341,93.12603235,6.35544263,24.15591199,muskmelon,19.50739193707802,1,10.309420771716686,13.09500964399885,447.9899862656617,2.1407430518478248,4,13.387100104290198,50.5871732995423,63.17126024087995,2,20.209542773383998,1,42.29062386902365,2.086266183341845 +94,5,55,28.5854649,91.89216849,6.085682344,26.88372572,muskmelon,27.222030667553973,1,6.331846341198947,12.84419977532843,441.65408157463065,6.209082053990619,6,11.99446675948649,60.352928621981135,190.863552769897,2,32.83311093761836,2,56.997686397369954,3.238840467391426 +84,18,46,27.08808014,93.42402083,6.781050373,25.32159689,muskmelon,25.91864489014054,1,10.197450527635374,4.9248388386609925,355.3148292517624,9.94491866946698,2,9.544343868042665,41.69138823387376,124.1025900374143,2,7.045371424843971,1,87.93010779255673,4.286926956440934 +107,22,54,27.99611732,90.84660317,6.630301421,21.61893763,muskmelon,20.16162587696318,2,9.824138501120643,17.668594822696768,393.91530047595893,4.158232502709762,1,5.997505240214519,63.208216837907415,94.99132894181619,2,19.99361997514048,1,75.08315022942173,1.835859575555392 +80,18,52,27.87317436,91.14849627,6.484799661,24.05207925,muskmelon,14.784101974786136,1,9.344367129292078,6.857865606471747,448.13206618859834,5.991302252355313,4,9.510362051755845,78.95000007566153,192.0184285933717,1,22.961499005069985,3,71.80111149308664,3.2132341433548826 +86,18,45,28.96586565,90.71832938,6.566759102,22.25838137,muskmelon,16.471461498623782,3,9.87012106919969,1.8665217509595,439.8231443134894,7.419260549313163,4,17.83632067864938,29.696969396198515,125.15825561875089,2,20.492286443781065,3,82.40960859076758,2.288983770873945 +113,28,48,28.87726019,92.48839665,6.170520518,24.44267592,muskmelon,13.269824653078505,1,11.915231981675301,10.071935571636443,437.07073527360427,5.5387191846109545,1,15.822031700037668,30.38958236119702,167.24947652345196,1,36.779909768035054,3,87.70096231705094,1.290468254845465 +115,18,53,29.17052093,94.19790371,6.012480351,22.06994464,muskmelon,10.46651067931968,1,11.674130478799597,7.8166714465501075,388.4641805196774,2.3277365922955138,2,19.009974476649695,21.223305439487238,191.2534211555187,1,48.10492068014467,1,35.34728303248589,3.4645696453671584 +82,20,54,29.34033587,90.01506395,6.541150335,21.44532907,muskmelon,22.544099713848766,1,5.577480723941079,19.373922943416197,421.0243098018575,7.232966499739604,6,10.718204463385636,96.1240084690219,64.62797134381891,3,31.288148581598442,1,24.380889443986366,2.3295731191590137 +98,22,47,29.07265321,91.91533173,6.341400922,28.83568362,muskmelon,13.380221053457213,3,10.836791280365704,11.681962726073227,371.3701392777109,5.5141816255440625,4,10.242318046886407,1.2037224042416361,198.59147432343514,3,42.09323521116467,3,14.804570015376639,4.394742104367708 +117,25,54,28.68275966,92.50969311,6.150686364,29.11187663,muskmelon,15.361999194858747,1,8.159323380818503,8.644988194826361,373.30580541351753,3.876341580506856,1,7.263364751911627,5.851772047107251,144.8429930713454,2,1.9631792354564137,3,19.68073993332523,4.040309246362766 +83,15,49,28.92705913,91.39356832,6.438008153,23.20076686,muskmelon,22.562206093934837,3,9.267047843993497,9.65394144697822,449.14082889708226,7.212928234778993,1,8.484867674081414,43.602494183157766,150.26071411915325,1,7.819564734264006,2,22.826766919921216,3.9425439691991575 +120,16,51,27.99901833,91.64193051,6.547041903,23.28618248,muskmelon,27.211679894626176,3,10.77387706506159,1.514685825955171,373.02349713017384,9.861717075859454,1,12.791451058527269,91.34897367468679,122.47174379802878,2,15.870121676044802,2,60.88704193758638,2.466909482668857 +111,5,50,27.59350075,91.79742953,6.399891457,24.84266123,muskmelon,24.993083810404304,3,11.465098626995124,7.567065933176145,439.4052439943528,9.209966342708796,1,9.055768001059771,48.860429816688786,173.2205530986008,3,0.586265820331483,2,47.412264995991585,4.158826968788709 +85,21,47,29.87331077,90.60932469,6.186770318,24.69720481,muskmelon,13.250136856946924,1,9.874079291509801,19.01282992596933,438.11742401922845,4.148439922778046,5,13.818517574024249,79.91656902358892,150.50872882532622,1,14.682090086336741,3,95.57530489195149,2.333417812650409 +90,23,54,28.55852465,90.45773041,6.159020864,27.26588346,muskmelon,26.294648510147127,3,11.362163527960796,2.731755147402972,448.70757203380674,6.780455562809036,1,5.615677243366121,73.20933440295346,86.9081438883487,3,2.8601849961395756,1,8.045140096250236,3.0777230351392344 +99,29,55,29.19378695,91.46241065,6.660954816,26.48240255,muskmelon,25.928223604828034,1,6.05546406838709,8.464184028529797,369.644691057783,5.864583435058228,6,17.23646954354306,38.005135587835305,62.94746357426948,1,48.04922799440387,3,25.863807215697364,4.462907039705271 +102,11,47,27.98780984,92.78226196,6.504906979,27.14509034,muskmelon,28.481007218753096,3,11.109384744664876,1.7521389789158737,449.8746009032937,1.194118089971927,6,14.204617252663011,9.288800893597726,93.57449174116586,2,29.00178474800576,3,65.92827119300644,1.2183965818323923 +80,18,51,28.05380704,91.81758779,6.706053225,20.76582087,muskmelon,18.225796698593392,3,5.440460682571888,16.433957292236027,366.6973300893702,1.741576779824213,1,12.125184964129952,46.29687765884996,133.09081500702848,2,34.416830540609844,2,53.21777218931247,3.354245145392753 +87,21,52,27.3506296,94.2911951,6.067665498,27.21244021,muskmelon,20.60551995945477,2,10.729707978944305,1.877667450616649,379.7764763601235,2.716918368359814,1,6.054194949761598,25.43901945527732,69.77260870422785,2,10.951517540471368,3,3.8937039252061934,1.0292237056295974 +114,8,52,29.34081108,94.5513539,6.419083092,28.22908103,muskmelon,18.841556958957675,1,6.341708908050131,13.334204522159311,379.55644024343394,6.743247949553963,3,5.832684267954753,65.88031730833627,143.1594121272793,1,6.8608075597379745,3,60.00700642106555,2.4120968768326327 +99,6,46,28.61475136,94.22253035,6.39637861,28.98574189,muskmelon,22.45000076480462,2,9.958328659406366,4.93926264472025,401.6464423611608,4.041363637254262,3,6.957555874770313,5.1504759761785035,110.55182732646747,3,28.791309684197262,3,54.97921380732518,3.705755697053242 +89,25,50,27.04863538,91.34685096,6.375923383,25.08146686,muskmelon,26.635816089859564,1,11.624317737115181,15.745226943627781,407.48487790145634,2.179661978402418,5,12.065139572408313,88.37735060652888,151.3622231215832,1,15.50446205915198,1,45.74186443256796,1.3362663765937088 +96,13,55,29.5275305,94.57459443,6.700337732,21.13545688,muskmelon,16.849234899690444,1,8.837008119081315,3.323558859844724,394.9634588682946,3.4318479194479736,4,15.281466133607982,32.91850237563174,185.74288171329474,2,29.53262229675718,3,2.648883649212097,4.944745176741174 +82,26,47,28.50416396,93.46806467,6.565312653,24.20007242,muskmelon,15.657421669721185,1,11.676280822703465,2.377220350584097,366.5140054861114,8.399343299512463,4,5.072500815542467,25.59491076272401,185.81203902731846,1,14.429855160047268,2,46.01182656696574,2.9974512535123834 +106,21,52,28.89578588,94.78993038,6.286515359,23.0362503,muskmelon,15.482003868699305,2,8.61680805634689,14.975863789132,360.7642728889954,7.802801491902769,1,18.58044252339773,49.87758785872427,107.83159242222382,1,14.507992404251002,1,34.5882641635808,4.662981290752115 +90,15,52,27.04927452,91.3821731,6.448061578,23.65747461,muskmelon,11.741406230490481,1,5.438513947236451,7.9726233472066195,356.1379867706952,4.9375135433387936,1,6.977587920958992,56.3205653465064,67.368604034944,1,4.045545810300616,3,48.48056701292186,4.742968839895399 +106,16,54,28.96017885,91.69532178,6.585872508,24.7458198,muskmelon,29.921922574458492,2,6.6737503932801445,18.342716429354244,382.53915892148837,7.15158658072494,6,19.691032571524275,53.71393636270786,99.89148389347699,3,42.62519408241319,2,20.092086493340776,4.3801032685414025 +24,128,196,22.75088787,90.69489172,5.521466996,110.4317855,apple,26.113409078104365,3,5.621583404334913,16.669208892696112,379.08411221752937,6.984264351462168,3,17.074309034166177,9.285506758817775,150.12815322700828,1,27.90621993963222,3,15.502632603083022,3.0578455868655303 +7,144,197,23.8494014,94.34814995,6.133220586,114.0512495,apple,21.628617930752583,2,8.151742374359522,1.8607778492011162,356.8658498534117,3.948965914865975,2,10.093617151993929,77.04491597686594,81.99904938066902,3,26.231749119446256,1,35.40714712102775,3.6352739837956403 +14,128,205,22.60800988,94.58900601,6.226289556,116.0396587,apple,21.30397886962401,3,6.923475312199255,15.039728558613943,364.3434206140549,8.844218421473037,4,9.2554467632293,52.42063475122271,196.67365090159973,2,8.896378605792538,2,84.68365830350277,1.5337411419524845 +8,120,201,21.18667419,91.13435689,6.321152192,122.233323,apple,11.002961984706909,3,8.081127863370025,2.7750994014365915,385.9538933896109,4.294376069853428,2,5.867122527803165,65.86097298068677,189.28173817132395,1,16.616911103824783,1,43.26134173738737,2.119677648183922 +20,129,201,23.41044706,91.69913296,5.587905967,116.0777931,apple,24.791169815355325,2,6.308866921643073,11.533330444463573,399.65657603669626,7.811191525128098,3,15.831761013185616,95.53222858557847,59.18255794147022,1,32.814371371326914,3,66.96640907922003,3.5565165151724907 +32,137,204,22.86006627,93.12859895,5.824151693,117.7296726,apple,13.751786166706776,2,9.400339333220803,6.183786032127541,380.9512071678573,7.774405830286413,3,8.015535412326578,49.743899705352966,131.7978832877462,1,19.731777079218343,2,29.448398209477777,3.5621767146977907 +27,139,205,22.48403042,93.40819246,5.772179946,105.5473627,apple,23.364054449528396,2,9.298277618138187,8.986275117935442,368.6592834666599,9.307500944861644,1,19.783244556386244,72.90855817364499,115.60950887521345,1,23.72242428934091,2,17.584086207736604,1.6243502773440892 +0,123,205,22.02775403,92.96129462,5.790993052,121.1349176,apple,17.59047228537555,1,10.599974027086876,13.599664567985197,392.37290933222306,2.074244862327348,4,11.11943876013884,85.91952410032852,170.3587734643768,2,47.09159133412476,1,83.45238329967579,3.943476780110519 +22,144,196,21.91191314,91.68748063,6.499226821,117.0761277,apple,17.407843898955466,1,8.48963089262138,10.289223334047776,370.69892093145774,8.709818720814503,1,12.134003684834209,93.74236451314682,144.24179620189426,1,17.681274636396104,3,82.87747936643055,3.8310953160235703 +1,124,199,23.71059131,93.27392415,5.658473817,112.6676589,apple,15.669133663247177,3,6.3497644443697165,9.490699408053889,374.6092547448996,4.122411256881715,6,13.138850940167648,73.17344422196199,85.6429552683619,2,9.1717551887112,2,59.68616571844463,4.603586382328723 +30,122,197,21.37784654,92.72043743,5.573241391,106.1417017,apple,18.039768848277355,2,7.543042980960142,17.23228386771088,415.3838878651672,8.219436947480073,2,6.337293516657176,51.37414294322088,104.58563697151983,1,40.011551414497006,2,98.02866892845384,1.0524354078522844 +29,121,196,22.84852833,94.32130209,6.079497202,123.5977843,apple,10.466711501339237,2,5.993289027099072,13.543693922088252,408.0719374833119,9.008981965205814,5,7.8540399068499,6.409650395793609,98.31206247356693,1,6.688184188273805,1,44.461166056348944,2.075028678665239 +13,126,204,23.1094265,92.79630809,6.383180271,108.183792,apple,24.538642736472042,2,8.157212304809194,11.436261038534347,400.708683459206,7.67850428199606,6,6.47076945465918,50.729604158342134,194.95483184509987,1,18.023716966617748,3,87.53474747181676,1.561452521344695 +9,139,199,23.25230817,94.54128292,5.867420996,105.3558408,apple,28.95136222774316,2,10.376097395224686,16.65073055747329,363.3090243089582,3.348172736199058,2,9.404779932964058,98.97977938293066,192.4802887911603,1,7.330069580390819,3,13.114707387271075,2.6162537779229997 +0,133,200,23.67287749,90.4935574,5.708418722,104.2298028,apple,28.865416817367652,2,8.033255774009048,2.2599532695363322,359.26665674506046,5.1114408101829545,2,6.125251711869819,90.18923846333755,174.60254598807913,1,13.199484649819237,3,24.33247321336274,1.180123023663588 +30,143,199,23.76881552,90.59810302,5.7983508,102.2648546,apple,12.078995269273094,3,5.36518580284117,14.45362947753797,374.4447056460166,1.286929694763968,6,19.747971755547184,69.81154264110334,164.8600279221189,3,14.03033476838912,3,46.7770812954997,4.478685780045687 +36,140,198,23.34386401,91.47684705,6.28188384,104.4267991,apple,22.234432314251368,1,7.749101906311659,19.243673442870705,431.4571659736606,4.479488173631107,5,12.448982638638235,97.75357442077551,169.1939008270801,1,38.11390775644986,1,36.96948720572648,1.1282867068574314 +37,137,199,22.63946441,90.18451645,5.697945522,108.3405879,apple,23.71006867594174,1,11.465042349200917,10.979130600906643,445.609192512271,3.224874876603921,3,6.803404084147484,62.0258423917545,104.58511312069894,3,18.062919480150825,2,57.582198563808376,4.005726302221098 +33,121,203,22.45696744,94.76285385,5.605934087,114.8407725,apple,12.466760932567718,2,10.941249180418676,10.044948777695478,409.24979148726055,4.053199195555896,3,17.563952888797466,88.03847942756924,140.52397899763963,3,48.94995904564159,2,73.26956261766703,2.359688480087921 +7,144,195,22.96388477,93.58065995,5.85648105,104.6472986,apple,24.528183417344557,1,8.254107466109113,15.329094911174737,366.73020742764106,8.850615391268795,2,7.995979277306931,85.25535724785782,174.44108517911155,3,23.054530026612973,2,66.5798243158472,2.7638020290283896 +35,128,205,21.07273439,93.56585985,6.041053829,107.8737015,apple,19.771778480633905,2,10.171859964204776,3.784421171922141,381.12523621880877,6.442526438876141,1,10.684494248150756,32.34621816218971,160.5700206671807,2,5.1861040797590965,3,37.81441834547138,1.5703249892899636 +29,128,198,22.44075021,92.70785115,5.685062404,121.4977331,apple,25.696439102543714,3,9.710584101582256,14.90244927550152,376.8832007555286,2.0825150516926465,6,16.024818203114986,85.51139569980164,160.86213273186013,3,3.953666259292338,1,94.90936900198176,3.166367460057713 +2,143,196,22.71271308,90.45261746,5.669489065,109.8852597,apple,10.188538529844916,1,9.333151503509018,9.638197433384985,381.72369348023176,1.4416871933951954,1,17.816396140708523,52.624550258489975,141.78113696913422,3,4.85770616327591,3,11.87071216461797,1.6979442528415398 +34,140,198,21.70416965,93.44006288,5.751707342,115.1781396,apple,22.752303725576123,1,11.275314174361974,3.198472097589513,381.9851567108684,7.825347272878908,5,10.866655917474315,49.21283865469586,173.23137948751315,2,44.260613793279944,1,48.09517326245175,1.3038288633284543 +29,144,204,22.43324518,92.48667725,5.800448951,119.1025189,apple,29.191682533654507,2,5.384690510961596,17.40675482367116,355.5013888577361,5.538018500451801,2,12.807097014583638,81.0871414282641,125.71728859286546,2,28.993780423600235,3,83.92509538841634,3.131361278450358 +32,141,203,21.25941052,92.84416234,5.821347769,109.0658471,apple,29.040954593241757,3,9.977978519061551,15.773026798214868,352.06298874793447,3.1496332064762615,5,12.2538532269354,88.82895314704271,70.62341794508433,3,26.410105997028875,3,94.78762978466987,4.457509251588907 +13,144,197,22.9215706,94.89613443,6.28022267,105.6941544,apple,11.796683603416323,3,6.911885653476768,2.370131212676747,437.71354550765875,9.485953231011878,5,12.430111687584922,91.01678855499253,90.73060862841277,3,3.8146131813965587,1,73.57680005067198,1.609364431091873 +25,143,198,22.81212536,91.51861705,6.027314401,107.855225,apple,14.077575939367343,3,6.5681385624156485,7.836216597319532,391.06713758669315,1.6916636269243086,6,9.10851640616632,67.62454758726645,169.86674221726693,1,22.13056015887715,3,44.01389950896366,1.4740639459763911 +9,137,200,21.12152071,90.6878768,5.636687393,102.8017203,apple,28.391716965545513,2,10.955960365197551,6.003783567289429,422.0754293900455,1.4684372558799361,6,6.248347757070528,64.4069494125463,196.56875145137522,2,37.4829693480207,1,79.83734609510363,4.0528485037961755 +6,144,198,21.11478672,90.31528693,5.559363609,104.5086618,apple,24.857790589963248,1,10.543174492945898,6.646143277085765,397.5003899609742,4.692630550808789,6,19.931017152656267,65.23134862893664,59.82756219742466,3,11.520506194214148,3,86.06387188638928,3.91880479900406 +37,126,196,23.59997268,90.97597665,5.596449493,107.1728191,apple,11.327962406700324,1,9.208614193036311,10.81299382063997,368.19957629616886,4.733134384052606,6,18.143418726048488,72.73466829816546,104.21391783535574,1,47.36262733961716,2,91.30440211671367,1.9683823039332502 +2,120,203,23.12652652,94.71203306,5.893492999,108.6211833,apple,11.652755460649235,3,11.203647226623977,6.583361973028772,437.9165663897639,7.59498587380866,4,11.925854446244674,71.98012289738294,157.67568209107765,2,11.060487862539702,2,78.85873960152453,3.268800161401563 +11,143,197,22.98458907,93.3204487,5.875718516,122.1952483,apple,11.705669623554444,2,11.031240437529217,17.366692334628027,426.23840690028584,4.370984167639188,3,19.14121524911196,65.91444188538958,59.69178781079182,3,28.57707369238071,2,6.308523739098004,2.3444790467945196 +10,141,201,22.12659387,90.97818277,6.386021424,104.5412275,apple,10.700504071186604,2,9.046328778507363,0.07656970112627226,355.2755985363056,6.739199806520464,2,11.438704757773696,28.985405935969034,163.8462939504583,1,29.191018446770666,2,84.98440849087187,1.5516334476106683 +24,142,202,22.53779727,91.48135786,5.710819862,101.8474768,apple,22.457282140177888,1,9.640734746097454,4.425803199160702,390.6865748319925,5.832870126857847,3,7.218131493967207,80.65324973090013,60.91262251652109,3,27.37464051611036,3,77.26682352728022,1.8327475126676775 +23,138,195,22.49095104,91.70292746,5.795985716,124.3915101,apple,15.408636668176758,3,10.722260489285855,17.533308252273486,392.6336743308372,2.7296052968950395,4,10.033602874505364,2.999618645745028,137.30222940779225,1,42.99058856535095,3,93.61971461617593,1.219502895172047 +18,125,204,22.35548159,94.47811755,6.046673619,116.7366261,apple,11.015540427381996,2,5.704090026213438,4.044418911456984,366.3274996474634,6.3337175212136305,3,6.652535258458606,80.03471242297287,123.05004763265089,2,3.4716370037594757,2,27.781142687466897,4.405346700346339 +13,121,196,22.20700989,93.50574163,6.443382913,120.1593771,apple,15.044791862789888,2,10.032106494956846,8.193768363143981,372.7338514444381,1.4615820591801136,1,16.457200074247048,54.90029596973691,174.4996444834756,2,36.6997390498239,1,6.2864426117470895,1.2894620186814234 +26,122,202,22.44516988,94.73763514,5.617227184,107.1843273,apple,28.0150103591898,1,10.486298914430199,11.829731193316842,426.55501727791886,8.372990329548342,3,19.689896252498805,32.0252839374364,107.05543847780143,1,11.068468775310992,3,52.15940392242747,1.1388091524055044 +28,123,202,22.76643029,92.12438519,6.442289294,120.4359949,apple,25.459885233714264,1,5.1028052778845225,18.175830265085906,389.04230521551426,2.571346738464109,6,15.945665620588693,86.24368585240424,181.56692027820043,1,37.80908705627367,3,77.21531893801831,4.797733058781564 +26,121,201,22.19109412,90.02575116,6.162034371,112.3126628,apple,24.98409356773446,2,9.192250122893032,16.892686288498965,387.82096577008446,3.3336991521250408,1,9.830019221408818,3.064195412509374,175.96911718670944,3,25.81084044661231,1,5.209928131144537,1.4386600771850069 +21,137,196,23.6119202,91.70293849,5.812781806,123.5900822,apple,27.128445818141675,2,11.171347054570935,7.122758635695847,352.325646860025,5.017100156545027,2,5.487048847995643,30.360659877186215,93.02423841221173,1,32.6511251720023,3,16.368409118904992,2.4760199736385102 +21,135,198,23.86087054,94.92048112,5.765015126,105.0241329,apple,14.478620647799481,2,11.298922587832092,7.969388208844901,426.4196448669358,7.440189127622578,4,7.15719933485485,90.44414379990286,193.83550134129888,1,27.680805205374785,3,27.114373206199815,3.066444166533497 +5,144,205,21.42177231,92.62665309,6.184922574,102.8045658,apple,20.29288616518152,2,8.285456976683873,4.586446640254831,447.5150901688916,6.726378685561305,2,11.889755912875064,39.91820678658994,106.23472911657637,1,26.27613205805379,1,84.09566900044896,2.7512805215959335 +2,123,205,22.36629253,90.78572467,5.739652177,124.9831618,apple,13.281507973530902,2,8.233735730247563,17.74189602946825,392.2929149385685,8.674493303581857,4,16.105623048618682,80.81300435190198,161.94945171388022,1,35.20106735688824,3,10.29083239496119,3.3475035762758143 +15,133,199,23.99686172,91.61001707,5.824778636,117.6102915,apple,20.392678220973345,3,11.812086723102553,12.200447946733542,437.84645314715465,7.361593647872302,1,7.049358564462459,38.49839072026045,148.03075467634451,1,0.5378753543326675,1,22.98689645648334,2.5036815486773345 +31,130,198,21.80129837,92.73446667,5.554823557,120.0586671,apple,29.129038954063134,1,5.814658409334124,17.747947582227706,439.7016315341487,5.751031984818836,1,7.891667489934145,90.52946052962176,53.4847086882253,3,40.63252553282783,2,80.28588808577001,1.7485936927943873 +25,143,200,23.80436344,92.80441624,6.024248787,100.6192543,apple,13.284375220342358,2,7.429618504094105,17.497085879528587,380.089829887688,8.471825889652436,6,16.415919482580634,25.50946069556246,57.85897807744242,3,8.719592588891656,3,97.47677951376403,1.6986311353935406 +16,143,204,23.71475278,91.53331177,5.631333387,121.8961665,apple,27.298593872039596,2,9.430529081424169,1.6642757523000928,422.44272356994077,2.83791485985693,4,13.969935388821282,16.629013552660744,98.52467306064543,2,34.995128280006156,1,75.11843265111946,2.2605698471333273 +19,122,202,23.34467359,90.37981478,5.811975094,112.8954016,apple,24.96711936033529,2,9.69490834725315,6.276119813895946,351.97277376136776,3.274927669898056,6,7.83897988552844,1.0834987569113275,117.46636790658357,3,34.85002152659934,3,52.00607622697277,2.8667695914995064 +10,125,196,22.31253665,90.03577124,5.730557448,113.0688155,apple,25.14922916131482,2,8.387144258994542,5.138988394687381,350.90765647628143,3.5006239383357087,2,12.033314663957109,7.559914917806088,126.3123458443681,2,49.15832669840089,3,54.3370933429266,1.2515084472231575 +20,139,202,23.50201428,92.21083961,5.66999105,107.9868949,apple,22.18492472302985,2,11.03529617894829,15.228318412840657,390.0620401144933,1.1121813240684073,3,5.724691157944489,62.80940210758123,72.08055734151029,3,37.50881665364456,2,25.929456508516026,2.6266729902691903 +28,123,198,23.46260321,91.45665004,5.682751473,111.7763395,apple,29.45623917366988,1,9.695310869541387,11.509248675036662,441.3875801300943,7.072278370548964,3,6.082107050622019,14.806039959644334,149.6601923340302,3,43.250045787091324,3,66.93606570207635,3.6986944970366578 +28,136,200,23.06204373,92.39544055,6.245858905,114.7399101,apple,25.878187165783373,1,7.754896103396362,10.487875934055822,394.0058145775637,5.836857867808051,1,14.209520287451905,8.072132384541476,151.21530925627837,3,17.43741928445831,1,92.43318217851075,3.9569331980348776 +2,131,199,22.47420512,91.22759742,6.017370134,124.2179699,apple,22.81780915004636,2,5.198944351061827,19.707940162368477,425.2853736559992,4.543947035689646,5,8.763190064831134,65.20241190578517,115.48482677845351,2,36.893697164761605,2,31.697533026912794,3.500847308552141 +2,140,197,22.69780133,92.82223419,5.53456749,105.0508234,apple,16.89041734173178,1,11.225156416831354,7.848277738217613,423.6263955972688,1.0957187442151055,6,14.87571565127692,61.8059845084544,111.23811524036503,1,7.591295401685294,2,71.35195702789136,4.975473976243485 +27,138,201,23.66682067,93.90191078,5.952367662,105.4004751,apple,18.299666264549607,1,11.468567589740895,12.894859312607235,438.11911526705927,6.883416201360504,5,12.985794360547679,95.23808412683682,89.35547461258068,2,14.619538059820863,1,68.93801879136409,2.686683996381606 +30,127,204,22.50050273,92.45878335,6.126436584,100.9343903,apple,28.473644184913486,2,9.905853590291304,14.411007635855984,358.6809607842175,3.7545816397387712,3,16.049618867419262,72.24277529680538,144.64149658791513,1,45.03423713384171,2,42.98005613891799,4.595343869587406 +32,145,203,23.83053666,90.84422164,6.406818518,109.5966791,apple,27.442749078151824,2,7.689288279713577,14.459306988039632,379.979666390656,6.674222806735619,3,6.100555838344038,54.45934387571172,189.37306810857336,2,20.341553659834986,3,48.662752876405435,1.5749519224299373 +29,139,205,23.64142354,93.74461474,6.155939453,116.6912176,apple,23.801940601085146,2,11.058301839545202,2.372009739479808,402.9195238159388,3.5528309952909622,6,19.695418912770812,56.91217009830151,89.08092975689905,1,42.597214083789545,1,0.8974103421731328,1.575353374099203 +26,126,195,21.41363812,92.99124545,5.878568981,118.3979065,apple,26.748256387300586,1,10.232746252125846,10.589147657042366,383.92605249107953,4.252788210929097,2,12.40111717858846,27.860302377534975,180.33018142635655,3,0.7814819256247385,3,31.37778855974026,1.0136596946918592 +40,136,202,22.85267372,94.5764581,5.935336308,117.5314026,apple,28.124263002625373,3,6.732131540042072,11.360404301718772,359.2341474802378,6.063193872583486,1,10.595382968638575,63.31907081107235,118.97639051041106,2,17.43533924848243,2,94.33279833163964,3.955237364610931 +6,124,200,22.98208095,93.84505029,5.971332179,109.5852253,apple,13.764079363863924,2,11.978029634264718,17.66546104251525,351.3128431856589,5.413102640120094,4,12.531261424108838,23.75819508799266,135.8812099297652,2,45.5576904291672,2,87.5853534121824,1.333507086993547 +35,138,200,21.19909519,90.80819418,5.67130617,103.6838922,apple,29.813522103232984,3,11.58593411209555,7.669961521355225,431.8131247158145,4.307175336428994,2,10.614359212381746,90.22131246176983,160.51334488902572,1,49.35109025590067,3,21.841123045104627,1.2592036814418845 +17,136,196,23.87192332,90.49939035,5.882155988,103.0548094,apple,29.997859652841264,3,11.250418266666054,18.489901702013633,409.7400810314742,6.997207550868217,3,19.40868417786845,40.32031112507306,64.11279989095868,2,41.3201405600203,2,89.49180692821872,4.809378924907595 +33,134,205,21.0365275,94.33919546,6.08551916,114.7412734,apple,25.260337171727148,3,8.822029979770353,6.322844163609096,385.98525222436604,8.418055439114934,4,13.800036650344984,5.626261207147221,195.02625406452367,1,45.82368162092179,3,1.9191871657191828,2.6735692730172165 +16,143,197,22.61711614,93.51978375,5.90402645,116.9256766,apple,19.021829020021308,2,5.960007143100009,8.550097444146385,406.8653140125368,3.203787816272686,4,12.96134996632858,47.4587736510368,162.18119764575658,1,41.67821177761068,2,13.41945558237223,4.755821290922206 +27,120,200,21.45278675,90.74531921,6.110218826,116.7036582,apple,14.434298232988604,3,10.185544937294978,1.4260515223715342,401.3091769624517,4.973672377758637,6,19.68736167095781,64.35522640634339,95.32043388213488,3,22.717898060710624,3,22.11724424469771,3.4726499999055687 +29,145,205,22.81227579,92.12992101,6.212302608,109.3383552,apple,25.836797345128105,3,7.311129495677685,1.6620239247414914,447.48240534382023,7.287522466873409,6,17.883587633694255,89.64286722954043,86.89475594378672,3,24.29705238384437,1,99.13242000851919,4.112970017914007 +3,141,197,21.98141856,91.12719303,6.142803397,115.4789148,apple,27.11813855530109,1,9.619702488480161,18.645523911152672,412.5758969740788,8.142762659599157,1,6.202621084039565,14.772800360645832,168.4121573583074,3,42.25093959020138,3,9.404887421815268,1.152071718984761 +15,123,204,22.52709326,92.54780429,6.365972688,115.3830068,apple,18.906002562998392,2,11.657186547471449,15.912336963668059,398.8980954455944,6.458582998782504,1,18.789972239712483,48.538228806963126,98.17994985320148,1,8.447824591652909,1,92.38368920499522,2.262732701697352 +5,136,195,22.35628673,91.92360477,6.264202804,107.7697413,apple,20.416138917943663,1,9.769855539005494,12.73541000594503,378.7577992835709,7.934085643947292,4,11.851252902102999,60.13613001589574,110.10457348712976,1,48.317640004432455,1,35.83278739097322,4.988039444453781 +10,136,204,21.19852186,92.15595143,6.276198595,105.8554351,apple,21.17401651116081,3,11.52034177751396,2.7709838137474763,352.65394337500305,8.035049627234114,6,6.657300601902539,6.1511736425680645,80.12740659571429,1,20.071950833158812,1,26.095039476701775,3.161331073712504 +7,141,195,23.8812458,93.45067555,5.514253142,104.9116663,apple,29.610669156605724,3,5.635475174501436,13.549388315241156,400.24912328368384,8.252349438205663,6,19.45496934264777,5.462051716870175,81.90678243999565,1,42.715315197679786,1,77.01631924384664,1.3085385402298986 +2,129,201,22.78234161,94.36803516,5.682343744,122.1449949,apple,18.113655543784688,3,6.865101788809612,1.0446668077159238,378.9333206442151,4.467042679252787,3,12.949364810746566,64.69117238501696,80.76446303613021,1,2.19651743292249,1,16.344431172361663,4.855056584467276 +29,138,197,22.19055385,92.43764169,5.830892252,121.6622761,apple,28.874737938819358,2,9.148677995984755,1.7911439786865557,439.3400190110597,7.254939696871271,1,12.035868474853583,15.185083235621622,137.05991608616355,1,32.78633563583087,2,0.46630993075584826,2.8720790780814434 +30,137,200,22.91430043,90.70475565,5.603413172,118.6044645,apple,15.869740923889342,3,7.4082874944495325,6.694860692047609,372.70957915237386,7.056425513641589,4,7.244277205428311,2.528157289536792,181.41354229855995,2,21.151099336515017,1,33.6597417545724,2.511405634088793 +29,132,204,23.08950736,90.22507299,6.0967531,108.2166601,apple,19.442630222088752,1,8.694694923671923,11.187965789269743,412.2500757534764,5.902645193054866,5,8.137432622099025,4.055445896800681,161.21925512563695,3,36.30760632275577,2,43.50416102039478,3.1832874497957 +14,139,197,21.72484506,92.83975602,6.056529526,121.6961761,apple,15.737639960939829,1,11.35276243849204,3.999796799961486,402.96708553336987,1.6591565916603892,1,11.588258399913094,99.60050638184555,171.3266760179174,2,35.16683809665268,2,24.477274712679154,2.6364459728415595 +18,125,203,22.44307715,91.59234006,6.160267496,102.5565807,apple,26.033503023353923,3,5.535032968970341,3.0752172523507526,392.2084427211176,4.049705621882335,2,6.097864783254899,47.664368560016804,185.20976571402218,3,37.80246307201909,1,13.409580698445689,1.7679611069396648 +33,143,204,21.1316077,91.95769858,5.814434775,122.5391946,apple,17.5931236747446,3,8.646476508198953,19.552043183117235,350.7285162547156,4.953177281453366,3,8.886680990963521,42.44087872388286,172.30570030808298,1,9.127786572187746,2,79.2025929060199,3.1758830795374138 +40,144,196,22.71750705,92.25479855,5.987262638,107.0289866,apple,24.430866480639864,1,10.734956288926846,1.4010561159543111,420.0482596515518,1.391766014391875,5,8.129636744396034,61.96483422312682,97.5131478051324,1,46.905515616894114,3,31.361396841471688,3.605665284977037 +9,143,197,23.75033085,92.88160462,5.570020684,117.6602827,apple,24.742094013552006,2,5.939698785620681,2.7983920638254545,369.21620077778795,5.991411682395984,2,15.444422825856863,92.75133572376133,116.19275842177083,1,28.581083023371708,3,42.03467507759805,3.278274611977272 +38,135,203,23.76121837,93.661643,5.965551311,100.825956,apple,24.262925420825326,1,8.648658942336464,8.462022556988565,415.69340318817615,3.458188801186802,5,11.347882665136888,8.519389590527416,147.27590151496395,3,3.4314829320139184,3,85.4307390698953,1.8915939986546446 +28,130,196,22.13450646,94.67695747,6.062356467,112.9203223,apple,29.69277638902345,3,6.861763476260014,6.114823970881044,434.44450973730005,1.7307007586365635,4,7.116611370981166,3.790408349118002,164.35702005773132,1,23.364534407816063,1,73.99830620821457,4.83405901495941 +35,142,203,21.17089176,90.23730166,5.895319002,123.6495149,apple,14.060726060721125,3,10.027869635696282,19.778642777029393,366.41008229688754,1.4471587504560395,4,12.187817689331201,25.59572346156054,57.50181445050095,1,48.57396125250497,1,62.2063515233653,1.5499472505052103 +12,129,205,22.36238282,91.15761594,6.119432215,118.6832725,apple,20.850269433774642,2,7.591362959540126,16.251538113141372,388.29191438401324,1.2048419196419973,3,16.851358346719245,80.60434936136231,91.27591632380502,2,1.2435731378973636,2,83.02144931433727,4.095623823584193 +1,135,203,22.77856513,92.70124029,5.624203283,113.7759219,apple,27.16869072995614,3,11.76314700212399,2.313462885118185,405.34817832554353,5.391244189707512,2,7.296410853360252,89.33240831353456,59.05723322645373,1,5.456456768746298,3,85.73170000447064,3.5772649394576326 +0,145,205,21.22503442,90.09877774,5.52078314,113.9760462,apple,22.32911133423478,3,5.55416258140868,4.41225412821886,448.3046516695685,4.911259472148124,5,9.078924693560321,40.410333684367984,136.84939977105222,1,40.8209249627524,3,78.5256001329812,2.1083923699823064 +31,121,201,23.15791104,90.34396882,5.731535258,110.712841,apple,14.993177552546504,1,7.52662385848792,9.379228764543555,352.94057503769005,2.2339431431866714,1,15.509559029965102,10.989172234264032,188.62834720899727,1,28.420708151164913,2,93.32479361009588,2.914949385299296 +35,131,203,22.42776057,93.91722423,5.893490899,102.7230739,apple,28.459380508253687,1,6.9056937744512314,5.147378840243251,399.9789519599269,2.2884113145845477,3,17.10328432722146,60.30887678392985,179.51523737879972,1,21.801301809294788,2,64.08311201883114,2.552570720391014 +29,140,195,23.64082979,90.95257927,5.560521058,116.7431319,apple,20.65470123376477,2,7.622637870245186,4.371516769321511,396.671259298517,1.9831264184972164,2,6.047528012366456,64.18483411400857,182.84727928285562,1,36.03309028695458,3,65.0058276479195,1.6951419584723424 +33,138,198,22.29423493,90.69033986,6.222390798,122.7418744,apple,18.513644209442305,1,7.633594637451805,3.4632817339101107,411.53586210620085,5.895681912458212,4,9.108077310496505,25.17687950060754,198.34024289581902,2,28.026076037162124,2,2.470816541005083,3.4975678808524044 +14,140,197,23.35225078,90.90054697,6.071255131,113.0381382,apple,18.59403333362835,3,9.448349325321047,12.794807990308044,389.3698139217082,3.7797553774641575,4,7.79159713285268,56.26388641777825,112.81273123519844,2,7.493823430737651,3,47.38491329145296,1.0149025632928677 +35,145,195,22.03911546,94.58075845,6.231950009,110.9804014,apple,27.06012525449916,2,10.305065825138264,1.9025005955645202,417.24258391509676,6.5045155878160825,3,14.752543623061493,33.72786602276808,166.40316489001674,3,20.629067688345614,1,70.45334558967762,3.36506485995545 +40,120,197,23.80593812,92.48879468,5.889480679,119.6335548,apple,14.299331698195829,3,8.044799744625099,18.76804693578339,353.2111378568333,5.695483977278562,6,8.449351067734415,95.83670165151942,119.64704260314237,2,49.352523407384105,1,7.621344726971746,2.4433366635023814 +25,132,198,22.31944084,90.85174383,5.732757516,100.1173443,apple,26.716653238873768,2,5.24069234004205,9.031118284734834,385.46193326095397,1.2207525154702994,3,6.567544569673749,66.65883918537111,95.91734238511444,3,1.3093317938740412,1,69.3564285622321,4.04444320550466 +31,137,196,22.14464104,93.82567435,6.400321212,120.6310784,apple,10.617323465665303,1,11.973004902774923,4.8751400201390505,436.19679470873456,8.209662804245049,6,10.913120518138854,60.90004993194691,61.13385788468252,3,32.598381934034535,2,50.7534858423501,1.3075454952884908 +36,144,196,23.65167552,94.50528753,6.496934492,115.3611268,apple,28.392120563130813,1,6.500656669541299,17.295104208061765,401.5209386625795,7.674012168842182,3,14.268967594569531,51.54497974078106,65.02089887532615,1,35.73065332322296,2,40.38518645970428,4.468498004051664 +10,140,197,22.16939473,90.27185592,6.229498836,124.4683112,apple,21.829635938189416,3,8.39664937244213,7.417253930796663,428.5798827658048,1.7614305814339923,2,15.773777685534716,94.44620680105751,169.17738776838848,2,31.164841794047298,2,92.55506734732954,3.8105431564131615 +22,30,12,15.78144173,92.51077745,6.354006744,119.035002,orange,17.717882107211548,3,11.35176105881973,5.465350999061971,443.1801115795268,1.3062412796793552,5,12.694493441461018,40.53007548944112,190.42776986798444,3,27.924397393901362,3,88.38392792310394,3.6340345658969535 +37,6,13,26.03097313,91.50819306,7.511755068,101.2847738,orange,10.448250249599369,3,11.210980353267024,2.04822353553602,414.7778954980439,3.4112575517307677,6,9.413081959516985,72.02399111878836,106.37861436089086,3,48.858209194899956,1,96.18362691483789,1.1521989570149924 +27,13,6,13.36050601,91.35608208,7.335158382,111.2266885,orange,28.669939256209126,3,6.1554113505591985,12.7362352898108,380.02989288702264,4.863111583054195,5,15.891874429660552,43.396446477751894,193.40341519764596,2,34.12978830261202,2,8.145829169708897,3.7905207775402476 +7,16,9,18.87957654,92.04304496,7.813916603,114.6659511,orange,13.866481527972097,2,10.802391219180112,14.028277080832376,372.56224301660006,7.684275746046994,5,10.525476626469791,38.99639196828688,170.58868315984253,2,24.20564883987074,3,58.09406660342895,2.584127200433268 +20,7,9,29.47741671,91.57802915,7.129136941,111.1727497,orange,19.956068032538454,3,9.640746221825342,12.622742907185433,417.77374720539717,8.159204588362776,2,15.087016658338912,40.26401349747897,81.96454449515383,2,3.0809550941842065,2,37.83366467273584,2.1539023856176835 +26,27,10,28.06903173,92.91487288,6.079998496,114.1339416,orange,26.9054708327141,3,11.857353603072575,1.6870445930652633,397.7173662554347,4.70365434126594,4,15.073197324958786,15.737003462095512,165.86602552643964,1,47.80287266540321,3,56.0014901012154,1.899256715514385 +5,23,15,25.66901098,92.04670813,7.408939392,112.5424199,orange,16.12102292698541,3,7.863965833011269,17.58660398167198,428.68594501042594,8.13033509426549,2,12.53880140779631,16.861421874450976,79.02620312711905,3,49.194489798213475,2,75.98014421129938,1.305136260409494 +0,18,14,29.77149434,92.00719952,7.207991261,114.4161786,orange,20.888830393151355,1,6.468970771820089,6.504407161997943,383.59458422331556,1.1407479394795246,5,7.012203739363134,38.26635295493901,150.2924298169918,1,15.335107904959338,2,87.39256467407293,4.738504638640478 +39,24,14,30.55472573,90.90343769,7.189259647,106.0711985,orange,17.156634719000138,3,5.958550887107908,1.0153344419068122,446.6748470742034,3.1462509315044223,5,7.989409454369653,80.4778900254728,186.84642598195143,1,44.33517050957706,2,88.68146899410435,3.182082000410789 +13,23,6,23.96147583,90.26408017,7.365338111,102.6958703,orange,19.909008468490686,2,7.6771030033571055,19.3327286368809,375.2356840846833,6.34539612439254,6,13.913874336018024,15.163486841693196,92.46507763832074,2,4.969032622644293,2,57.4460199775499,1.3385371378037596 +21,17,15,23.98289638,91.5473145,7.455991072,118.4901697,orange,15.989417171902998,2,11.444717866598808,1.159192741961026,396.22752404732137,5.216013699038715,5,18.146992568694927,32.71417629489021,110.31192747268996,3,14.870276405923534,2,35.94595984343179,4.8318051870071494 +33,12,8,25.26052689,90.31153735,6.822282114,117.3695296,orange,26.230585132311415,3,8.353557032325424,4.094466655393177,438.0803112537542,3.089381987273313,6,11.131519005932315,42.191909963862784,179.40272482446582,2,14.416936615252235,3,94.81366896209894,1.703206970282657 +6,9,12,31.08368929,90.14362642,7.028746406,109.6894658,orange,22.937653086720488,1,9.538533959149103,16.242510645637473,424.4455310643519,8.436483220336374,3,5.727102920388845,73.31581149423934,103.29541656452723,1,24.66085360338556,1,55.35242096235542,2.573438094361388 +19,7,10,14.78003032,91.22062116,6.118430299,100.1961762,orange,13.180987216876519,2,7.858049598547611,0.8759995313270608,355.09923417026636,4.784511628078753,2,11.169359964333271,32.91812312885416,184.38900754740877,2,12.805655974103974,1,77.0674661726959,4.869240801173041 +24,18,6,26.56608303,94.45239715,6.285312759,116.3796525,orange,17.802350182908363,1,6.193539461714605,3.2343829605319607,359.88391325899124,1.6360442871283665,5,19.32990423505377,14.499709314630781,191.44476595253826,3,38.43485536101973,2,40.053313952972566,1.6840580100023588 +9,11,8,24.85903405,94.39000473,6.559236744,111.7803734,orange,18.574942276133196,1,11.972127839672524,10.945623862086935,356.1224091652143,4.809273141377284,5,15.597234387541775,59.348261198964344,109.95121303031766,3,21.60537014282362,3,25.513000230878934,3.3271738083017146 +31,8,7,34.51465139,93.63812684,7.163245982,103.5684926,orange,29.390817717049742,2,8.236420020585403,3.295278848252976,438.34466939317036,3.944116906792146,6,10.4599805925133,80.06747238249568,185.7874166292933,3,49.61895012056338,2,85.74127139429972,2.7508205987625964 +22,17,5,24.12188673,90.72351622,6.945562889,102.835632,orange,11.32035076596589,1,8.972845595590751,4.521170124207668,382.7640028696985,1.0860582463271964,1,18.593496363179952,90.83390475114247,57.47004577105094,3,24.540838679451447,1,7.504140711886286,1.5723283095617826 +13,5,8,23.85340379,90.10522549,7.474710503,103.923226,orange,14.455353203603693,2,8.96264038095052,3.800672426702738,427.60415568181725,3.793205339749476,2,13.137225789444502,23.942921092273718,82.0149498853448,2,1.8196697349286306,2,54.893943910897235,4.185674079400814 +16,8,9,24.60297538,91.28408653,7.601189843,111.2948115,orange,19.163848127334347,1,11.643485409402835,5.401151520397893,420.26878526953624,3.101680256497172,1,11.901938488829956,3.7983580495319136,108.78074946007291,2,10.862276731269976,1,95.23273218198315,3.613685635739064 +4,13,6,15.63211033,94.25966183,7.561143224,101.4705704,orange,11.838777321520137,2,10.179915336485859,7.63469399759479,397.4920344540514,2.9031834146767843,1,6.25578196613688,0.9386876215173312,126.99140157649605,2,19.1582012267955,2,78.0139735419078,2.5030789160078455 +0,25,14,19.33516809,91.97978938,6.361671475,116.450422,orange,14.183409491732808,2,7.888856825737378,9.164391486622293,423.6691179428264,6.4969772483720005,2,19.639259047776875,6.038912513880145,93.67651031815072,1,27.623178450584774,3,74.39868542126251,2.0665624177815225 +8,7,10,28.2620488,91.98317355,6.929216014,105.2132259,orange,22.904246694002484,2,9.554330258679352,7.882935109044984,431.45046126851014,4.959474059786627,5,16.92628960783366,95.2663638833872,102.4069556815423,2,12.14047903617545,2,49.035460930036336,4.0446704272220115 +4,23,5,22.67594476,93.36348717,7.477935216,110.3332655,orange,19.8048853159575,3,8.236676203856277,0.39934018736729193,363.5396936970841,3.1638232514478757,6,16.587874909047756,3.41743717537889,124.78645033513807,2,38.18690362118949,2,28.558636387543867,2.8214617659847456 +33,14,8,21.03200078,92.9641969,7.684420446,110.6823944,orange,23.899279293392844,2,7.911919253711135,13.042352552578993,433.687910605299,2.4510699917155736,1,15.77584807574976,56.60630079586457,163.74878454383958,3,21.224834168974215,1,4.123057646690221,2.2292311285015076 +30,7,15,33.23453301,91.06053924,7.825531916,115.7659902,orange,21.33762498122857,1,5.629478435234409,16.23092226312683,421.89680962152795,6.431777015677185,6,17.799332411860593,52.03743417568446,177.4639238170444,2,19.568101778880976,3,88.86018715331524,1.859056522156811 +21,29,12,22.30318989,92.15987039,6.438668989,117.3688104,orange,24.068975135530803,2,10.017654314100827,7.134735806660338,428.0222144861716,1.2581789315642469,5,13.384804307176468,14.593309229835128,106.14279701735036,3,19.586460157658347,1,85.29529746054924,4.493409669219764 +11,14,5,11.50322938,94.8933184,6.946354724,115.5683776,orange,29.703517930278306,2,7.2423125107732,7.984765774568075,374.51703009237013,9.013009544722385,1,15.187582911450233,45.77258189825273,75.19724981776201,3,31.553162533083484,3,99.19692963917733,1.929278350603337 +9,8,15,14.34320488,94.35734702,7.994465371,110.2223123,orange,21.181482515177116,2,7.437795594195423,19.803440504500024,367.252320595142,4.487539325509476,1,8.640195657799072,80.11399442801658,100.9614403749539,3,17.39868293379802,3,24.25051166013995,1.8380848361249518 +5,18,14,33.1056981,93.48447453,7.434118807,119.1709113,orange,15.53648439502464,1,10.736894523574657,4.418354103839739,366.3763492368787,1.806217077941277,3,6.200889266973281,40.161169051925974,147.10704852975869,3,35.37955804118021,3,72.01179843603326,3.833708111204844 +29,25,14,30.49183837,90.4582865,7.781988584,113.3302105,orange,15.540157561344648,1,11.38975876585445,2.6953499542897297,363.7281182779157,5.2822752983622,4,15.758517471143774,83.86533690705689,176.2610977771576,3,2.9100869578331245,3,94.02367370079484,4.8340036312968975 +33,12,15,30.25578031,92.03272799,6.052318465,116.7173125,orange,25.18414908131166,2,8.841332956391252,7.096222161608314,393.2208465382737,6.286496728226956,3,16.92965587233621,71.83185882714704,50.575000290818906,2,34.75748936939234,1,52.71860897017806,3.598348174635466 +8,16,6,12.22816189,90.26457428,7.106650373,108.4161706,orange,25.073403819008497,1,6.666063585709995,17.882781174570784,405.57275153310286,3.261230064907643,1,18.560141171085682,33.1872188296365,187.10537729212092,2,17.565320274772322,1,98.40430772431009,3.851433418383737 +15,14,8,10.01081312,90.22399223,6.22094286,119.3941064,orange,19.209226074995136,1,10.913494691906799,9.258281069068037,445.85170016482664,5.780389747461326,3,11.294654999619393,0.7968872666747617,183.63879303410212,3,25.7767381446179,1,42.82834829120782,3.348017298965436 +16,7,8,22.79196751,90.60901895,6.420457311,116.5084074,orange,24.595538730717397,1,9.580872817965432,5.330284110429218,363.9307984592116,4.527172622611916,2,18.36532659824169,37.416680135213035,136.83634986946586,3,15.21701936811713,3,10.862265201735378,1.1207582795350195 +0,12,7,20.18432263,90.65458473,6.969249676,116.8130969,orange,26.909881697175123,3,10.559544927778472,14.572726228000326,398.78482592988223,4.4651282492677975,5,16.02713438900652,22.687324298809276,96.95276030578059,1,23.46979064359252,2,74.24751874738308,2.033096744375815 +5,25,6,30.72119881,94.01331956,6.011302181,106.8118019,orange,22.79244210343155,3,9.748895290710367,6.315816079954568,424.7288607368821,7.337118380898586,6,6.296548795435126,95.35509045270324,125.19015966383434,3,26.805479371396366,1,47.33684442247203,1.7692104742850447 +6,8,11,24.35590861,92.39651663,6.600948788,119.6946577,orange,28.698326575942925,2,11.480737028284894,18.67330046216329,446.42943364563695,6.319000642195205,2,16.126241838258064,29.27905759198883,89.34279936720634,1,1.6043705227876293,3,44.27944786611202,2.843978950318198 +10,5,5,21.21306973,91.35349216,7.817846496,112.9834361,orange,29.417382729870795,2,8.858378115096784,11.423919834585725,418.10824529855466,8.876126817210217,5,14.951419950917282,66.84309430569877,147.03936237480713,1,13.968036875310307,1,12.857864249253137,2.257118014353904 +1,17,6,10.78689755,91.38411917,6.8198271,117.5293447,orange,19.724745791936545,1,11.478897800573964,2.270295509396665,409.71141988184047,8.60163353359939,4,16.283006596891767,65.46069173007292,92.09545066731167,3,19.51570861844315,2,31.9520323507658,3.633153560981519 +1,30,10,11.89925671,91.34663797,7.291405641,103.5771468,orange,15.35845327102689,3,8.708804364311758,6.432159323526712,353.91442476933975,6.474478096213587,5,14.06346144856095,3.3969950129027815,114.4325132652738,3,0.14164494139790595,2,64.81361309988972,2.9747706920685144 +0,23,15,22.56664172,93.37488907,7.598729065,109.8585753,orange,18.269990490067194,2,6.7380793492675375,1.5749297677061658,414.5950625533006,6.485464692601147,3,6.92326640206021,13.680753741687157,70.9905654056479,3,29.18793875424081,3,9.723331738962848,2.65779162395643 +24,27,9,18.86883219,93.24688124,6.157135092,119.3936976,orange,11.21451611986409,2,7.702836020860988,1.3147801655058355,368.0124981990626,1.2276225973497468,3,6.695035052254912,30.187275860883023,185.3772992005613,3,46.93837180747231,2,33.86781455784733,1.5215500755213114 +36,11,13,17.34083741,93.04897191,7.1917274,112.7194284,orange,15.44449160051871,3,11.795494183700711,13.783475710201564,387.8222179503325,2.5771201356813056,6,15.72625617903873,96.01580620642608,75.79029387760536,3,3.4513361573214327,3,72.4814574786914,4.9623749661681344 +40,21,8,34.90665289,92.87820148,7.418761774,102.1906333,orange,25.40678677362972,3,7.210015507968928,17.79457251627108,408.66637701380034,9.036034735995337,1,14.625995181574915,83.32897883579858,89.24663345599373,3,48.80631028614603,3,26.989926709604372,3.0539152073632194 +40,22,6,24.53610067,91.90997228,6.488221135,115.9787989,orange,17.067900874513,3,6.261862244192833,14.800355293734208,440.6841595601946,1.6525507333498413,3,9.613227559064844,16.64939691870626,143.83393670900392,1,17.66579816691674,3,45.84171089277081,1.3445933753595685 +32,18,13,13.8377282,91.74780462,6.044167236,107.9873218,orange,16.876425328309097,2,9.36382328513665,17.40035305882657,419.7294128123874,1.6394300641299677,1,7.580221373353777,34.451966058248594,157.6281074590163,3,38.807426802156506,1,68.4152749300404,2.462070566678687 +9,10,10,22.3551049,93.52211892,6.010391864,101.5164589,orange,28.399772046687875,2,10.653238823796048,8.687350837169262,416.14392850243024,9.04387955399735,5,16.651399796083403,14.428318793715821,138.27973225942475,3,25.178810839699317,2,60.32481976290992,2.373822352629356 +13,16,8,34.74004942,93.12316972,6.949838549,100.1967854,orange,28.580723922361642,1,11.706888410405455,5.717466966588169,415.10146996862863,9.1137955145022,6,5.875869792731601,42.450731077778215,91.57575487446013,1,36.18637295440541,2,15.609798534695884,3.413619340137101 +15,9,11,11.54785707,94.14861001,7.907956251,108.8289171,orange,17.11827360971662,1,11.971045780920512,6.847881058209369,404.43869739904096,1.129856704252913,6,10.281415302889293,78.48717442795412,107.88225859661671,2,16.503529694818848,2,70.68077128376137,3.8429218152689404 +29,11,5,23.13338811,91.94670335,7.639788459,104.4224145,orange,20.87396807319682,2,7.616996239200082,5.688598550380735,410.28654275614974,3.3509635448773247,4,11.910097894762586,11.472389469661493,74.10143217829099,1,40.77750511182843,2,43.298687026046956,1.6053512121616156 +1,15,9,29.98364695,94.55239717,7.53350946,115.3560318,orange,15.532948055771902,1,11.113951510497884,10.154415155035947,435.80862839346673,3.31150124492547,4,6.0555133816011875,7.847222919683839,89.48404263562622,1,26.565047735840032,1,81.83857283391397,4.4557628118522 +18,5,11,20.87947369,90.93756231,6.251586885,102.4550786,orange,16.398181219278232,1,7.511546293823313,11.722983967516472,406.5161104918442,5.0900634795321755,5,11.233066575375597,87.89581589546484,72.82220008131874,3,48.029160404047815,2,35.67409507494806,2.8638056898191477 +14,22,9,17.24944623,91.13772765,6.543191814,112.5090516,orange,18.83368125391216,2,11.768008548015032,1.4879144892807172,442.64260833650246,3.6747245879814203,3,9.2644040706104,35.08684140225905,191.58883746638037,3,43.89248362199549,1,29.249259860061528,1.8062121109731994 +33,15,7,15.83388699,91.68293851,7.651225301,109.7571416,orange,27.152322635424106,1,7.892048333285752,3.4335021092044093,369.14091436018316,7.584044295156777,6,12.354686535187465,56.64746342450406,114.59086392109123,3,19.362254054353034,3,82.12315051263278,1.2358251523368189 +4,6,7,23.01014302,91.11764246,6.708889665,112.6738296,orange,18.55664335060832,3,10.99382106534437,19.871115531315773,435.1333793125931,9.321260818615375,4,17.98275130314844,56.64360918856618,65.99854769206468,3,14.465073224966313,3,88.46440806064892,2.3242347834158372 +17,16,14,16.39624284,92.18151927,6.625538653,102.944161,orange,24.289082081608385,2,6.69379390586635,7.626637607719586,445.43504502966135,5.101994471824836,1,15.49771957577355,60.967159303999885,185.81229799325797,2,39.725170782024406,3,68.97798630695648,3.0674816297608913 +12,20,10,24.45132792,93.10527686,6.528354932,109.4711098,orange,12.90586964418318,3,5.654151819909976,3.711491053259328,395.6033776988041,5.086627646753048,3,12.190252871687047,92.57190893243364,52.065936254978766,1,43.46120027198124,1,7.6896563615334586,4.098692267021198 +34,29,8,31.87859192,91.15248149,6.450640306,105.3437825,orange,20.95082841874664,1,6.356475642247812,0.4749767809801497,440.05436710799574,9.035216619470686,4,17.560435418026735,89.41115034395668,88.55773094043481,3,15.21077146764933,1,57.683129018582434,1.8882487385422637 +39,28,10,31.34920143,91.48247612,7.181907673,109.1549823,orange,11.417388819274958,1,8.169594935563422,16.030366863059395,383.48786843989865,5.062599712264464,1,6.931793252519581,92.06873852251721,62.14217555060688,2,4.233709674900959,3,99.6711833584999,2.441067173312076 +31,25,12,18.05142392,90.03969587,7.016482298,111.7793889,orange,11.170926213548938,1,8.954606384703032,9.449275432238402,434.99215487370105,8.983904296848353,6,9.175239439364876,33.94903913879806,166.23807531343883,3,46.907949366284534,2,1.8519188576182177,2.4332455749321715 +12,6,8,30.84835031,92.86773675,6.388617138,107.4142681,orange,12.740281803182636,2,9.340071976673787,7.302321716095683,438.1066017903882,6.992863044569792,2,17.542047890482092,51.36085243080865,194.57941698092668,2,17.423743826131428,1,76.82174145174606,4.213638731828076 +12,29,13,22.45616931,91.52781832,7.57125447,118.0069295,orange,23.109576281045193,3,8.500433767004509,15.696301737879262,375.77880003506203,9.011863606320459,3,14.209690490435735,48.94438553454279,117.53397303357163,1,6.980330803421742,1,16.173813941752723,1.3749037296360882 +26,11,11,13.70319166,90.95589386,7.609348255,106.2944879,orange,20.54809814425508,2,6.444050705537716,4.149650513655145,367.19701820321575,7.463924406158924,6,7.142349060158192,50.36402731589222,132.56550711881744,1,34.72324376152116,3,96.08959201847739,1.8285835461149418 +19,24,15,20.48954522,93.72485075,7.137136973,111.8391951,orange,16.501703945706403,3,8.659614756162684,17.63063548516436,386.49150599849855,9.228068662016668,1,8.687252019497576,76.67992739258796,145.50793722437163,3,28.562472633108264,3,92.8078915111719,3.0121732861924264 +39,21,9,13.20844373,94.02769434,6.354022554,106.2696156,orange,22.392835420014194,2,11.751601563416376,11.595938890234539,418.87789018994624,6.262045734707961,4,18.978082353191496,96.35201995676985,53.51958225515938,2,22.600933826134284,2,43.545487375284885,1.5888352355132445 +16,29,13,32.31944397,93.67804556,6.196907944,117.6236473,orange,25.085537537321954,2,6.49299939240038,19.609114911456494,410.6477140827169,1.2509433219663904,4,8.494867752981106,87.36195926298227,150.30286046657392,3,35.50360862680123,1,16.353592760953028,4.3950130812828005 +36,29,13,20.68185224,90.91510525,7.829507245,109.7513927,orange,22.445022425005092,2,10.0860109211849,15.351356743375618,387.25174140270263,5.781025588047305,5,9.739675976754972,25.997072512920404,154.41193582304,2,14.05083979751574,3,56.7143482205333,4.604491383086153 +37,23,12,31.52675982,90.50621806,6.395258356,113.1169398,orange,25.43151559963759,1,8.984180250929612,16.16180591833384,354.88273356929244,8.40843928213271,2,14.125397025429086,89.85704123536237,65.0409171983259,3,48.55123189669431,3,54.301643121103304,2.506593669963315 +39,9,15,25.35467646,91.81183218,7.992041984,116.7555937,orange,13.451100527527979,1,8.801889520430612,7.834730825810732,441.5990458292334,8.170814999508394,1,19.908104210845423,53.42581604273447,89.49092365451492,1,20.32132420591245,1,55.91429975695262,2.581088618326298 +31,5,14,17.66545409,91.69865887,6.583411671,110.6857506,orange,24.63273222117275,2,8.932815593544875,4.719546057011696,444.73265782731175,5.952679051463292,4,6.597333083396782,40.99961880221786,113.38844856342548,1,18.547290393583932,1,6.838801095381985,1.3581024920315081 +18,12,8,12.59093977,91.81668769,6.206053072,119.3916718,orange,27.154031934792112,1,6.762180113710674,10.24435421702012,424.1857623339886,1.050747468592599,6,7.427601834888229,72.92305231027387,55.32030300915854,1,21.011290484585523,2,83.1087785412069,4.005910025367338 +20,20,10,11.86631922,93.68394562,6.976997772,106.060149,orange,12.655452806967292,2,6.662520579307481,7.527889802251533,430.40056605529213,6.448828397710349,5,14.719515613495986,35.622518707622916,88.93148007885189,2,0.251000860857703,1,89.6654480408335,1.570648787569879 +5,8,5,11.03367937,92.22706805,6.562594972,112.7715925,orange,14.717822426835927,1,11.186505684314644,11.225745040940433,358.5474689895075,6.5712565836006895,4,8.707565685061398,49.13614302519179,152.74213974097623,2,21.414098445610634,3,75.70951781701228,2.7996056927889295 +20,8,12,25.2990432,94.96419851,7.260416405,117.9733424,orange,16.20937179133302,1,8.371574695234713,15.86056052695022,401.9603378965288,1.1448052161569588,1,6.539236550523825,93.0686963601568,162.02079593347247,3,36.567692920756755,1,95.98977742772796,3.074705668958859 +25,21,11,32.23797837,90.15406807,6.460044778,104.7052254,orange,22.92781897921299,3,5.000710080682302,13.124342810727505,410.12016986716264,5.869718048404425,2,14.59148690402554,98.32453762959287,112.80201618444775,3,29.619525783010126,2,75.57690417949613,4.836127959217324 +14,19,14,17.68408797,94.35815354,6.699164936,108.0638166,orange,21.559579416776657,3,6.0096615538817675,12.727896256492508,354.6239250964563,8.997240622298271,1,9.71295181256832,43.934765931993525,58.999059259472595,2,42.223744860809326,3,17.90409370930902,3.4236055170858277 +37,18,12,10.2708877,90.19147747,7.401121811,106.6955204,orange,25.887347343767445,1,10.355202962513575,12.39395894059313,377.67148291309263,5.481284597057175,4,10.675260798826514,19.34727528480462,188.5975130541039,2,15.166063743280917,1,77.33427498190974,2.0243416320660406 +26,15,6,17.22034507,94.78797376,6.912033409,108.0054343,orange,11.861813428727714,2,10.729333831577774,0.8399640912733086,360.1742264957766,7.666731264334597,6,5.712658741012413,71.63868014748995,87.60129437319478,2,1.4795347064884223,2,2.528538799708624,3.594436144785529 +13,22,5,19.667056,90.50096668,7.764040111,100.1737964,orange,10.355936537780083,1,11.750802012423375,13.274567256926307,397.22509852364834,3.6822550471145483,2,15.296518295046582,2.2147900278036814,136.52812950472273,2,22.710003136056127,2,52.40966895683095,2.6155018300008064 +32,25,9,10.35609594,93.75652041,7.796034006,101.1456947,orange,23.61436403669359,1,10.899322802178885,15.541522450409177,355.0610845574044,4.7032077466614854,6,6.469321282341008,52.76661604139462,124.67794503336515,1,30.929859934318944,1,99.5404036802573,1.7693760976960973 +19,7,9,27.255435,91.71369387,6.969883483,101.139435,orange,16.42877792270614,2,10.764748606168567,13.975093807954803,375.4028471018693,5.335701217564553,4,7.1258708549672445,73.84948850374343,102.53898626170165,2,49.5873913949624,1,11.409450972799162,4.639499899533814 +28,7,9,34.5917846,92.13229786,6.730757538,115.5650287,orange,24.740204926385942,2,10.047253504017728,17.88530992724566,376.59148566381083,6.5549251413833804,4,16.802034184765375,30.887002304879484,101.21400155139924,2,19.07171393255254,2,14.492699234510342,2.0984153728082933 +24,30,11,32.39523995,94.51768464,6.601395755,113.25373,orange,21.525716715643995,3,7.996244143718383,4.203030333504345,424.0717114345999,9.064114726747224,1,18.927473997799098,34.144506700398225,120.89310430237936,2,29.28802688928845,2,22.887870506133602,2.0614103878407315 +7,17,10,10.16431299,91.22320999,6.465913274,106.362551,orange,20.631926709490557,1,5.58174605264778,1.3232647114493012,428.7289405006187,8.360252482739533,6,14.307530808593446,49.24770120075143,93.65842811310648,2,38.66123242490566,3,2.8531764492088274,3.070001259540684 +18,23,8,21.49118657,93.43949693,6.41354791,101.4819888,orange,25.316237537597253,3,11.179797230921329,19.133791324607024,446.61504908231177,6.433390896154507,6,8.654212700613044,13.416580343785734,117.6780472521611,3,2.6096072744295764,1,45.81780590631021,1.2630426338034582 +7,20,12,16.53460397,94.76759975,6.475275337,110.0447896,orange,28.583340186150693,2,9.864223862655276,1.734824222199054,355.3336258630668,4.61309432041703,2,12.636172350267554,85.08895259939267,169.56510545966745,3,35.08375395186483,2,1.04188272944995,4.0354547537709555 +20,23,11,31.8520694,90.12220323,6.407715561,109.9455062,orange,20.02249345533638,3,6.874258310635085,18.666201351896497,409.484642538185,3.116208477313757,4,15.211573187022479,57.326149065205314,182.26869813426958,3,14.551559803824404,3,37.82239711955726,4.9653417509013185 +18,14,11,28.04799508,90.00621688,6.550814117,117.1311498,orange,22.869532887274897,2,8.587313261994838,7.922319446652121,387.84634290242104,7.779835713220713,2,7.1102644709296605,64.61569832958212,92.14397378109769,1,39.65138581681208,2,26.654595488460288,2.947492918417998 +34,11,10,31.75048899,94.59551226,7.36220835,115.1989301,orange,14.254746874307285,3,8.960905953506693,7.566988593463185,383.4597979845663,8.740819600198664,5,13.624906828981723,74.60810283473947,168.49897510630896,1,12.175771911781268,2,43.17363104938712,3.2580766764831663 +20,29,10,29.07412717,93.27189064,7.36549204,100.7896871,orange,25.539072244651912,3,8.326355753647563,1.3105386082768877,434.4716514175756,9.012403832897608,4,7.8438639958009,87.45876366000755,166.03620633787108,2,20.17134735669679,1,19.777216228956817,1.272364247112745 +37,24,13,19.14381903,90.71037456,7.8546243,108.0230792,orange,23.599825691650366,3,7.334661089219814,16.28699907996174,400.2016697565316,8.487696549685722,6,10.084859142084298,43.55435354770469,79.44622616438309,2,25.571541901878405,3,54.93452961416415,4.372006929395216 +12,8,10,16.14820285,91.4448027,7.995848977,107.4287664,orange,16.894401696679736,1,11.45499524273139,3.041298621958042,440.682271389205,6.379016067384335,3,18.37894166031314,51.71251281440138,191.83888015193796,1,44.60141807255605,2,16.78358364946464,2.966327329656014 +34,10,14,34.05296914,92.05811721,6.725600855,116.8020848,orange,17.506104900338258,3,9.114820452651495,8.45336227560044,358.2793320388245,8.035496694272082,3,17.141056005643904,46.767643466717104,170.98431012417248,2,22.35500151470561,3,80.9470779703431,1.2580828402296014 +6,13,9,34.51423957,90.56151463,7.786725333,118.3271968,orange,19.163308249971102,2,6.819156019852653,8.839790083656265,362.10524316418224,7.5323717553088425,4,5.623301400882795,80.19024787724877,171.42217387702792,3,38.13603118619196,1,16.198476173144993,1.2401881162127695 +27,30,5,32.71748548,90.54608254,7.656978112,113.328978,orange,11.768006354308664,1,6.9699264699318935,11.316674401678116,359.41147411181356,9.716501139302677,4,16.31744074513373,9.89526588465981,166.71645261209306,2,37.610386505085195,3,75.31940044338373,1.6113025466327913 +13,8,12,25.16296632,92.54736032,7.105904818,114.3117197,orange,27.860009940953976,1,9.97886116575005,17.831446868567294,374.3814059451658,6.873719327038133,6,9.360785218696265,62.68066235280879,180.46991613636726,3,9.696227166782394,3,33.693318780117146,1.6513751622031072 +6,7,7,27.68167318,94.47316879,7.199106204,113.9995146,orange,25.60735510337522,2,6.967093237374402,12.129292441528431,357.65056473216436,1.4228640212963475,1,15.42018748878223,3.9808351658158103,152.36010018960314,1,16.022261879889665,3,72.56256925315027,1.9593741859894371 +40,17,15,21.35093384,90.9492967,7.871063004,107.0862095,orange,21.39328988700335,1,8.711074061499891,4.518362357829813,410.6515834598084,1.220154370379202,4,9.362445434811189,43.869084369758326,175.70639527144527,2,30.447488064278883,1,22.997895059095384,2.147489768866198 +31,26,9,11.69894639,93.25638873,7.566165721,103.2005992,orange,15.937523119314164,3,5.678524831557894,16.37610434128878,387.1922370686192,5.417500347773751,4,11.642705479373998,71.43570882742895,193.8331498056675,1,9.229531219906828,1,12.432670447790084,1.6675712023085776 +61,68,50,35.21462816,91.49725058,6.793245417,243.0745066,papaya,26.906733431816313,1,8.094210582447051,2.0660174985644875,446.9325387470367,3.920678375657516,3,5.086447882270382,86.10868470482323,66.74536138917273,2,14.936392741512616,3,57.95921654296209,3.204117994026548 +58,46,45,42.39413392,90.79028064,6.576261427,88.46607497,papaya,24.630705355725333,1,9.838624617032437,18.190395150816975,363.06215302282817,3.3132409707557793,4,6.490374642651632,68.01188642108986,50.59066760979931,1,32.90311473854091,3,11.799474243377794,1.9022084360826694 +45,47,55,38.4191628,91.14220381,6.751452932,119.2653877,papaya,17.689887685589287,3,11.175264150700041,6.569357454569538,407.09745971095145,4.798338484512312,1,10.176805108419803,0.8654756258006935,125.27353873095547,2,43.05637000038992,1,63.34066977781042,3.466943518981163 +39,65,53,35.33294932,92.11508608,6.560743093,235.6133585,papaya,16.71903240081658,2,11.151482968541915,10.815078076407067,351.5093383830188,6.072254754974239,4,6.598211906090004,57.01665059662613,152.79247734549136,1,20.573181973892574,1,61.27501270590579,4.524903668201279 +31,68,45,42.92325255,90.07600528,6.938313356,196.2408242,papaya,15.801002180552725,2,6.026434694085877,14.078401611270515,400.4953542179545,8.991706634831914,3,17.860215516125063,48.90689955107425,69.10336413976074,1,11.878644674996808,1,87.64536289421878,1.7044095180990748 +70,68,45,33.83508569,92.85470152,6.991626158,203.4044028,papaya,28.512721027027055,2,9.542551013372059,9.224507775402422,434.0869495846282,5.254636730080238,2,17.59844998412885,32.77240037238561,154.32569950263985,3,21.545900102369757,2,58.583438221532006,2.187542885444324 +68,62,50,33.20258348,92.76437927,6.977700268,197.5282582,papaya,23.133311760793937,3,5.206002585320413,2.7220625942126286,351.4400030229795,7.86238864701007,6,12.406974006437828,69.50674197633258,108.49517382088933,1,11.735814165293856,3,61.0481026527256,2.4017162933623766 +34,65,47,23.48546973,93.71043692,6.833768535,191.7760562,papaya,24.362217189717786,3,5.037598914860304,0.9506230913500091,357.86444260247777,4.444785569908624,3,5.407246224312809,95.99603278086792,59.532249196801075,1,32.62218355104567,1,50.81909003287165,1.7630783986805638 +38,68,54,29.33710543,90.81781439,6.739170045,202.0572747,papaya,20.578068710416716,1,6.833847467393654,18.145297492281568,440.69803506302543,7.410568087358063,2,10.072509415060125,23.114126992495066,107.33412752411095,2,14.367363248491577,1,76.35230042592845,3.3772346047797104 +69,64,47,40.21199348,94.50766912,6.993473247,186.6762324,papaya,15.998537217446074,1,7.3166649415101475,17.273196082461514,405.3691060633182,2.964596839625187,2,17.283527910863036,63.22546938807508,78.34577613332343,1,37.69922958191017,1,15.3120703457867,3.0943473529472554 +58,51,47,42.13473976,91.70445386,6.757470637,197.402901,papaya,29.96510053555073,1,8.091486437429555,19.334668986797283,427.6542033390301,1.7356708014733542,3,5.464154838674412,54.95685805073961,124.1169805911808,1,24.029489466466575,1,5.161945204883378,4.895614819383168 +59,47,53,32.86316618,91.4618874,6.850663232,47.271547,papaya,26.025871410582305,2,7.415531516033113,12.283523789446525,410.8754041553115,5.6381706484143095,1,9.505507741923022,38.40749139477475,186.5566680066508,3,21.202105698521724,3,5.92244288790903,4.513311803384234 +44,64,54,29.80744318,91.38048469,6.74274935,232.7046126,papaya,11.758750963342976,2,8.360162510605644,19.00724781436812,439.15025429327636,7.291198665003789,5,7.193765811567969,99.47548862979225,75.79408069846781,3,39.434818193305574,2,56.9683301313197,4.455614137052466 +56,57,48,31.56213762,93.0484859,6.506120752,63.62250788,papaya,13.076911809501063,1,9.659630983448285,17.759614934585414,431.29710584392524,2.238405727159805,5,12.98619881435225,65.89579442962766,181.59828241549238,3,9.99030023792492,3,40.84458192147834,3.735124605425648 +69,60,54,36.32268069,93.06134398,6.98992719,141.1736926,papaya,27.59924349991253,1,9.9096545383974,15.987677774702107,371.9735878548882,2.041180424074371,5,5.119143674990672,54.67695373423778,81.17609615791619,3,10.88439472386979,1,48.49318090893677,1.2544298567877963 +56,58,49,37.13165026,94.60761797,6.69215564,172.4788062,papaya,21.47270928827209,3,5.2166001960276125,14.25633099653329,439.94582557217785,9.936981461252437,1,8.781959167896172,85.18943562238923,120.40306872208537,3,5.459624815826652,1,17.40881027102068,2.805362862489642 +49,55,53,38.4418717,93.63739039,6.544029776,77.71566883,papaya,22.71343868769598,1,8.647468397564543,18.77641232930091,389.99455344596447,3.060253670188509,6,6.527762889353231,42.46925257979663,142.17322355458816,1,36.30348835818834,1,93.12351227250657,2.2826201878292416 +38,51,52,32.66160599,90.78931681,6.927803911,78.85085502,papaya,25.44367907664966,2,9.134143278405844,1.1084683310541577,412.60519392092544,7.5510876242909655,5,16.645852250142823,79.17011358573075,102.1504071970941,2,25.90220694201879,2,7.833210983014283,3.035429616070879 +54,65,47,27.92765919,91.55594211,6.721835879,149.9107557,papaya,14.28098187089007,3,5.354143026222712,14.494575976548925,394.73910909525534,8.388763233400805,6,7.934476763413258,46.806362427064066,154.58206965789654,1,6.693182383138313,1,50.6261137422763,2.0287466663480376 +57,57,51,39.01793345,91.48815629,6.99223441,105.8841531,papaya,21.400903958536936,1,7.844285145399443,15.35247668810769,421.87255149973345,2.2279379242233213,4,18.170863322480024,24.70869650119084,184.73272952225392,1,35.70284525558453,2,59.05815825970737,2.198677305316353 +39,52,53,32.51247398,94.65904123,6.704204398,51.07048113,papaya,27.516807825357716,2,6.992055418883276,1.890310008505014,378.9777047711418,3.516676472381503,2,10.349963216486518,6.960984551302696,147.14808723458407,2,33.12847621860298,2,46.68542083107605,4.913667981122673 +58,67,45,38.72382798,91.72514851,6.702424548,62.62377075,papaya,12.320207864219695,1,11.773380409258277,2.316834685609477,382.4298910249767,8.33050988302454,6,11.08262795129849,0.5227106537737614,197.05595676081188,2,29.236995857587484,3,94.5746511094091,1.489841654607448 +61,64,52,43.30204933,92.83405443,6.641098708,110.562229,papaya,15.672019150218633,2,10.030074710130775,13.44020869618549,448.4191630291825,9.840912677984292,2,12.660968660407129,47.59342342240968,114.86358735953718,1,5.142477751892111,3,2.3462482287189723,2.0596477209071975 +34,62,55,27.58548913,90.72526502,6.585346229,238.5008779,papaya,26.472415085220625,3,8.590445796367279,1.5558427117537854,360.673574659591,2.8688470412931126,4,6.3372051053078255,60.72385480187431,94.40254529005638,2,30.396497199810828,3,28.499070714580622,3.717652960445682 +31,48,45,40.78881819,92.90951393,6.563134737,132.7923586,papaya,28.25670425292909,1,11.458037887774132,3.658048811152408,439.12809790291857,8.591694555751893,3,5.081397657201462,60.63446560673727,170.09907330161417,3,41.676474464564805,3,7.080324140033422,2.5622868585316074 +47,46,52,23.19451074,91.40301608,6.502289473,206.3999208,papaya,20.606943190508154,1,8.726609010417487,19.32264605654963,379.57753443749294,2.9426436364263338,1,18.400506682900755,14.793830305625299,56.41980671575871,2,11.26188457057985,3,72.99377525855077,1.2159752419855931 +32,68,52,32.68067385,92.61715632,6.800321319,248.8592986,papaya,10.219504284814157,3,6.528084248073613,2.7023307566143107,378.99029642474386,1.8489564520902593,6,6.708324762602832,19.676514114086675,60.56229027196673,2,27.800775879554564,1,66.17521865360345,3.205485314166244 +36,59,46,34.28879307,93.61082872,6.721130543,127.2509777,papaya,23.422196413132237,1,10.576337451363687,16.702058209596892,370.85797489597354,5.84110010299529,2,13.260582832511524,91.85179332529364,101.46591411909165,3,25.249274374987195,3,32.65532705631999,3.761597949368809 +61,51,51,39.30050027,94.16193416,6.574677594,120.9512466,papaya,20.59763893573932,3,11.189501728091354,11.623244947074433,360.30001444180607,5.628031564274114,3,16.011833019515777,70.17442105783033,53.72585695826199,1,10.249037880259749,3,8.997141415084531,2.7822301947388 +70,54,46,39.73149053,91.12220596,6.919342407,122.7628653,papaya,17.24999906685082,2,10.637673626098021,8.431380922679798,380.7922374145383,7.215520273330089,2,14.015378362192097,40.39908931779029,113.51330147935133,2,34.38434019612725,2,52.53853843441071,2.882459460578002 +44,56,49,39.23342464,91.25589286,6.519779583,64.4478499,papaya,27.566634062193124,1,7.352670480122367,15.114052095897318,391.4204131931389,2.6888545478662484,5,15.643539581496574,11.375796551074458,85.82814174036538,3,48.62571461609988,2,29.316157901922356,2.7249647544869933 +34,68,51,27.34734861,94.17756725,6.687088098,40.35153141,papaya,27.51495608001963,1,5.773551576938972,6.7181941504499605,404.93839970571764,2.830494856107055,1,6.486628046071109,35.00164519555007,191.3005440783266,1,7.181554809152896,3,18.5181708109026,1.0346472637902475 +50,59,47,40.76998685,92.09278584,6.747975732,209.8678411,papaya,26.183722397131493,2,6.665055808453818,12.992449126652554,421.44695223860947,2.6742102193299173,3,7.832651727915643,75.42079335688204,129.9997512190551,2,3.7551038389567526,3,4.759044689874747,3.4096438290776216 +39,70,52,26.26559543,90.79668055,6.65149129,59.49373381,papaya,20.285976168436644,1,5.609673138826256,5.010912993994965,393.70959058923677,4.65996897605013,1,15.703495896172328,73.95022853040551,82.84986088221976,1,2.1014026676201114,1,62.829604346663125,3.210794024330161 +34,61,49,28.12971499,93.3210737,6.502675132,117.8201907,papaya,20.130745125682004,2,6.564028474815308,16.534473966776076,375.65245835940925,5.087438590623886,1,8.905278575896794,73.68068397665377,163.98884187683456,3,40.36432662227764,3,15.017200746442194,2.788066871961795 +44,60,55,34.2804607,90.55561637,6.825371185,98.54047745,papaya,12.809403007775856,2,7.8275319751854795,1.080898815719138,401.06867047390074,4.418145675284098,2,12.60845435673258,83.12929147372763,114.98219635444806,3,8.80320267414722,3,75.26073973287257,3.469455773290845 +31,62,52,33.7960155,93.00754254,6.99104104,182.026807,papaya,22.964571499729807,2,11.930850096151314,19.48137663130575,366.30591606235316,7.580859339817239,4,7.202646078170261,17.19012917426086,134.10739186977622,2,22.024747121528026,3,15.364204558712114,1.8172802056863455 +65,62,51,31.53243779,90.87394933,6.511624841,207.0735119,papaya,18.957775711030546,1,11.5911098887517,6.67949597320423,406.14212451613696,7.208279182835694,3,11.319976021967491,74.44860891473986,57.82081369386991,3,1.4138136359547526,2,70.05643133376056,2.9059129255087033 +44,57,53,42.30495821,90.51431779,6.93172108,74.876786,papaya,17.770429913323206,3,7.276931187299709,7.50676031375201,383.9926581856439,3.876440498844548,3,12.790605998482658,6.04008008220659,134.47108304361345,2,28.61957387713023,1,70.22310069561225,2.134185349751638 +50,47,48,24.63676897,90.61964344,6.712772333,218.2299187,papaya,25.068816466184494,3,8.662425238583827,14.183036513709075,401.9428811151547,8.672049259031706,6,14.876171486223594,14.688525995303737,86.20303706454222,1,19.537614002021453,2,76.82706844145144,3.2544395971297897 +43,50,48,28.28222883,91.37059792,6.63016515,179.2720807,papaya,24.540671714294106,1,8.389691867117223,18.83898870736472,415.0911056518783,1.1404350027543855,2,5.598452534399535,90.69093242038544,170.1647152006396,3,5.49256345807253,1,91.78224171437517,3.8076959913999326 +60,46,53,24.48620746,92.98254537,6.761953186,183.49095,papaya,10.813331473912516,2,5.806257740065444,16.335482367717262,359.6413904532819,7.525397644638131,5,19.313643469309497,41.637161690102474,158.06994422078694,2,38.53714625685462,2,58.25969184738252,2.487073272913899 +70,68,55,42.84609252,94.63548176,6.691202286,78.8099639,papaya,14.210942109074615,2,9.241738751752361,2.7449025659840487,369.6457490373534,2.755744736713435,5,15.127758283757213,89.03373395819946,181.66015259294798,1,25.605297254845205,2,80.84732996195902,4.0040752376835105 +59,62,52,43.67549305,93.10887229,6.608667684,103.8235658,papaya,28.041730230148186,2,7.165073062005311,4.177753373762592,358.4591609771533,2.540372573126564,4,12.65293440926693,22.14552295132607,93.28564001541702,1,36.743496476225395,3,78.27637874114806,1.5709683922364603 +60,58,51,42.07213781,92.92203105,6.840802254,165.7412972,papaya,13.498886949366664,3,9.941021228876401,19.4321955183357,375.81662396986087,9.402403445699731,4,19.892158274465224,0.3217760729364971,143.50055339798018,1,24.241040301227617,3,63.4699931313985,1.6773411482611893 +42,60,47,33.46873719,92.12746225,6.834808348,136.8277041,papaya,29.622119391729193,1,11.547187533828355,19.742814426025763,392.32152437216854,7.1922562698004135,2,8.978021378558742,63.630787477322095,63.53919782739117,3,44.57212810031251,2,76.12493818616466,2.911644709444103 +35,66,47,31.7018373,91.66232213,6.953439161,48.83810592,papaya,20.921706614577328,2,9.434376180303488,16.61131301925184,423.81430538338856,2.3565205093486394,5,11.94038729292574,83.19017242246758,161.26803363553313,2,8.629335603992056,3,96.23414057549489,2.4291314522607563 +34,65,48,41.41968393,90.03863107,6.665024508,199.3096432,papaya,18.770621098934726,1,10.856886340709183,15.736742020632375,357.8232048174297,5.302015968926105,2,14.063853511368078,32.28991247337195,98.18447734746894,3,39.79718856062269,2,2.6882951078088357,1.145611131528693 +36,54,46,42.54744013,94.94482086,6.662875839,214.4103848,papaya,18.03068164167901,2,11.514039151714515,18.22558548134182,445.3386353659589,3.232496516349841,3,11.74028434493579,44.77956453129058,177.31715702677437,1,41.69235353970873,3,7.025568880222687,3.353358995520278 +39,64,52,28.91842453,94.63676767,6.678695788,63.68794608,papaya,21.730712007454187,2,7.761420487723624,19.080889220396656,411.33613034850794,7.778507643858717,4,9.938197664559569,4.95235848878367,112.80966771998814,3,0.71324937125537,2,20.54522016954997,2.420171713873082 +37,52,47,43.08022702,93.90305729,6.54277684,211.8529059,papaya,19.46664456625801,1,11.199261682818033,8.806075323720691,441.05031329217735,1.2017990819681237,6,14.609822870805317,73.3965229109283,65.17159146098425,1,40.943491933765294,2,96.16895525838733,1.6717061744282304 +33,47,46,29.20300896,93.96834049,6.839443833,209.4083305,papaya,27.678201472007082,2,8.521516084785803,17.702169238439,402.7840041902042,7.89188269914041,3,9.043884867843012,57.977666047433566,94.00318740089085,1,29.77358446231071,2,60.166297257067136,3.6237414558172927 +34,48,48,41.04224355,91.37258067,6.805277038,181.527598,papaya,15.734266193167041,3,9.735432027750162,9.432378051021868,429.0868837795624,5.084611206021507,3,14.780374554885272,17.939552308629757,178.05323719589924,3,8.911509102256849,3,53.06378012871572,2.930380970282047 +49,54,50,25.62446619,93.18240298,6.762522087,97.26336657,papaya,14.374812486344187,2,11.748985339044086,15.743080935005551,369.896977042358,2.9788455862889034,5,19.79169107668557,60.81298209716215,182.99513743485267,3,43.574048331860084,2,23.106662437992842,2.1099600376825043 +40,65,49,35.32876402,91.06138506,6.678449318,163.9069365,papaya,22.554211948709074,2,9.83705306002928,16.564874194367537,368.86347557526057,3.6652809715675794,3,8.797278891971754,19.131150782360617,100.1668949802169,2,31.263032268085112,1,36.39230928188796,1.4694572323428394 +68,52,49,24.42561272,92.27749066,6.577192175,63.35298768,papaya,18.330854935563828,3,8.677039258369108,2.441921071159605,431.75835903619753,5.218635730026867,4,16.340099262584914,74.9703357884373,68.94242075091458,2,22.194022039680622,2,97.6919553392209,3.3318685494246507 +50,46,52,31.18298415,90.21646909,6.734005648,54.01872359,papaya,21.933087138686584,3,11.902642349589078,19.829188096606252,406.19031144533125,9.61608337455316,4,6.956132614540701,77.5274938892253,196.91445049736095,3,30.885049689178008,2,82.47113419314682,1.708254809825104 +65,63,50,31.88342554,91.3256535,6.524459342,79.27201575,papaya,21.227273586286934,1,10.850690165679472,9.535915588450738,383.7154492040982,7.417555386326712,4,16.06754447979937,42.766874691805015,94.1873355775962,2,19.516590325948258,2,65.79516897785618,1.1782839230453748 +40,49,47,42.93368602,91.1756748,6.501521192,246.3613268,papaya,16.915447591455198,3,8.372122167960576,15.785627987640787,374.6563668485717,6.25128424837692,4,12.740502778581202,79.19836974154278,182.87264546141486,3,27.23682986930511,1,91.72068723163248,4.9260926731668615 +42,53,48,23.11407669,94.31994776,6.758479569,231.5153161,papaya,17.999723092851553,3,5.892382831145507,9.475687317503992,403.20816847270726,8.565270869918454,3,17.045972180827384,52.453433320175336,80.62705005038588,1,20.244674119406664,2,3.0887146513079555,4.022441244895647 +49,55,51,24.87212063,93.90560147,6.676578778,135.1694525,papaya,20.619743664679493,1,6.710702041729311,1.979500417577753,367.67034634948277,1.5916360204889952,1,8.880582160582783,88.37770947761771,106.88710097637318,2,2.2978205029076495,1,12.614755926887678,4.436880179594342 +59,62,49,43.36051537,93.35191636,6.941496806,114.778071,papaya,28.774145801388677,1,8.87551048853432,1.013899193577854,359.25108074627644,8.032944307166947,1,18.824119163729915,89.61166749060816,81.9316744947925,1,27.126745352429666,3,37.01121006327831,1.1663237104469935 +63,58,47,26.83054058,90.75379971,6.864143752,144.6656444,papaya,11.309831371911493,2,5.0714170037735,1.201724779870974,448.68884472991476,6.569731440223268,5,5.501491884920891,0.9669516224375396,198.4233497105028,2,24.463013693482626,2,46.65083117194778,3.3359476777672854 +70,65,52,30.42012134,93.12659793,6.583528529,75.95295,papaya,29.31039012136707,2,7.270578820475427,12.792222261103007,350.4886293609476,7.640509255123416,5,13.010250638679173,78.83125140122658,54.67683678575163,3,30.68328187998927,2,50.43879297866323,1.9471604522268282 +63,50,52,28.64555584,93.22642604,6.751747609,115.8163936,papaya,15.463638590216256,2,6.183840318458334,14.980473561370394,417.35711415412857,9.054292199110408,4,18.480995830886016,18.95046005018535,100.41706609048377,3,15.747526080499835,3,2.795558879660809,1.0300894491404358 +40,64,47,32.50037548,93.47888842,6.893509446,71.73759526,papaya,15.812551079924324,2,6.6803211772998115,5.770664896923343,383.794161597533,9.111232868530431,4,19.02234439833697,11.85807321354746,101.15899477501355,3,25.0311189788939,3,61.41433368890779,2.6486918209819312 +63,58,50,43.03714283,94.6428898,6.720744449,41.5856585,papaya,24.05184914662287,3,9.098483094141415,14.821606287440654,440.28735842551663,4.432708265144523,4,14.913553995010304,37.461647522841645,65.24990332515739,1,35.017212418599684,2,53.96992972376326,3.3503873732527203 +45,58,49,30.10773379,90.34546355,6.827812549,75.24521981,papaya,29.96830156148778,1,11.03634322695537,13.324071728205023,449.57696849086807,6.008998996760948,6,16.020718052630762,95.91286100596706,91.15100840908072,1,39.338528050310586,3,0.14390755213732342,4.513938518643689 +66,69,47,23.69212243,93.61055571,6.912299695,87.53393983,papaya,18.989186141616887,3,10.481355952176196,4.5855337152387925,441.5363105686713,5.275658090605019,4,14.934250291623254,87.67793720687128,160.90684564463436,1,10.16398553599242,3,58.91784880309253,3.85447289874224 +54,67,52,35.67667332,93.30641944,6.586107335,141.3381168,papaya,26.59149417677811,2,7.635273212664643,19.152759416267994,352.68705674271735,4.77058540314957,6,15.642807724177548,74.22359549670105,185.26459460837034,3,9.387056290304919,3,78.95524569271083,1.2692963297637418 +69,67,52,27.71948962,94.43877142,6.827305908,82.83061083,papaya,20.81634125783072,2,5.132527921778863,5.321725101418013,388.2653687962985,5.5279896765606455,4,7.42738077743748,71.79933375408353,159.54434391971674,2,5.162644854398296,1,56.172914408248296,1.2537204641209572 +67,68,49,35.26824831,92.38282957,6.821774589,149.8488208,papaya,12.880182784384493,2,6.745046811389404,17.315164557944577,392.4968055649721,2.7608346613183103,1,10.96605503299493,52.038419946178536,60.29661497795084,2,43.008030609853925,2,58.21671804391604,1.9457053436250797 +45,57,47,23.16855863,90.78821158,6.656458831,161.6892093,papaya,16.67152868568296,1,8.700724638793965,17.569523172197258,435.0955856703055,7.816589136829344,6,5.2878802757282735,75.63688981705394,121.30225101269946,1,16.469339678281226,1,31.902202029316207,2.294668765366497 +56,50,52,33.08706051,92.25197542,6.770384816,88.1300769,papaya,20.750323641039678,3,10.036085975127882,12.680488209745754,431.42833870235,2.521669263174357,2,11.448820808894716,60.81698725418547,70.42554517173872,2,46.86041868326887,1,94.12762043342873,4.6137593476509 +70,50,53,37.4620912,90.44967809,6.933809743,172.3458448,papaya,19.413015581216605,3,9.303116707579065,8.999118680779654,436.41307559091797,3.9090680923993326,3,14.721670644719623,89.53190634045289,191.42914361796926,3,41.521310293166174,2,80.28606550409954,3.524611999682914 +44,47,45,38.73218907,94.73613484,6.579441304,218.142147,papaya,18.391528472838345,1,11.247329936870983,15.206732284527673,426.29370594045065,7.830391402220913,6,14.718416297645899,57.357700176347606,179.056655107626,1,4.455124811854915,3,73.79141278579317,2.6150014090920637 +50,60,47,32.57720726,92.74889453,6.92791761,93.7942847,papaya,12.678302090759459,1,9.991371069784996,12.013647389717136,402.5532718370433,3.497648945840512,4,17.884493900476688,57.357683598093224,63.08649555151926,2,34.32298041048651,2,34.225897872771284,2.33376536952347 +52,51,53,38.38231475,93.10378595,6.985804083,210.2735346,papaya,15.356110722789815,2,9.974753841432836,19.02483649098899,441.01475750056363,3.6174639346133195,2,11.303823623947743,70.92182332883787,107.52075426853717,2,41.20751085007263,2,38.19774710376428,3.172125528410933 +35,68,45,42.93605359,90.09448142,6.612429546,234.8466111,papaya,19.521057794343292,3,6.714192007809455,3.337713376609246,350.29545308835804,7.265497884825528,4,19.9086338734396,59.813927900868435,192.04750810771645,2,47.80726745251792,3,31.27534008793973,1.7185682599891527 +68,69,52,25.65492304,92.74501561,6.813383387,52.95477913,papaya,16.461293179091783,2,8.700239373898267,5.725583654965611,414.10923926490676,4.9531026445120885,5,6.536903794378271,40.33546374508583,161.07910666314754,2,1.0279710430287359,2,16.217751868107165,4.860232893132721 +32,55,52,37.58899717,91.99740365,6.9677596,159.6577388,papaya,18.503828246385154,1,7.344281639266306,7.796568347685302,429.5751203680354,6.83264075429295,6,5.732479676605217,25.468451954780345,115.7926273533095,2,34.99229299593874,3,46.922434142972534,1.332565278368023 +32,55,51,29.60718808,93.15642801,6.57398033,62.68710535,papaya,12.691603799052496,2,9.53448807748208,6.994659512948386,446.6578846141623,2.124489139822238,6,15.985269952696695,4.854128851179185,134.14889439761612,1,29.301132801214635,3,48.220711649934834,4.01847793069694 +48,62,47,25.34756111,93.02871078,6.803094965,174.4012337,papaya,22.17965115410852,1,11.265646666045278,18.654104117601072,396.6818245994328,4.231152880520806,3,6.775329697182644,93.70777460410218,175.09407966380383,2,9.1722701878795,2,13.063397614844108,4.912017992681892 +39,69,53,25.9300384,93.02357765,6.964955435,241.8202079,papaya,11.945961317375023,1,11.131772235434482,18.672345373882603,431.7912529958204,7.6397713428666245,5,8.043340862079674,48.788305812554796,178.76407708699185,3,46.55891756333332,3,47.32347822146975,2.554775988429912 +49,61,45,32.76795887,94.57377401,6.764213299,240.4795923,papaya,12.892683752551335,1,9.10811222510089,15.999654391558293,415.51132544348036,6.3562475253121375,1,14.309197843194378,7.485672410851274,135.1950054273402,2,45.79562077076363,3,95.07379611170764,1.2640625727425063 +48,57,54,29.02328049,90.20396783,6.617703178,126.8069869,papaya,16.968561324686164,3,11.572935894240398,3.038221665352012,433.1599342778494,5.301945095584878,4,13.554681942403231,85.3941027991614,100.96652717377616,3,31.056762483785572,2,4.238608443518288,4.171586465264122 +69,66,49,40.00439101,90.17015833,6.52711001,92.11877372,papaya,16.741834928011453,2,10.695138267239702,15.726241162858926,392.83263482042787,8.856081603957097,2,13.064711715937605,74.45537944418284,141.23295765435176,1,22.334565701480507,3,16.992405445615134,1.6612679593527049 +53,55,55,33.32315744,91.25271223,6.709668804,234.496633,papaya,24.095735927477847,1,6.613956303206415,8.198115035106019,410.4029842986661,4.493367579564108,1,15.912671018677674,9.13870491309382,95.49500531647567,3,20.466353553402044,2,30.023342556457656,4.296252487702731 +38,61,52,31.22790131,94.94021378,6.620729882,46.44279118,papaya,11.562340048755221,1,9.441318144058988,14.155338900084573,435.51242286257803,9.996494756977654,4,7.107477661386849,89.19969192682682,134.12398555651336,1,36.93200118429885,2,74.02092254638963,1.4988838087059593 +57,64,55,26.68386496,92.9585411,6.583760499,62.50689682,papaya,22.223867837770705,1,11.991041166034698,14.589779481624507,445.0928333539549,1.886001730409155,3,11.226564327193003,20.142149977528067,174.27636651034845,1,19.409105910701197,1,35.00561501213141,3.208657837756228 +51,57,55,24.70528368,90.14732171,6.676407337,108.4103158,papaya,12.229344875900477,3,8.68766313523803,6.402836391212665,440.23874476367916,3.442893940839815,3,16.20675510641274,38.94334989197564,72.54698356028473,1,4.135160141236738,3,12.011771389272507,2.2463374171046158 +56,65,45,38.2016825,93.97379963,6.751298936,218.0908814,papaya,14.351445771487693,3,9.375731170843437,1.1025022672056517,370.9920181602563,5.656166490407234,4,10.464934334631067,65.4660565481061,151.52352085772986,3,33.02091619695332,2,52.29908125021113,3.1497096367342334 +54,66,52,36.56769731,93.79503425,6.867554147,104.4218596,papaya,24.49355731481664,3,6.450266954796312,10.022000901292135,375.09442208396945,2.266358430532646,1,11.436974356712856,23.095767642106622,93.8098323732352,2,34.96469382307829,1,19.201751864245608,1.8500976423155522 +58,55,47,26.05375792,93.69111672,6.742490027,240.6863901,papaya,28.531181252908315,3,10.90337389622462,3.911184248935491,362.9395830741769,7.329607316752662,2,7.039409912344215,23.70713749066993,128.9369769910385,1,49.84185964277522,2,63.131271468400485,2.783781641349805 +68,70,54,31.29986342,92.76039164,6.986228647,54.77830202,papaya,12.158081008876225,1,7.188123807435622,7.541698335706233,445.296645879074,5.219172619797352,4,14.760684097783846,9.717404684582032,54.97336759069617,2,41.26265107376678,1,53.70979089693826,2.361947237546152 +42,59,55,40.10207731,94.35110201,6.979102243,149.1199989,papaya,25.366454882396546,3,7.14360928854546,13.70629312142627,384.47262725564104,6.760240468982643,4,6.465600765200482,4.919546003054465,90.3097770916595,3,11.277726887542382,2,71.2109121341481,3.0001213235409967 +43,64,47,38.58954491,91.58076549,6.825664782,102.2708231,papaya,27.92906065152072,2,10.729712021368183,4.0439282906086715,428.95427221803067,9.713981867311643,6,7.711800955757495,78.24936784611874,127.77157188414058,2,22.688049789306387,3,43.832128593021956,3.159455868493706 +35,67,49,41.31330062,91.1508798,6.617066674,239.7427554,papaya,12.306331603805344,3,7.963601147559772,2.872931461188706,383.27592212103394,1.787321456254477,2,14.899383011863458,27.781503163030674,144.60947761319864,3,8.042056296817135,2,35.98467126815653,3.409441250119234 +56,59,55,37.03551903,91.79430166,6.551892638,188.5181422,papaya,22.134314334633135,3,6.284040826899938,2.7083621496831456,429.8422302507863,5.960430291189909,4,6.99047861180536,18.90789671752726,54.618607941952675,3,49.523914504217444,1,76.58257422951633,1.4186635029589838 +39,64,53,23.0124018,91.07355541,6.598860305,208.3357976,papaya,28.859922890212687,1,9.247626207408473,16.40353425714151,394.44898774360695,5.6302071582870825,1,18.825408719856078,62.06961459548421,175.53791354036383,3,32.71855246345728,1,60.53873964236536,1.566720672407286 +18,30,29,26.7627493,92.86056895,6.420018717,224.5903664,coconut,25.848772595105103,2,5.547351615501868,14.272734194250026,390.5855324394667,8.28326825402684,1,13.458606746572837,52.6936920119495,56.22154661233122,3,28.782786127617587,2,86.90835446619704,3.228162944026294 +37,23,28,25.61294367,94.3138837,5.740054567,224.3206759,coconut,24.071076576558717,2,9.884225554204342,16.111706454072166,414.6855482190528,3.5456378341544306,4,11.395353533807324,17.847818357543055,105.15702117516886,1,33.54418661323647,2,16.87378899273292,1.1860927395825334 +13,28,33,28.130115,95.64807631,5.686972967,151.0761899,coconut,14.661277834572319,3,11.823272625945926,19.09420673941376,361.80249137561276,8.031681659150008,4,13.565912819319019,69.65648569264833,67.56219982028082,3,47.788664966928664,1,57.92638043160563,3.295214526946238 +2,21,35,25.02887163,91.53720922,6.293662363,179.8248944,coconut,26.786372404317703,3,5.787683954209762,2.2288285242667394,438.5321089204349,2.3913406423202,5,9.441842097337059,73.28436594821346,97.50124190798894,1,40.34586597240338,2,76.54050054237997,4.539231288477778 +10,18,35,27.79797651,99.64573002,6.381975465,181.6942283,coconut,13.724500461809814,1,8.85214723602008,0.9886687856390708,447.5694448079091,8.50884732939286,3,17.98412741661102,52.67998376856517,177.32410271255884,1,26.95689641904852,2,13.971143414225661,2.0448814301619764 +7,11,32,29.25902906,95.11294697,5.542169139,184.7624496,coconut,27.986669616025814,1,10.616487657847614,7.685490931443903,418.0574324719819,8.916261573075296,4,19.174559591940742,75.36782654686527,84.75182050119653,3,44.95260600869276,1,41.67681974197368,3.2028463502379396 +39,5,31,27.10134661,93.69979946,5.551963184,150.9502632,coconut,16.73073733205308,3,11.717986457524564,11.297318191302148,360.53902392299557,9.397210307914765,2,16.40720175617409,21.129917133810437,65.63500131260565,2,35.97203078390796,3,16.279393942759334,1.3168933038965633 +34,6,27,25.84726298,90.92669463,5.860740481,147.8888994,coconut,23.35090475621889,3,5.599985356972839,14.365949659816845,360.70201174019525,5.594179131462967,3,14.671505331385358,34.238089004748616,149.76693048590732,1,32.376606063618354,3,1.1463639995337283,1.2360242136237929 +31,30,29,26.58580443,90.98617591,5.558807063,178.8116076,coconut,19.017648864009292,1,6.536459743956851,13.169834840878138,439.8660486511585,9.374167451791369,1,10.233263959631138,55.635454591236275,140.73452527343795,3,38.1834060759755,1,7.234705049700763,3.8704917749047145 +25,7,35,28.38503882,99.18843684,5.55771171,189.6711349,coconut,17.547595379350277,2,8.707117330592318,9.764707954262544,436.6246490506351,6.287436117170946,3,8.199473571458563,78.22148629227657,121.23391848759056,1,22.175216898480905,2,18.00314014625949,3.2613881573364427 +16,18,26,28.43647052,91.81320717,5.568365926,145.5414413,coconut,26.40740953331289,3,11.297603687864655,15.56613650254012,353.12362330738256,9.389334451498408,1,13.821696720786909,44.87018346856762,175.23213079211382,3,38.76295223048526,3,30.1754582629694,2.775337736329966 +26,10,33,28.27298134,96.93649473,6.07071786,198.8234862,coconut,18.117574438098707,1,8.963345323362665,12.647827069740348,378.7289576179632,4.592996797171095,4,17.057474074908278,20.275325742721286,72.99056012372688,1,35.9305218594811,3,32.005523354215434,4.670269946289473 +27,8,32,27.00648436,96.46168931,5.627860549,144.3331315,coconut,28.56188591654544,3,7.706410327644193,1.145099716517386,408.61938414241837,5.592807401309599,4,16.808380108198783,1.440997992392612,94.42803996985612,3,8.381102818845227,1,35.36367605492692,1.215543310172753 +37,18,30,27.63551259,99.34854917,6.38488418,157.9171537,coconut,18.384330070476658,3,9.674221139746836,12.88808186844137,445.2675017756765,5.743174968767658,4,9.488548111149395,12.204933052511002,183.29324661949198,1,9.884740192966241,2,90.15347084010465,3.2155212181542865 +19,15,34,26.29644905,99.65809151,5.685889066,215.9195049,coconut,27.59950501573593,3,10.912798116359667,9.75766345478357,384.13291280822597,8.66441275661731,2,15.292969156754282,90.89262724815732,149.26296739232674,1,41.81402304518285,3,98.414343864732,2.0374935570731485 +0,19,33,27.1326009,95.23797989,6.234458417,204.7206567,coconut,20.57670167577803,2,6.115956850773732,5.34548629994889,368.3515065086322,8.873144890005161,6,17.563959473449845,92.65544384376709,93.64842568508509,1,4.4635301060318096,1,71.0715314989671,3.6669127324325643 +31,20,26,25.56567803,97.61361544,6.443168642,199.7936345,coconut,25.891091923669727,1,6.854979293600513,2.4419349254374434,376.7681591658557,8.28943947037218,6,7.298792305001493,83.3875693178738,151.77160143792923,2,18.550187281459053,3,61.523442486545434,4.481283984224433 +9,17,32,25.94951662,93.40548703,5.842317989,172.0540491,coconut,17.55277740851953,2,11.837722915945754,13.159299659295407,355.1512753867021,2.1042390120694714,2,15.29700229464038,97.46579183575956,135.80677614696026,1,11.598309753565761,1,66.1925945428744,2.7370862697583846 +22,11,29,28.03380598,95.01630593,5.955742971,218.0055713,coconut,11.05272560848151,1,5.531733445891389,6.84086461811543,375.04871580093203,4.880434146886413,4,17.5448948018278,4.456910723721952,192.6223789965811,2,10.014136216086595,1,25.294236576929073,3.4394920997583496 +31,6,26,29.12859129,91.30924833,5.741367375,157.2388553,coconut,27.681269146344203,1,5.374747741644067,14.716859870449351,393.7850278283644,6.06728532238868,4,11.361140170733787,55.20479783507155,66.86053887350167,3,44.135838513048455,2,29.49503294394119,2.4923597381904417 +34,6,30,27.0828252,97.00155491,5.948342571,171.7575545,coconut,22.469894116469312,3,10.85840698912399,10.84789296562101,420.049052282345,2.987938381696102,2,13.019380642194628,67.82602810678344,139.14192755622366,3,30.115191022036996,3,31.215059318287242,2.555057247232391 +24,6,32,28.11321494,90.01734526,6.387067562,172.4813641,coconut,14.004584887409244,3,11.313738989522694,16.5794148210242,406.1676368069324,2.3300450196021534,6,18.433642030902078,61.758970986025616,184.9570551423476,1,11.377722982991934,3,86.8781127123308,1.0975100570808571 +1,8,26,27.5136304,94.18955816,5.562911913,156.6732553,coconut,22.495507240741382,2,6.055778348566757,7.02089151205014,400.5690145514541,1.457593507557051,5,5.190790374351771,9.24078295850056,60.4888514255235,1,29.650833334357042,3,91.55400506408226,2.435053724201106 +31,13,33,27.63834933,95.48763389,5.85971872,205.5463111,coconut,12.574152069211795,2,10.7480932106707,7.610724490453656,422.4297040073561,8.401641427601552,5,5.5423792205635785,21.559675242408115,80.29805841517742,3,25.129455553620794,3,49.655719050317735,2.3393151728749717 +10,9,28,29.01256899,94.01014388,6.282955073,150.0500312,coconut,24.142284407863755,1,6.485303235512436,3.811598748855274,352.6204626403467,5.289825012425604,4,5.004340767425479,34.25699110653097,168.81338534918729,2,5.569805288773994,2,82.35062333040922,1.3231094101864604 +36,27,26,26.58413917,95.78923137,6.25449571,171.6262299,coconut,11.24208903278504,2,7.04475007710191,1.754439076844787,376.7105078801732,2.527133229632409,6,12.839695071642161,97.39876898294546,83.283197225888,3,3.6339073874645464,2,78.22225812300844,4.565973375676789 +38,24,33,28.28905147,97.00396405,5.973853124,142.9403233,coconut,20.03952444600173,2,9.627592649295245,18.371837190309463,394.718892949172,4.507961040377258,3,19.518715488207324,35.90318545895853,86.28313765206225,1,33.95428035367268,1,18.398970529890136,2.7765813065274765 +11,6,25,28.69164799,96.65248672,6.081568052,178.9635457,coconut,19.062385610598604,2,10.754512309632851,19.970441163239528,366.33881360203884,3.487197732746554,3,9.331495819922349,25.657587157150463,75.259615568046,3,1.7014440614145587,2,84.59806825974326,1.1690589366443565 +16,14,30,29.70931288,96.30484325,6.37466756,209.8453993,coconut,11.058341903418379,1,8.871298508138285,7.694941951886594,374.9140671027305,4.198836310710563,2,16.627645914686546,36.912524230001544,87.43087634094319,2,46.13059373455805,3,66.07595319509304,2.045980501353539 +33,14,35,27.14865285,96.66355213,6.027707171,149.2433497,coconut,18.293565781396197,3,6.088326787733639,17.104685333469043,378.5933231407973,9.29983812110335,4,19.178908008836473,69.5388579162638,144.17810757194673,3,46.9321147490321,1,51.74039408610194,4.876742919570183 +16,6,29,29.28725038,91.95614918,5.868285082,132.1491176,coconut,25.228928159218903,2,10.03037958625439,0.4117896734478732,401.08699444694514,1.5094955318273016,3,7.6641434114598175,36.012221776047305,180.31079213191427,1,2.7290278552525358,2,57.558659287397994,4.476653221395323 +32,11,31,25.06871967,93.31410447,6.205931638,134.8419069,coconut,12.625081303372337,3,5.306418161939492,5.40440011228319,431.1380754243637,2.165378716453869,2,14.972692050997374,50.90643886325006,74.42049577513521,1,28.65868200990836,2,53.85855942012785,4.771829181939327 +38,14,30,26.92449525,91.20106019,5.570745386,194.9022136,coconut,28.29449724086291,3,9.738087070163392,4.907581688566049,379.92057502773247,1.0465451476663004,1,19.70828805148331,44.133141556303165,133.9575737472112,3,33.18471121979653,2,1.8888241114359272,4.0871545878315745 +8,6,33,28.27804288,93.64761266,6.095261013,171.9457959,coconut,22.082172890648796,1,9.291660158272716,18.081538349892533,399.87124864571956,9.835765583622349,4,10.520070495044301,75.72776407443669,176.4055643498201,3,16.377262963583185,2,29.845589972386964,3.1950230148120493 +23,6,33,29.18032562,92.73041222,6.025789594,204.9603677,coconut,14.876704045611946,2,9.300566906802162,10.09274801839493,357.96770969945476,4.784934121371961,3,18.803329634116043,69.964443474296,137.99130805882788,1,38.61365947991386,1,98.25145452716546,3.1448994330223963 +29,25,35,28.3575072,91.64509299,5.542873799,160.7306991,coconut,27.998149316613365,3,6.085519692208339,8.847914118680965,442.7296468425696,7.642429161247153,3,9.660214847614892,89.3841878511034,128.13692601333287,2,20.93395029188794,1,68.0697123686819,2.1021966502761478 +24,14,33,29.38072512,93.27565685,6.366219551,218.5241851,coconut,24.56715739284488,2,9.144626929274759,7.402525453783504,354.3879530164388,4.803794856713791,1,13.77835849835835,33.54431568469738,144.7176546223448,2,39.37225494483591,3,9.292087124966342,4.320542789736342 +32,12,30,25.39241091,98.08951196,5.579845008,218.080385,coconut,18.09387348442908,3,8.809491754778822,17.509260606355213,414.99527386166443,4.0536861944107665,3,9.982963618915335,35.57855010389389,94.15617383873035,3,2.7723671327484323,3,43.71205135510061,1.5834887489557548 +30,25,31,26.31270635,98.62048026,5.804965067,208.1181381,coconut,19.72179833331019,2,9.846962210939342,19.066191226276864,436.8449979193406,4.996545250108882,2,13.74331165195205,70.09632455090939,101.56258993777911,1,21.618255043146977,2,5.544445858018965,3.9545736664460933 +14,21,35,29.52501367,91.91185319,6.121005506,194.3100272,coconut,16.634203658287515,2,10.946292106549755,14.482028809584445,355.4610442229236,3.239739204779615,6,6.886535898420032,18.38082581481454,78.15967896209568,1,3.835744661342233,1,80.61491941919309,4.185019617162384 +27,22,29,28.83214859,92.17170353,6.000248647,145.4172387,coconut,17.416790208175474,2,10.983112036109588,19.48846078856254,398.08737130329934,2.1188440022465618,6,13.062652170542012,10.090699125368507,78.10462445279582,3,5.484265314638731,3,83.67987787492609,2.7039101194804567 +40,5,29,28.48444906,97.76865458,5.820978791,160.389421,coconut,25.65831556186053,2,9.340298445388672,5.352605401219861,350.0019704275458,2.697532528386203,4,11.736979605440796,16.588145587948343,174.81614704427233,3,13.275442141524325,1,26.14688170791749,2.342886968272908 +17,11,32,28.74013335,93.39676499,5.620733794,156.7650823,coconut,13.2375226078302,3,11.040453474568938,16.072299392316207,353.88128664791844,8.976531730207482,1,15.12227071685752,97.29541590307547,137.0380544014738,1,43.69025374890722,1,39.56814483673989,2.9872146398744386 +30,30,35,25.00872392,95.59224018,6.001936419,165.8092179,coconut,11.009555763380677,1,7.12754520742984,14.119915343746595,403.9513641256721,7.809220331832718,6,14.221272592623645,48.769375508606835,167.7721023607175,3,11.2935824687049,1,40.26866798805755,3.914909181777554 +28,10,30,29.8690834,91.14723422,6.305740522,192.7678575,coconut,24.25224762503133,1,7.258072697703888,16.305813647135828,422.0706975655987,7.772412350121893,4,7.746319755008896,34.24390576794464,165.13135951267918,3,44.07953255829499,3,21.249620150241565,3.6587338091096275 +39,7,29,27.54273211,94.59086121,6.362544111,150.2012138,coconut,16.945426191100967,2,7.273847794633417,10.713095702888676,427.62245686474733,8.900399880948605,6,6.293750618670728,14.573784428232328,186.93995831870922,3,16.344519890731448,3,83.55349924558985,3.2317714630525525 +32,20,35,26.52166434,98.38227669,5.588655387,144.6261698,coconut,20.43053659143354,1,7.278447655319965,12.6515101991832,371.2845395402207,4.220940201478813,1,9.546412017973461,14.614540249207252,180.31568719636812,2,10.697940754976015,2,74.73484543420795,2.1413744741204264 +7,15,32,25.03512351,95.89739958,6.182232762,174.796583,coconut,19.77070664765405,2,9.601293077974397,12.069532294179954,356.8766082316087,9.028791249236892,5,17.539376486383397,45.6128857691609,136.49242735419497,1,39.677527382192736,1,2.379873000605004,2.6536749804560773 +29,17,29,29.20394909,95.66997327,5.959493188,211.2506267,coconut,17.79403942451213,3,6.892845591645347,7.873028092137615,401.420922363291,6.707536762907776,3,15.150744603270617,40.03812129918569,185.57782693209828,1,48.23867219694311,1,65.45209666101243,2.445839470804731 +34,15,34,27.05826457,91.10510371,5.677282678,224.7006953,coconut,24.179420794925647,1,11.826794772073345,0.47961136749896216,410.229758806242,8.779319087217802,1,13.434857249597874,36.2641984441752,196.9320972619908,2,21.76446713324915,1,60.48668488552663,1.6637220105463348 +14,23,25,26.18552389,96.96637916,5.612122797,135.4186222,coconut,22.675344541677383,3,11.047891097451831,19.57423003137672,396.5693714846035,6.65731665202184,4,12.856475096204854,41.541409907562844,131.5541856611116,1,46.58608581661767,2,77.64465454693773,3.5482305122366657 +18,19,29,27.59376845,92.48519606,6.206077742,162.8432736,coconut,16.09776051154894,1,6.533611710247795,14.676555553721322,361.0268665641798,4.974977366355132,5,12.306141133068598,9.578282746338962,189.98897760140355,2,48.12687102437533,1,50.14492417769626,2.5517433014992545 +7,21,35,25.76011662,94.65830608,5.764812076,131.2451414,coconut,26.44792305494764,2,9.396610679011397,2.397055468664986,411.25269500982733,3.7724037573310696,4,11.733337208926862,62.90332698068691,131.5650423396615,1,34.70236354806225,1,70.17955654326195,4.599074388838497 +24,27,34,28.87862994,95.11320315,6.203376525,145.0583117,coconut,23.206261667690804,2,7.848375167905652,1.7169268952159267,362.84787910823246,3.6093745672041173,3,9.577223449035564,53.046189167257786,50.31997772500782,1,9.47207576865824,3,46.79608631867072,1.572903397315621 +39,29,29,26.50908611,94.48414544,6.143662699,199.8778403,coconut,19.058790654673643,3,5.617939168016613,15.585200485433187,401.5783193970102,9.942528818910803,6,5.346366304189206,16.13425110697505,162.28107493535322,3,0.28860366820475103,2,48.67482780259612,4.573651637061884 +29,8,28,26.87037587,91.72546257,6.100429497,214.4128874,coconut,15.851283719089107,3,10.939865092557294,1.7730783948841,384.3346705719765,1.572693434480249,1,14.202752278366733,99.0965657950097,163.86326353788792,3,2.2055244119176476,1,37.13838432636795,2.6640535561408454 +10,24,27,27.57283516,94.90485697,5.708409601,145.9298935,coconut,16.6099940590011,3,10.642414376528967,3.4511561361623344,426.04198407209594,9.81505971055905,1,17.347107961732988,68.5329893241213,181.90371227730054,3,7.342749318181285,2,73.43218342075525,2.735781201681713 +0,29,32,28.05912437,98.3670985,5.868255858,171.6516396,coconut,14.99349287714806,3,7.9533027633440465,14.94517012815803,370.28353956045237,5.0025093676678445,6,12.109278652234508,78.79843977510215,139.65365017368123,3,37.74274479322497,3,43.18773072097601,3.8502804642853725 +32,11,31,29.51611558,92.56492864,6.461225827,131.2116167,coconut,17.412679457142016,2,10.874366965850182,8.070844303516447,356.67022479917455,5.5857979892634555,6,16.518367047056174,79.77446236308893,126.83110482603189,1,41.018469557293336,3,84.28495797061926,1.825274004322329 +37,10,32,28.96318258,95.16333673,6.165084855,222.803013,coconut,12.174878731885316,1,11.383576155488297,2.0662734953242112,382.0061739243414,3.818037927769606,2,14.50526314726747,15.549190720796735,98.59417987999171,2,18.084048618197855,1,66.76971928814804,4.5299873386056975 +20,29,27,25.09897688,92.36099489,6.047044342,157.7592626,coconut,19.288614241246933,1,11.016505233110502,6.001648294834128,369.01414334298767,6.218029999229503,4,11.80363531657043,37.275119335705796,64.36667170653654,1,19.53098383245056,2,52.052286929373594,2.141115269364994 +31,29,35,27.1872282,92.19906776,6.137102505,141.3220576,coconut,19.58991367356124,1,11.812742290914208,0.7333317708192455,355.20729929836784,8.985826085051984,4,12.80074515522442,5.352578303793509,74.81203324299918,1,29.065973393824017,3,62.30899516297257,2.3570763302645443 +17,30,27,29.03065024,90.79093862,5.894027065,205.5720367,coconut,20.27668828856517,1,10.681520821111908,17.538593670010737,449.4365181807699,1.3258452533308507,1,9.890520369815066,18.084830383971873,170.52421669656854,3,23.033501999914247,2,48.71949793959893,4.793763729418501 +1,12,30,27.754298,95.94643831,5.56222383,131.0900076,coconut,28.400944581202737,3,8.072236813128825,1.4322560741361845,376.57112597588565,3.849708705957164,2,6.928370597781435,58.799048258830155,142.4222220056953,3,9.76322669578053,1,39.4079691721129,2.3161492475171714 +6,13,29,27.31155708,99.96906006,5.832608028,201.8258633,coconut,23.787005205803695,3,10.617535843134,0.20688376301716715,445.3073647583381,6.025939064501026,4,16.550053769129196,51.89154674577606,179.6158835391582,2,11.721211876680432,1,16.60003042164322,2.8299870957704063 +15,28,32,28.84270971,99.64328526,6.218571874,224.4016682,coconut,14.736332609330848,2,10.301020177317902,12.392705627999405,425.7532461575619,2.6090356619493478,4,19.814575017660896,77.49295886357427,75.87943190560725,1,41.266510054221364,2,89.75614773196276,3.776193149188283 +27,24,29,26.61423461,96.97300803,6.142010637,191.006688,coconut,16.200104463510527,3,5.1704923379083985,16.23809842876627,395.27031002128,9.70238467132262,6,19.013666216949005,24.369925330050602,165.94990266410656,1,22.866389652085388,2,61.280593138273495,1.4479227459250534 +3,23,30,29.70143197,95.65754365,6.078807239,215.1968037,coconut,25.401156173317116,1,7.158422122566827,3.555028582556561,371.14856728558976,6.279669873102464,4,10.61819445538476,94.73685499808505,182.39127625283862,2,41.322494437908055,3,50.19204495771796,2.4511784393674114 +8,26,26,25.54759871,91.64194826,5.702484758,212.867626,coconut,17.951842175818115,1,8.395177722367302,8.484334708244099,356.8031428850272,5.022077841085935,4,14.74427461771406,9.381201328707933,123.32145763633378,2,18.949184620993215,3,7.605898200000006,2.371780715630507 +20,28,26,26.37978453,91.49882979,5.547594847,167.0470997,coconut,21.683632285849384,3,11.774566744168599,5.572140519616675,377.8808975360482,6.767524913487785,3,18.65780128357209,20.122019108692314,143.1656958270931,3,26.7786271103117,1,47.12323485743698,4.852254127441504 +26,18,27,27.45907759,92.90736493,5.836075368,142.1430003,coconut,12.972852034216771,1,6.732214959570486,4.826529559088231,401.0203581127265,7.5892258338961724,1,16.2315139237791,21.25612381553299,116.83778477149886,1,20.624727118819834,3,9.221100678294935,3.922775754156191 +1,6,35,27.02269204,95.71935435,6.231662767,147.1682459,coconut,18.437517983027025,3,8.749067327161278,5.4937074139469955,434.52247583987713,3.481288673535208,3,12.888425236135037,77.78708272672694,50.6998237195811,3,3.9008014942622795,1,41.503011409128845,1.5287183852884159 +27,30,31,28.98545306,90.73966792,5.718120393,148.8398374,coconut,10.536952344835463,2,9.682660248294887,1.834695594858966,443.0465008431289,7.916605211107285,2,12.389341953244813,31.372624421314665,137.13483831777512,3,47.21529359121132,3,96.51552654666304,1.219241500773847 +23,7,34,26.1055118,91.52421214,5.852038202,134.1279669,coconut,22.054673318944317,1,9.683031892389705,15.199917237695672,403.5923655569988,1.5605090304147797,6,7.888187257135622,4.952906113269318,116.92754646988347,2,0.6585268262909172,2,11.455543326311535,4.605279792956713 +0,26,31,25.0707247,95.02156793,5.547933273,192.9036306,coconut,11.895978475898856,2,10.098279992897664,2.478786450935242,392.54476019040794,8.949847051161699,3,5.332131250918137,35.26848896332236,70.45662778244707,1,49.58573645519264,2,65.68537637510846,2.1303088354530875 +38,6,25,25.54963273,96.92786777,6.156259104,191.2996157,coconut,21.81278116743666,1,7.431273146946202,14.411416824973784,440.02490113815884,8.703555589340887,1,18.16500726557228,10.201279321839763,198.7775467148289,1,26.306940796964263,2,52.04284193630134,4.943596368297641 +25,12,26,28.56973521,95.67906668,6.436314406,134.8370348,coconut,25.23095719185271,2,11.126094163564801,14.041233984592374,385.76193138435906,2.7946141341626625,3,18.991112242429367,35.20003695511315,151.80501921476332,1,8.890726437896474,3,18.12960776736756,4.425768674446557 +40,5,32,26.07010807,96.7036223,5.981169595,143.533473,coconut,18.217609908990507,2,8.759752820381056,5.116676553230468,408.93826905324335,3.1141932475840655,6,8.881732048816382,98.4994217497938,76.3411469234891,3,33.873943905299136,1,55.590478915846994,2.7323716598146843 +0,19,31,25.51791333,94.38420565,6.271952833,178.7297725,coconut,21.341096377656136,3,10.853760783085717,18.78665932140445,418.5962862464961,5.471524692998466,3,17.40463820811879,48.139322813751676,177.53011086159125,2,13.110857578813578,3,19.437771349461176,3.8521870827098925 +26,9,32,25.9490364,94.73860514,6.470465614,144.1571109,coconut,24.905226935465855,1,8.673997495443304,12.69539209168434,399.05875581047184,6.682388313251266,2,8.381227557053851,7.073426705972185,94.03569099237106,3,1.4904323779681983,3,79.10905133474354,2.9248939513378662 +35,30,34,28.2974764,95.41122824,6.141502001,182.4482352,coconut,26.156173665281365,2,11.986368999772056,0.9905689892464831,364.0036020562467,3.9320113764915483,4,11.040976991022971,71.76904979618085,129.736753962676,1,29.809647584195133,2,29.268193047679425,2.0203446532303966 +19,30,30,29.56549169,91.40896307,5.826381164,224.8315729,coconut,15.895201313092835,1,9.894367062593885,0.9061640540828586,419.8671922494208,2.1768988819001738,3,9.53158569565441,59.88238023447008,64.1520826617105,3,32.16457332286512,1,84.56037123735092,3.943953628367089 +31,13,33,29.69952329,95.21224392,6.342463714,148.3003692,coconut,23.74527134815115,3,8.191548540718255,0.9572056956251118,368.50467966078656,7.336677734280704,4,17.76482368730233,98.19559919446121,199.30090227814614,1,0.3468537761907975,1,91.2901819866141,3.40487869388468 +17,29,26,26.14162144,93.28415295,6.071897347,195.4115025,coconut,23.838467121601667,3,10.186602971811064,5.63610560571435,432.7489898118616,1.7370671632065424,1,11.641764381703132,43.907379027373814,86.80142964390808,2,3.4430112297040685,3,73.36582784981785,3.778093163228831 +2,30,30,26.00175125,94.79998418,6.331051715,209.540094,coconut,28.69534537930209,2,9.939970884906234,0.3768794452922686,366.2566965778691,8.59041779106591,5,16.131165328719682,46.28842713838526,186.4145156277011,2,15.436361578757563,3,49.956619223883706,1.1777379554290581 +30,13,25,27.15116142,91.48889469,6.413184638,164.9182225,coconut,12.474496428881569,1,8.921713861193423,12.484714118201797,449.82555571371574,8.502650509760787,5,7.119077455587313,14.502048674541356,195.2346878090338,1,5.173570137464223,2,1.9471521187476304,2.0054771664901434 +8,15,33,28.97318719,98.09861043,5.50158009,213.9011021,coconut,28.180092196465523,2,7.907547340160317,18.484791596464806,423.2506609317392,9.264236972939036,4,18.037775701017843,99.6684209563422,145.056030870783,1,9.839023564020783,2,45.09682338320101,2.696681874970835 +18,12,35,26.13958446,96.38580769,6.338720873,131.3387935,coconut,29.57022162944702,3,8.093444382471684,19.242312233300062,397.63695435589506,8.0764411364167,5,19.70732659938346,55.071847256059684,177.2173099167901,3,5.571976682300289,1,93.42226807802851,3.5274405228635906 +8,28,30,25.51618488,94.33465411,6.015672239,135.1272491,coconut,28.060707035450058,1,6.485153277466552,7.637386809804346,372.21549390655105,1.8655918312830844,1,7.879957386032463,47.541704670734575,192.10339009153964,3,38.31194945694569,2,87.25348144964026,2.8818110889414146 +40,22,29,27.55821802,99.98187601,5.735364307,174.6256481,coconut,12.210252656801796,2,8.859667956493599,16.745313746476388,371.0067261229897,4.855768308965072,5,15.250148894102129,87.09815402262855,103.38397336666306,3,20.446461403052147,3,89.74674867276813,4.026058803994525 +27,10,33,27.81132822,97.48410555,6.465906333,154.0621221,coconut,14.741278343993498,3,9.328639984893313,1.1935742562762996,446.7417610640879,7.491005407145096,6,11.820608315775585,47.98496554990229,188.716862852116,1,47.607017618485855,2,42.277028234385114,1.6881469038288905 +21,20,31,25.60033702,99.7240104,5.855457599,165.8248732,coconut,28.12604180725749,2,5.40072177442714,5.185362660895885,434.7076079525561,9.637050283518512,1,15.265340706844102,59.1138176230815,122.58442238147552,1,43.203217466568354,2,43.01389246689581,4.564384847015996 +3,9,35,26.91641934,99.84671638,6.318552973,225.6323656,coconut,16.93443790337816,2,10.888459274360965,9.809974863905165,368.7938768970136,2.2267925402219273,5,17.694079774630737,80.45711705179862,86.26157186087448,3,18.827792340580235,2,29.765104493625582,3.9225139607847455 +22,16,27,29.1797902,90.27214288,6.006784979,188.9252083,coconut,21.96322763179176,2,8.547045891536722,13.787657583125714,407.7857176357071,7.653834022489334,1,18.403023885331322,51.50388237755143,170.5637601461586,3,15.543429870501813,1,55.98452929497086,2.8395605114251925 +27,8,30,26.44600063,98.29937782,6.008386283,221.2258168,coconut,15.487296064798892,3,8.642128093237076,5.397216577191378,420.0996482606779,6.1592475717438635,4,5.926601651596215,86.35810662034218,137.7724042089612,2,42.472746862084,1,42.93455008251487,2.749871790355241 +22,8,33,28.43572863,95.8840407,5.665785202,203.9283708,coconut,29.45772492848779,2,5.597789328109737,5.501089929048042,379.3502566085638,8.492168647316173,1,9.294338660707552,74.8422898426792,67.75173160506834,1,0.650495067016188,3,95.74661550951494,2.8964688658012023 +28,27,32,28.94099669,93.00109012,5.764615485,191.7723087,coconut,12.198011201576115,3,10.954285012613457,15.075379612952366,407.4361134570364,8.89300630040144,4,18.716423271569244,85.25994683735094,54.41892583282219,2,36.896148536344306,1,51.32046753360403,2.790193767039074 +23,21,26,26.45488737,93.45042636,5.901495544,149.2220255,coconut,11.92328660471102,2,9.396827899714566,5.935529450709827,389.13035058846236,7.512536459483438,3,14.547714547081373,63.02410024863647,70.42307201385259,2,19.69002302050215,2,69.64535216730927,4.961514634682835 +37,5,34,25.79490531,93.84150618,5.779032666,152.4238712,coconut,15.359092106395897,2,5.13312265498206,1.292314308145881,443.0797828400724,9.86737834267835,5,14.149697315739127,25.11948531806113,164.07256879546418,3,6.0837073672858955,1,13.464062374517349,3.491540076360256 +19,26,29,26.93141945,98.80313612,5.67154928,166.5712879,coconut,28.987565067884525,1,10.619209385938056,15.579491168432183,401.73191600635,7.534615691431686,5,16.455495153147222,11.126372266143447,102.93148149286115,3,11.067760834809388,2,77.30143834522663,3.4583236016918066 +133,47,24,24.40228894,79.19732001,7.231324765,90.8022356,cotton,28.151764283507713,3,11.955776009621214,11.08083798541207,392.08986740457584,4.695821304347842,1,12.099801993788903,85.40715087311865,109.26891104685524,3,38.461349633764634,3,68.02076696864347,1.439020925003649 +136,36,20,23.09595631,84.86275707,6.925412377,71.29581071,cotton,25.180208907319944,2,7.699290418161665,7.197522686034141,404.1230358724431,7.778894115907043,4,12.070334008191303,29.330144777337196,107.03346401279396,2,15.307644584172685,2,6.137302526801392,2.2860047366533163 +104,47,18,23.9656349,76.97696717,7.633437412,90.75616738,cotton,27.742538159100572,1,8.745028257890997,15.603674976603203,350.3601471144205,3.8729622757471502,4,17.567928764578088,71.64484159111477,104.12017826725753,1,0.7720179223026591,1,79.10298233065387,3.6702524881754948 +133,47,23,24.88738107,75.62137159,6.827354668,89.76050416,cotton,14.713067683521118,1,9.95690591265646,17.404727154425906,418.0982208429959,5.29070969311196,6,15.97491022386533,27.954159782431653,89.52382420142655,3,7.7608456130557,2,35.402222227436866,4.436006882542839 +126,38,23,25.36243778,83.63276077,6.176716425,88.43618918,cotton,27.36776018446006,3,6.416953204079634,2.7281910512790675,437.423465947126,3.8703783047229616,2,6.543027340063996,33.46864086903457,174.02835566705483,2,33.400583749590815,1,36.55407893042033,3.672545177107925 +126,50,19,24.69457084,81.7358876,6.628722836,78.58494391,cotton,26.552156408079167,3,10.129147866317142,12.799494901222651,419.6961158553345,4.258425854221295,4,10.697538972401745,89.80737201169843,128.37322086780338,2,29.254426470281057,3,12.278700047022785,3.2003145681395506 +113,41,20,25.0017188,80.53965818,7.256877571,96.32600992,cotton,16.55843681624998,2,8.579862214019014,16.405636456180986,395.5142655898112,7.794219005255698,5,5.819710634487638,31.635987946150735,178.67179927151457,1,10.835636642198265,3,36.0268631433922,4.556578168541916 +121,45,22,22.45942937,81.30681027,6.443785385,64.23026638,cotton,17.731656666551252,3,10.263876923889867,18.57957030481279,391.58740589986127,1.0925324953994249,2,9.89666636967814,77.70747338432817,149.45025430962647,3,35.27045568596509,1,57.877375581428545,3.35342276063425 +121,47,16,23.60564038,79.29573149,7.723240151,72.49800885,cotton,12.053316118446787,3,5.230294451819854,5.732803086369493,409.31818927800487,5.372018034515619,3,18.88689174102189,62.437928824163706,147.20971780241265,3,18.751874532650277,1,24.036190512940493,2.072035605645054 +129,60,22,24.58453146,79.12404171,5.947448589,71.94608134,cotton,19.960784182786412,2,7.963666543720995,12.463024786384032,424.5597416379186,4.472672717247164,2,15.108542334918805,24.15995215583202,79.1857549214844,3,49.152541957163045,1,12.420410648160408,1.9718145162304355 +107,45,25,23.0865933,83.55546146,7.227745516,71.84080724,cotton,19.26752852271388,1,11.778235179133645,2.5848840268927176,372.56999879759115,1.2663080251503462,1,15.262781461660067,13.39615899461074,83.87280823515061,1,31.848073284652163,3,73.8536845695366,1.9750627825581861 +122,59,18,23.5000992,83.63488952,6.219469084,79.81328183,cotton,15.499224597901106,2,11.522603942528242,18.183149392868383,378.6989028975375,8.321196329223717,5,9.709491411229388,57.79310255760484,199.20101231526823,1,31.476969101152218,2,97.13714617392647,1.2999811783033226 +140,38,15,24.1472953,75.88298598,6.021439523,69.91563467,cotton,20.14844764267223,1,11.263396494389593,3.5469471265631336,391.7079448119956,3.9190757120710678,6,16.75268018528669,41.6501605994882,72.6739770822027,3,12.925644049047852,1,35.69201501899556,4.9827527241577485 +102,49,21,24.69315538,84.84422454,6.253343655,89.799462,cotton,20.412640997085227,2,9.471022633004079,9.5961367158153,380.664141353043,2.8063090301049947,2,12.603308395687005,31.3139622375319,188.73347457189928,3,47.44298883836125,3,10.982116254669139,3.5079747253130944 +111,40,25,24.484692,84.44932014,6.187455799,90.94342484,cotton,19.623758417713837,3,8.131240889369506,18.132260798013988,370.5996019770353,8.386367017874296,3,13.267600689994126,21.36041333983646,59.234874881477324,1,18.03746085964169,2,87.77633147185472,1.9177994224069774 +131,35,18,24.49112609,82.24415809,7.057693366,64.02949379,cotton,21.64441452211161,1,5.130420458515761,12.539926183933602,351.7499825728234,5.1341826227804175,1,8.454061083806492,13.079243500530813,76.11877588535036,2,14.802124600130162,1,72.00251533792262,3.8194004215387025 +135,43,16,23.47986888,81.73049149,6.720449769,86.76287924,cotton,29.540618719300586,3,9.73780575553948,19.675927976347122,393.00883823743516,5.520647811516404,1,12.179633540231311,11.983669715567757,95.16569532665645,2,19.571437705459577,2,37.509265126747295,4.8399594103813826 +100,46,18,24.18586246,76.04203958,6.431689506,69.08056728,cotton,13.46017893156329,3,6.769630559847569,11.926986090826182,374.08214074808956,7.468823608217285,5,19.379321381251906,76.41057578574383,56.82282248985309,3,48.34980529262561,3,0.07327715517134736,4.032387994778632 +123,39,24,25.00755095,78.17952126,7.453106264,86.06411872,cotton,25.451043959019973,2,5.450609756798874,14.561291122170223,373.3117907934975,9.682291487971431,2,14.641495741262371,60.91771178464786,159.6303245962132,2,18.175763458679373,1,73.99698152051417,2.5378162992915434 +117,56,15,25.99237426,77.0543546,7.368258226,89.1188212,cotton,10.904882585409903,1,6.6115696164741315,12.188095014964828,405.7825221380586,8.764136830392598,3,12.279267875031955,86.53946101505369,161.77995862720428,2,10.903805385839672,2,7.615969274712652,2.2728908168651176 +121,36,24,23.66457347,81.69105088,7.352401887,99.36898373,cotton,14.073355684063886,1,7.231411225977618,9.055657280819773,364.53270821073056,8.059020382757954,4,6.112562444052075,4.057829067310714,159.23748821557956,1,39.849325227288546,3,38.046937555546535,3.77394163190111 +101,58,18,25.66891439,81.38103349,6.652143699,78.59595817,cotton,27.81550260634695,1,10.656090212883413,19.83642343205428,413.8062052511188,5.61108637756993,2,10.783717374547942,31.32553918753187,68.87109012508323,3,16.056923214249537,1,7.45490066245007,2.191200978658704 +107,42,24,22.04612876,84.62978302,6.144631795,86.00758678,cotton,16.766984283148666,1,7.011374838983805,11.614276926453437,376.48286322921257,7.374192756996922,2,5.006916159749311,40.963689237668156,192.08180975102624,2,13.38511950392835,3,66.26871372790183,4.650178179856588 +100,41,22,22.4204752,84.55794703,7.318802162,93.46595573,cotton,28.371856639154164,3,9.210787081190144,18.542511827064803,431.7404550138395,3.4765659547602974,2,11.614458084707161,37.23698618973456,81.23566601135084,2,20.022704668355264,1,31.807785395596365,1.4878121571954348 +125,39,21,25.03149561,82.21276599,7.954629324,95.0191318,cotton,25.389506442999338,3,8.699757899631175,9.291155984956518,390.13345671807076,6.490102725461301,1,13.75949150703064,77.9132640255481,181.2150130763364,2,38.17305861459076,1,2.4402431880092834,1.5997781580550448 +105,60,23,23.53371386,77.21705554,6.207652157,87.54004943,cotton,16.03414939021262,1,8.890457606870449,13.083673827561086,374.36831096135444,2.7801573010201777,1,18.601445330672867,53.06598697099145,141.98522239273007,2,44.41560328454103,2,34.93332038418638,4.886453001060524 +102,46,19,22.77076388,82.5993307,6.631005298,81.49543437,cotton,18.788352745099555,2,6.524711838644853,18.132296514481087,356.8775740122797,7.125961150394913,6,12.244670222516978,81.5500929277901,175.20228947499794,1,3.0518096558960983,2,52.86390654497723,2.7725691023139576 +131,49,22,25.49848236,79.9751579,7.306918817,67.05961949,cotton,18.305105619287808,2,10.364054606247024,5.8043005565905155,365.41887242615695,5.378809010519477,2,9.516371205622862,49.27118072929883,140.7413931209254,1,47.02620023338281,1,9.555144618643608,1.6778813259510046 +139,35,15,25.248679,83.4630147,5.898293044,86.55517751,cotton,14.897121614712535,2,9.42837343370543,10.954925170606888,367.2667563248214,7.11717116720989,1,14.916183771511976,74.26872923606575,143.28337589266903,2,27.212137008629693,3,22.695007718847528,2.744706079544496 +108,36,19,22.78249615,77.51235009,7.238566893,64.61444234,cotton,25.840350215857782,2,7.449663623939765,14.090090335296608,441.78934846838615,6.786972148930851,1,6.455849106837389,82.1279826091589,138.13067316391346,1,38.899494610335424,1,37.83360826682076,1.3076102600485435 +118,45,23,23.37044424,77.43198948,7.977651226,71.67870701,cotton,27.146712359936515,1,7.048998021004702,9.663174629190685,386.2054331027368,6.300996280001526,4,6.90310627082483,52.73674984778831,198.2633189264652,1,48.293466061707235,1,36.62356957228623,1.1517124601557778 +107,51,22,24.86560781,78.22080815,5.983075895,79.56866268,cotton,20.29456309019759,3,7.672901302405044,19.642948763198717,403.02520036157483,5.50922576933047,6,14.965917012762839,24.919602686303865,94.53131341632513,2,31.291900239037496,1,14.897813856217045,2.794146792100462 +125,60,17,24.14386157,84.51591287,6.785723961,80.36146974,cotton,13.104822277411008,3,5.2380290717357765,9.829434228942649,363.34137845092175,8.248410306883107,5,12.478509235084678,15.885072218634155,56.382029648308986,2,46.36784593915917,3,58.52650653742594,1.2160704932196689 +113,37,20,25.03300222,79.04368718,7.393441155,97.10087029,cotton,20.72493921446361,3,11.796321039464981,7.459591263195263,379.08242566247196,2.7360745656249366,2,18.577987110679466,41.24176654635223,141.12587680711164,3,16.310988822139468,2,92.68418186695952,2.9686097950799843 +131,52,16,23.65724079,84.47601498,6.486068274,88.54479121,cotton,29.052629196736945,1,7.638445276990009,6.237417031451214,428.99129422084843,2.2485304549899965,4,6.77374974902325,30.719937420607156,132.26659088462736,3,22.565256226686238,2,80.84568734237124,1.1570695113994094 +115,48,16,25.54359718,84.09229796,7.175934962,88.94245493,cotton,13.534667485478327,1,7.174165988779304,19.043315291676638,424.1510879875234,8.774174910090501,3,11.448186446270526,70.12886318669163,104.44203206712541,3,32.933887648066026,3,69.79383095899672,1.5994700009596778 +113,38,25,22.00085141,79.47270984,7.388265888,90.42224164,cotton,23.388197721763337,2,6.675978604610906,6.984465302075797,358.7862440180498,5.089879132858498,2,11.482222240258755,23.534018860224716,83.36220378606667,3,5.834697406921857,2,44.93298668426583,2.057124794288806 +111,41,18,23.64328417,78.1258666,6.10539819,80.96157332,cotton,25.703843421335844,2,5.042195988491201,8.601495748613381,385.5707664476288,1.940663617738732,3,19.083346493044857,58.01378616361883,189.56443997068646,3,34.006209353796045,1,80.93485958017843,3.0161057056701157 +111,53,19,23.96436009,78.02763149,6.419536555,84.63148859,cotton,24.612350370749787,1,9.933686503383129,16.279915298855023,403.6620998948868,2.823956348100763,4,12.355143445089553,78.02783991460164,132.23336948200455,1,5.913048462057108,2,82.34808245660746,2.680242661859615 +122,48,16,24.65425757,75.6350708,6.307585854,61.82980133,cotton,23.945131298494015,1,8.928584142699053,7.06179738062966,364.2675572824239,8.649918777709786,1,5.428441060440491,95.86357335482812,110.12290359038155,2,32.8635621902962,3,28.0116829815946,2.537135738827808 +108,46,17,24.3017998,84.87668973,6.93221485,65.0247867,cotton,10.893473198635954,2,10.083731137972979,3.9358448765710663,399.7727236433949,3.705209348583125,3,14.423943901095331,4.951688716808478,73.30732639108447,3,36.264292729096645,1,54.60347379983076,1.7247284697617409 +132,41,22,24.29144926,81.02453404,7.810865753,90.41694635,cotton,18.16567313749778,3,6.946458734128365,18.4516548792447,396.5445042647143,9.2404749631236,5,13.853139119483233,3.6724494764209914,158.875942161854,3,30.114403359677944,1,52.913698619602734,1.7894509813278012 +103,42,17,24.29470232,84.61527627,6.527541661,81.05902285,cotton,10.950341192921014,3,10.151309655748838,3.420297467084774,388.46093759492766,5.582075950204377,3,11.251599775792222,74.9204389900727,111.66790528806303,2,15.611220868086361,1,16.756663653133952,1.2012077616535595 +133,50,25,25.72180042,81.19666206,7.569454601,99.93100821,cotton,12.356822407038637,2,5.814740524688752,19.255800603106948,446.25182855488845,2.7009914185505597,5,5.746376412601678,24.373200035119847,198.85529488313992,3,24.515087851405276,2,77.7423197071933,4.812279891661529 +127,37,18,24.87663664,76.30050373,7.041065585,91.9223468,cotton,21.428113716760716,1,5.457210684558276,12.985560919701125,394.9594380003614,6.647956323883029,5,16.26583970526756,74.74421807387068,150.15861990749454,2,6.771608780020999,1,13.487324004847846,4.106624320742881 +110,39,25,22.60612115,77.34264002,7.208795456,75.13617229,cotton,19.20594693836033,2,8.270083920039438,8.367184645797977,434.32774055279054,6.351260892520663,3,14.13206420276677,45.951737120773096,69.17362155791035,3,32.35894482182171,2,86.85752244723486,4.497163284906004 +131,38,19,23.86814008,75.68339729,6.814341946,90.4547185,cotton,14.077493384495256,1,11.752059124277896,19.215326402799054,438.60222157748404,9.968953928627243,6,8.62626571728173,12.653194512390066,190.94487581847693,2,17.712528214043527,2,58.67322566300185,1.4733820949536773 +108,38,24,23.41022496,76.43836957,7.442217061,78.82199603,cotton,22.912703877555476,3,11.18279265524372,5.481892506943198,382.51962647738264,9.302335437558048,3,17.041560862472167,59.26940336328378,53.43846084784839,2,40.68077991495638,2,36.174270948917,3.5718657298515266 +122,40,17,24.96440768,81.31677618,6.854558957,80.03995829,cotton,21.080100035680427,2,9.364610876016208,2.9271108646512367,433.3213432233627,7.498809617663081,6,6.150484113796052,25.580870190991146,147.55935061973446,1,35.11018868528435,1,8.433993175711517,2.941031093157038 +111,50,15,25.16820129,80.30351815,7.884550475,84.62419032,cotton,16.446264715540345,1,8.919073520175008,3.676154353828447,414.6803777868065,8.425970755263307,6,15.494936546545905,25.390876606654565,167.77064746707694,1,43.916187044545765,1,1.5214516912189446,3.8255592818403694 +140,40,17,22.72767171,77.07598065,6.006085786,77.55176318,cotton,28.651471114102804,1,11.114211342146731,2.9824252548794705,382.2187609468167,2.3354601726143476,1,15.190903184692964,25.091289064833656,165.17017328031994,1,19.8775290985029,1,60.93492251710073,1.9107718294553728 +100,40,20,22.45145981,76.25674874,7.432043735,86.84998693,cotton,12.808849928179967,1,11.813289649769779,11.216207103356519,403.9755539596891,6.405026613263242,5,11.888721316835781,89.56458775494617,143.70680006155115,1,6.202160617644953,2,29.11056699660324,1.097307815309963 +123,50,16,23.04920461,75.53835214,6.498052108,70.65644296,cotton,13.858547155976744,2,9.354336506825092,9.953738376692794,384.13793125542537,6.856893643644829,1,10.916862686670434,12.986825240417089,147.23531360005626,3,22.021138762734427,3,21.806255938188702,2.284079521392807 +107,36,21,25.29250148,75.66653335,6.205263534,62.64174227,cotton,13.750481184875479,2,8.57706875090549,15.363657399398829,352.17829430470425,2.709760218482492,2,8.121680877770583,14.446538657901641,134.6839331670297,3,44.579170554334326,3,11.598617592232552,4.567257950852788 +118,50,19,22.95604064,82.33733678,6.360812227,66.48339303,cotton,28.133405808609727,3,7.351589844112478,19.48736833138378,431.94945515124914,5.358479877765539,5,5.815847806346584,65.83464416471226,166.09544676544482,2,17.956391641312518,1,80.6860874778635,2.9349276296983646 +103,51,20,22.80213132,84.14668447,7.046607434,91.6389565,cotton,23.597865975941378,2,8.2678961751759,2.871317121556445,360.12789074603444,1.8288906005972985,1,6.6842985906695755,31.242267525532974,165.65608019107017,2,3.853887383258292,2,10.576808559063577,2.9959695625912515 +133,57,19,23.54234715,75.98203329,7.947011366,84.12536744,cotton,18.496974931380997,1,5.456600314634341,13.259173626449712,388.53193332373996,7.760505860034132,5,10.590836490841497,32.69961064470955,72.33558555901921,2,32.21281191543937,3,39.84200085946706,3.33493171572539 +129,47,20,24.41212325,80.80343786,6.281913858,98.60457373,cotton,25.658079513064248,3,9.042823844892308,10.103214175633745,371.8106890652331,4.213637008856952,6,14.646191956104303,58.85647833499469,150.88962236027547,2,30.754664622235055,3,83.58091978846177,1.2689889557438132 +116,52,19,22.94276687,75.37170612,6.114525877,67.08022574,cotton,11.299444545646747,3,10.08396838839369,14.59494589995392,360.9387416283293,4.74716782755805,5,13.215552101093696,65.10915994588645,104.51908699800339,3,3.672825917807143,2,2.3663271972851563,4.884827216836367 +114,40,23,25.53676123,81.13668716,6.753978061,95.4262599,cotton,19.537341490138353,3,8.211086305922594,9.614688279037207,354.1407482579642,3.7323953553574363,5,18.669297059296518,29.684740170866796,187.63211963423333,1,0.8228097238958176,2,15.602045354034244,4.055317019746899 +131,60,17,25.32023717,81.79475917,7.425041316,83.46532547,cotton,11.368837456120149,2,8.816594487390685,0.31562132596205883,392.41565692017673,9.826030445070014,3,19.75281430561502,13.418535334500238,72.97582053038352,3,29.76609937804332,2,64.15550840614524,3.247005147557258 +107,43,18,22.426733,81.53480799,6.745104394,65.54475812,cotton,23.39587454955139,2,8.921316309333184,7.795046195045994,379.01556197955676,5.066455865382414,5,8.446782800396454,50.7437285969786,133.75254055073822,2,43.968521944739635,2,29.701184310001693,2.8713812053772427 +123,44,21,25.78544484,75.00539324,7.641116569,91.39578861,cotton,16.989144158084784,1,11.11529225937914,0.7295328252160793,398.3178779259425,4.886379170702017,2,19.29746523639279,93.83030322951053,115.45965542586548,2,23.415335205717163,2,46.63679227783927,4.879503781827148 +112,49,25,25.68959532,77.90621048,6.470135478,66.19426787,cotton,16.548476182735968,1,8.102046048722038,0.9594015654352583,353.7443802359333,8.940130057945872,3,11.445036356813228,75.57576663299153,196.30363652833282,1,27.063149132499927,1,64.40016425030977,2.9412223847581944 +119,44,15,22.14593688,82.8597549,7.091992365,60.65381719,cotton,17.927525764518553,3,8.96623863596472,19.029153577895265,397.3128062150633,1.3967414520049983,3,19.5995219209858,23.69599570115527,114.74288319379255,1,48.87402315936698,1,48.34961649137095,2.9835382348903567 +130,59,19,25.07278712,82.50257909,6.520403794,93.51042684,cotton,18.392844686935806,3,11.617691965140015,4.592301521774709,411.61247988920036,2.791544240883138,6,18.515613108805518,79.95477775039265,111.55123078154233,3,31.862310121157893,2,41.460292977783794,4.157859012748555 +127,53,24,22.21506982,76.17851932,6.127939628,70.40557612,cotton,28.28785501593033,1,10.651890766766135,12.279560410488003,406.3559725922266,3.3122460650360144,1,19.105252684172534,42.569033077842654,118.81855400711677,2,16.759417087699163,2,14.74816755921422,2.874441364259564 +134,52,18,23.9643129,76.59175937,7.994679507,76.13090645,cotton,20.189868918154012,3,11.216177439580607,5.8969720759305,422.7666727143589,4.734976408775425,6,16.95816687397687,28.91747696800716,124.49528681289583,2,2.8477566172317506,2,66.3107406693747,1.349766288939899 +109,36,18,25.40059227,76.53237965,7.524707577,62.5138867,cotton,24.22831197302924,3,8.613266387278342,6.519033364573681,365.19121464231114,7.511357096390567,1,17.041413536263462,95.87193779503632,95.96432240050252,1,10.975537971306732,2,22.28862878301343,2.995600355731928 +100,48,17,23.7805123,83.03878838,7.827877818,66.26555904,cotton,28.177092280000103,2,7.245895374782523,16.83879083610578,411.75945998901545,9.85472497317278,2,8.10484183197612,99.216638493547,98.66878477944559,2,14.355624187448962,2,34.8257502963094,3.2106785765750288 +132,52,19,24.16402322,76.7433897,6.436691764,61.94626051,cotton,19.89646830056421,3,10.804023470209728,19.338018797763564,381.54457505677385,2.5432862758432613,2,7.814507975108542,65.41354078230823,103.66412530165333,3,38.19362146774877,1,32.22375032542995,2.091110754154142 +102,37,25,25.31468463,77.91757121,5.907930899,72.82902109,cotton,18.178618713912194,1,5.178290862765839,18.13407894270338,356.0045768973722,3.37206655013728,1,15.557433503554815,31.550125705435928,115.07131093177586,1,0.6031900057873574,2,83.38894045853674,4.259559344710851 +111,39,22,22.60361557,80.3509046,6.135025006,88.57395505,cotton,25.065182462428773,1,10.08944135593795,17.571937730412866,377.5556467310298,3.8319406981073922,6,14.466007584582087,54.36771667925756,64.67934168250915,3,36.61745228841443,1,42.52966207350389,2.392077312836356 +117,51,15,22.9535715,78.71555832,6.044556594,99.75336197,cotton,25.766288275380234,1,5.592013854303504,8.390318789236437,431.131650347325,9.179226528254352,6,8.572740906285082,81.98425082376126,147.87500524727858,3,35.885202998956736,3,23.291139081656485,3.306983159599747 +136,36,24,22.74446976,80.41198458,7.59781958,90.07326633,cotton,13.467257051310288,1,6.062233006437458,15.518830701659601,400.3356827960958,9.795358129480359,6,6.641312474685122,72.7151523464044,182.09581558472433,3,47.59362168428595,1,75.95540772356551,4.143892130743773 +134,56,18,23.80834611,83.91902605,6.691268104,70.97358303,cotton,22.873426498240647,3,9.065676466358717,11.828669523887532,404.52874884660633,9.339148192728784,2,12.787337726186745,87.26160734748224,199.32393909616934,3,30.504132778164273,3,83.96540776900801,4.439673833448767 +112,54,15,25.46228792,81.56641891,6.175492306,76.88582484,cotton,22.76091181290154,2,7.386374826438994,6.1537849960961495,398.14706956348573,5.700917639637359,4,19.754520311569376,44.22490937532035,82.29613921373789,2,13.960754528985643,3,15.731727941785678,3.835107660339237 +105,56,15,25.96779712,81.97904282,7.272316209,74.14169043,cotton,13.022598702062304,3,11.809362025557908,0.18316000385301567,396.0909221177417,5.014479723361223,3,14.884890757034709,30.0516860144041,117.21200367055792,2,22.316850434674702,1,44.92553979198437,4.17737487115499 +140,45,15,25.5308271,80.04662756,5.801047545,99.39557151,cotton,24.064683454107026,2,11.508539472849545,13.998674565573324,428.9813666051365,3.320507894185029,5,7.804737871401688,73.10490604309965,74.79008678109258,1,4.248663134646103,1,74.89848150051472,4.168670734974292 +126,46,25,24.43847399,81.69801729,6.757457943,60.79645852,cotton,27.904626369169414,2,11.754195476803133,3.597513708237716,443.2702606334837,7.42526349465857,6,15.551570915686904,79.44196312121973,152.93451220153014,2,3.104145924264934,3,83.21858964188698,3.2033850543922817 +106,49,24,23.03887865,76.47039772,6.983395573,90.64770699,cotton,11.074538017793023,2,6.883082739087616,6.627340403171624,427.9341285220829,7.761244933785096,5,14.067149851961437,85.93575540239962,135.43701561765482,1,39.27648104951812,3,29.602826171450978,2.2817908366668846 +121,53,19,23.51308653,76.72621429,7.976889498,80.11272117,cotton,28.914272623918485,1,7.931491404456913,1.0831561350996188,414.5398333736644,7.9765460438031965,4,12.862438027581144,93.42653644843071,59.97389337799559,3,40.843416105668396,1,67.05678364391477,4.855826023576963 +108,60,17,22.75805656,76.75768356,6.558902588,97.76600619,cotton,27.198144361077667,2,9.352468944495424,3.474253862975638,408.66950695971383,1.4167656776265622,4,7.39685945679343,67.16843519724114,128.67267650647986,2,17.095188460778875,3,22.64489584716195,1.7986080328796237 +116,56,17,24.71252544,77.7293114,7.979090365,85.24963302,cotton,27.255177027938572,1,8.095487396564256,17.28670417570428,375.69709711568294,6.8701360489868195,6,8.532349934760967,10.784445777547468,82.92349760862379,2,34.33353590354385,2,5.645844384372323,1.2849472978780403 +100,52,19,23.45969093,82.44777468,7.903528673,93.50153555,cotton,18.146939370630957,1,6.259547853655869,18.710499163743012,358.3806884002048,7.903640036293845,6,19.125194907006332,11.593773261884088,101.24255467488207,2,29.8112756733523,2,47.26517324644933,1.1178466486469363 +129,43,16,25.5503704,77.85055621,6.73210948,78.58488484,cotton,18.251191668156316,2,9.846418472622364,19.35145071950972,403.5119885768368,6.895821423792727,6,16.136255805220966,16.930808537915464,127.10205805068446,2,19.210768382586902,1,85.21689981482926,1.7077554941749495 +118,44,23,22.08458267,82.82904143,6.691690476,67.06459777,cotton,11.976562880987379,3,9.932916505559007,10.411331323890467,428.91273377158865,4.46972997760623,4,11.417062100772057,82.30579120911639,188.86501570851527,2,3.976749933629992,2,53.784179222459926,1.7567328842269223 +117,43,25,24.68854799,78.51206972,7.839849298,69.31153566,cotton,11.85753074361109,3,5.815439838678499,1.2804362251307477,392.33623628177384,5.994470645424461,4,14.799086401847935,14.015417092366732,152.5063049534814,3,39.73068609409254,3,26.535610312955992,2.4968274666990133 +126,37,21,25.84997269,84.16855231,6.61448588,77.03421249,cotton,27.24570257396686,2,9.062056008036237,4.4776403519396695,354.45783306873136,3.384221477244054,4,16.56920958267621,70.02841333536716,158.8562078594927,1,10.392941728960064,3,12.216221126342464,1.0164297519510859 +120,48,16,22.46054478,75.40989245,7.456971816,71.85436078,cotton,19.114633279682554,2,7.231303593125018,3.7957078157848523,441.9517549248638,7.0427733643497445,5,16.89087111460446,64.98478526789985,59.796599479496955,1,40.4709250429555,1,8.286654204706457,4.562280408280323 +102,45,16,23.65629976,77.52425987,7.2942193,74.8984994,cotton,20.017110778545348,2,7.687168112600203,15.213949270597508,444.9521990314827,6.910347464769371,2,9.024254760135705,67.8433725368516,78.21373884062353,3,44.91389285465697,2,43.918183329495875,3.346505226593783 +131,56,20,22.00817088,81.83896111,7.762647875,92.23645249,cotton,19.292906949041857,3,7.583181167762778,6.333184040442661,386.34644424819277,6.944161684202751,5,14.65131344066336,52.86706157446772,75.57770860579086,1,30.272533052328182,1,80.53832359598032,4.743608000731834 +114,40,17,24.32630461,80.13456404,6.363406102,69.45072055,cotton,22.065704318970912,2,9.269441149821507,3.2645398750779053,432.2661112817719,9.94540873529359,4,14.564653710694856,44.74239243001102,71.19616412063493,2,15.786405729711628,2,9.172952536950053,4.455575020315186 +101,37,18,22.92360984,82.68738535,7.63737841,92.91915074,cotton,20.7554159609588,2,8.617874563563923,6.723813702969026,409.31363506845713,1.123275620241906,4,17.910165198809295,77.39191934681148,144.46740640892386,2,2.3616627853399184,1,43.80830583715942,4.972600054285735 +106,46,20,23.43821725,78.63388824,6.200671976,81.15072105,cotton,22.22145513378657,2,6.671864132446136,14.605737127677894,387.55295768550195,6.992197099951964,4,13.430313343292774,17.55642366934844,136.9636866266148,3,7.237031439759711,3,81.89093856093395,3.245223116980456 +113,38,20,22.10718988,78.58320116,6.364729934,74.94136567,cotton,26.092338856094248,3,10.20069756507834,17.760919561843668,404.2636624943853,8.314314338045156,3,15.622168006844422,25.08264943674936,100.1320043635288,1,23.703225222301107,3,77.2982896463321,1.202531566650809 +102,53,21,23.03814028,76.11021529,6.913678684,91.49697481,cotton,14.05782656290738,1,10.204616717910426,12.182892805650988,363.6025116457563,1.0508770548252115,5,7.35575005184657,90.99922868809242,197.7090028013876,3,12.846762897858726,3,21.129192235481376,4.240513280784109 +110,39,18,24.54795322,75.39752705,7.766259769,63.88079866,cotton,23.505031095110127,2,9.143337537960415,10.578909970420707,354.94630269592705,8.441332715993141,3,15.120236624219512,45.63814997948141,183.1554601710277,1,3.1213978931366517,3,90.86297907582734,4.421955734933681 +107,58,15,23.73868041,75.77503808,7.55606399,76.63669195,cotton,17.976434107611972,1,10.027443505191567,16.66741740873986,408.1755344769629,1.639757824131599,6,19.25478146862218,98.53229078954443,132.68552470180828,1,45.02552451204915,1,99.62994129270884,4.005809101831249 +120,60,15,22.31871914,83.86129998,7.288377241,65.35747011,cotton,13.813650400922198,1,8.34975507621451,14.326331553602799,391.089095045729,5.776348680904549,6,14.923220129406543,45.27869732010279,95.54231955177747,2,28.81577359646763,3,42.635369692011615,3.236825024347703 +89,47,38,25.52468965,72.24850829,6.002524871,151.8869972,jute,17.767683965423615,2,9.24746776742683,14.680317390318269,420.1000900008126,8.75845417057526,3,7.997518618377588,52.91560582796904,90.71533074830157,2,43.10518429596646,3,69.66298346028168,2.2241059528731455 +60,37,39,26.59104992,82.94164078,6.033485257,161.2469997,jute,20.422314869535914,1,6.0764845949667485,11.756233084592074,369.78696760665053,2.10592277076431,1,9.891199366937476,83.45287359178775,100.40715349485444,1,16.8094340224828,1,7.423019530632702,3.353406467216856 +63,41,45,25.29781791,86.8870535,7.121933579,196.6249511,jute,12.431389871478997,3,6.733361719861563,17.751678332968215,441.7362374391358,4.907929732960426,1,8.901491954158882,19.157895378069657,161.0080343731229,2,23.391924006508287,3,63.807194314743334,2.798712482095221 +86,40,39,25.72100868,88.16513579,6.207459637,175.6086697,jute,16.512402224199015,3,11.269242967838313,8.987912284006311,385.99808561626315,8.527743639802813,2,6.42993893600279,71.06261228056438,135.45289720660742,1,16.862349244139306,3,32.05330198079628,4.272710238879335 +96,41,40,23.58419277,72.00460848,6.090060478,190.4242157,jute,14.229025841834247,2,5.561572493298895,19.238925768731562,359.8558802432774,1.4453703757831506,6,14.260611280555425,62.376352253333934,85.88590405276469,3,16.031511525305074,2,50.416130306797044,3.94206601951254 +100,35,36,25.31042337,72.01364411,6.346715209,190.5577618,jute,17.990423375580363,2,10.262731519378825,8.99188949654484,400.33010827374966,4.83100499038849,2,9.462593963298579,60.35737145365153,88.94495060282044,2,20.72517039256299,1,49.93076567390665,3.345452893925041 +63,37,43,23.41798979,85.08640476,6.661957897,185.7446728,jute,26.249124348998215,3,5.626831309488358,15.877737264026162,386.76453970930186,8.401675895564635,3,8.339141467465225,56.88373015104903,123.14717910109164,2,3.622663818946792,1,35.404079381607104,2.976793938377692 +70,43,40,24.35564134,88.80391021,6.176860192,169.1168028,jute,27.017947967752875,3,9.496189645224483,16.3579459690587,371.886326792061,3.439075166005021,4,9.46824599786829,63.6220624923512,73.49137448584582,3,24.832284470914423,2,11.579721296613766,2.372034634020892 +67,55,44,26.284017,75.14640198,7.251847296,182.2685447,jute,27.402756808604323,2,7.4821032292117025,10.35719060995433,426.1112481116733,8.89331473980176,2,7.9737513964034346,97.44507306191579,112.25170312985539,3,2.5611892000023895,1,51.67677987168441,2.3585933566574573 +74,40,40,25.13842773,83.12053888,6.386259978,169.3388465,jute,21.105961905060312,2,10.796293780301173,18.53200464709446,410.8531797346537,8.017029518865794,4,17.474163541068187,71.53490768087742,79.66631936179628,2,20.95133930639092,1,75.12174838978406,2.1227589461284238 +89,53,44,24.88692811,71.91711523,7.319735475,150.2498675,jute,19.88370803016982,3,5.1596917751025835,19.003014029944328,444.985436205019,4.438269860791205,3,15.5024548909328,99.81644429020956,134.1933724816157,3,30.21716770552493,2,64.25674130317589,1.755518263380937 +74,46,45,25.75734909,88.36668522,6.025028997,189.4263485,jute,20.078951481109264,3,11.222086580953107,9.733990856436389,360.3151911757925,9.810546937416392,2,13.260990528686474,85.10087981678474,194.10699264547532,1,5.54547659472534,1,48.75868557633613,1.6168913263026266 +89,41,38,23.12844351,74.68322732,6.344751947,199.8362913,jute,14.583181562911136,3,7.49781843907736,2.9391253985665533,366.1989278079281,5.559056159769827,5,11.43216934773972,18.825127922492555,94.96520622103009,1,21.762485598341584,1,19.971489310920663,2.6713633638228154 +60,55,40,24.9949957,88.95692783,7.02777956,151.4935635,jute,12.098465709033965,3,9.08177787769231,5.483546753598121,374.526673255268,6.400409456622779,1,19.922403817825092,59.48086971098988,71.62654108913281,3,47.67804519527812,2,85.22970852232609,2.700308140184361 +67,43,38,25.21622704,70.88259632,7.299304715,195.8645552,jute,20.862010923388805,1,9.253882618460917,10.027409738697388,430.4077230987176,1.0310227940628014,6,9.556684341786667,91.94988842604161,58.24457670898289,3,1.66379851977983,1,24.781288716057194,2.981652711923935 +70,38,35,24.39736241,79.26861738,7.014063944,164.2697011,jute,25.5540921371553,2,10.944399559588474,0.6055709586598002,407.7958455610235,5.123366713150365,5,13.698363683190635,90.7053073177228,170.1537412234884,1,6.478122584416507,2,46.529169611643894,3.2691769932608463 +74,49,38,23.31410442,71.4509053,7.488014404,164.4970373,jute,19.39425294377949,2,5.2273008772391805,8.526774416813183,432.29269742835584,6.7332293521811994,1,14.531521511232175,12.007742453468161,122.50392426926217,3,49.65884380372649,1,88.16679647488482,2.333453165849807 +90,40,39,25.72668885,81.86171563,6.626503893,191.9649389,jute,29.94912784213244,3,5.396148610190196,13.511455649060967,377.2772137178462,4.034119518122074,2,5.500704440738595,16.871414166767686,106.32891145592926,1,7.417749045577294,1,1.4315976075256698,4.049714365698599 +82,35,44,26.96656378,78.21047693,6.239011,169.8391177,jute,14.31311338573272,3,6.241785974632963,9.918380333427434,367.1083431343126,5.0389558427860575,6,13.214941716593131,45.12270660480904,155.06950392314099,1,44.376322057743714,1,71.01908660131659,2.8205620881680487 +73,45,37,23.70467146,74.63745355,6.742688094,181.2783964,jute,21.56306756467927,3,7.830123596933335,10.844084673782135,394.66327190725974,8.57495743789919,2,11.38753515468073,65.69245658490343,146.4564477356177,1,22.221172411911827,3,38.2197253352357,3.882760168179311 +85,53,38,24.90075709,73.84186449,6.588017308,153.8990984,jute,17.575165149551303,3,9.992272922742142,15.232742156913446,353.24811903748906,5.500261723922966,3,5.734388528682154,71.02663901734522,113.18721463676529,1,22.150585269805518,2,86.55994085398837,2.3994895422471476 +81,56,36,23.39605743,72.60512854,7.097586415,174.7876411,jute,14.638855477740059,3,9.077472656154495,16.31891887906267,435.9599507861081,2.035834728763751,5,6.908954174898226,45.89259574210707,163.76032302773902,1,41.7889397801761,2,90.84126774421891,4.153582799935988 +84,55,38,26.8748389,79.78725152,6.956682743,173.1017097,jute,28.5806924590807,1,11.258799588250945,14.916138262997256,358.13206798864894,6.028935973872074,5,13.924241397843442,38.72725459523664,110.76121976142352,1,14.735106781470291,1,85.18235193298005,1.0717928009851154 +80,45,42,23.1426498,74.99739774,7.380396262,151.9035477,jute,10.265691516507482,3,5.116130450561281,9.579143319509086,413.44148364884154,5.823237578048962,3,12.379285897985827,92.17311108764581,126.8711016436148,2,32.975009926555735,3,87.53566075896472,3.7279016031387324 +76,54,45,24.29496635,77.62976013,6.176618831,184.9800516,jute,17.012374321392628,3,6.704144652357225,12.827229434025904,423.55375149509314,6.970662109515863,3,13.299365283075856,67.79059128250118,154.3674169663304,1,36.72884309782761,2,63.45354402782729,4.799408723107563 +76,56,39,24.39459498,89.89106506,6.551130445,197.1220049,jute,12.251031402864811,1,11.430076189429538,7.165210134980775,417.6484722028566,8.95327905545188,2,19.35190321156272,34.16757228100553,188.98464258484907,1,20.117278719689153,3,62.17363241511248,2.5774274351424085 +81,40,45,25.7629429,80.76238215,6.427726565,174.5071843,jute,22.172512053007235,2,9.12947889086137,11.499532401087109,411.9692935578049,3.5276756830934155,4,10.200625398563737,56.776725971070775,163.43800910790304,2,13.63577058723649,1,42.65007853304426,1.63418930964745 +76,44,45,25.4879684,84.48235878,6.740947635,168.7848886,jute,26.664930509052184,1,5.263525883776876,17.44817398431784,416.72536946201933,1.8445663680715214,4,11.12617812642248,22.367452141122612,116.41522335611764,3,26.63894046543474,2,41.91785585791475,2.154479839742195 +69,47,40,25.37122686,76.2403666,6.130136384,183.8270791,jute,29.77048860504933,3,10.123506146431708,10.432171219734457,421.4597292172297,6.914028064959028,3,12.043594271092097,3.06784500729661,118.58062381506628,3,14.075684987558951,1,31.794670978265682,2.538568257266978 +82,40,45,26.21312799,81.70476368,6.667633355,180.1237765,jute,29.67466452862166,2,8.64698442766475,11.018114873347084,406.37165600380274,6.783038538433298,3,15.440119474236585,55.75103266542835,62.46309885826663,2,21.42986710466336,1,44.29469574490456,3.065387058868739 +69,57,35,24.30748599,78.54340987,6.186814392,186.2337571,jute,15.922194233655581,1,11.239182777496858,12.380550603201518,367.32723342253973,6.489729198381788,3,15.965260706472467,5.398529688285136,189.26313026753405,3,42.05640490351614,3,88.12767084566,1.1351287563757833 +81,36,38,23.76554749,87.98329901,6.334837865,150.3166152,jute,15.89334840525219,1,9.472242736053078,17.68356485511799,367.18793334703327,9.450640968771255,2,14.625473787333044,35.827188388035644,145.03510317652302,2,4.635155102792682,1,83.79120850951658,2.8873059670486554 +67,60,38,24.79853023,78.53037059,7.16214284,162.2847429,jute,26.59739862161026,2,8.572153555461853,16.50972829337684,364.21018617871647,2.606474878980598,4,5.625841727264945,93.88260361612299,130.48816989268084,1,33.240860469289096,3,0.2508888234327711,4.386982662477605 +72,51,40,23.20683504,74.09956958,7.422318499,199.4766779,jute,11.137798914431894,2,10.378997906023601,9.581843529616501,402.46865250999616,4.107923311533053,6,7.290051877798707,16.237775739019455,77.82306231727507,3,6.044217553171244,3,12.582194424427195,3.236258100302575 +65,39,45,23.66805429,70.89000744,6.768001309,184.4633281,jute,19.603952652223036,3,5.473169555710194,6.4180953950326325,350.7298872165086,7.09338449705408,6,10.495326010422605,47.86033763688574,173.81983700175644,1,6.398978234102392,3,64.6070806289935,1.8280742397629242 +78,50,43,25.12417673,85.72530641,6.348441469,159.5718087,jute,17.350890535659858,1,5.064500727080514,19.891359237156397,438.7106174320103,2.603252179611136,4,7.741566483943583,25.50140248336029,129.77503007345933,3,27.718515918084314,3,66.06295865572535,1.0868548056620013 +77,52,41,23.89069041,83.46409075,6.097294061,167.7230632,jute,17.84149031862414,2,5.127257454972142,1.149361261665307,444.4014318840174,3.431119672408978,4,14.55462999730052,30.591092095286665,181.6964047695043,3,38.50766078731926,2,26.863909962896738,2.830012078902274 +89,52,42,23.09433785,81.45139295,6.14132902,196.6587013,jute,29.625506980443276,2,6.142220009582688,11.269879598959584,363.0069045499851,8.76577843751461,3,18.793348259533833,10.937526990128177,104.25653441626622,1,3.0383432774663097,1,90.32671959557645,2.3188701659292983 +62,49,37,24.21744605,82.85284045,7.479248124,166.1365886,jute,11.875862812343085,3,9.194175525919775,12.078937651326488,377.6270610977994,8.852485971004233,3,14.906872718034846,17.29066392277635,82.71159650091977,3,49.751257587928535,1,38.574184427302185,2.8957400988965825 +90,48,45,24.06475727,71.31342851,6.509174789,153.6390212,jute,14.490836811297964,3,6.1295810327624825,19.4627349732693,380.8524847776745,5.106708340896481,3,17.494095423291867,14.971882003786853,144.02693009425735,1,47.457602220865816,1,41.70962599917929,2.9672987313075656 +66,47,36,24.85441411,74.4407048,6.57256106,175.572958,jute,11.994661224514807,3,10.740460823444455,14.986502906266972,440.5133918928584,3.545555849801357,6,16.74076713861924,34.89744684346886,53.769386633263714,2,27.54442735425232,1,50.497446230445675,3.9614108372897725 +80,52,39,26.41915161,76.85691248,7.165696848,197.2101782,jute,12.025555160607496,3,11.502773055482097,18.3362417244968,429.6499234756257,9.12541851147823,2,19.04888314690641,9.732517356502035,191.9222335308865,1,20.86266450103737,3,3.1224825676689494,3.591886705824868 +89,52,45,24.89326318,77.01222585,7.207457208,196.469984,jute,28.032778544167666,3,11.735085538919598,13.22612812409573,363.45765749432763,6.695144712226781,3,12.260730378013658,48.36410392206982,55.32314645831764,1,41.346725954144475,1,14.102486265261827,3.4464401949818826 +77,51,44,23.25583402,82.7015932,7.124333547,166.2160846,jute,25.99834271950722,1,6.698883076662428,18.430729354895863,355.5107802090195,5.1835225732782595,4,19.485552587518715,85.35205824147219,79.37790618089647,1,34.80442678288334,3,52.05343791940511,4.026407531558388 +94,37,41,24.7634518,87.06071115,6.463538707,179.1630865,jute,11.134390991338917,1,8.19651499448606,9.56457376247278,404.8906890138018,4.116215620399335,3,12.9392204236633,25.011433941214868,160.82450580403122,2,8.585772558600718,3,95.15689267511213,4.736787186596204 +75,41,35,24.97042599,78.62697699,6.856833064,166.6415254,jute,14.322326690337254,3,10.968102409782272,10.966450241503587,383.10942648838915,1.601384826761013,6,14.473097137569189,87.14838007901878,159.0612978646435,2,45.20089373642154,1,92.18671159612418,2.9687401809365452 +60,55,36,26.12797248,80.49172597,7.132389299,150.6326874,jute,16.88963551289274,2,6.591885012250675,6.960989134289177,391.04340031421026,5.530480550220615,6,6.979575013746615,93.05107884349137,64.45643709539047,2,15.808148130673672,1,77.02308799492903,2.2310537530519565 +62,56,35,25.97825807,81.65769588,6.235357638,163.3488091,jute,26.306562440467495,1,7.48385675944597,13.639386482609092,431.9402962828727,9.616125223275146,5,19.510352840424616,38.33685412106932,139.84111070565928,1,32.81934165726439,1,65.82511491607632,4.114013671651049 +84,40,42,26.2830571,73.35763537,6.704273839,186.6898282,jute,29.71032072499192,2,8.922195595344332,10.893424175552033,428.51296752378516,3.427352404866317,2,6.593802686203436,46.835185865894815,70.41464777902965,1,17.195741346948136,1,8.877128923948586,3.7225664825743654 +100,56,40,26.38905406,83.31240346,7.433313409,176.1516409,jute,11.006101701812238,2,9.179618395674481,2.296052054917568,427.3508020333488,4.504569519073457,3,8.305587074015538,56.60715839324294,83.12536483820415,3,23.717992316796288,3,2.1151469236361353,1.8199942634533621 +75,56,44,25.2746335,73.7459581,6.109478059,168.0432282,jute,13.26369870476028,1,5.409148034004714,5.019200610647474,380.68142226451135,8.966918357994103,1,7.610540361206949,86.01012654087322,166.65320338716063,1,35.06452841225753,2,57.52084758629658,1.063358046293696 +78,46,42,23.09499564,78.45959697,7.095413294,155.3851533,jute,11.137747225723729,1,10.021245422120973,16.790591433294576,437.4852342847917,8.404334245212892,4,18.323473563219412,52.88872290446511,194.24591722676288,1,3.5382986185505336,2,6.867685490414754,4.288786780063352 +82,48,36,25.79351957,81.76904006,6.352076783,193.2418382,jute,12.836443377920073,1,8.838726255734736,2.979816130587931,385.8714198266298,4.11318736520715,5,10.703988199612043,40.877345510381005,123.20713263261099,3,31.01097440313042,3,71.89863036444987,2.986431213197381 +100,58,41,23.17403323,87.88255345,6.658769991,160.6217342,jute,18.736511205272386,1,7.502184619010908,3.9697229123105338,438.78502873012576,3.5332825284730442,4,10.096929694398503,58.914217131157706,124.88223124446776,2,5.319566387141656,3,8.040522337600542,1.9997382421904684 +88,50,40,25.63215038,79.95150917,7.051822472,182.2582277,jute,17.777651691177912,3,5.018772898195607,8.135712439433295,400.2651142413955,7.123659848901249,6,9.276307352952415,35.58475953580368,160.15599456340544,1,39.68329243988673,1,22.47515661757048,2.0834285073429104 +67,41,40,25.848795,87.81661683,7.333143205,152.6194403,jute,26.81682366852201,1,6.143599104231096,9.985184312548885,383.9369475736786,7.692059939499588,5,5.429196509169849,87.9271720959559,84.74815710430966,1,27.699122227521233,2,53.55230143307077,2.0629336913391367 +72,42,43,26.56767277,80.90424543,6.352771037,181.2915605,jute,17.892974836777494,3,9.933301211672209,9.862542721168593,371.1748826617812,8.008564755023752,6,5.838973087826488,91.73647020818817,135.55711637823214,1,41.48127831159649,3,38.567315745934735,4.086867740684302 +89,40,43,26.24532085,72.97198375,7.124050134,189.9711184,jute,23.955364277581367,1,5.245321767685116,4.6604449066349645,389.42258312483654,8.334702710487006,3,18.95130140886981,44.22949462670083,189.47641097269684,3,31.70702114898425,3,39.38362485316691,3.4925682024049065 +89,57,43,26.91515043,73.19897535,6.998787171,177.2233048,jute,12.913834094108532,3,9.200138808027319,11.43323513442606,384.84116082778723,3.3193200378571968,3,7.3267015601255805,76.80558635676003,145.40000161285036,2,5.752068871491778,1,50.28211383974072,1.0738626302699452 +61,41,44,24.36972377,82.11319791,6.537914958,159.9210934,jute,21.172513216459222,3,6.212717363147618,18.65374607013702,365.4885544526411,1.2207058551813796,6,15.265092570184205,38.75887172963689,95.98675306839488,3,28.025086489447443,2,10.280069310577266,3.7130655695985144 +79,45,43,25.71901283,79.15532398,7.171054239,187.1735424,jute,24.77096279268085,3,10.670456914084781,10.616964136757023,381.4463733247702,5.785821528686622,4,14.921742036339147,56.633221401366306,194.74072848189388,2,43.255674278717144,3,51.218068313817454,4.952564990496803 +84,40,43,25.01157559,88.3313023,7.228268228,169.4168014,jute,12.080064187509754,3,9.056126892858865,11.848274025860402,428.2111580281057,7.938710883539316,4,14.818179279707113,37.62304480665454,114.7675902185968,1,21.268610740007603,2,91.48876486051678,1.7687145425439246 +98,43,35,25.40785911,76.44048625,7.319952206,188.6372826,jute,10.667365708154236,2,7.770554294053623,0.5772072600864209,380.31700505097444,9.973918015413116,2,7.86732542323147,91.51431149859158,152.24530754196064,3,44.68888808879602,2,23.758428463079373,3.334200790718736 +75,36,44,23.28081,74.27607475,6.613341343,153.7447398,jute,28.927231809538224,2,7.431161152925293,9.701124825449373,368.2032681605779,4.366096362170447,5,18.955063317930524,10.007672253102706,170.75711101332783,2,23.027155908900305,2,75.8766558126532,2.668625038217788 +89,58,35,23.98651719,82.09053379,6.096838784,167.0576456,jute,29.050821528983043,3,11.634342487107205,1.4827051164736638,449.2404637687343,2.5495264723337474,2,15.626385189785164,89.78860094482148,138.30857038925944,2,33.39776232237774,3,57.440073851722396,4.437861915979597 +91,41,37,24.48556447,83.20630007,6.132570523,192.2316221,jute,13.212868150613586,1,11.625648002961775,11.779330983841996,421.681766610136,3.5460593367695408,2,6.079580245001063,46.52277116668753,109.46797305121976,1,0.017756487870346227,3,73.80931189845381,3.8113204210435168 +77,48,36,25.86705009,84.09985284,7.36008498,154.8390847,jute,17.858398944634363,1,6.5574348766597605,8.278964635491622,370.0576266141891,7.8650026138232425,5,17.801158026982968,49.029673836300304,182.14286974123425,1,8.699633045728527,3,28.540357076043588,4.888516890744399 +66,58,35,23.5643831,79.46283115,7.321619041,185.25947,jute,22.499873585836895,3,5.86017438648281,2.394227952007797,362.4246970466864,2.154198966694744,4,6.750341509679826,14.739703314315534,125.25619331237893,2,10.554650995058923,2,75.16468097664662,3.490640218111445 +62,59,41,24.2248758,74.89465426,7.175170657,192.4931257,jute,11.922048094408044,2,7.621747158783365,11.884933241875718,431.35107778622665,1.120510222551913,6,5.386530687181983,94.48427266266204,62.499750155210734,2,3.94187966560135,3,62.40100760454808,4.060180132901364 +82,35,35,25.49386782,86.97061481,7.299076163,176.5268267,jute,17.605865260017964,3,11.476868391255879,6.258734179135876,429.54701458969635,6.8130367402479415,1,7.262267913270218,56.8688398022247,96.17313824721589,1,37.02472559730669,2,60.38916828800481,2.38496586412348 +61,41,35,24.97178693,79.47557931,6.842966479,195.7571622,jute,11.984999511463448,1,9.932451794794478,14.753736567493318,405.16814471312983,1.6883070937097573,3,19.803971639010996,82.47447413212741,60.08990062055904,2,39.09496142337822,2,55.03750739272601,3.0040569532628436 +99,57,38,24.80624984,82.09281674,6.356295568,156.3616174,jute,19.28135004488415,1,6.32506460728552,18.410888039022858,381.6521268522965,2.8734546121485165,2,9.193849968779896,62.27677997458439,100.14754802451725,3,41.73320937570238,1,55.5349248760158,2.8405194444648627 +70,42,43,23.16814977,76.66724969,6.508342839,157.1215052,jute,12.4965744125287,2,7.005498522421753,6.050143864254065,386.65571535436766,7.445109092493055,2,10.780705803147663,69.5452500519428,112.63467953972662,1,32.35289347480727,1,15.092263460459144,1.5712091824280439 +90,59,35,24.25133493,89.86454053,7.098227926,175.1742112,jute,19.444760785788063,1,9.356738405047029,18.67595292718299,354.12957623423733,6.332189619128233,4,12.623615346166527,76.37917034180974,185.73748184444383,1,25.325221000588417,1,40.39873564046235,4.969862795835384 +73,43,42,26.58361011,78.00774772,6.310699968,154.8238864,jute,15.963386244910323,3,6.860736818152619,19.734880531005665,381.4761389743183,2.953499534536744,1,5.053442809642982,55.06142002484171,126.83764777540219,3,16.562645375218505,2,82.15709750124108,4.645126134000405 +67,46,44,26.82489244,78.20392774,7.093328631,153.9199807,jute,23.950618925887262,1,10.803973301795464,4.840692202455726,357.14696548221514,3.304826875573523,3,6.300291902802643,75.23881751194001,109.79351370442777,1,35.10126378302781,2,75.56065016887388,4.9740810527644586 +84,37,42,25.49674786,81.13449097,6.691074249,169.9288234,jute,13.140437181572864,1,8.454868693367024,13.36831056353401,376.3522231292921,1.7343297453139424,1,5.905922959828323,94.46746962139066,63.117866933613044,2,18.84345601473514,2,72.78694824865862,2.7531855148627478 +72,41,36,24.09874353,80.57226761,6.187746776,176.8604109,jute,29.320803383265737,1,10.841926539136791,18.631198966721502,364.2674914846384,1.658430548187897,6,6.5218069665328064,69.34609122363452,142.44955946225565,3,42.12640789582993,1,19.055958194611865,3.987503989226313 +71,56,37,23.18866654,86.20899734,6.491506245,176.103677,jute,26.199649350380096,3,9.964893812816793,3.7473276705016922,362.52965050613307,6.849496257352614,2,5.7933133349089,4.6110747812501796,199.75266291345133,3,33.06477543051305,3,53.56666920631632,2.0953954384820723 +64,53,38,26.24347471,78.51063754,6.855362875,183.4065252,jute,10.118846759257703,2,5.118375024424661,6.603132039148272,435.28271909803834,3.885159959649005,6,7.1346539969886145,4.489255783363144,172.22092375134736,1,44.07522386861299,3,4.0943242751614655,3.109778381318859 +65,54,39,23.75091572,71.14782585,7.124571593,160.0889553,jute,11.050504794004382,3,10.588695965157463,13.924831278157335,448.56757217546794,8.924681315281795,3,17.605915960081088,35.65522207290639,97.48689569336932,1,9.485963946883375,2,48.363226213866916,1.806879656139118 +60,58,37,26.13871511,79.1188943,6.067302109,171.4892533,jute,26.41942634413939,3,11.361445878468135,15.662910187543817,435.75884997288887,9.404974771471563,1,10.270208953810023,93.59315801966027,199.34446417920003,1,12.108435535070022,1,64.09621659221952,2.932772605210914 +86,39,43,26.14576648,71.23690851,6.432051512,193.1007598,jute,12.48595862613647,2,7.565290591076894,5.88187837312309,436.8874430350763,9.208335246905435,1,9.470339439382071,98.88813288802014,159.0952197795702,1,36.304976747221644,2,51.421557915924545,2.233196702121453 +90,50,44,26.91643698,73.48655995,6.253408852,171.4716375,jute,23.761118388351555,1,9.419083757405009,15.49630228183555,419.779603842178,6.088037386347792,5,12.836103729011995,4.715132596730265,154.70129490087294,3,35.14543854738259,2,62.55155278676211,4.168697656470995 +91,38,36,26.5232969,77.17331847,7.287318723,157.8548562,jute,23.02564795658849,1,5.9338743984225255,16.19440523796098,402.45516834916634,2.264156767016056,2,6.133304571294452,65.54843999688579,65.09420423563071,3,49.73172075544813,2,22.759620525228286,3.102235025945145 +87,48,38,23.81579631,80.94023552,7.161865733,190.312216,jute,18.557868625019392,3,11.65546072136413,13.538593785585496,422.0592725568684,3.9405173164145446,5,12.579110202036654,95.46660912035786,138.0230653907572,3,31.897183482156056,2,46.074354531794334,4.704657227625956 +72,41,36,26.50838667,86.84264005,6.065898283,152.9801697,jute,28.006202417960747,2,10.183317757799433,3.6315757059271525,430.03204578413875,1.9170308287310052,4,16.970912729573435,48.80995443313253,56.805639525914344,2,26.210458387566455,2,33.56073120901877,1.1318943837651338 +71,54,35,26.63952463,70.95705996,7.311077075,199.3355744,jute,11.561910846849637,1,9.944284454255342,16.646938108837432,404.69962519162704,4.401180132393311,1,9.360578419676846,34.58255180685946,198.86073234488808,1,20.50857791576981,2,80.13442318753802,3.315584393336151 +82,46,41,23.3250131,79.79609448,6.581693772,187.3096148,jute,19.674002504009138,3,7.194031324863888,18.727523567556258,351.5400665170053,6.9118608410910305,6,13.585421861314131,11.24183505642453,191.4007487508728,2,36.637408573008585,3,91.09228252614432,3.7445758829956732 +71,52,43,26.47549543,73.96164569,6.732826127,180.2513601,jute,12.517358028129866,2,7.237639560452281,18.34265595840626,405.8698101113194,2.2032986970045148,5,15.895780389122958,40.59706941397187,127.21378031444581,3,24.70711934608553,3,53.21674080467842,3.031458054777814 +80,43,43,23.78756036,74.36794079,6.014572075,172.6442654,jute,21.5002212703505,1,6.557478499022682,17.98384125542337,363.97253400695536,1.062429854016699,6,9.947151785602163,65.1174354413185,51.66199226377792,2,34.27646107086543,2,80.61574613972056,2.8571517456674624 +77,55,43,25.49941707,75.99987588,6.663559451,193.7141828,jute,11.088520793789092,3,8.40502494360062,12.050390828615868,439.73591164795357,3.509343624454489,2,11.663635871842502,75.51772535807635,99.06898365313128,1,0.4646916044235394,2,61.42541169732802,4.786888355173611 +95,57,41,23.24925555,73.65346838,6.434610995,184.7674863,jute,11.44437823312961,2,6.041058440305131,9.68846764543952,418.9531220231509,4.061761878039832,5,17.918977008851776,18.134493918106532,118.66986461047102,1,44.012513505014375,3,58.41955363899935,4.63983534575153 +63,47,35,26.98582182,89.05587886,7.432768147,193.8778713,jute,23.93873377525639,2,5.047153245226385,19.686258442681755,431.5642628573068,4.5968979569268225,4,12.53029419552388,35.60159477374293,105.3239386518253,3,37.89723190983225,2,68.33173969981779,3.653334447654966 +93,43,38,23.61475336,86.14290267,6.987332927,150.2355238,jute,11.720043870140849,3,8.515349483132098,10.245721875957983,445.71728785225775,9.036817289710042,4,17.350657159236547,35.75693194773114,158.38056632585693,3,38.758431547688744,2,6.981094761541485,3.373874541095973 +87,44,43,23.87484465,86.79261344,6.718725189,177.5147313,jute,12.30894752723027,3,8.480217336443202,12.605264170896982,394.31301792814014,8.319157844477353,1,10.86886197640732,54.796305664556286,138.81985669420368,2,0.23219932669062415,3,18.253669156618958,4.657908004216723 +88,52,39,23.92887902,88.07112278,6.880204617,154.6608736,jute,16.93858398518732,2,9.734811612028617,12.495273162751632,368.36703225905217,9.091342681635734,5,19.325948359834356,9.04826478160884,150.99400840434404,2,1.3122056164789897,1,32.55192296295766,1.4627658027764285 +90,39,37,24.81441246,81.68688879,6.86106911,190.7886386,jute,15.927120810494973,1,11.674255493857405,17.418627767011085,410.0294704795285,2.8152329263365234,1,17.428483959022095,98.5067769897878,193.13772937258116,2,30.056925725189267,3,11.27326978860691,4.571747674944234 +90,39,43,24.44743944,82.286484,6.7693455,190.9684885,jute,21.21379205595914,3,7.054745566339386,16.293026287998263,387.43149764451476,5.4670209929278695,4,11.297967586811211,85.35233658076258,61.46814443865807,3,12.72833734534507,3,48.771518717789775,3.635942303262281 +84,38,43,26.57421679,73.81994896,7.26158085,159.3223075,jute,10.634014274492007,2,11.07108106947015,15.329634833418943,389.9805891906126,2.1501301781889675,1,9.954171085128598,51.01640011726777,51.34953291673352,2,49.59779840017758,2,94.93679909826729,4.677841273603107 +91,21,26,26.33377983,57.36469955,7.261313694,191.6549412,coffee,16.916588897200498,3,8.322250326195732,12.489344587899271,383.7683486342638,9.284903975541589,5,14.08058697043257,38.817196672062025,194.40826186030029,2,34.60445014220267,3,80.5965801827112,4.054186324487022 +107,21,26,26.45288458,55.32222678,7.235070264,144.6861336,coffee,20.00090771188006,2,5.997592758159894,16.67279126837885,376.59011332198287,1.3365686620541266,5,15.997287220851982,0.9548984428930374,62.746796299975976,2,34.5724475577184,2,7.240345838789219,2.2841433635883246 +83,38,35,25.70822684,52.88667115,7.18915558,136.7325092,coffee,11.578841258002317,2,5.596198992370247,1.7063358475191448,387.8157092686401,7.545382620232643,3,5.26962877095298,17.872940262843805,169.4328990770692,3,38.12049172683803,3,56.071971669895134,1.1941787147555347 +108,24,31,24.12832546,56.18107663,6.431899748,147.2757818,coffee,13.433545248794221,1,11.341732874161586,4.32065858210178,414.71916295621105,8.102974756019195,5,11.652720409596064,23.148377767822904,177.33795580329695,3,14.838059795350533,1,55.74397757937463,4.23632507139286 +116,28,34,23.44372334,60.39523266,6.42321105,122.2103248,coffee,23.830296279767076,2,6.952318185060211,3.2035436389486804,441.573071314323,9.637916965207218,1,9.433745782156521,71.37818411303755,172.23787995872692,1,28.82273680988995,1,36.522919740673274,4.234085453496045 +116,23,25,23.4123707,52.26994674,6.869720196,139.3670753,coffee,26.868761849494668,3,6.741904344963057,8.41635863687356,373.9811437698543,3.7946831762811923,6,13.390083100988004,0.74848518583156,118.79743314609638,3,34.373895240317914,3,67.43339040151072,2.3733735137751237 +109,31,27,23.05951896,50.40609436,6.973839707,164.4971875,coffee,17.16695726308303,3,8.791710334527412,6.4835020954602385,380.53872995131246,7.858590562339093,5,9.234018321560303,6.748173922094958,194.01625314522278,2,17.60502386664487,1,79.86352668208652,2.381105886389448 +89,25,34,23.07895447,63.65861483,7.184801627,129.8765443,coffee,19.445991850298363,2,8.920955547529887,15.016815881742122,440.7218160890714,2.9919734928461716,1,17.036117239023092,64.20942524246426,71.74147023571035,3,27.889843918751673,2,84.92609587908677,1.2660512890893645 +118,18,32,27.6496114,51.11044023,6.351823783,122.8392822,coffee,12.866965731468312,3,8.200849142363136,9.108346507900686,425.1492620193771,1.0899159635120048,5,9.1748432673774,42.272878359003805,109.65935973017294,2,21.352809502308332,3,8.53882363897036,1.664673504741212 +111,32,34,25.46743689,69.35161206,6.392048018,171.3764462,coffee,26.42388036618837,1,5.582737093186684,3.8416527831679215,402.81011459049824,3.3894509451347945,5,17.562629008370585,47.865090077993386,186.78007391067695,1,24.530938832305253,2,35.27464384812342,4.469232980006041 +84,36,28,26.7350622,55.55164819,6.119892347,140.6305213,coffee,25.934870608420166,2,6.87062451863867,8.20102788141164,421.7018818546001,4.761386660039561,1,7.534289230795107,39.533832009071105,132.539808630373,1,20.297399455595976,1,30.64104503980819,4.566577052121682 +85,33,25,26.20811417,52.50987966,6.910823945,189.0944824,coffee,26.916367433487636,2,7.72310561741066,10.364404240106808,435.41885890370287,9.504703403549605,4,10.884613360191281,64.55332518907078,57.67770222215255,2,45.504405791529585,2,96.26984290979632,4.678008463443028 +99,15,27,27.0424167,57.27927475,6.501157208,165.6872119,coffee,16.94238300403439,3,6.685100446153818,9.51182933917507,377.29932607094895,2.160944282391657,1,18.141281482948308,81.65063846921899,92.9304495126446,1,25.03183409893038,3,38.74461816236238,3.8197818453118257 +81,30,31,24.65090184,51.93952357,7.027585559,135.1386537,coffee,22.03348979894905,2,9.471675369785345,16.913309274121527,355.83539287539054,5.773249150206677,5,18.8785842849939,75.65218934306105,176.59092295464893,1,20.53880265922825,2,44.18727050767567,1.2043462712267807 +95,39,29,27.35152643,55.99375012,7.13411409,148.9812525,coffee,25.97531776991413,1,8.759641046749937,8.1157406827446,425.70262113387287,6.719291746197753,1,5.113985611369592,52.074861433269206,109.21864820697499,1,47.42083381640764,2,69.71479809617928,3.5841472420105807 +81,34,30,25.17787724,62.26244581,6.647765997,135.0119649,coffee,24.825085623972114,1,7.810870397388827,18.496616238907183,382.8688718983793,6.285865703210486,4,17.42275832646464,84.0329804506935,68.3801094728131,2,7.143310879960091,3,97.70431233551578,2.503320576370518 +80,15,28,23.11438731,68.00096043,6.703270635,161.8944624,coffee,25.920081711119483,2,7.766460938619801,8.65143206869541,391.09000656417174,3.925577399039941,1,19.01023658524253,32.09426098797306,102.84335315147642,2,2.6945611484281984,2,6.546134405495641,3.904308218488823 +104,20,26,27.22783677,52.95261751,7.493191968,175.7260273,coffee,14.878731823205325,3,9.457576993628766,2.465405187756997,350.7416042469828,3.627023972297582,1,7.769744237160524,94.85465885767583,175.31427140341918,2,2.1599421218509773,1,34.12740969424276,2.5969156838480405 +109,29,28,23.26316991,60.5160021,6.724688503,194.1755471,coffee,18.125823639529287,3,6.5941879720830725,11.217091366295413,378.0270202057277,2.161077032329504,1,5.158685708348761,98.35448069181356,72.0669149096092,2,46.06738954651159,1,84.33116388808432,3.7796075864123932 +100,32,26,25.234661,57.53161469,6.043485685,124.2261737,coffee,23.488739391031093,2,6.565148259904972,1.8310462402710415,354.5374430412891,2.7719447906836194,5,6.473784700860738,3.3092000435237745,153.87646097082438,3,21.1109007668489,3,60.421301478639236,1.9955817380281506 +100,24,28,25.59535262,57.72920846,7.101661011,195.7733251,coffee,23.64276517827145,3,9.48364054176714,6.254329596332527,402.90296188444654,6.201465171267159,3,19.117097987275823,3.8974908777130723,187.73782800471034,3,10.672672296407699,2,64.669241714141,2.4839975692669443 +83,21,28,25.5674832,60.49244602,7.466900683,190.2257843,coffee,20.171119633832074,3,9.500470579224508,7.873987200482951,364.6431887093297,5.538343628677435,2,7.934372693516788,68.00752974815153,126.42537348564473,1,3.5939648060086924,1,4.2878278650621215,1.4397503379883756 +120,23,28,25.67324193,51.29043632,6.877799264,196.2736367,coffee,21.98237866520371,1,8.403351292704773,11.133358666191604,384.4297077162644,5.911817507142141,4,15.379318695467202,95.03373072027792,171.98740161433886,2,40.042576134251775,2,64.22533392921035,4.53711817253506 +104,26,30,24.40726724,62.65692638,6.410992833,148.6977358,coffee,20.85250953487601,2,8.588136130677029,9.739311339126038,396.304411058434,3.8415993435422795,4,10.446419209675437,23.99420262724924,175.60337085708113,2,43.07619350394103,2,29.896496935944928,2.0292342925038973 +108,33,31,23.69287069,66.76090123,7.393825704,144.6576424,coffee,17.95969252858681,1,10.910253684547104,5.7962118545833174,402.5011555829496,2.096677800913018,5,7.9702972204956435,14.593575400066783,69.12106949243804,1,27.0285614457992,2,49.9305541237568,4.106368559610237 +91,25,26,24.53460016,66.99765375,7.482414225,180.5059257,coffee,29.205015410113376,3,7.537815906152752,6.901906185551501,391.10751734789454,8.402343278817686,5,16.780821414763565,16.439099715152427,75.27380919682,3,48.03162081905489,2,46.47107682406776,1.9434617022857994 +86,26,27,27.13140403,52.89368299,6.081172981,192.4280381,coffee,17.618101316413828,3,5.161529270817132,6.871980420539455,371.4524946176257,8.977993989151095,5,7.8285927179946935,21.803855897666725,131.16201999919878,1,30.177367396895388,3,4.903798637906542,1.3227768834028955 +98,18,27,27.56088634,68.49299897,6.516312148,167.4358075,coffee,17.474198570513803,1,7.551936808345527,19.8217752220514,370.75486062461,2.7576524573708188,2,6.617098682772124,75.44616907713775,120.79611718561684,1,33.69700694597779,3,81.70894573723741,2.7730142551607755 +111,27,31,23.59302313,55.27564977,6.043330951,191.3980675,coffee,27.915403777453818,1,11.579607701972815,10.880401582552341,394.31605037234056,5.00134330151204,6,8.35909361772702,12.223184170410061,158.20434068766141,3,3.446747772795178,2,28.642625753517382,2.3926696995845544 +84,39,35,23.17714381,52.13864034,6.959404135,117.3113562,coffee,18.698452007810552,2,10.484388433306886,11.112022092007308,439.4925335174895,6.330618213080838,5,7.910196360014545,38.275536689267234,152.25029262775763,1,30.94654401839317,3,14.697580847456582,4.630534390586973 +98,27,27,24.71384065,51.29142534,7.238109556,197.6439711,coffee,17.69191629213744,2,9.617037122852143,15.00147553346293,396.57144766926046,4.675713369494713,4,13.1879616385799,35.070426874924074,135.37321556442475,1,2.7817769705454576,3,63.36944309925347,4.555295950610086 +118,21,34,24.38534644,64.72543073,7.234258375,119.6324109,coffee,26.01864083720235,3,9.812013773967553,14.028149818755848,418.5921240609715,5.345179101455301,1,15.495212584730785,44.23022550567304,75.28772241518749,3,2.1564420678990395,3,18.144492798392854,3.5162359246792 +103,27,31,27.15998538,51.59100753,6.691541233,126.1752206,coffee,19.81781935120666,2,7.540677613010809,8.288063452270364,353.2357380876273,5.356498015130511,1,16.17283424555942,58.48123322103663,165.54972520658617,3,20.492573230553624,1,88.70234539466348,3.327802044989469 +82,24,33,26.53543168,67.09608099,6.809593554,120.6494434,coffee,28.955131876380158,1,6.211927667006483,15.0936907713128,372.027630992602,4.777856305739771,1,13.43596537490813,31.963150966422948,92.53851990842693,3,39.84691544011017,1,9.364126748529555,4.08150987580696 +86,31,35,27.01207284,60.76645256,6.485761419,191.4508931,coffee,10.88876336302598,2,6.803256378557872,16.326659634443054,433.077292196548,6.637538916676462,6,19.34132349839401,82.68538018412187,127.2907759934466,2,8.728063733841596,1,7.90397700911476,1.9358896831346333 +88,35,35,27.55906475,58.45742907,6.784460602,117.9389993,coffee,23.33584732483188,1,11.370563794545754,6.908108887612245,390.0262964148799,4.262605739254374,6,5.304437832189704,44.01276031533013,82.73182407679495,1,46.11475082967226,3,32.82728208766051,2.9921160009964383 +84,27,29,23.32293161,53.00366334,7.167092586,168.2644287,coffee,12.461034644235166,2,5.163131303483213,6.579202941292737,377.1336250830626,6.6997934448361836,4,18.956149817477424,43.88923426777917,89.16928413341029,3,42.17098287519199,3,50.89498989752007,4.5322357677786576 +120,40,33,24.23850608,54.30329632,6.73410539,115.1564012,coffee,18.562032154156064,3,8.584143607039973,13.293567236355631,421.41988764991333,6.081613993988652,6,5.704819243892765,5.924323150352418,114.84027538489265,1,38.82775300909674,1,91.75900255129447,2.6881813776916252 +106,40,30,23.42611644,64.10651528,6.779984384,122.6847408,coffee,19.804419449448886,1,5.3774357291552075,15.250893398049383,363.62088282884275,9.6120587774359,5,9.432235978845185,25.493397950602947,93.09903631181432,3,9.846050336849899,2,27.834756928642246,4.986842732804034 +113,21,33,26.02241444,55.83288958,7.277422738,176.9020924,coffee,18.465641718085436,1,7.609507154217084,10.481325821629678,404.84198817136405,5.217395888061986,2,15.2863285902774,8.034086025467534,65.68642913016697,3,24.946083650105177,1,42.50795108554677,2.9301700188220696 +117,34,25,24.83846178,56.7685316,7.21270048,124.4135035,coffee,13.942680096299735,2,7.42341351260527,0.15885111858717993,384.1442158977517,2.020414046539497,2,11.442483997875195,30.64912676234236,166.57501416749966,3,11.637725372254904,3,51.709073126071715,3.8114812538330933 +80,30,25,26.24092174,65.64381357,7.487266991,148.3771202,coffee,17.74077642660384,3,10.06869948232751,5.179562507387647,376.2624448280993,2.4385856988018513,2,8.238429011489572,16.818533029190462,182.68185488913196,3,31.490926881752102,3,8.85472183645084,1.571257851929254 +88,21,27,24.43011925,66.02411187,7.231166546,181.6368274,coffee,26.454242212497533,1,8.068528842901864,11.58470558286256,419.99047822491036,8.129205417413557,2,7.470654004508477,54.98857936836136,66.89124515598157,3,19.263454889800546,2,67.34876070877073,3.218585099934662 +113,33,34,26.00373964,62.1445102,6.559817161,153.477776,coffee,13.805697759104326,3,7.008864400801875,0.027266730200721234,444.24348180256067,7.539755744879675,5,8.38950345486913,84.17840066530927,124.50062155154735,3,10.786827216800743,2,65.7283546353167,4.889080257735876 +87,23,28,26.22367404,62.26594559,6.979590627,193.7461968,coffee,10.987355311073593,3,5.995550680786487,9.568174257293776,354.5034516926162,2.6803619601910316,3,17.46386329976527,42.386471200213435,115.09741006321192,2,36.34155309779391,2,14.942843914778969,1.7612233067739829 +113,15,29,27.09617155,63.55324262,6.779230041,190.2440566,coffee,26.8667605830191,1,10.33057406192339,14.829222126888844,423.3618374428057,6.6541564946165135,2,12.108328252368707,91.98233865959496,60.642981087775425,1,1.5118172905314031,2,96.06943708853981,3.858408927614959 +98,29,30,25.64004392,61.03273481,6.217974349,199.4735636,coffee,14.964746086605501,2,7.202522250308457,8.289854153350635,422.852472559832,1.1760837129932753,3,16.518957370572725,32.482353803212995,126.57700672097238,3,1.8233206878226826,1,97.63366600901301,2.232463278343964 +97,29,27,27.74576987,54.36976075,7.205078785,139.8619431,coffee,18.27194337646806,3,5.532054683888051,19.58005647104767,376.9964505985208,1.8452535409159283,1,17.15917599386367,61.727031588119566,138.38444969912157,2,19.28654263776132,1,85.64415164406498,2.185036640543267 +85,35,32,26.24928198,54.28617819,6.854011265,133.1120232,coffee,15.18124692778565,1,6.923654041807552,10.931841788583245,352.28795791204976,8.304706474922893,5,5.548701761496407,71.45275596371704,135.722410908214,2,6.830558302286066,3,26.532216979132272,2.932988909429761 +82,29,35,26.67377159,52.24226285,6.246872394,156.1543898,coffee,20.360542273772705,3,7.971147115660766,12.146829041140057,432.801439502271,6.7678656104988635,6,15.91158754885152,46.38185167197619,79.34047464731918,2,27.84434288905225,1,79.09971437221193,3.815820836253507 +103,33,25,27.10210397,55.7497332,6.911066044,139.5013171,coffee,15.577928881206464,3,11.050800992437654,6.396214529677968,374.8130436211387,9.630145044867614,4,12.729657811177278,61.64521246539113,93.76314280269176,1,42.78385548913406,1,25.125033705292378,4.838865636424938 +112,17,28,27.62975458,61.26002598,6.777417989,196.6492664,coffee,23.62934942267834,2,9.519423243741885,3.1076415442978256,424.1598888473711,5.710620665058844,1,11.070684148173559,86.6528603885178,192.2284224026493,2,39.41843864440398,1,9.27232416564604,2.269833309131303 +99,19,33,27.5364547,55.51673151,6.273741983,130.6377143,coffee,24.574820971757987,1,10.255972786240116,1.1263787771041511,359.09536608185357,8.77023963686544,3,11.079961320804662,32.63584569365378,189.86700450442996,3,19.01309225560744,1,85.12113162671386,3.483038619705111 +120,20,34,23.56960509,50.56339727,6.906124587,130.3797119,coffee,21.863339690008956,3,11.675816121834451,8.049470973808745,357.96448814964316,7.936910590489785,5,16.013856640294275,16.566582868615455,89.48282616023883,3,0.516971074202166,3,47.252726581012524,2.116204208439644 +114,27,28,24.99451759,57.93250202,7.162802357,192.8736822,coffee,19.028658412710797,2,10.141647777709782,10.308429043068037,411.30687952841754,3.192323345541473,6,15.203162722600416,22.842667364725155,123.46346585724932,1,10.079795388537004,3,21.744516317387973,3.2390969596436667 +100,40,35,27.56441788,54.41094079,6.955787351,177.816092,coffee,22.442720008302917,1,5.481214317637382,15.147706520820865,387.7656063373172,6.283476894869647,3,8.221832193195354,66.20934108850935,174.45942019867135,3,28.496012044302745,1,66.13962203449972,3.748584840236874 +108,35,25,23.98143338,61.10935084,6.971963169,161.5279095,coffee,14.446748288731563,2,10.900613027177164,8.408809474239122,422.9443590813135,7.350693924000186,1,14.645313457675018,47.819014530624116,108.46449825704295,3,36.829016314957876,1,48.345058584849895,3.2750798765187428 +115,31,30,24.22984659,67.37768353,6.840927967,122.4073418,coffee,26.40756000379458,3,10.183756187835456,0.947895995719461,431.6035329030406,3.152548284596577,1,5.220482308825394,41.87255586939409,194.62078796851173,1,28.940488767992896,2,20.110531563232968,3.786630187482751 +87,28,30,25.60153969,68.66257977,6.536676653,168.8383605,coffee,13.881553404963675,2,5.686624894851892,15.176478570438343,379.33956966204863,4.50524847018094,4,15.615052141826004,23.850571517281704,148.19280414683715,3,16.812477345191922,1,98.92645001277512,1.1590750571469184 +82,24,26,24.31274458,53.57285558,6.089443603,184.4103931,coffee,13.627410365181795,2,10.786744207932113,10.803630859178071,409.3152900536683,3.8632105019350043,6,8.82084048911401,16.51742823287401,111.12982220320889,1,33.551075053742146,3,66.46545093026657,1.1596409371574303 +94,26,27,26.36629861,52.25738495,7.456460375,177.3176161,coffee,14.924920535153532,3,5.051497384979205,16.50105882482992,406.9730796778742,3.552977353389375,3,6.534388460632296,73.42347271310275,196.27523976474484,1,49.174936066687586,3,10.712846420267674,3.783307479052532 +87,28,35,26.5602777,57.1621814,6.759211911,152.0616227,coffee,28.338278758974212,3,8.669736494107692,1.5170890918765667,399.39164092341747,5.43708127345178,2,18.8791784811104,44.334138932159675,127.13765674405565,3,1.1175626742326805,2,95.08139101201051,3.7943786141155194 +118,40,35,26.35034208,58.50650238,7.460174812,121.5586297,coffee,10.531476639175859,3,10.984976791896047,11.572138207663711,385.5478722920658,8.949744370304002,5,8.275422279810961,34.34895330560608,85.29477172866314,2,45.81951611886165,1,87.60060947665231,1.5299657808794591 +87,38,29,25.20406808,57.88370456,6.652642579,156.1457255,coffee,27.713555970828143,3,10.138153276039969,8.032639147485622,426.6243207428062,5.971270844370167,2,10.90026131382653,30.46837392008581,96.41907705083196,3,27.341598902691523,2,72.38911275613678,4.177386852401883 +92,40,30,23.35723208,55.18792166,6.026287448,171.6976946,coffee,26.861193728314632,2,6.797145617548467,14.201781817205735,422.0344641940794,9.779439333088275,2,8.966929634446341,83.31292847891467,110.28795171033818,1,23.514133483259442,3,80.32368486196576,2.090534646022882 +97,22,26,23.60567546,59.68849145,6.074190142,185.1568059,coffee,12.621666672039906,3,6.91387497730444,17.355367751534445,369.746464940774,4.8327148639504705,5,19.566175484160148,38.85909183357593,180.4040271237194,3,42.53396483974716,3,84.74198522321528,2.697325550662706 +99,40,32,24.18471151,69.94807345,7.045543056,163.2708732,coffee,28.39294463671515,2,10.980414962783447,0.4017499553811543,437.01222826349294,8.633899475658115,2,6.197976595452292,97.92414732321859,131.22994914109483,1,36.208966225710334,3,94.90199339518003,1.9729951717892789 +89,28,33,26.44414097,53.83876189,6.993236001,175.3723314,coffee,19.50173649981193,3,7.928251969651355,5.063784366042103,412.6056260090952,2.8723466516038494,1,18.405801540761587,68.67657623259036,152.11563219325274,2,22.30156525341923,2,99.95727435075142,2.2998482900662403 +112,39,29,26.12492233,63.37479229,6.726528895,147.8035305,coffee,13.125280180931995,1,10.81370162055756,1.9360362776695106,400.92712760364265,8.648942117140823,6,13.482653811136325,80.0685522103035,178.03667223125913,1,37.75695863112446,3,26.82312106302622,1.6797349062832052 +111,28,26,27.77363343,64.47858698,6.937352845,192.7121236,coffee,10.701686070933352,1,7.780727629804778,15.228052697719969,354.518088205616,2.766148119518961,4,11.985369986207587,32.284062561735524,64.62505399434005,2,13.439963235932211,1,4.5597034587093415,3.6675172597370365 +114,20,26,25.55656667,62.67087838,7.27905689,193.5866233,coffee,20.081173044273452,1,9.527135134219911,10.687313692458869,402.98790890682227,4.077329983806473,5,17.720447196694607,1.445438927997944,135.68817026686753,2,31.069536938050657,3,6.33018616582156,1.2508612696172636 +117,26,30,27.92374437,67.96910852,7.079850922,115.2325531,coffee,19.74268426244155,2,5.316290311731468,13.042700790209757,355.00655880103204,5.049767015491457,5,13.309928893935993,36.050161038040315,59.70947809294499,3,5.4205104699727835,3,82.63755085080179,3.147423747976089 +111,29,31,26.05968403,52.31098539,6.136286518,161.3432535,coffee,11.829330126916311,1,5.303627120250579,3.9967224329424877,437.5212181900934,9.007977687485857,6,17.974165761339318,70.12681863034099,66.67036516664521,1,9.66209681504836,1,73.8273655097617,3.503331307324854 +119,30,28,26.35770906,64.57578034,6.505203696,163.6269496,coffee,29.312188107723514,3,11.319390083366885,9.53472545785585,364.8397279643288,6.022753528950439,6,5.519763549834372,3.7978939551608026,174.3271360684962,2,31.009862239298357,1,9.489044280624093,3.5030056677317405 +116,40,33,24.91370487,54.15319242,7.042089492,129.5481144,coffee,10.224701079795066,2,9.521125131530539,18.53444195934275,417.5384051292857,6.348511003032453,2,16.59347953094382,15.390573068871227,159.70702350617273,1,39.59455757717947,1,60.87966558059489,4.868037459853507 +95,37,35,27.31317116,68.4233391,6.348337519,192.4288139,coffee,18.19586032923393,1,8.708390763414553,13.786265719306156,388.7159821051115,9.429577497926854,6,17.87575322421299,70.77257001930647,66.28186119150821,1,43.880534205950944,3,99.0519237400327,2.9792091927190705 +86,40,33,26.1387869,52.26311691,7.432322234,136.3027766,coffee,26.430133215983638,2,5.94943080378396,19.457113661310363,393.9986146068263,1.9681381465168055,2,12.397630367211349,31.245745823086313,179.59627225967,3,32.78539224267655,2,81.81949398373696,2.5801279762560094 +117,37,32,23.1069385,67.06230539,6.787658922,162.5769606,coffee,23.57746130613952,1,5.643907629623579,10.900040851367125,394.80540150166723,4.901662339897946,1,17.746623463406042,15.20054937730736,111.91387152472907,3,2.007879864575396,2,78.73955619008586,4.592643496455722 +105,18,35,23.52648086,68.44030686,6.743417121,171.8839938,coffee,18.51719879757228,1,8.671470953871694,17.17783873014142,400.6513360157355,5.906657077143388,5,11.334299363286746,22.74388018482325,109.21127413478104,2,14.836567732239692,3,84.04635579237056,4.696973632871321 +109,23,25,25.11711046,68.48030408,7.00733163,194.8773479,coffee,18.216295729891232,3,8.98387529345675,8.745359870122705,434.27194699821666,3.243587863004935,1,13.803449832943658,38.45188690913947,179.57380433576606,3,44.99435109295262,2,94.16407939728629,3.5894390868279378 +80,18,31,24.02952505,58.84880599,7.303033217,134.6803969,coffee,18.438362596834466,2,7.521657810214986,5.134494634741458,422.2391345728312,4.9641246131319985,6,15.878609245039211,31.095419628381915,91.95561593563122,2,20.524902772071542,1,78.23544150287918,3.3490378361786117 +101,31,26,26.70897548,69.71184111,6.861235184,158.8608887,coffee,28.749579004480516,2,9.852503337545429,13.557580290453558,379.52279688142926,7.479056987422591,1,17.458456440050384,17.830304125834274,77.12408576411302,1,26.87026221198051,1,19.837487964758203,1.2407546076958842 +103,33,33,26.71717393,50.50148528,7.131435858,126.8073984,coffee,27.90183973869437,1,11.66215282517514,2.324002312151414,438.36866057502823,6.266938412264151,3,19.346227666480704,81.89223232175623,105.9968443526586,1,6.007991296173759,1,39.23306104212055,3.472756440751938 +93,26,27,24.59245684,56.46829641,7.288211994,137.7044047,coffee,27.089995715785612,2,9.42978416041818,10.275427926514247,393.01596681145656,4.374038495571538,5,15.607354266430418,56.8657618981927,198.91613969105796,1,39.15095646997872,1,49.969046240847234,1.3563719536903451 +104,35,28,27.51006055,50.66687215,6.983732393,143.9955548,coffee,10.07925126717412,1,11.3301193793534,4.954276345902895,351.1478149280893,9.505791216886305,5,17.0311121644123,5.972522237408418,164.8741166092643,3,36.252923984774284,2,15.241243599169717,4.0998090816280275 +116,36,25,27.57847581,58.52534263,6.172090205,156.6810374,coffee,23.320519483369498,1,8.95465588980817,2.4321991616505967,421.1340156439659,5.954199019964379,1,10.76005474288019,25.288482409847802,77.71366892549729,1,13.320951967207744,1,70.72992777701124,3.4648755910985582 +107,38,29,26.65069302,57.56695719,6.35118177,145.105065,coffee,29.826788511722356,3,5.318890246281764,10.135087530393662,375.99906818123117,3.116471461937506,4,15.190427078147515,3.9680439611720852,64.4249385943942,2,13.292637681930126,2,49.68992057136191,3.1815580762133138 +101,33,33,26.97251562,62.0183627,6.908671379,142.8610793,coffee,13.803873596397116,3,8.167869320141943,9.245461706769673,395.21538339259575,5.433539466539919,2,14.27072427384216,2.886482133037094,169.0513042392963,2,15.742623579605135,3,92.33155479967935,2.04930024802155 +107,31,31,23.17124551,52.97841162,6.766184468,153.1201644,coffee,18.296256557764067,3,6.428406067678784,19.67639070238986,387.01617304183736,9.131122698029502,6,13.613430487856641,56.26405680409713,167.9479081298221,1,49.18989936970398,2,30.920527378909245,2.804276166517868 +99,16,30,23.52652084,65.44340921,6.392791654,186.1728203,coffee,12.642971277836256,1,9.915135131088793,2.3828545976045,410.448728365925,4.703797877840363,2,13.047310684076043,47.299550417957605,56.844488326645646,1,21.54864289531475,3,40.225686814412796,2.673302215982513 +103,40,30,27.30901814,55.196224,6.348316257,141.4831644,coffee,13.19095469253482,1,11.480067670234824,19.10683130377579,416.92345520012736,4.263579466352212,5,19.214758722391522,5.553972078659197,98.21511003205694,1,0.3850781073277909,1,54.08781998123497,1.8613324331258077 +118,31,34,27.54823036,62.88179198,6.123796057,181.4170812,coffee,13.508814256108518,1,11.133205665256487,19.494306071184837,408.1436896431313,7.9003183504683765,2,11.986324682910077,18.598826821563087,146.35756501438993,2,22.823900390909053,3,17.753445252430023,4.132128235196412 +106,21,35,25.627355,57.04151119,7.428523634,188.5506536,coffee,28.477930555278643,1,5.424222642810601,0.6607793529765438,407.41728729455775,4.563434130489964,2,17.445867297359342,41.2482653682816,82.204708513072,1,34.92379920967915,3,75.56941295940646,4.56478144525538 +116,38,34,23.29250318,50.04557009,6.020947179,183.468585,coffee,28.4397675573041,1,8.12970400936235,4.974811975921371,374.2861496672049,5.416485126000258,5,18.190925676595686,24.31642879356446,78.32780393541171,2,1.7378188762561364,2,87.93801307518771,1.9886621355781835 +97,35,26,24.91461008,53.74144743,6.334610249,166.2549307,coffee,22.85402795020538,3,5.486677104030552,11.21095734965416,429.49592157786026,1.9750399242878052,4,6.827054608640566,47.227095787149906,125.67752272746043,3,33.2609226712504,2,38.221399353845165,1.1837923585608587 +107,34,32,26.77463708,66.4132686,6.78006386,177.7745075,coffee,10.697756538547269,1,10.330875167890733,19.19236115355042,439.0790691558914,4.720354769209413,5,18.597260283179743,87.43119850497924,185.8333807032337,3,31.415618482726305,1,77.7196389992403,4.111618985243631 +99,15,27,27.41711238,56.63636248,6.086922359,127.92461,coffee,12.203829682038808,3,6.0705583715897875,10.603401086640208,405.2595398699845,4.141147768764266,6,15.417978614467254,36.95835436323838,198.54102074271773,2,18.79750965801862,3,22.336838635661127,4.190796328348858 +118,33,30,24.13179691,67.22512329,6.362607851,173.3228386,coffee,28.989175529548135,3,11.097182116394928,13.842015985356813,360.48260538132587,1.5996140522008162,5,12.95667508217523,79.67865796464093,86.72438065470436,2,38.805888450916044,3,41.782729211210835,2.4470104278279448 +117,32,34,26.2724184,52.12739421,6.758792552,127.1752928,coffee,13.642304736922634,2,8.097337065880122,16.537830888139748,415.5143138839929,8.934076932532399,6,16.86813051240768,31.00715649291702,72.19142098690531,2,8.395497784784894,3,49.619791249176885,4.1193880040029285 +104,18,30,23.60301571,60.39647474,6.779832611,140.9370415,coffee,23.911727578408552,3,8.639741540157758,14.481756579084477,413.1237161382283,6.401993710119095,3,14.652123227033409,3.5741911097655232,175.10424061806972,3,26.784996183697135,2,47.27126705890273,2.758819113898633 diff --git a/AgCloud/mqtt_and_kafka/Sensor_edge_device/Dockerfile.edge b/AgCloud/mqtt_and_kafka/Sensor_edge_device/Dockerfile.edge new file mode 100644 index 000000000..71eebac1f --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Sensor_edge_device/Dockerfile.edge @@ -0,0 +1,24 @@ +# ---------- Base image ---------- +FROM python:3.11-slim + +# ---------- Work directory ---------- +WORKDIR /app +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt + + +RUN chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 +# ---------- Copy simulation scripts ---------- +COPY ./run_sim.py ./fill_system_with_fake_data.py ./Crop_recommendationV2.csv ./place.csv ./ + +# ---------- Install dependencies ---------- +RUN pip install --no-cache-dir paho-mqtt pandas + +# ---------- Default command ---------- +CMD ["python", "run_sim.py"] diff --git a/AgCloud/mqtt_and_kafka/Sensor_edge_device/fill_system_with_fake_data.py b/AgCloud/mqtt_and_kafka/Sensor_edge_device/fill_system_with_fake_data.py new file mode 100644 index 000000000..2297eba38 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Sensor_edge_device/fill_system_with_fake_data.py @@ -0,0 +1,55 @@ +import pandas as pd +import random +from datetime import datetime, timezone + +def generate_sensor_data(): + df = pd.read_csv("Crop_recommendationV2.csv") + dfs = pd.read_csv("place.csv") + + row = df.sample(1).iloc[0] + place_row = dfs.sample(1).iloc[0] + + sensor_id = int(place_row["id"]) + plant_id = int(place_row["plant_id"]) + sensor = str(place_row["sensor_type"]) + value = float(place_row["value"]) + lat = float(place_row["lat"]) + lon = float(place_row["lon"]) + + data = { + "sid": f"sensor-{sensor_id}", + "id": sensor_id, + "timestamp": datetime.now(timezone.utc).isoformat(), + "msg_type": "telemetry", + "value": value, + "plant_id": plant_id, + "sensor_name": sensor, + "n": float(row["N"]), + "p": float(row["P"]), + "k": float(row["K"]), + "temperature": float(row["temperature"]), + "humidity": float(row["humidity"]), + "ph": float(row["ph"]), + "rainfall": float(row["rainfall"]), + "label": str(row["label"]), + "soil_moisture": float(row["soil_moisture"]), + "soil_type": int(row["soil_type"]), + "sunlight_exposure": float(row["sunlight_exposure"]), + "wind_speed": float(row["wind_speed"]), + "co2_concentration": float(row["co2_concentration"]), + "organic_matter": float(row["organic_matter"]), + "irrigation_frequency": float(row["irrigation_frequency"]), + "crop_density": float(row["crop_density"]), + "pest_pressure": float(row["pest_pressure"]), + "fertilizer_usage": float(row["fertilizer_usage"]), + "growth_stage": int(row["growth_stage"]), + "urban_area_proximity": float(row["urban_area_proximity"]), + "water_source_type": int(row["water_source_type"]), + "frost_risk": float(row["frost_risk"]), + "water_usage_efficiency": float(row["water_usage_efficiency"]), + "lat": lat, + "lon": lon, + "sensor_type": sensor + } + + return data diff --git a/AgCloud/mqtt_and_kafka/Sensor_edge_device/place.csv b/AgCloud/mqtt_and_kafka/Sensor_edge_device/place.csv new file mode 100644 index 000000000..8ea9d97a7 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Sensor_edge_device/place.csv @@ -0,0 +1,47 @@ +lat,lon,value,plant_id,id,sensor_type +32.045,34.835,12.3,1,1,Soil_Moisture +32.045,34.835,28.7,1,1,Soil_Moisture +32.045,34.835,62.4,1,1,Soil_Moisture +32.067,34.812,15.1,2,2,Ambient_Temperature +32.067,34.812,23.4,2,2,Ambient_Temperature +32.067,34.812,45.8,2,2,Ambient_Temperature +32.081,34.875,33.0,3,3,Humidity +32.081,34.875,56.7,3,3,Humidity +32.081,34.875,95.3,3,3,Humidity +32.052,34.885,10.8,4,4,Soil_Moisture +32.052,34.885,26.9,4,4,Soil_Moisture +32.052,34.885,75.1,4,4,Soil_Moisture +32.099,34.880,16.2,5,5,Ambient_Temperature +32.099,34.880,25.5,5,5,Ambient_Temperature +32.099,34.880,49.0,5,5,Ambient_Temperature +32.082,34.790,27.8,6,6,Humidity +32.082,34.790,58.9,6,6,Humidity +32.082,34.790,99.9,6,6,Humidity +32.020,34.760,11.7,7,7,Soil_Moisture +32.020,34.760,30.5,7,7,Soil_Moisture +32.020,34.760,65.2,7,7,Soil_Moisture +31.992,34.935,14.0,8,8,Ambient_Temperature +31.992,34.935,24.8,8,8,Ambient_Temperature +31.992,34.935,47.9,8,8,Ambient_Temperature +31.975,34.965,29.3,9,9,Humidity +31.975,34.965,53.5,9,9,Humidity +31.975,34.965,92.6,9,9,Humidity +31.980,34.910,9.5,10,10,Soil_Moisture +31.980,34.910,25.4,10,10,Soil_Moisture +31.980,34.910,71.2,10,10,Soil_Moisture +31.995,34.985,17.8,11,11,Ambient_Temperature +31.995,34.985,27.6,11,11,Ambient_Temperature +31.995,34.985,55.3,11,11,Ambient_Temperature +32.000,35.000,20.1,12,12,Humidity +32.000,35.000,59.4,12,12,Humidity +32.000,35.000,97.8,12,12,Humidity +32.040,34.915,13.9,13,13,Soil_Moisture +32.040,34.915,29.2,13,13,Soil_Moisture +32.040,34.915,78.5,13,13,Soil_Moisture +31.960,34.950,15.4,14,14,Ambient_Temperature +31.960,34.950,26.8,14,14,Ambient_Temperature +31.960,34.950,51.7,14,14,Ambient_Temperature +31.985,34.925,22.5,15,15,Humidity +31.985,34.925,55.9,15,15,Humidity +31.985,34.925,90.1,15,15,Humidity + diff --git a/AgCloud/mqtt_and_kafka/Sensor_edge_device/run_sim.py b/AgCloud/mqtt_and_kafka/Sensor_edge_device/run_sim.py new file mode 100644 index 000000000..f272d8e61 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/Sensor_edge_device/run_sim.py @@ -0,0 +1,25 @@ +# run_sim.py +import time +import json +import paho.mqtt.client as mqtt +from fill_system_with_fake_data import generate_sensor_data + +BROKER = "mosquitto" +PORT = 1883 +TOPIC = "sensors" + +def main(): + print("🚀 Simulation started... publishing sensor data to MQTT") + client = mqtt.Client() + client.connect(BROKER, PORT, 60) + + print("🚀 Simulation started... publishing sensor data to MQTT") + + while True: + data = generate_sensor_data() + payload = json.dumps(data) + client.publish(TOPIC, payload) + print(f"📤 Published to {TOPIC}: {payload}") + time.sleep(20) +if __name__ == "__main__": + main() diff --git a/AgCloud/mqtt_and_kafka/connect.Dockerfile b/AgCloud/mqtt_and_kafka/connect.Dockerfile new file mode 100644 index 000000000..19d48919e --- /dev/null +++ b/AgCloud/mqtt_and_kafka/connect.Dockerfile @@ -0,0 +1,25 @@ +# Base image for Kafka Connect +FROM confluentinc/cp-kafka-connect:7.6.1 + +# Switch to root to allow installation and file copying +USER root + +# Define target directory for custom plugins (location within the container) +ENV CUSTOM_PLUGINS=/usr/share/confluent-hub-components + +# Ensure the plugins directory exists +RUN mkdir -p ${CUSTOM_PLUGINS} + +# Copy the entire plugin directory (including lib) into the image +# The directory within the image will be "confluentinc-kafka-connect-mqtt" +COPY connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6 \ + ${CUSTOM_PLUGINS}/confluentinc-kafka-connect-mqtt + +# Set the plugin path environment variable to include the default path plus our custom plugin path +ENV CONNECT_PLUGIN_PATH="/usr/share/java,${CUSTOM_PLUGINS}" + +# Switch back to the default non-root user for security +USER appuser + +# Expose the default Kafka Connect port +EXPOSE 8083 diff --git a/AgCloud/mqtt_and_kafka/docker-compose.yml b/AgCloud/mqtt_and_kafka/docker-compose.yml new file mode 100644 index 000000000..38ea9f513 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/docker-compose.yml @@ -0,0 +1,110 @@ + +services: + kafka: + build: + context: ./kafka + dockerfile: dockerfile + container_name: kafka + environment: + - ALLOW_PLAINTEXT_LISTENER=yes + - KAFKA_ENABLE_KRAFT=yes + - KAFKA_CFG_PROCESS_ROLES=broker,controller + - KAFKA_CFG_NODE_ID=1 + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@kafka:9093 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT + - KAFKA_CFG_LISTENERS=INTERNAL://:9092,EXTERNAL://:9094,CONTROLLER://:9093 + - KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://kafka:9092,EXTERNAL://localhost:29092 + - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL + - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false + - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 + - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 + - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 + ports: + - "9092:9092" + - "29092:29092" + networks: + - mesh + - minionet + healthcheck: + test: ["CMD-SHELL", "/opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list >/dev/null 2>&1"] + interval: 10s + timeout: 5s + retries: 20 + + mosquitto: + image: eclipse-mosquitto:2.0 + container_name: mosquitto + command: ["mosquitto", "-c", "/mosquitto/config/mosquitto.conf"] + ports: + - "1883:1883" + volumes: + - ./mosquitto/config:/mosquitto/config:ro + depends_on: + kafka: + condition: service_healthy + networks: + - mesh + healthcheck: + test: ["CMD", "mosquitto_sub", "-h", "localhost", "-p", "1883", "-t", "$$SYS/#", "-C", "1", "-W", "15"] + interval: 10s + timeout: 5s + retries: 12 + + connect: + build: + context: . + dockerfile: connect.Dockerfile + image: local/connect-with-mqtt:1.0.0 + container_name: connect + depends_on: + kafka: + condition: service_healthy + mosquitto: + condition: service_healthy + ports: + - "8083:8083" + environment: + - CONNECT_BOOTSTRAP_SERVERS=kafka:9092 + - CONNECT_GROUP_ID=agcloud-connect + - CONNECT_CONFIG_STORAGE_TOPIC=_connect_configs + - CONNECT_OFFSET_STORAGE_TOPIC=_connect_offsets + - CONNECT_STATUS_STORAGE_TOPIC=_connect_status + - CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR=1 + - CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR=1 + - CONNECT_STATUS_STORAGE_REPLICATION_FACTOR=1 + - CONNECT_KEY_CONVERTER=org.apache.kafka.connect.storage.StringConverter + - CONNECT_VALUE_CONVERTER=org.apache.kafka.connect.storage.StringConverter + - CONNECT_REST_ADVERTISED_HOST_NAME=localhost + - CONNECT_PLUGIN_PATH=/usr/share/java,/usr/share/confluent-hub-components + networks: + - mesh + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:8083/connectors"] + interval: 10s + timeout: 5s + retries: 12 + + init-connector: + image: curlimages/curl:8.7.1 + depends_on: + connect: + condition: service_healthy + volumes: + - ./connectors:/connectors + networks: + - mesh + entrypoint: > + sh -c " + echo '==> Creating MQTT connector...'; + curl -X POST -H 'Content-Type: application/json' --data @/connectors/mqtt-source.json http://connect:8083/connectors; + echo '==> Done.'; + " + +networks: + mesh: + name: agcloud_mesh + driver: bridge + minionet: + external: true + name: storage_with_mqtt_minionet diff --git a/AgCloud/mqtt_and_kafka/kafka/dockerfile b/AgCloud/mqtt_and_kafka/kafka/dockerfile new file mode 100644 index 000000000..928189796 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/kafka/dockerfile @@ -0,0 +1,21 @@ +# Bitnami Kafka single-broker (KRaft) +FROM bitnamilegacy/kafka:latest + +USER root + +# Install kcat (kafkacat) for the smoke test +RUN install_packages kafkacat + +# Copy scripts to a writable location for user 1001 +COPY --chmod=755 kafka-files/create-topics.sh /opt/bitnami/create-topics.sh +COPY --chmod=755 kafka-files/smoke-test.sh /opt/bitnami/smoke-test.sh +COPY --chmod=755 kafka-files/app-start.sh /opt/bitnami/app-start.sh + +# Expose ports: internal 9092, external 9094 (will be mapped to host 29092) +EXPOSE 9092 9094 + +# Back to non-root (Bitnami default) +USER 1001 + +# Wrapper entrypoint: start Kafka, wait, create topics, run smoke +ENTRYPOINT ["/opt/bitnami/app-start.sh"] \ No newline at end of file diff --git a/AgCloud/mqtt_and_kafka/kafka/kafka-files/app-start.sh b/AgCloud/mqtt_and_kafka/kafka/kafka-files/app-start.sh new file mode 100644 index 000000000..ad6f253a0 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/kafka/kafka-files/app-start.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +# -------- +# app-start.sh +# Purpose: +# 1) Start Kafka (using Bitnami entrypoint/run scripts) +# 2) Wait until the broker is ready +# 3) Run create-topics.sh (7-day retention) +# 4) Run smoke-test.sh (kcat produce/consume) +# 5) Keep the container alive while Kafka runs +# -------- + +# Internal bootstrap for in-container admin/tools +BOOTSTRAP="${BOOTSTRAP:-127.0.0.1:9092}" + +# Start Kafka in background using Bitnami's scripts +/opt/bitnami/scripts/kafka/entrypoint.sh /opt/bitnami/scripts/kafka/run.sh & +KAFKA_PID=$! + +echo "[entry] Kafka starting (pid=${KAFKA_PID}) ... waiting for readiness" + +# Wait until kafkatools can talk to the broker +for i in {1..120}; do + if /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server "${BOOTSTRAP}" --list >/dev/null 2>&1; then + echo "[entry] Kafka is ready." + break + fi + sleep 2 + if [[ $i -eq 120 ]]; then + echo "[entry] ERROR: Kafka did not become ready in time." >&2 + kill ${KAFKA_PID} || true + wait ${KAFKA_PID} || true + exit 1 + fi +done + +# Create required topics (idempotent) +echo "[entry] running create-topics.sh ..." +RETENTION_DAYS="${RETENTION_DAYS:-7}" BOOTSTRAP="${BOOTSTRAP}" /opt/bitnami/create-topics.sh + +# Smoke test (kcat one-shot produce+consume) +echo "[entry] running smoke-test.sh ..." +BOOTSTRAP="${BOOTSTRAP}" /opt/bitnami/smoke-test.sh || { + echo "[entry] WARNING: smoke test failed (continuing so Kafka remains available)" >&2 +} + +# Stay attached to Kafka process +wait ${KAFKA_PID} \ No newline at end of file diff --git a/AgCloud/mqtt_and_kafka/kafka/kafka-files/create-topics.sh b/AgCloud/mqtt_and_kafka/kafka/kafka-files/create-topics.sh new file mode 100644 index 000000000..2305707ff --- /dev/null +++ b/AgCloud/mqtt_and_kafka/kafka/kafka-files/create-topics.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Kafka bootstrap used by admin tools inside the container +BOOTSTRAP="${BOOTSTRAP:-localhost:9092}" + +# 7 days retention in milliseconds +RETENTION_DAYS="${RETENTION_DAYS:-7}" +RET_MS=$((RETENTION_DAYS*24*60*60*1000)) + +log() { echo "[topics] $*"; } + +# Wait for Kafka to be ready (admin tools can list topics) +log "waiting for Kafka at $BOOTSTRAP ..." +for i in {1..60}; do + if /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server "$BOOTSTRAP" --list >/dev/null 2>&1; then + log "Kafka is up." + break + fi + sleep 2 + if [[ $i -eq 60 ]]; then + log "ERROR: Kafka did not become ready in time." + exit 1 + fi +done + +TOPICS=( + dev-robot-alerts + dev-robot-commands + dev-robot-status + dev-robot-telemetry-raw + dev-robot-state + + dev-camera-security + sensor-telemetry + sensor-anomalies + dev-robot-telemetry-anomalies + + sensor_anomalies + sensor_zone_stats + dev-robot-telemetry-anomalies + summaries.5m + irrigation.control + irrigation.control.dlq + sound.new + image.new + aerial_images_metadata + dev-security-images-keys + alerts + + aerial_image_object_detections + aerial_image_anomaly_detections + aerial_image_segmentation + aerial_images_complete_metadata + + # --- imagery (MinIO -> Kafka) --- + image.new.aerial + image_new_aerial_connections + image.new.fruits + image.new.leaves + image.new.ground + image.new.field + image.new.security + image_new_security_connections + + # --- sound(sound) (MinIO -> Kafka) --- + sound.new.plants + sound.new.sounds + sounds_ultra_metadata + sounds_metadata + sound_new_plants_connections + sound_new_sounds_connections + + inference.dispatched.sounds + dlq.inference.http + event_logs_sensors + sensors + sensors_anomalies_modal + aerial_images_keys +) + +# Idempotent creation with retention.ms +for T in "${TOPICS[@]}"; do + /opt/bitnami/kafka/bin/kafka-topics.sh \ + --bootstrap-server "$BOOTSTRAP" \ + --create --if-not-exists \ + --topic "$T" \ + --partitions 1 \ + --replication-factor 1 \ + --config "retention.ms=${RET_MS}" +done + +log "✅ ensured topics with retention.ms=${RET_MS} (7 days)" diff --git a/AgCloud/mqtt_and_kafka/kafka/kafka-files/kafka.env.example b/AgCloud/mqtt_and_kafka/kafka/kafka-files/kafka.env.example new file mode 100644 index 000000000..2c90b9b2f --- /dev/null +++ b/AgCloud/mqtt_and_kafka/kafka/kafka-files/kafka.env.example @@ -0,0 +1,29 @@ +# Base (local dev only - NOT for prod) +ALLOW_PLAINTEXT_LISTENER=yes + +# KRaft single-node (no ZooKeeper) +KAFKA_ENABLE_KRAFT=yes +KAFKA_CFG_PROCESS_ROLES=broker,controller +KAFKA_CFG_NODE_ID=1 +KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER +KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@localhost:9093 + +# Dual listeners: internal (9092) / external (9094 -> host 29092) +KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT +KAFKA_CFG_LISTENERS=INTERNAL://:9092,EXTERNAL://:9094,CONTROLLER://:9093 + +KAFKA_CFG_ADVERTISED_LISTENERS=INTERNAL://localhost:9092,EXTERNAL://localhost:29092 + + +KAFKA_CFG_INTER_BROKER_LISTENER_NAME=INTERNAL + +# Topics: no auto-create +KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=false + +# Single-broker internals +KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=1 +KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=1 +KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=1 + +# Optional: internal tools bootstrap (used by scripts) +BOOTSTRAP=localhost:9092 diff --git a/AgCloud/mqtt_and_kafka/kafka/kafka-files/smoke-test.sh b/AgCloud/mqtt_and_kafka/kafka/kafka-files/smoke-test.sh new file mode 100644 index 000000000..2d318aa46 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/kafka/kafka-files/smoke-test.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Use the in-container bootstrap; from host use localhost:29092 +BOOTSTRAP="${BOOTSTRAP:-localhost:9092}" +TOPIC="dev-robot-telemetry-raw" + +# Do NOT create the topic here. It must already exist (created by create-topics.sh) + +MSG="hello-from-kcat-$(date +%H:%M:%S)" +echo "[smoke] producing: $MSG" + +# Prepare a temporary file to capture the consumer output +TMP="$(mktemp -p /tmp smoke.XXXXXX)" + +# Start a one-shot consumer from the END (so it waits for the next produced message) +# Run in background and capture exactly 1 message +kcat -C -b "$BOOTSTRAP" -t "$TOPIC" -o end -e -q -c 1 > "$TMP" & +CID=$! + +# Small delay to ensure the consumer is ready to receive +sleep 0.5 + +# Produce a single message (no key delimiter) +printf "%s\n" "$MSG" | kcat -P -b "$BOOTSTRAP" -t "$TOPIC" + +# Wait for the consumer to finish with a soft timeout loop (up to ~10s) +for i in {1..20}; do + if ! kill -0 "$CID" 2>/dev/null; then break; fi + sleep 0.5 +done +# In case still alive, let it end (won't block if already exited) +wait "$CID" 2>/dev/null || true + +echo "[smoke] consuming..." +OUT="$(cat "$TMP" || true)" +rm -f "$TMP" +echo "[smoke] got: $OUT" + +if [[ "$OUT" == "$MSG" ]]; then + echo "[smoke] ✅ PASS" +else + echo "[smoke] ❌ FAIL" >&2 + exit 2 +fi diff --git a/AgCloud/mqtt_and_kafka/mosquitto/config/mosquitto.conf b/AgCloud/mqtt_and_kafka/mosquitto/config/mosquitto.conf new file mode 100644 index 000000000..c61691867 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/mosquitto/config/mosquitto.conf @@ -0,0 +1,7 @@ +listener 1883 +allow_anonymous true +persistence false +log_type error +log_type warning +log_type notice +log_type information diff --git a/AgCloud/mqtt_and_kafka/mqtt-router/Dockerfile b/AgCloud/mqtt_and_kafka/mqtt-router/Dockerfile new file mode 100644 index 000000000..3ec5173ee --- /dev/null +++ b/AgCloud/mqtt_and_kafka/mqtt-router/Dockerfile @@ -0,0 +1,39 @@ +FROM python:3.12-slim + +# ---- Build-time toggle for NetFree CA injection ---- +ARG USE_NETFREE=false + +# ---- System deps (CA, curl). librdkafka1 helps if confluent-kafka wheel is not fully static on your base ---- +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl librdkafka1 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# ---- Optional NetFree certificates (mount or COPY your certs/*.crt alongside the Dockerfile) ---- +# If you keep certs in repo, uncomment the next line: +# COPY certs/*.crt /app/certs/ +RUN if [ "$USE_NETFREE" = "true" ] && [ -d /app/certs ] && ls /app/certs/*.crt >/dev/null 2>&1; then \ + echo "Configuring NetFree certificates..."; \ + cp /app/certs/*.crt /usr/local/share/ca-certificates/ && update-ca-certificates; \ + else \ + echo "No NetFree certs applied (USE_NETFREE=$USE_NETFREE)."; \ + fi + +# ---- Make requests/libs use system CA (works both with and without NetFree) ---- +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PYTHONUNBUFFERED=1 + +# ---- Install Python deps ---- +COPY requirements.txt . +# When behind NetFree, trusted-host can help even אם אין צורך זה לא מזיק: +RUN python -m pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + -r requirements.txt + +# ---- App code ---- +COPY app.py . + +ENTRYPOINT ["python", "app.py"] diff --git a/AgCloud/mqtt_and_kafka/mqtt-router/app.py b/AgCloud/mqtt_and_kafka/mqtt-router/app.py new file mode 100644 index 000000000..3383671aa --- /dev/null +++ b/AgCloud/mqtt_and_kafka/mqtt-router/app.py @@ -0,0 +1,154 @@ +import os +import re +import signal +import sys +from typing import Optional + +import paho.mqtt.client as mqtt +from confluent_kafka import Producer, KafkaException, KafkaError +from confluent_kafka.admin import AdminClient, NewTopic + +# ---------- Env ---------- +MQTT_HOST = os.getenv("MQTT_HOST", "mosquitto") +MQTT_PORT = int(os.getenv("MQTT_PORT", "1883")) +MQTT_USERNAME = os.getenv("MQTT_USERNAME", "") +MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "") +MQTT_TOPIC_FILTER = os.getenv("MQTT_TOPIC_FILTER", "mqtt/#") + +KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:9092") +KAFKA_CLIENT_ID = os.getenv("KAFKA_CLIENT_ID", "mqtt-router") +CREATE_TOPICS = os.getenv("CREATE_TOPICS", "false").lower() == "true" +DEFAULT_PARTITIONS = int(os.getenv("DEFAULT_PARTITIONS", "1")) +DEFAULT_REPLICATION = int(os.getenv("DEFAULT_REPLICATION", "1")) + +# Optional security (set via env if needed) +KAFKA_SECURITY_PROTOCOL = os.getenv("KAFKA_SECURITY_PROTOCOL", "") # e.g. "SASL_PLAINTEXT", "SASL_SSL", "SSL" +KAFKA_SASL_MECHANISM = os.getenv("KAFKA_SASL_MECHANISM", "") # e.g. "PLAIN" +KAFKA_SASL_USERNAME = os.getenv("KAFKA_SASL_USERNAME", "") +KAFKA_SASL_PASSWORD = os.getenv("KAFKA_SASL_PASSWORD", "") + +# ---------- Topic mapping ---------- +# Allow arbitrary depth after "mqtt/" → replace "/" with "." +VALID_CHARS = re.compile(r'[^A-Za-z0-9._-]') + +def map_mqtt_to_kafka_topic(mqtt_topic: str) -> Optional[str]: + prefix = "mqtt/" + if not mqtt_topic.startswith(prefix): + return None + tail = mqtt_topic[len(prefix):].strip("/") + if not tail: + return None + parts = [seg for seg in tail.split("/") if seg] + dotted = "_".join(parts) + dotted = VALID_CHARS.sub("_", dotted) + return dotted[:249] if dotted else None + +# ---------- Kafka clients ---------- +producer_conf = { + "bootstrap.servers": KAFKA_BOOTSTRAP, + "client.id": KAFKA_CLIENT_ID, + + # Strong delivery semantics + "acks": "all", + "enable.idempotence": True, + + # Throughput tuning + "compression.type": os.getenv("KAFKA_COMPRESSION", "lz4"), + "linger.ms": int(os.getenv("KAFKA_LINGER_MS", "5")), + "batch.size": int(os.getenv("KAFKA_BATCH_SIZE", str(64 * 1024))), # bytes + + # Resilience + "socket.keepalive.enable": True, + "delivery.timeout.ms": int(os.getenv("KAFKA_DELIVERY_TIMEOUT_MS", "120000")), + "request.timeout.ms": int(os.getenv("KAFKA_REQUEST_TIMEOUT_MS", "30000")), +} + +# Optional security +if KAFKA_SECURITY_PROTOCOL: + producer_conf["security.protocol"] = KAFKA_SECURITY_PROTOCOL +if KAFKA_SASL_MECHANISM: + producer_conf["sasl.mechanism"] = KAFKA_SASL_MECHANISM +if KAFKA_SASL_USERNAME: + producer_conf["sasl.username"] = KAFKA_SASL_USERNAME +if KAFKA_SASL_PASSWORD: + producer_conf["sasl.password"] = KAFKA_SASL_PASSWORD + +p = Producer(producer_conf) +admin = AdminClient({"bootstrap.servers": KAFKA_BOOTSTRAP}) # kept for CREATE_TOPICS toggle + +def ensure_topic(topic: str): + if not CREATE_TOPICS: + return + try: + fs = admin.create_topics([NewTopic(topic, num_partitions=DEFAULT_PARTITIONS, + replication_factor=DEFAULT_REPLICATION)]) + fs[topic].result() + print(f"[router] Created topic: {topic}", flush=True) + except Exception as e: + msg = str(e) + if "exists" in msg.lower() or "TopicExistsError" in msg or "TOPIC_ALREADY_EXISTS" in msg: + return + print(f"[router] create_topics warning for {topic}: {e}", flush=True) + +def delivery_report(err, msg): + if err is not None: + print(f"[router] Delivery failed for {msg.topic()}: {err}", flush=True) + else: + print(f"[router] Delivered to {msg.topic()} [partition {msg.partition()} offset {msg.offset()}]", flush=True) + +# ---------- MQTT callbacks ---------- +def on_connect(client, userdata, flags, rc, properties=None): + if rc == 0: + print(f"[router] Connected MQTT {MQTT_HOST}:{MQTT_PORT}, subscribe: {MQTT_TOPIC_FILTER}", flush=True) + client.subscribe(MQTT_TOPIC_FILTER, qos=0) + else: + print(f"[router] MQTT connect failed: rc={rc}", flush=True) + +def on_message(client, userdata, msg): + src = msg.topic + dst = map_mqtt_to_kafka_topic(src) + if not dst: + print(f"[router] Skipping topic (no match): {src}", flush=True) + return + try: + ensure_topic(dst) + p.produce(dst, value=msg.payload, on_delivery=delivery_report) + # Poll to serve delivery callbacks; small 0 keeps loop snappy + p.poll(0) + except KafkaException as e: + # Helpful message when topics are not pre-created + kafka_err = e.args[0] if e.args else None + if isinstance(kafka_err, KafkaError) and kafka_err.code() == KafkaError.UNKNOWN_TOPIC_OR_PART: + print(f"[router] ERROR UnknownTopicOrPartition for '{dst}'. " + f"CREATE_TOPICS=false → please pre-create this topic.", flush=True) + else: + print(f"[router] Kafka produce error: {e}", flush=True) + +# ---------- Main ---------- +def main(): + client = mqtt.Client(client_id="mqtt-router", protocol=mqtt.MQTTv5) + if MQTT_USERNAME or MQTT_PASSWORD: + client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) + + # Gentle reconnect backoff + client.reconnect_delay_set(min_delay=1, max_delay=30) + + client.on_connect = on_connect + client.on_message = on_message + + def handle_sigterm(signum, frame): + print("[router] SIGTERM received, flushing producer...", flush=True) + p.flush(10) + sys.exit(0) + + signal.signal(signal.SIGTERM, handle_sigterm) + signal.signal(signal.SIGINT, handle_sigterm) + + client.connect(MQTT_HOST, MQTT_PORT, keepalive=30) + print(f"[router] Boot: MQTT={MQTT_HOST}:{MQTT_PORT} Kafka={KAFKA_BOOTSTRAP} " + f"CREATE_TOPICS={CREATE_TOPICS}", flush=True) + client.loop_forever() + +if __name__ == "__main__": + main() + diff --git a/AgCloud/mqtt_and_kafka/mqtt-router/requirements.txt b/AgCloud/mqtt_and_kafka/mqtt-router/requirements.txt new file mode 100644 index 000000000..3d853480c --- /dev/null +++ b/AgCloud/mqtt_and_kafka/mqtt-router/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt==2.1.0 +confluent-kafka>=2.4 diff --git a/AgCloud/mqtt_and_kafka/requirements.txt b/AgCloud/mqtt_and_kafka/requirements.txt new file mode 100644 index 000000000..5ef5a3d7f --- /dev/null +++ b/AgCloud/mqtt_and_kafka/requirements.txt @@ -0,0 +1,10 @@ +pandas>=2.2 +confluent-kafka>=2.4 +paho-mqtt>=1.6 + +# Optional: Parquet support (only needed if you want to load Parquet files). +# The simulator works with CSV files without these. +# If you need Parquet, install EITHER pyarrow OR fastparquet (not both). + +# pyarrow>=14 +# fastparquet>=2024.2 diff --git a/AgCloud/mqtt_and_kafka/run_all.sh b/AgCloud/mqtt_and_kafka/run_all.sh new file mode 100644 index 000000000..49c59f5c9 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/run_all.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ------- config ------- +CONNECTOR_NAME="mqtt-source" +CONFIG_FILE="connectors/mqtt-source.json" # config-only JSON (no top-level "name" or "config") +COMPOSE_CMD="docker compose" # change to 'docker-compose' if needed +NETWORK_NAME="agcloud_mesh" # must match your docker-compose.yml +MOSQUITTO_SVC="mosquitto" +KAFKA_SVC="kafka" +CONNECT_SVC="connect" +# ---------------------- + +echo "==> Bringing stack down (volumes too) ..." +$COMPOSE_CMD down -v || true + +echo "==> Starting stack ..." +$COMPOSE_CMD up -d + +# --- wait for health of a container by name --- +wait_healthy() { + local name="$1" + echo "==> Waiting for '$name' to become healthy ..." + for i in {1..60}; do + status="$(docker inspect -f '{{.State.Health.Status}}' "$name" 2>/dev/null || echo "unknown")" + if [[ "$status" == "healthy" ]]; then + echo " $name is healthy." + return 0 + fi + sleep 2 + done + echo "ERROR: $name did not become healthy in time." >&2 + docker logs "$name" || true + exit 1 +} + +wait_healthy "$KAFKA_SVC" +wait_healthy "$MOSQUITTO_SVC" +# connect sometimes reports 'starting' while REST is already up; still wait: +wait_healthy "$CONNECT_SVC" + +# --- verify connector plugin is available --- +echo "==> Checking connector plugins ..." +PLUGINS_JSON="$(curl -sf http://localhost:8083/connector-plugins)" +echo "$PLUGINS_JSON" | grep -q 'io.confluent.connect.mqtt.MqttSourceConnector' || { + echo "ERROR: MQTT Source Connector not found in /connector-plugins" >&2 + echo " Make sure plugin is mounted to: /usr/share/confluent-hub-components/confluentinc-kafka-connect-mqtt" >&2 + exit 1 +} +echo " MQTT plugin detected." + +# --- extract kafka.topic from config (without jq) --- +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "ERROR: Config file not found: $CONFIG_FILE" >&2 + exit 1 +fi + +TOPIC="$(grep -oE '"kafka.topic"\s*:\s*"[^"]+"' "$CONFIG_FILE" | sed 's/.*"\([^"]*\)"/\1/')" +if [[ -z "${TOPIC:-}" ]]; then + echo "WARN: Could not parse kafka.topic from $CONFIG_FILE ; defaulting to 'dev-robot-alerts'" + TOPIC="dev-robot-alerts" +fi +echo "==> Will use Kafka topic: $TOPIC" + +echo "==> Verifying Kafka topic exists: $TOPIC" +if ! docker exec -i "$KAFKA_SVC" /opt/bitnami/kafka/bin/kafka-topics.sh \ + --bootstrap-server localhost:9092 --list | grep -qx "$TOPIC"; then + echo "ERROR: Kafka topic '$TOPIC' does not exist." + echo " Please ask the Kafka admin to create it (auto-create is disabled)." + exit 1 +fi + +# --- upsert connector (POST if missing, otherwise PUT) --- +echo "==> Upserting connector: $CONNECTOR_NAME" + +# Try GET status +set +e +STATUS_JSON="$(curl -s http://localhost:8083/connectors/${CONNECTOR_NAME}/status)" +set -e + +if echo "${STATUS_JSON:-}" | grep -q '"name"'; then + echo " Connector exists -> updating (PUT /config)" + curl -sf -X PUT -H "Content-Type: application/json" \ + --data-binary @"$CONFIG_FILE" \ + "http://localhost:8083/connectors/${CONNECTOR_NAME}/config" >/dev/null +else + echo " Connector missing -> creating (POST /connectors)" + # Build a POST body that wraps the config into {"name": "...", "config": {...}} + CONFIG_CONTENT="$(cat "$CONFIG_FILE")" + POST_BODY="$(printf '{\"name\":\"%s\",\"config\":%s}\n' "$CONNECTOR_NAME" "$CONFIG_CONTENT")" + echo "$POST_BODY" | curl -sf -X POST -H "Content-Type: application/json" \ + --data-binary @- \ + "http://localhost:8083/connectors" >/dev/null +fi + +# Confirm RUNNING +echo "==> Verifying connector status ..." +for i in {1..30}; do + S="$(curl -sf http://localhost:8083/connectors/${CONNECTOR_NAME}/status || true)" + if echo "$S" | grep -q '"state":"RUNNING"'; then + echo " ${CONNECTOR_NAME} is RUNNING." + break + fi + sleep 1 + if [[ $i -eq 30 ]]; then + echo "ERROR: Connector did not reach RUNNING state:" >&2 + echo "$S" >&2 + exit 1 + fi +done + +# --- E2E test: publish MQTT + consume Kafka --- +echo "==> Publishing test MQTT message ..." +docker exec -i "$MOSQUITTO_SVC" mosquitto_pub -h mosquitto -p 1883 -t mqtt/test -m '{"hello":"world"}' + +echo "==> Consuming from Kafka (kcat) ..." +docker run --rm --network "$NETWORK_NAME" edenhill/kcat:1.7.1 \ + -b kafka:9092 -C -t "$TOPIC" -o end -q -c 1 || true + +echo +echo "✅ All done!" +echo " - Stack is up" +echo " - Connector '${CONNECTOR_NAME}' is RUNNING" +echo " - MQTT -> Kafka bridge verified on topic: ${TOPIC}" diff --git a/AgCloud/mqtt_and_kafka/simulator/air_simulator.py b/AgCloud/mqtt_and_kafka/simulator/air_simulator.py new file mode 100644 index 000000000..7f8f11493 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/simulator/air_simulator.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import argparse +from pathlib import Path +import sys +from typing import Optional + +from simulator.simulator import Simulator +from simulator.metrics import Metrics +from simulator.utils import positive_float, load_data + + +def build_parser() -> argparse.ArgumentParser: + """ + Build the CLI parser for the simulator. + """ + parser = argparse.ArgumentParser(description="Replay telemetry at a fixed QPS to MQTT/Kafka") + parser.add_argument("--qps", type=positive_float, help="Messages per second to send (>0)") + parser.add_argument("--duration", type=positive_float, help="Total run time in seconds (>0)") + parser.add_argument("--out", choices=["mqtt", "kafka", "both"], default="both", + help="Publish target (default: both)") + parser.add_argument("--file", required=True, help="Path to .csv or .parquet file with telemetry data") + + parser.add_argument("--mqtt-host", default="localhost", help="MQTT broker host (default: localhost)") + parser.add_argument("--mqtt-port", type=int, default=1883, help="MQTT broker port (default: 1883)") + parser.add_argument("--mqtt-topic", default="telemetry", help="MQTT topic (default: telemetry)") + + parser.add_argument("--kafka-bootstrap", default="localhost:29092", + help="Kafka bootstrap servers (default: localhost:29092)") + parser.add_argument("--kafka-topic", default="dev-robot-telemetry-raw", + help="Kafka topic (default: dev-robot-telemetry-raw)") + + parser.add_argument("--window-sec", type=positive_float, default=5.0, + help="Rolling window (seconds) for instantaneous QPS (default: 5)") + parser.add_argument("--status-every", type=positive_float, default=1.0, + help="Print status every N seconds (default: 1)") + + parser.add_argument("--loop", action="store_true", + help="Loop over the sample data until duration elapses") + parser.add_argument("--stability", action="store_true", + help="Shortcut: run 60s at 1k msgs/s and report KPI PASS/FAIL") + parser.add_argument("--perf", action="store_true", + help="Run 15m at 10k msgs/s (Kafka recommended); report KPI PASS/FAIL") + return parser + + +def _apply_shortcuts(args: argparse.Namespace) -> None: + """ + Apply --stability/--perf presets. + """ + if args.stability: + args.qps = 1000.0 + args.duration = 60.0 + args.loop = True + + if args.perf: + args.qps = 10000.0 + args.duration = 15 * 60.0 + args.loop = True + + +def _kpi_verdict(args: argparse.Namespace, metrics: Metrics) -> Optional[bool]: + """ + KPI verdict in profile modes: loss_rate <= 0.5% for all selected transports. + Returns True/False in profile mode, None otherwise. + """ + if not (args.stability or args.perf): + return None + + loss = metrics.loss_rates() + targets = [] + if args.out in ("kafka", "both"): + targets.append(loss["kafka_loss_pct"]) + if args.out in ("mqtt", "both"): + targets.append(loss["mqtt_loss_pct"]) + + ok = all(l <= 0.5 for l in targets) + print(" KPI verdict:", "PASS" if ok else "FAIL (loss > 0.5%)") + return ok + + +def main() -> None: + """Parse args, run simulator, print KPI when relevant.""" + parser = build_parser() + args = parser.parse_args() + + if args.stability and args.perf: + parser.error("Choose only one profile: --stability OR --perf") + + # Require qps/duration unless a profile provides them. + if not (args.stability or args.perf): + if args.qps is None or args.duration is None: + parser.error("--qps and --duration are required unless --stability or --perf is set") + + _apply_shortcuts(args) + + print(f"[config] qps={args.qps} duration={args.duration}s out={args.out} loop={bool(args.loop)}") + + df = load_data(Path(args.file)) + print(f"[info] Loaded {len(df)} records from {args.file}") + + sim = Simulator(args, df) + sim.run() + + verdict = _kpi_verdict(args, sim.metrics) + if verdict is False: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/AgCloud/mqtt_and_kafka/simulator/metrics.py b/AgCloud/mqtt_and_kafka/simulator/metrics.py new file mode 100644 index 000000000..85c37bc32 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/simulator/metrics.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List + + +@dataclass +class Metrics: + """Counters and timing samples for Kafka and MQTT.""" + sent_kafka: int = 0 + acked_kafka: int = 0 + lost_kafka: int = 0 + sent_mqtt: int = 0 + acked_mqtt: int = 0 + lost_mqtt: int = 0 + + kafka_latencies: List[float] = field(default_factory=list) + mqtt_latencies: List[float] = field(default_factory=list) + inter_arrivals: List[float] = field(default_factory=list) + + def loss_rates(self) -> Dict[str, float]: + """Loss rate (% per transport).""" + lk = (self.lost_kafka / max(self.sent_kafka, 1)) * 100.0 + lm = (self.lost_mqtt / max(self.sent_mqtt, 1)) * 100.0 + return {"kafka_loss_pct": lk, "mqtt_loss_pct": lm} diff --git a/AgCloud/mqtt_and_kafka/simulator/simulator.py b/AgCloud/mqtt_and_kafka/simulator/simulator.py new file mode 100644 index 000000000..2745177b7 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/simulator/simulator.py @@ -0,0 +1,232 @@ +from __future__ import annotations + +import argparse +import json +from collections import deque +from statistics import pstdev +import time +from typing import Dict, Optional + +import pandas as pd +from confluent_kafka import Producer +import paho.mqtt.client as mqtt + +from simulator.metrics import Metrics +from simulator.utils import iterate_records, percentile, fmt_latency, extract_sid_from_headers + + +class Simulator: + """Send loop, rate control, and metrics aggregation.""" + + def __init__(self, args: argparse.Namespace, df: pd.DataFrame): + self.args = args + self.df = df + + # MQTT + self.mqtt = mqtt.Client(client_id="simulator", protocol=mqtt.MQTTv311) + self.mqtt.on_publish = self._on_mqtt_publish + + # Kafka + conf = { + "bootstrap.servers": args.kafka_bootstrap, + "client.id": "simulator", + "acks": "all", + "linger.ms": 5, + "batch.num.messages": 10000, + "message.timeout.ms": 10000, + "enable.idempotence": True, + "delivery.report.only.error": False + } + self.kafka = Producer(conf) + + # Inflight maps: mid/sid -> send_time + self.inflight_mqtt: Dict[int, float] = {} + self.inflight_kafka: Dict[str, float] = {} + + self.metrics = Metrics() + + # Rate / status + self.window_sec = float(args.window_sec) + self.status_every = float(args.status_every) + + # Book-keeping + self._kafka_sid_seq = 0 + self._last_send_time: Optional[float] = None + + # --- Callbacks --- + + def _delivery_report(self, err, msg): + """Kafka delivery callback.""" + if err is None: + self.metrics.acked_kafka += 1 + sid = None + + try: + k = msg.key() + if isinstance(k, (bytes, bytearray)): + sid = k.decode() + elif k is not None: + sid = str(k) + except Exception: + sid = None + + start_ts = self.inflight_kafka.pop(sid, None) if sid else None + if start_ts is not None: + self.metrics.kafka_latencies.append(time.perf_counter() - start_ts) + + else: + self.metrics.lost_kafka += 1 + try: + print(f"[kafka-error] topic={msg.topic()} key={msg.key()} reason={err}") + except Exception: + print(f"[kafka-error] reason={err}") + + def _on_mqtt_publish(self, client, userdata, mid): + """MQTT on_publish: update ack counters and latency.""" + self.metrics.acked_mqtt += 1 + start_ts = self.inflight_mqtt.pop(mid, None) + if start_ts is not None: + self.metrics.mqtt_latencies.append(time.perf_counter() - start_ts) + + # --- Core run --- + + def run(self) -> None: + """Run the send loop, enforce QPS/duration, print status and summary.""" + args = self.args + + # Connect MQTT + self.mqtt.connect(args.mqtt_host, args.mqtt_port) + self.mqtt.loop_start() + + # Record iterator (optionally loop) + records = list(iterate_records(self.df)) + if not records: + raise ValueError("Input file contains no records") + if args.loop: + from itertools import cycle + iterator = cycle(records) # WHY: Sustain long tests with a short sample. + else: + iterator = iter(records) + + # Rate control + interval = 1.0 / args.qps + send_window = deque() + + start = time.perf_counter() + end_time = start + args.duration + last_status_t = start + sent_total = 0 + + while True: + now = time.perf_counter() + if now >= end_time: + break + + try: + record = next(iterator) + except StopIteration: + break + + # Inter-arrival (jitter) + if self._last_send_time is not None: + self.metrics.inter_arrivals.append(now - self._last_send_time) + self._last_send_time = now + + payload_str = json.dumps(record, separators=(",", ":")) + + # MQTT + if args.out in ("mqtt", "both"): + info = self.mqtt.publish(args.mqtt_topic, payload_str, qos=1, retain=False) + self.inflight_mqtt[info.mid] = now + self.metrics.sent_mqtt += 1 + + # Kafka + if args.out in ("kafka", "both"): + self._kafka_sid_seq += 1 + sid = str(self._kafka_sid_seq) + self.inflight_kafka[sid] = now + hdrs = [("sid", sid.encode())] + + try: + self.kafka.produce( + args.kafka_topic, + key=sid, + value=payload_str.encode(), + headers=hdrs, + callback=self._delivery_report + ) + except BufferError: + self.kafka.poll(0.1) + self.kafka.produce( + args.kafka_topic, + key=sid, + value=payload_str.encode(), + headers=hdrs, + callback=self._delivery_report + ) + self.metrics.sent_kafka += 1 + self.kafka.poll(0) + + sent_total += 1 + send_window.append(now) + while send_window and (now - send_window[0] > self.window_sec): + send_window.popleft() + + # Periodic status + if (now - last_status_t) >= self.status_every: + qps_inst = len(send_window) / self.window_sec + jitter = pstdev(self.metrics.inter_arrivals) if self.metrics.inter_arrivals else 0.0 + p95_k = percentile(self.metrics.kafka_latencies, 95) or 0.0 + p95_m = percentile(self.metrics.mqtt_latencies, 95) or 0.0 + print( + f"[status] t+{now - start:6.2f}s qps~{qps_inst:6.2f} jitter~{jitter:.6f}s " + f"kafka sent/ack/lost={self.metrics.sent_kafka}/{self.metrics.acked_kafka}/{self.metrics.lost_kafka} " + f"(p95={p95_k*1000:.1f}ms) " + f"mqtt sent/ack/lost={self.metrics.sent_mqtt}/{self.metrics.acked_mqtt}/{self.metrics.lost_mqtt} " + f"(p95={p95_m*1000:.1f}ms)" + ) + last_status_t = now + + # Metronome to target QPS + elapsed = now - start + target_elapsed = sent_total * interval + sleep_for = target_elapsed - elapsed + if sleep_for > 0: + time.sleep(sleep_for) + + # Drain & close + self.kafka.flush() + try: + self.mqtt.loop_stop() + finally: + self.mqtt.disconnect() + + # Summary + runtime = time.perf_counter() - start + qps_avg = sent_total / runtime if runtime > 0 else 0.0 + + p50_k = percentile(self.metrics.kafka_latencies, 50) + p95_k = percentile(self.metrics.kafka_latencies, 95) + p99_k = percentile(self.metrics.kafka_latencies, 99) + p50_m = percentile(self.metrics.mqtt_latencies, 50) + p95_m = percentile(self.metrics.mqtt_latencies, 95) + p99_m = percentile(self.metrics.mqtt_latencies, 99) + jitter = pstdev(self.metrics.inter_arrivals) if self.metrics.inter_arrivals else None + + print("[summary]") + print(f" total: sent={sent_total} runtime={runtime:.2f}s qps_avg≈{qps_avg:.2f}") + print(f" jitter (std of inter-arrival): {jitter:.6f}s") + print( + " kafka: " + f"sent={self.metrics.sent_kafka} acked={self.metrics.acked_kafka} lost={self.metrics.lost_kafka} " + f"p50/p95/p99={fmt_latency(p50_k)}/{fmt_latency(p95_k)}/{fmt_latency(p99_k)}" + ) + print( + " mqtt : " + f"sent={self.metrics.sent_mqtt} acked={self.metrics.acked_mqtt} lost={self.metrics.lost_mqtt} " + f"p50/p95/p99={fmt_latency(p50_m)}/{fmt_latency(p95_m)}/{fmt_latency(p99_m)}" + ) + + losses = self.metrics.loss_rates() + print(f" kafka loss: {losses['kafka_loss_pct']:.3f}%") + print(f" mqtt loss: {losses['mqtt_loss_pct']:.3f}%") diff --git a/AgCloud/mqtt_and_kafka/simulator/utils.py b/AgCloud/mqtt_and_kafka/simulator/utils.py new file mode 100644 index 000000000..d0ba505c1 --- /dev/null +++ b/AgCloud/mqtt_and_kafka/simulator/utils.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any, Dict, Iterator, List, Optional, Tuple +import pandas as pd + + +def positive_float(x: str) -> float: + """argparse type: positive float.""" + try: + value = float(x) + except ValueError: + raise argparse.ArgumentTypeError("must be a number") + if value <= 0: + raise argparse.ArgumentTypeError("must be > 0") + return value + + +def load_data(file_path: Path) -> pd.DataFrame: + """Load CSV or Parquet.""" + if file_path.is_dir(): + raise ValueError(f"Path points to a directory, not a file: {file_path}") + if not file_path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + name = file_path.name.lower() + if name.endswith((".parquet", ".parq", ".pq")): + try: + return pd.read_parquet(file_path) + except Exception as e: + raise RuntimeError("Failed to read parquet. Install 'pyarrow' or 'fastparquet'.") from e + + if name.endswith(".csv"): + return pd.read_csv(file_path) + + raise ValueError(f"Unsupported file type: {file_path.name} (expected .csv or .parquet)") + + +def iterate_records(df: pd.DataFrame) -> Iterator[Dict[str, Any]]: + """Yield the dataframe rows as dicts.""" + cols = list(df.columns) + for row in df.itertuples(index=False, name=None): + yield dict(zip(cols, row)) + + +def percentile(vals: List[float], p: float) -> Optional[float]: + """Small helper for p-th percentile (0..100).""" + if not vals: + return None + vals_sorted = sorted(vals) + k = (len(vals_sorted) - 1) * (p / 100.0) + f = int(k) + c = min(f + 1, len(vals_sorted) - 1) + if f == c: + return vals_sorted[f] + d0 = vals_sorted[f] * (c - k) + d1 = vals_sorted[c] * (k - f) + return d0 + d1 + + +def fmt_latency(x: Optional[float]) -> str: + """Format seconds -> 'X.Yms' or 'n/a'.""" + return f"{x*1000:.1f}ms" if x is not None else "n/a" + + +def extract_sid_from_headers(headers: Optional[List[Tuple[str, bytes]]]) -> Optional[str]: + """Extract 'sid' header from Kafka message headers.""" + if not headers: + return None + for k, v in headers: + if k == "sid": + try: + return v.decode() if isinstance(v, (bytes, bytearray)) else str(v) + except Exception: + return str(v) + return None diff --git a/AgCloud/prometheus/postgres-alerts.yml b/AgCloud/prometheus/postgres-alerts.yml new file mode 100644 index 000000000..adf9fa86a --- /dev/null +++ b/AgCloud/prometheus/postgres-alerts.yml @@ -0,0 +1,63 @@ +groups: + - name: postgres_alerts + interval: 30s + + rules: + # 1) Very high WAL rate (abnormal traffic) + - alert: HighWalThroughput + expr: pg:wal_bytes:rate_per_s > 5e6 # > ~5MB/s for 5 minutes + for: 5m + labels: + severity: warning + annotations: + summary: "High WAL throughput" + description: "WAL rate {{ $value | humanize }} B/s above threshold for 5 minutes" + + # 2) Almost no WAL (risk: system 'frozen' or no traffic) + - alert: LowWalThroughput + expr: pg:wal_bytes:rate_per_s < 1000 # < 1KB/s + for: 10m + labels: + severity: info + annotations: + summary: "Very low WAL throughput" + description: "Almost no WAL writes for 10 minutes" + + # 3) Replication delay (if replicas exist) + - alert: ReplicationLagSecondsHigh + expr: pg:replication_lag_seconds > 120 + for: 5m + labels: + severity: critical + annotations: + summary: "Replication lag high (seconds)" + description: "Replication lag higher than 120 seconds for 5 minutes" + + - alert: ReplicationLagBytesHigh + expr: pg:replication_lag_bytes > 50e6 # > ~50MB + for: 5m + labels: + severity: warning + annotations: + summary: "Replication lag high (bytes)" + description: "Replication lag greater than 50MB for 5 minutes" + + # 4) Low BRIN efficiency + - alert: BrinHitRatioLow + expr: pg:brin_hit_ratio < 0.6 + for: 10m + labels: + severity: warning + annotations: + summary: "BRIN hit ratio low" + description: "BRIN hit ratio dropped below 60% for 10 minutes" + + # 5) Exporter down + - alert: PostgresExporterDown + expr: up{job="postgres_exporter"} == 0 + for: 2m + labels: + severity: critical + annotations: + summary: "postgres_exporter down" + description: "Exporter not available for 2 minutes" diff --git a/AgCloud/prometheus/prometheus-recording.rules.yml b/AgCloud/prometheus/prometheus-recording.rules.yml new file mode 100644 index 000000000..dab962a23 --- /dev/null +++ b/AgCloud/prometheus/prometheus-recording.rules.yml @@ -0,0 +1,25 @@ +# Save as prometheus-recording.rules.yml +groups: +- name: postgres_recording + interval: 30s + rules: + # WAL throughput (bytes per second) + - record: pg:wal_bytes:rate_per_s + expr: rate(pg_wal_stats_wal_bytes[5m]) + + # Replication lag in seconds (standbys) + - record: pg:replication_lag_seconds + expr: max(replay_lag_seconds) + + # Replication lag in bytes (primary perspective per replica) + - record: pg:replication_lag_bytes + expr: max(bytes_lag) by (application_name) + + # BRIN hit ratio over time by index (avoid divide-by-zero) + - record: pg:brin_hit_ratio + expr: | + sum by (index_name, schemaname, table_name)(rate(idx_blks_hit[10m])) + / clamp_min( + sum by (index_name, schemaname, table_name)(rate(idx_blks_hit[10m]) + rate(idx_blks_read[10m])), + 1 + ) diff --git a/AgCloud/prometheus/prometheus.yml b/AgCloud/prometheus/prometheus.yml new file mode 100644 index 000000000..350fd4f8f --- /dev/null +++ b/AgCloud/prometheus/prometheus.yml @@ -0,0 +1,52 @@ +# prometheus.yml +global: + scrape_interval: 15s + +rule_files: + - /etc/prometheus/prometheus-recording.rules.yml + - /etc/prometheus/postgres-alerts.yml + +alerting: + alertmanagers: + - static_configs: + - targets: ['alertmanager:9093'] + +scrape_configs: + # Sensors metrics (GUI) + - job_name: 'sensors-demo' + static_configs: + - targets: ['sensors_metrics:8000'] + + # Postgres exporter (RelDB) + - job_name: 'postgres_exporter' + static_configs: + - targets: ['postgres_exporter:9187'] + + - job_name: minio-hot + metrics_path: /minio/v2/metrics/cluster + static_configs: + - targets: ['minio-hot:9000'] + + - job_name: minio-cold + metrics_path: /minio/v2/metrics/cluster + static_configs: + - targets: ['minio-cold:9000'] + + - job_name: sound_metrics + static_configs: + - targets: ['sound_metrics:8005'] + + - job_name: 'pushgateway' + honor_labels: true + static_configs: + - targets: ['pushgateway:9091'] + + - job_name: 'security-models' + static_configs: + - targets: + - 'mega-detector:8007' + - 'animal-classifier:8008' + - 'anomalies-classifier:8011' + - 'mask-classifier:8012' + + diff --git a/AgCloud/pytest.ini b/AgCloud/pytest.ini new file mode 100644 index 000000000..4e7ce1af4 --- /dev/null +++ b/AgCloud/pytest.ini @@ -0,0 +1,17 @@ +; # AgCloud/pytest.ini +; [pytest] +; testpaths = services/rover_ingest/tests +; pythonpath = . +; addopts = -q + + +[pytest] +minversion = 8.0 +addopts = -ra +testpaths = + services/tests + services/mqtt_gateway/tests +pythonpath = + . +norecursedirs = + storage_with_mqtt diff --git a/AgCloud/results/benchmarks.csv b/AgCloud/results/benchmarks.csv new file mode 100644 index 000000000..4f54ff285 --- /dev/null +++ b/AgCloud/results/benchmarks.csv @@ -0,0 +1,3 @@ +file,codec,orig_bytes,encoded_bytes,compression_ratio_orig_over_encoded,encode_time_sec,encode_cpu_avg_percent,timestamp,age_days +robot-03_20251028t120000z.mp3,flac,6747480,33505376,0.201,0.71,152.2,2025-10-28T12:00:00,5.0 +robot-03_20251028t120000z.mp3,opus,6747480,2452450,2.751,3.593,143.9,2025-10-28T12:00:00,5.0 diff --git a/AgCloud/script_to_minio/data_upload_simulator.py b/AgCloud/script_to_minio/data_upload_simulator.py new file mode 100644 index 000000000..be2ad9a4a --- /dev/null +++ b/AgCloud/script_to_minio/data_upload_simulator.py @@ -0,0 +1,144 @@ +import os +import time +from minio import Minio +from minio.error import S3Error +import paho.mqtt.client as mqtt + +# --- MinIO connection details --- +MINIO_ENDPOINT = "localhost:9001" # MinIO server address (change if remote) +ACCESS_KEY = "minioadmin" +SECRET_KEY = "minioadmin123" +BUCKET_NAME = "audio-files" # The MinIO bucket where we will upload files + +# --- Connecting to MinIO --- +minio_client = Minio( + MINIO_ENDPOINT, + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + secure=False # Set to True if using TLS/SSL +) + +# --- MQTT connection details --- +MQTT_BROKER = "localhost" # Address of the MQTT broker (Mosquitto) +MQTT_PORT = 1883 # Default MQTT port + +# --- MQTT Topics --- +MQTT_AUDIO_TOPIC = "audio-files/audio/uploaded" +MQTT_JSON_TOPIC = "audio-files/json/uploaded" + +# --- Ensure the bucket exists, if not, create it --- +if not minio_client.bucket_exists(BUCKET_NAME): + minio_client.make_bucket(BUCKET_NAME) + +# --- Local directories --- +LOCAL_AUDIO_DIR = "./mqtt_images/data/real_images/audio" # Folder where the audio files are stored +LOCAL_JSON_DIR = "./mqtt_images/data/real_images/json" # Folder where the JSON files are stored + +# --- Valid file formats --- +valid_audio_formats = [".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"] +valid_json_formats = [".json"] + +# --- Function to check if the file has a valid format --- +def is_valid_audio_format(file_name): + return any(file_name.endswith(ext) for ext in valid_audio_formats) + +def is_valid_json_format(file_name): + return any(file_name.endswith(ext) for ext in valid_json_formats) + +# --- MQTT Client Setup --- +mqtt_client = mqtt.Client() + +# --- Connect to MQTT Broker --- +mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60) + +# --- MQTT Callbacks --- +def on_connect(client, userdata, flags, rc): + print(f"Connected with result code {rc}") + +mqtt_client.on_connect = on_connect +mqtt_client.loop_start() # Start MQTT loop to keep connection active + +# --- Function to send MQTT message --- +def send_mqtt_message(file_name, file_type="audio"): + if file_type == "audio": + topic = MQTT_AUDIO_TOPIC + elif file_type == "json": + topic = MQTT_JSON_TOPIC + else: + raise ValueError("Invalid file type specified.") + + message = f"New {file_type} file uploaded: {file_name}" + result = mqtt_client.publish(topic, message) + if result.rc == mqtt.MQTT_ERR_SUCCESS: + print(f"Sent MQTT message: {message}") + else: + print(f"Failed to send message: {message}") + +# --- Function to upload a file --- +def upload_file(file_path, file_type="audio"): + file = os.path.basename(file_path) + object_name = f"{file_type}/{int(time.time())}_{file}" + + # Define content type based on file type + if file_type == "audio": + content_type = "audio/wav" if file.endswith(".wav") else "audio/mpeg" + elif file_type == "json": + content_type = "application/json" + else: + raise ValueError("Invalid file type specified.") + + try: + # Upload the file to MinIO + minio_client.fput_object(BUCKET_NAME, object_name, file_path, content_type=content_type) + print(f"File uploaded: {object_name}") + + # Notify via MQTT + send_mqtt_message(object_name, file_type) + + # After uploading, delete the file from the local folder + os.remove(file_path) + print(f"File {file} removed from the local folder.") + return True + except S3Error as e: + print(f"Error uploading {file}: {e}") + return False + +# --- Simulation loop --- +def run_simulation(): + empty_checks = 0 # Tracks the number of consecutive failed checks + + while True: + # Get all valid files from the audio and json directories + audio_files = [f for f in os.listdir(LOCAL_AUDIO_DIR) if is_valid_audio_format(f)] + json_files = [f for f in os.listdir(LOCAL_JSON_DIR) if is_valid_json_format(f)] + + # Upload audio files if there are any + if audio_files: + empty_checks = 0 # Reset empty counter + print(f"Found {len(audio_files)} audio file(s). Uploading...") + for file in audio_files: + file_path = os.path.join(LOCAL_AUDIO_DIR, file) + upload_file(file_path, file_type="audio") # Upload the audio file + + # Upload JSON files if there are any + if json_files: + empty_checks = 0 # Reset empty counter + print(f"Found {len(json_files)} JSON file(s). Uploading...") + for file in json_files: + file_path = os.path.join(LOCAL_JSON_DIR, file) + upload_file(file_path, file_type="json") # Upload the json file + + # Sleep after checking both directories + if audio_files or json_files: + print("All files uploaded. Sleeping for 5 minutes...") + time.sleep(300) # Wait 5 minutes before checking again + else: + empty_checks += 1 + print(f"No valid files found (check {empty_checks}/6). Sleeping 10 minutes...") + if empty_checks >= 2: + print("No valid files added for 1 hour. Stopping the script.") + break + time.sleep(600) # Wait 10 minutes before checking again + +if __name__ == "__main__": + run_simulation() diff --git a/AgCloud/services/API-notifications/README.md b/AgCloud/services/API-notifications/README.md new file mode 100644 index 000000000..696a71408 --- /dev/null +++ b/AgCloud/services/API-notifications/README.md @@ -0,0 +1,86 @@ +# Task Scheduling API + +This API allows you to manage task schedules for clients. You can create, update, and delete schedules. +Base URL +`http://127.0.0.1:5000` + +Endpoints + +## 1. Create a Schedule (POST) + +URL: /schedule +Method: POST +Request Body: + +` +{ + "client_id": 1, + "team": "Development", + "cron_expr": "0 3 * * 1", + "active_days": "Monday, Friday", + "time_window": "08:00-17:00" +} +` + +Response: +{"message": "Schedule added successfully", + "schedule_id": 5} + +## 2. Update a Schedule (PUT) + +URL: /schedule/ +Method: PUT +Request Body: + +` +{ + "client_id": 1, + "team": "Development", + "cron_expr": "0 3 * * 1", + "active_days": "Sunday, Friday", + "time_window": "08:00-18:00" +} +` + +Response: +{"message": "Schedule updated successfully"} + +## 3. Delete a Schedule (DELETE) + +URL: /schedule/ + +Method: DELETE + +Response: +{"message": "Schedule deleted successfully"} + +Example Usage with curl + +Create a Schedule: + +```powershell +curl -Uri http://127.0.0.1:5000/schedule -Method POST -Headers @{"Content-Type"="application/json"} -Body '{"client_id": 1, "team": "Development", "cron_expr": "0 3 * * 1", "active_days": "Monday, Friday", "time_window": "08:00-17:00"}' +``` + +Update a Schedule: + +```powershell +curl -Uri http://127.0.0.1:5000/schedule/5 -Method PUT -Headers @{"Content-Type"="application/json"} -Body '{"client_id": 1, "team": "Development", "cron_expr": "0 3 * * 1", "active_days": "Sunday, Friday", "time_window": "08:00-18:00"}' +``` + +Delete a Schedule: + +```powershell +curl -Uri http://127.0.0.1:5000/schedule/5 -Method DELETE -Headers @{"Content-Type"="application/json"} +``` + +Errors + +If an error occurs, the response will contain an "error" field with a description. Example: +{"error": "Failed to add schedule"} + +Notes + +- Ensure the server is running (python app.py). +- The API is available at `http://127.0.0.1:5000`. +- You can use any HTTP client (curl, Postman, etc.) to interact with the API. diff --git a/AgCloud/services/API-notifications/jest.config.js b/AgCloud/services/API-notifications/jest.config.js new file mode 100644 index 000000000..6f5045ce6 --- /dev/null +++ b/AgCloud/services/API-notifications/jest.config.js @@ -0,0 +1,51 @@ +// jest.config.js +// Configuration file for Jest + +module.exports = { + // Set the test environment to jsdom (browser simulation) + testEnvironment: 'jsdom', + + // Where to find the test files + testMatch: [ + '**/tests/**/*.test.js', + '**/__tests__/**/*.js', + '**/?(*.)+(spec|test).js' + ], + + // Code coverage - only collect from src directory + collectCoverageFrom: [ + 'src/**/*.js', + '!**/node_modules/**', + '!**/tests/**', + '!**/htmlcov/**', + '!jest.config.js' + ], + + // Ignore certain files and directories + testPathIgnorePatterns: [ + '/node_modules/', + '/htmlcov/' + ], + + // Coverage thresholds + coverageThreshold: { + global: { + branches: 80, + functions: 80, + lines: 80, + statements: 80 + } + }, + + // Additional settings + verbose: true, + + // Clear mocks between tests + clearMocks: true, + + // Coverage directory + coverageDirectory: 'coverage', + + // Coverage reporters + coverageReporters: ['text', 'lcov', 'html'] +}; \ No newline at end of file diff --git a/AgCloud/services/API-notifications/package-lock.json b/AgCloud/services/API-notifications/package-lock.json new file mode 100644 index 000000000..6af59f0e4 --- /dev/null +++ b/AgCloud/services/API-notifications/package-lock.json @@ -0,0 +1,5082 @@ +{ + "name": "API-development", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.18.tgz", + "integrity": "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", + "integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.25", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.25.tgz", + "integrity": "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/AgCloud/services/API-notifications/package.json b/AgCloud/services/API-notifications/package.json new file mode 100644 index 000000000..9fb5034be --- /dev/null +++ b/AgCloud/services/API-notifications/package.json @@ -0,0 +1,7 @@ +{ + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0" + } +} diff --git a/AgCloud/services/API-notifications/pytest.ini b/AgCloud/services/API-notifications/pytest.ini new file mode 100644 index 000000000..fcccae197 --- /dev/null +++ b/AgCloud/services/API-notifications/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = src diff --git a/AgCloud/services/API-notifications/src/Dockerfile b/AgCloud/services/API-notifications/src/Dockerfile new file mode 100644 index 000000000..c17f4ae4c --- /dev/null +++ b/AgCloud/services/API-notifications/src/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.9-slim + +WORKDIR /app + +COPY requirements.txt . + +COPY certs /app/certs + +RUN apt-get update && \ + apt-get install -y ca-certificates && \ + cp /app/certs/*.crt /usr/local/share/ca-certificates/ && \ + update-ca-certificates && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["python", "backend/app.py"] \ No newline at end of file diff --git a/AgCloud/services/API-notifications/src/__init__.py b/AgCloud/services/API-notifications/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/API-notifications/src/backend/__init__.py b/AgCloud/services/API-notifications/src/backend/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/API-notifications/src/backend/app.py b/AgCloud/services/API-notifications/src/backend/app.py new file mode 100644 index 000000000..f4ef4c959 --- /dev/null +++ b/AgCloud/services/API-notifications/src/backend/app.py @@ -0,0 +1,203 @@ +from flask import Flask, request, jsonify, send_from_directory +from flask_cors import CORS +import psycopg2 +import datetime +from psycopg2 import sql +import os + +app = Flask(__name__, + static_folder='../window-client', + static_url_path='') +CORS(app, resources={r"/*": {"origins": "*"}}) + +@app.route('/') +def index(): + return send_from_directory('../window-client', 'notification-popup.html') + +@app.route('/script.js') +def script(): + return send_from_directory('../window-client', 'script.js') + +@app.route('/styles.css') +def styles(): + return send_from_directory('../window-client', 'styles.css') + +# define the connect to the DB +def get_db_connection(): + conn = psycopg2.connect( + dbname="missions_db", + user="missions_user", + password="pg123", + host="postgres", + port="5432" + ) + return conn + +@app.route('/api/schedules', methods=['GET']) +def get_schedules(): + client_id = request.args.get('client_id') + + conn = get_db_connection() + cur = conn.cursor() + + try: + if client_id: + cur.execute(""" + SELECT schedule_id, client_id, team, cron_expr, active_days, time_window, last_updated + FROM clients + WHERE client_id = %s + ORDER BY last_updated DESC + """, (client_id,)) + else: + cur.execute(""" + SELECT schedule_id, client_id, team, cron_expr, active_days, time_window, last_updated + FROM clients + ORDER BY last_updated DESC + """) + + schedules = cur.fetchall() + + schedule_list = [] + for schedule in schedules: + schedule_list.append({ + 'schedule_id': schedule[0], + 'client_id': schedule[1], + 'team': schedule[2], + 'cron_expr': schedule[3], + 'active_days': schedule[4], + 'time_window': schedule[5], + 'last_updated': schedule[6].isoformat() if schedule[6] else None + }) + + return jsonify(schedule_list), 200 + + except Exception as e: + return jsonify({"error": str(e)}), 400 + finally: + cur.close() + conn.close() + +@app.route('/api/schedule/', methods=['GET']) +def get_schedule(schedule_id): + conn = get_db_connection() + cur = conn.cursor() + + try: + cur.execute(""" + SELECT schedule_id, client_id, team, cron_expr, active_days, time_window, last_updated + FROM clients + WHERE schedule_id = %s + """, (schedule_id,)) + + schedule = cur.fetchone() + + if not schedule: + return jsonify({"error": "Schedule not found"}), 404 + + schedule_data = { + 'schedule_id': schedule[0], + 'client_id': schedule[1], + 'team': schedule[2], + 'cron_expr': schedule[3], + 'active_days': schedule[4], + 'time_window': schedule[5], + 'last_updated': schedule[6].isoformat() if schedule[6] else None + } + + return jsonify(schedule_data), 200 + + except Exception as e: + return jsonify({"error": str(e)}), 400 + finally: + cur.close() + conn.close() + +@app.route('/api/schedule', methods=['POST']) +def add_schedule(): + data = request.json + client_id = data.get('client_id') + team = data.get('team') + cron_expr = data.get('cron_expr') + active_days = data.get('active_days') + time_window = data.get('time_window') + + conn = get_db_connection() + cur = conn.cursor() + + query = sql.SQL(""" + INSERT INTO clients (client_id, team, cron_expr, active_days, time_window, last_updated) + VALUES (%s, %s, %s, %s, %s, now()) + RETURNING schedule_id; + """) + + try: + cur.execute(query, (client_id, team, cron_expr, active_days, time_window)) + result = cur.fetchone() + + if not result: + return jsonify({"error": "Failed to add schedule"}), 400 + + schedule_id = result[0] + conn.commit() + + return jsonify({"message": "Schedule added successfully", "schedule_id": schedule_id}), 201 + + except Exception as e: + conn.rollback() + return jsonify({"error": str(e)}), 400 + finally: + cur.close() + conn.close() + +@app.route('/api/schedule/', methods=['PUT']) +def update_schedule(schedule_id): + data = request.get_json() + + conn = get_db_connection() + cur = conn.cursor() + + try: + cur.execute(""" + UPDATE clients + SET client_id = %s, team = %s, cron_expr = %s, active_days = %s, time_window = %s, last_updated = %s + WHERE schedule_id = %s + """, ( + data['client_id'], + data['team'], + data['cron_expr'], + data['active_days'], + data['time_window'], + datetime.datetime.now(), + schedule_id + )) + conn.commit() + + return jsonify({"message": "Schedule updated successfully"}), 200 + + except Exception as e: + conn.rollback() + return jsonify({"error": str(e)}), 400 + finally: + cur.close() + conn.close() + +@app.route('/api/schedule/', methods=['DELETE']) +def delete_schedule(schedule_id): + conn = get_db_connection() + cur = conn.cursor() + + try: + cur.execute("DELETE FROM clients WHERE schedule_id = %s", (schedule_id,)) + conn.commit() + + return jsonify({"message": "Schedule deleted successfully"}), 200 + + except Exception as e: + return jsonify({"error": str(e)}), 400 + finally: + cur.close() + conn.close() + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/AgCloud/services/API-notifications/src/backend/requirements.txt b/AgCloud/services/API-notifications/src/backend/requirements.txt new file mode 100644 index 000000000..e579155be --- /dev/null +++ b/AgCloud/services/API-notifications/src/backend/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.0 +flask-cors==4.0.0 +psycopg2-binary==2.9.6 \ No newline at end of file diff --git a/AgCloud/services/API-notifications/src/requirements.txt b/AgCloud/services/API-notifications/src/requirements.txt new file mode 100644 index 000000000..e579155be --- /dev/null +++ b/AgCloud/services/API-notifications/src/requirements.txt @@ -0,0 +1,3 @@ +Flask==2.3.0 +flask-cors==4.0.0 +psycopg2-binary==2.9.6 \ No newline at end of file diff --git a/AgCloud/services/API-notifications/src/window-client/README.md b/AgCloud/services/API-notifications/src/window-client/README.md new file mode 100644 index 000000000..91e52c5dd --- /dev/null +++ b/AgCloud/services/API-notifications/src/window-client/README.md @@ -0,0 +1,51 @@ +# AgCloud Desktop Application + +This project contains a desktop application and a notification API for an AgCloud system, utilizing Docker for containerization, a PostgreSQL database, and various services like noVNC and PyQt. + +## Overview + +The desktop application is built using PyQt6 and includes a GUI that interacts with a set of services running in Docker containers. The application also displays a web page (HTML) in the "Sound" tab, which is rendered in a web view. + +## Requirements + +Docker (for running containers) + +Python 3.8+ (for running the application locally) + +Docker Compose (for orchestrating multiple services) + +Setup + +- 1. Docker Compose Setup + +To build and run the application in Docker: + +Clone this repository. + +Navigate to the directory containing docker-compose.yml. + +Run the following command to start the application: + +docker-compose up --build + +- 2. Running Locally + +You can also run the app.py service directly from your computer (instead of inside the container). To do this: + +Ensure that all dependencies are installed: + +pip install -r requirements.txt + +## Run the application + +python app.py + +Features + +GUI: Displays an interactive dashboard with embedded web views (Grafana, etc.). + +Notification API: A Flask-based API interacting with a PostgreSQL database for storing schedules. + +Sound Tab: Displays an HTML page in the desktop application. + +noVNC: Web-based VNC client to interact with the desktop application. diff --git a/AgCloud/services/API-notifications/src/window-client/notification-popup.html b/AgCloud/services/API-notifications/src/window-client/notification-popup.html new file mode 100644 index 000000000..6d21ea502 --- /dev/null +++ b/AgCloud/services/API-notifications/src/window-client/notification-popup.html @@ -0,0 +1,115 @@ + + + + + + VAST - Notification Manager + + + + + + + + diff --git a/AgCloud/services/API-notifications/src/window-client/package.json b/AgCloud/services/API-notifications/src/window-client/package.json new file mode 100644 index 000000000..9d7e195a9 --- /dev/null +++ b/AgCloud/services/API-notifications/src/window-client/package.json @@ -0,0 +1,74 @@ +{ + "name": "my-v0-project", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "1.2.2", + "@radix-ui/react-alert-dialog": "1.1.4", + "@radix-ui/react-aspect-ratio": "1.1.1", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-checkbox": "1.1.3", + "@radix-ui/react-collapsible": "1.1.2", + "@radix-ui/react-context-menu": "2.2.4", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-hover-card": "1.1.4", + "@radix-ui/react-label": "2.1.1", + "@radix-ui/react-menubar": "1.1.4", + "@radix-ui/react-navigation-menu": "1.2.3", + "@radix-ui/react-popover": "1.1.4", + "@radix-ui/react-progress": "1.1.1", + "@radix-ui/react-radio-group": "1.2.2", + "@radix-ui/react-scroll-area": "1.2.2", + "@radix-ui/react-select": "2.1.4", + "@radix-ui/react-separator": "1.1.1", + "@radix-ui/react-slider": "1.2.2", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-switch": "1.1.2", + "@radix-ui/react-tabs": "1.1.2", + "@radix-ui/react-toast": "1.2.4", + "@radix-ui/react-toggle": "1.1.1", + "@radix-ui/react-toggle-group": "1.1.1", + "@radix-ui/react-tooltip": "1.1.6", + "@vercel/analytics": "1.3.1", + "autoprefixer": "^10.4.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "1.0.4", + "date-fns": "4.1.0", + "embla-carousel-react": "8.5.1", + "geist": "^1.3.1", + "input-otp": "1.4.1", + "lucide-react": "^0.454.0", + "next": "14.2.25", + "next-themes": "^0.4.6", + "react": "^19", + "react-day-picker": "9.8.0", + "react-dom": "^19", + "react-hook-form": "^7.60.0", + "react-resizable-panels": "^2.1.7", + "recharts": "2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^3.3.1", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "3.25.67" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.1.9", + "@types/node": "^22", + "@types/react": "^18", + "@types/react-dom": "^18", + "postcss": "^8.5", + "tailwindcss": "^4.1.9", + "tw-animate-css": "1.3.3", + "typescript": "^5" + } +} diff --git a/AgCloud/services/API-notifications/src/window-client/script.js b/AgCloud/services/API-notifications/src/window-client/script.js new file mode 100644 index 000000000..66fc45b59 --- /dev/null +++ b/AgCloud/services/API-notifications/src/window-client/script.js @@ -0,0 +1,465 @@ +class NotificationManager { + constructor() { + this.apiUrl = "/api" + this.currentEditId = null + this.currentClientId = null + this.initializeElements() + this.attachEventListeners() + } + + initializeElements() { + this.popup = document.getElementById("popupOverlay") + this.closeBtn = document.getElementById("closeBtn") + this.cancelBtn = document.getElementById("cancelBtn") + this.saveBtn = document.getElementById("saveBtn") + this.statusMessage = document.getElementById("statusMessage") + this.formTitle = document.getElementById("formTitle") + + this.teamSelect = document.getElementById("team") + this.notificationTimeInput = document.getElementById("notificationTime") + this.startTimeInput = document.getElementById("startTime") + this.endTimeInput = document.getElementById("endTime") + this.checkboxes = document.querySelectorAll('.checkbox-group input[type="checkbox"]') + + this.tabBtns = document.querySelectorAll(".tab-btn") + this.tabContents = document.querySelectorAll(".tab-content") + + this.userClientIdInput = document.getElementById("userClientId") + this.continueBtn = document.getElementById("continueBtn") + this.changeClientBtn = document.getElementById("changeClientBtn") + this.currentClientIdSpan = document.getElementById("currentClientId") + + this.clientIdScreen = document.getElementById("clientIdScreen") + this.mainScreen = document.getElementById("mainScreen") + + this.refreshBtn = document.getElementById("refreshBtn") + this.notificationsList = document.getElementById("notificationsList") + } + + attachEventListeners() { + if (this.closeBtn) this.closeBtn.addEventListener("click", () => this.closePopup()) + if (this.cancelBtn) this.cancelBtn.addEventListener("click", () => this.cancelForm()) + if (this.saveBtn) this.saveBtn.addEventListener("click", () => this.saveNotification()) + + if (this.continueBtn) this.continueBtn.addEventListener("click", () => this.setClientId()) + if (this.changeClientBtn) this.changeClientBtn.addEventListener("click", () => this.showClientIdScreen()) + + if (this.refreshBtn) this.refreshBtn.addEventListener("click", () => this.loadNotifications()) + + this.tabBtns.forEach((btn) => { + btn.addEventListener("click", () => this.switchTab(btn.dataset.tab)) + }) + + if (this.popup) { + this.popup.addEventListener("click", (e) => { + if (e.target === this.popup) { + this.closePopup() + } + }) + } + + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + this.closePopup() + } + }) + + if (this.userClientIdInput) { + this.userClientIdInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + this.setClientId() + } + }) + } + } + + setClientId() { + const clientId = this.userClientIdInput.value.trim() + if (!clientId) { + this.showStatus("Please enter a valid Client ID", "error") + return + } + + this.currentClientId = Number.parseInt(clientId) + this.currentClientIdSpan.textContent = this.currentClientId + this.showMainScreen() + this.loadNotifications() + } + + showClientIdScreen() { + this.clientIdScreen.classList.add("active") + this.mainScreen.classList.remove("active") + this.userClientIdInput.focus() + } + + showMainScreen() { + this.clientIdScreen.classList.remove("active") + this.mainScreen.classList.add("active") + } + + switchTab(tabName) { + this.tabBtns.forEach((btn) => btn.classList.remove("active")) + this.tabContents.forEach((content) => content.classList.remove("active")) + + document.querySelector(`[data-tab="${tabName}"]`).classList.add("active") + document.getElementById(`${tabName}Tab`).classList.add("active") + + if (tabName === "add") { + if (!this.currentEditId) { + this.resetForm() + } + } else if (tabName === "list") { + // Reset form when switching to list view + this.resetForm() + this.loadNotifications() + } + } + + async loadNotifications() { + if (!this.currentClientId) { + console.log("No current client ID") + return + } + + console.log(`[v0] Loading notifications for client ${this.currentClientId}`) + this.notificationsList.innerHTML = '
Loading notifications...
' + + try { + const response = await fetch(`${this.apiUrl}/schedules?client_id=${this.currentClientId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + + console.log(`[v0] Response status: ${response.status}`) + + if (response.ok) { + const schedules = await response.json() + console.log("[v0] Loaded data:", schedules) + this.displayNotifications(schedules) + } else { + const error = await response.json() + console.log("[v0] Error response:", error) + this.notificationsList.innerHTML = `
Error loading data: ${error.error || "Unknown error"}
` + } + } catch (error) { + console.error("[v0] Network error:", error) + this.notificationsList.innerHTML = '
Error connecting to server. Check that server is running on port 5000
' + } + } + + displayNotifications(schedules) { + if (!schedules || schedules.length === 0) { + this.notificationsList.innerHTML = '
No notifications for this client
' + return + } + + const notificationsHtml = schedules.map(schedule => ` +
+
+
${schedule.team} Team
+
+ + +
+
+
+
Schedule ID: ${schedule.schedule_id}
+
Notification Time: ${this.formatCronTime(schedule.cron_expr)}
+
Active Days: ${schedule.active_days}
+
Time Window: ${schedule.time_window}
+
Last Updated: ${this.formatDate(schedule.last_updated)}
+
+
+ `).join('') + + this.notificationsList.innerHTML = notificationsHtml + + // Attach event listeners to the newly created buttons + this.attachNotificationListeners() + } + + attachNotificationListeners() { + // Edit buttons + const editButtons = this.notificationsList.querySelectorAll('.btn-edit') + editButtons.forEach(btn => { + btn.addEventListener('click', (e) => { + const scheduleId = e.target.getAttribute('data-schedule-id') + this.editNotification(scheduleId) + }) + }) + + // Delete buttons + const deleteButtons = this.notificationsList.querySelectorAll('.btn-delete') + deleteButtons.forEach(btn => { + btn.addEventListener('click', (e) => { + const scheduleId = e.target.getAttribute('data-schedule-id') + this.deleteNotification(scheduleId) + }) + }) + } + + formatCronTime(cronExpr) { + const parts = cronExpr.split(' ') + if (parts.length >= 2) { + const hour = parts[1].padStart(2, '0') + const minute = parts[0].padStart(2, '0') + return `${hour}:${minute}` + } + return cronExpr + } + + formatDate(dateStr) { + if (!dateStr) return 'Unknown' + return new Date(dateStr).toLocaleString('en-US') + } + + async editNotification(scheduleId) { + console.log(`[v0] Editing notification ${scheduleId}`) + this.showStatus("Loading notification for editing...", "loading") + + try { + const response = await fetch(`${this.apiUrl}/schedule/${scheduleId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + + console.log("[v0] Response status:", response.status) + + if (response.ok) { + const schedule = await response.json() + console.log("[v0] Loaded schedule data:", schedule) + + if (schedule.client_id !== this.currentClientId) { + this.showStatus(`Schedule ID ${scheduleId} does not belong to client ${this.currentClientId}`, "error") + return + } + + this.currentEditId = Number.parseInt(scheduleId) + this.formTitle.textContent = `Edit Notification (Schedule ID: ${scheduleId})` + + // Change tab button text to "Edit Notification" + const addTabBtn = document.querySelector('button[data-tab="add"]'); + if (addTabBtn) addTabBtn.textContent = "Edit Notification"; + + this.teamSelect.value = schedule.team + + const cronParts = schedule.cron_expr.split(" ") + const hour = cronParts[1].padStart(2, "0") + const minute = cronParts[0].padStart(2, "0") + this.notificationTimeInput.value = `${hour}:${minute}` + + const timeWindow = schedule.time_window.split("-") + this.startTimeInput.value = timeWindow[0] + this.endTimeInput.value = timeWindow[1] + + const activeDays = schedule.active_days.split(", ") + this.checkboxes.forEach((checkbox) => { + checkbox.checked = activeDays.includes(checkbox.value) + }) + + this.switchTab("add") + this.showStatus("Notification loaded for editing", "success") + } else { + const result = await response.json() + console.log("[v0] Error response:", result) + this.showStatus(`Error: ${result.error || "Failed to load"}`, "error") + } + } catch (error) { + console.error("[v0] Error loading notification:", error) + this.showStatus("Error connecting to server", "error") + } + } + + async deleteNotification(scheduleId) { + if (!confirm(`Are you sure you want to delete notification with Schedule ID: ${scheduleId}?`)) { + return + } + + console.log(`[v0] Deleting notification ${scheduleId}`) + this.showStatus("Deleting notification...", "loading") + + try { + const response = await fetch(`${this.apiUrl}/schedule/${scheduleId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }) + + if (response.ok) { + this.showStatus("Notification deleted successfully", "success") + this.loadNotifications() + } else { + const result = await response.json() + this.showStatus(`Error: ${result.error || "Failed to delete"}`, "error") + } + } catch (error) { + console.error("Error deleting notification:", error) + this.showStatus("Error connecting to server", "error") + } + } + + resetForm() { + this.currentEditId = null + this.formTitle.textContent = "Add New Notification" + + // Reset the tab button text back to "Add New" + const addTabBtn = document.querySelector('button[data-tab="add"]') + if (addTabBtn) { + addTabBtn.textContent = "Add New" + } + + this.teamSelect.value = "" + this.notificationTimeInput.value = "09:00" + this.startTimeInput.value = "08:00" + this.endTimeInput.value = "17:00" + + this.checkboxes.forEach((checkbox) => { + checkbox.checked = checkbox.value === "Monday" || checkbox.value === "Friday" + }) + } + + cancelForm() { + this.resetForm() + this.switchTab("list") + } + + closePopup() { + this.popup.style.display = "none" + this.showClientIdScreen() + this.currentClientId = null + this.currentEditId = null + this.userClientIdInput.value = "" + if (typeof window !== 'undefined' && window.parent && window.parent.closeNotificationPopup) { + window.parent.closeNotificationPopup() + } + } + + validateForm() { + if (!this.teamSelect.value) { + this.showStatus("Please select a team", "error") + return false + } + + const selectedDays = this.getSelectedDays() + if (!selectedDays) { + this.showStatus("Please select at least one active day", "error") + return false + } + + return true + } + + async saveNotification() { + if (!this.validateForm()) { + return + } + + this.saveBtn.disabled = true + this.showStatus(this.currentEditId ? "Updating notification..." : "Saving notification...", "loading") + + const notificationData = { + client_id: this.currentClientId, + team: this.teamSelect.value, + cron_expr: this.generateCronExpression(), + active_days: this.getSelectedDays(), + time_window: this.getTimeWindow(), + } + + try { + const url = this.currentEditId ? `${this.apiUrl}/schedule/${this.currentEditId}` : `${this.apiUrl}/schedule` + const method = this.currentEditId ? "PUT" : "POST" + + const response = await fetch(url, { + method: method, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(notificationData), + }) + + if (response.ok) { + const result = await response.json() + const action = this.currentEditId ? "updated" : "saved" + this.showStatus(`Notification ${action} successfully!`, "success") + + if (!this.currentEditId && result.schedule_id) { + this.showStatus(`Notification saved successfully! Schedule ID: ${result.schedule_id}`, "success") + } + + this.resetForm() + this.loadNotifications() + this.switchTab("list") + } else { + let errorMessage = "Unknown error occurred" + try { + const result = await response.json() + errorMessage = result.error || result.message || errorMessage + } catch (e) { + errorMessage = `Server error: ${response.status} ${response.statusText}` + } + this.showStatus(`Error: ${errorMessage}`, "error") + } + } catch (error) { + console.error("Network error:", error) + this.showStatus("Network error. Please check if the server is running.", "error") + } finally { + this.saveBtn.disabled = false + } + } + + getSelectedDays() { + const selectedDays = [] + this.checkboxes.forEach((checkbox) => { + if (checkbox.checked) { + selectedDays.push(checkbox.value) + } + }) + return selectedDays.join(", ") + } + + generateCronExpression() { + const time = this.notificationTimeInput.value.split(":") + const hour = Number.parseInt(time[0]) + const minute = Number.parseInt(time[1]) + return `${minute} ${hour} * * *` + } + + getTimeWindow() { + return `${this.startTimeInput.value}-${this.endTimeInput.value}` + } + + showStatus(message, type) { + this.statusMessage.textContent = message + this.statusMessage.className = `status-message ${type}` + this.statusMessage.style.display = "block" + + if (type === "success") { + setTimeout(() => { + this.statusMessage.style.display = "none" + }, 3000) + } + } +} + +// Export for Node.js/Jest +if (typeof module !== 'undefined' && module.exports) { + module.exports = NotificationManager; +} + +// Browser initialization +if (typeof window !== 'undefined') { + let notificationManager; + + document.addEventListener("DOMContentLoaded", () => { + notificationManager = new NotificationManager(); + }); + + window.openNotificationPopup = () => { + document.getElementById("popupOverlay").style.display = "flex"; + }; +} diff --git a/AgCloud/services/API-notifications/src/window-client/styles.css b/AgCloud/services/API-notifications/src/window-client/styles.css new file mode 100644 index 000000000..7d83a16c3 --- /dev/null +++ b/AgCloud/services/API-notifications/src/window-client/styles.css @@ -0,0 +1,419 @@ +/* Complete redesign with white, light gray and green color scheme */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + background: rgba(0, 0, 0, 0.1); +} + +.popup-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.popup-container { + background: #ffffff; + border-radius: 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15); + width: 600px; + max-width: 95vw; + max-height: 90vh; + overflow: hidden; + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.popup-header { + background: #ffffff; + color: #2d3748; + padding: 20px 25px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 2px solid #f7fafc; +} + +.popup-header h2 { + font-size: 20px; + font-weight: 600; + color: #2d3748; +} + +.close-btn { + background: none; + border: none; + color: #a0aec0; + font-size: 24px; + cursor: pointer; + padding: 5px; + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s; +} + +.close-btn:hover { + background-color: #f7fafc; + color: #2d3748; +} + +.tabs { + display: flex; + background: #f7fafc; + border-bottom: 1px solid #e2e8f0; +} + +.tab-btn { + flex: 1; + padding: 15px 20px; + background: none; + border: none; + font-size: 14px; + font-weight: 500; + color: #718096; + cursor: pointer; + transition: all 0.2s; + border-bottom: 3px solid transparent; +} + +.tab-btn:hover { + background: #edf2f7; + color: #2d3748; +} + +.tab-btn.active { + background: #ffffff; + color: #38a169; + border-bottom-color: #38a169; +} + +.tab-content { + display: none; + min-height: 400px; +} + +.tab-content.active { + display: block; +} + +.list-header { + padding: 20px 25px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #e2e8f0; +} + +.list-header h3 { + font-size: 18px; + font-weight: 600; + color: #2d3748; +} + +.notifications-list { + max-height: 350px; + overflow-y: auto; +} + +.notification-item { + padding: 20px 25px; + border-bottom: 1px solid #f7fafc; + transition: background-color 0.2s; +} + +.notification-item:hover { + background: #f7fafc; +} + +.notification-item:last-child { + border-bottom: none; +} + +.notification-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.notification-title { + font-weight: 600; + color: #2d3748; + font-size: 16px; +} + +.notification-actions { + display: flex; + gap: 8px; +} + +.notification-details { + color: #718096; + font-size: 14px; + line-height: 1.5; +} + +.form-container { + padding: 25px; +} + +.form-container h3 { + font-size: 18px; + font-weight: 600; + color: #2d3748; + margin-bottom: 20px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #2d3748; + font-size: 14px; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 12px 16px; + border: 2px solid #e2e8f0; + border-radius: 8px; + font-size: 14px; + transition: all 0.2s; + background: #ffffff; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #38a169; + box-shadow: 0 0 0 3px rgba(56, 161, 105, 0.1); +} + +.checkbox-group { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-top: 8px; +} + +.checkbox-group label { + display: flex; + align-items: center; + font-weight: normal; + margin-bottom: 0; + cursor: pointer; + padding: 10px; + border-radius: 6px; + transition: background-color 0.2s; + border: 1px solid #e2e8f0; +} + +.checkbox-group label:hover { + background-color: #f7fafc; +} + +.checkbox-group input[type="checkbox"] { + width: auto; + margin-right: 8px; + margin-bottom: 0; +} + +.time-range { + display: flex; + align-items: center; + gap: 15px; +} + +.time-range input { + flex: 1; +} + +.time-range span { + color: #718096; + font-weight: 500; +} + +.form-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #e2e8f0; +} + +.btn { + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + min-width: 100px; +} + +.btn-cancel { + background: #f7fafc; + color: #718096; + border: 1px solid #e2e8f0; +} + +.btn-cancel:hover { + background: #edf2f7; + color: #2d3748; +} + +.btn-save { + background: #38a169; + color: white; +} + +.btn-save:hover { + background: #2f855a; +} + +.btn-refresh { + background: #f7fafc; + color: #718096; + border: 1px solid #e2e8f0; + padding: 8px 16px; + font-size: 12px; +} + +.btn-refresh:hover { + background: #edf2f7; + color: #2d3748; +} + +.btn-edit { + background: #38a169; + color: white; + padding: 6px 12px; + font-size: 12px; +} + +.btn-edit:hover { + background: #2f855a; +} + +.btn-delete { + background: #e53e3e; + color: white; + padding: 6px 12px; + font-size: 12px; +} + +.btn-delete:hover { + background: #c53030; +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.status-message { + padding: 15px 25px; + margin: 0; + font-size: 14px; + font-weight: 500; + text-align: center; + display: none; + position: relative; + z-index: 10; +} + +.status-message.success { + background: #f0fff4; + color: #22543d; + border-top: 1px solid #c6f6d5; +} + +.status-message.error { + background: #fed7d7; + color: #742a2a; + border-top: 1px solid #feb2b2; +} + +.status-message.loading { + background: #e6fffa; + color: #234e52; + border-top: 1px solid #b2f5ea; +} + +.loading-message, +.empty-message { + text-align: center; + padding: 40px 20px; + color: #718096; + font-style: italic; +} + +/* Added screen management and client info styles */ +.screen { + display: none; +} + +.screen.active { + display: block; +} + +.client-info { + padding: 15px 25px; + background: #f7fafc; + border-bottom: 1px solid #e2e8f0; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: #2d3748; +} + +.btn-change { + background: #e2e8f0; + color: #718096; + padding: 6px 12px; + font-size: 12px; +} + +.btn-change:hover { + background: #cbd5e0; + color: #2d3748; +} + +/* Made content scrollable to fix scroll issue */ +.scrollable-content { + max-height: 350px; + overflow-y: auto; +} diff --git a/AgCloud/services/API-notifications/tests/__init__.py b/AgCloud/services/API-notifications/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/API-notifications/tests/notification-manager.test.js b/AgCloud/services/API-notifications/tests/notification-manager.test.js new file mode 100644 index 000000000..f7fb8a897 --- /dev/null +++ b/AgCloud/services/API-notifications/tests/notification-manager.test.js @@ -0,0 +1,1120 @@ +/** + * @jest-environment jsdom + */ + +const NotificationManager = require('../src/window-client/script.js'); + +describe('NotificationManager', () => { + let notificationManager; + let mockFetch; + + beforeEach(() => { + // Create a basic HTML structure with select options + document.body.innerHTML = ` +
+ + + +
+

+ + + + +
+ + + + + + + +
+ + +
+
+ + + + +
+
+ +
+ `; + + mockFetch = jest.fn(); + global.fetch = mockFetch; + + notificationManager = new NotificationManager(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('Initialization', () => { + test('should initialize all elements correctly', () => { + expect(notificationManager.popup).toBeDefined(); + expect(notificationManager.closeBtn).toBeDefined(); + expect(notificationManager.teamSelect).toBeDefined(); + expect(notificationManager.apiUrl).toBe('http://127.0.0.1:5000'); + }); + + test('should start with currentEditId as null', () => { + expect(notificationManager.currentEditId).toBeNull(); + }); + + test('should start with currentClientId as null', () => { + expect(notificationManager.currentClientId).toBeNull(); + }); + }); + + describe('Event Listeners', () => { + test('should close popup on Escape key', () => { + const closePopupSpy = jest.spyOn(notificationManager, 'closePopup'); + const event = new KeyboardEvent('keydown', { key: 'Escape' }); + document.dispatchEvent(event); + + expect(closePopupSpy).toHaveBeenCalled(); + }); + + test('should close popup when clicking on overlay background', () => { + const closePopupSpy = jest.spyOn(notificationManager, 'closePopup'); + const event = new MouseEvent('click', { bubbles: true }); + Object.defineProperty(event, 'target', { value: notificationManager.popup }); + + notificationManager.popup.dispatchEvent(event); + + expect(closePopupSpy).toHaveBeenCalled(); + }); + + test('should not close popup when clicking inside content', () => { + const closePopupSpy = jest.spyOn(notificationManager, 'closePopup'); + const innerElement = document.createElement('div'); + notificationManager.popup.appendChild(innerElement); + + const event = new MouseEvent('click', { bubbles: true }); + Object.defineProperty(event, 'target', { value: innerElement }); + + notificationManager.popup.dispatchEvent(event); + + expect(closePopupSpy).not.toHaveBeenCalled(); + }); + + test('should set client ID on Enter key press', () => { + const setClientIdSpy = jest.spyOn(notificationManager, 'setClientId'); + notificationManager.userClientIdInput.value = '123'; + + const event = new KeyboardEvent('keypress', { key: 'Enter' }); + notificationManager.userClientIdInput.dispatchEvent(event); + + expect(setClientIdSpy).toHaveBeenCalled(); + }); + + test('should call cancelForm when cancel button clicked', () => { + const cancelFormSpy = jest.spyOn(notificationManager, 'cancelForm'); + notificationManager.cancelBtn.click(); + + expect(cancelFormSpy).toHaveBeenCalled(); + }); + + test('should call saveNotification when save button clicked', () => { + const saveNotificationSpy = jest.spyOn(notificationManager, 'saveNotification').mockImplementation(() => {}); + notificationManager.saveBtn.click(); + + expect(saveNotificationSpy).toHaveBeenCalled(); + saveNotificationSpy.mockRestore(); + }); + + test('should call showClientIdScreen when change client button clicked', () => { + const showClientIdScreenSpy = jest.spyOn(notificationManager, 'showClientIdScreen'); + notificationManager.changeClientBtn.click(); + + expect(showClientIdScreenSpy).toHaveBeenCalled(); + }); + + test('should call loadNotifications when refresh button clicked', () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + notificationManager.refreshBtn.click(); + + expect(loadNotificationsSpy).toHaveBeenCalled(); + loadNotificationsSpy.mockRestore(); + }); + }); + + describe('setClientId', () => { + test('should set the client ID when valid', () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + notificationManager.userClientIdInput.value = '123'; + notificationManager.setClientId(); + + expect(notificationManager.currentClientId).toBe(123); + expect(notificationManager.currentClientIdSpan.textContent).toBe('123'); + + loadNotificationsSpy.mockRestore(); + }); + + test('should show error when client ID is empty', () => { + const showStatusSpy = jest.spyOn(notificationManager, 'showStatus'); + notificationManager.userClientIdInput.value = ''; + notificationManager.setClientId(); + + expect(showStatusSpy).toHaveBeenCalledWith( + 'Please enter a valid Client ID', + 'error' + ); + }); + + test('should load notifications after setting client ID', () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + notificationManager.userClientIdInput.value = '456'; + notificationManager.setClientId(); + + expect(loadNotificationsSpy).toHaveBeenCalled(); + loadNotificationsSpy.mockRestore(); + }); + }); + + describe('showClientIdScreen and showMainScreen', () => { + test('should show client ID screen and focus input', () => { + const focusSpy = jest.spyOn(notificationManager.userClientIdInput, 'focus'); + + notificationManager.showClientIdScreen(); + + expect(notificationManager.clientIdScreen.classList.contains('active')).toBe(true); + expect(notificationManager.mainScreen.classList.contains('active')).toBe(false); + expect(focusSpy).toHaveBeenCalled(); + }); + + test('should show main screen', () => { + notificationManager.showMainScreen(); + + expect(notificationManager.clientIdScreen.classList.contains('active')).toBe(false); + expect(notificationManager.mainScreen.classList.contains('active')).toBe(true); + }); + }); + + describe('validateForm', () => { + test('should return false when no team is selected', () => { + notificationManager.teamSelect.value = ''; + const result = notificationManager.validateForm(); + + expect(result).toBe(false); + }); + + test('should return false when no active days are selected', () => { + notificationManager.teamSelect.value = 'Development'; + notificationManager.checkboxes.forEach(cb => cb.checked = false); + + const result = notificationManager.validateForm(); + expect(result).toBe(false); + }); + + test('should return true when all fields are valid', () => { + notificationManager.teamSelect.value = 'Development'; + notificationManager.checkboxes[0].checked = true; + + const result = notificationManager.validateForm(); + expect(result).toBe(true); + }); + }); + + describe('generateCronExpression', () => { + test('should generate correct cron expression', () => { + notificationManager.notificationTimeInput.value = '09:30'; + const cronExpr = notificationManager.generateCronExpression(); + + expect(cronExpr).toBe('30 9 * * *'); + }); + + test('should handle single hours correctly', () => { + notificationManager.notificationTimeInput.value = '05:15'; + const cronExpr = notificationManager.generateCronExpression(); + + expect(cronExpr).toBe('15 5 * * *'); + }); + }); + + describe('getSelectedDays', () => { + test('should return a string of selected days', () => { + notificationManager.checkboxes[0].checked = true; // Monday + notificationManager.checkboxes[4].checked = true; // Friday + + const days = notificationManager.getSelectedDays(); + expect(days).toBe('Monday, Friday'); + }); + + test('should return an empty string when no days are selected', () => { + notificationManager.checkboxes.forEach(cb => cb.checked = false); + + const days = notificationManager.getSelectedDays(); + expect(days).toBe(''); + }); + }); + + describe('getTimeWindow', () => { + test('should return time window in the correct format', () => { + notificationManager.startTimeInput.value = '08:00'; + notificationManager.endTimeInput.value = '17:00'; + + const timeWindow = notificationManager.getTimeWindow(); + expect(timeWindow).toBe('08:00-17:00'); + }); + }); + + describe('resetForm', () => { + test('should reset all form fields', () => { + notificationManager.currentEditId = 123; + notificationManager.teamSelect.value = 'Development'; + + notificationManager.resetForm(); + + expect(notificationManager.currentEditId).toBeNull(); + expect(notificationManager.teamSelect.value).toBe(''); + expect(notificationManager.formTitle.textContent).toBe('Add New Notification'); + }); + + test('should set default values for times', () => { + notificationManager.resetForm(); + + expect(notificationManager.notificationTimeInput.value).toBe('09:00'); + expect(notificationManager.startTimeInput.value).toBe('08:00'); + expect(notificationManager.endTimeInput.value).toBe('17:00'); + }); + + test('should check Monday and Friday by default', () => { + notificationManager.resetForm(); + + expect(notificationManager.checkboxes[0].checked).toBe(true); // Monday + expect(notificationManager.checkboxes[4].checked).toBe(true); // Friday + expect(notificationManager.checkboxes[2].checked).toBe(false); // Wednesday + }); + }); + + describe('cancelForm', () => { + test('should reset form and switch to list tab', () => { + const resetFormSpy = jest.spyOn(notificationManager, 'resetForm'); + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + notificationManager.cancelForm(); + + expect(resetFormSpy).toHaveBeenCalled(); + expect(switchTabSpy).toHaveBeenCalledWith('list'); + + switchTabSpy.mockRestore(); + }); + }); + + describe('loadNotifications', () => { + test('should load notifications successfully', async () => { + notificationManager.currentClientId = 123; + const mockSchedules = [ + { + schedule_id: 1, + team: 'Development', + cron_expr: '0 9 * * *', + active_days: 'Monday, Friday', + time_window: '08:00-17:00', + last_updated: '2025-01-15T10:00:00' + } + ]; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSchedules + }); + + await notificationManager.loadNotifications(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://127.0.0.1:5000/schedules?client_id=123', + expect.any(Object) + ); + }); + + test('should show a message when no client ID', async () => { + notificationManager.currentClientId = null; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + await notificationManager.loadNotifications(); + + expect(consoleSpy).toHaveBeenCalledWith('No current client ID'); + consoleSpy.mockRestore(); + }); + + test('should handle API error response', async () => { + notificationManager.currentClientId = 123; + mockFetch.mockResolvedValueOnce({ + ok: false, + json: async () => ({ error: 'Database error' }) + }); + + await notificationManager.loadNotifications(); + + expect(notificationManager.notificationsList.innerHTML).toContain('Database error'); + }); + + test('should handle network errors', async () => { + notificationManager.currentClientId = 123; + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.loadNotifications(); + + expect(notificationManager.notificationsList.innerHTML).toContain( + 'Error connecting to server' + ); + }); + + test('should show loading message before fetching', async () => { + notificationManager.currentClientId = 123; + mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves + + notificationManager.loadNotifications(); + + expect(notificationManager.notificationsList.innerHTML).toContain('Loading notifications...'); + }); + }); + + describe('displayNotifications', () => { + test('should display empty message when no schedules', () => { + notificationManager.displayNotifications([]); + + expect(notificationManager.notificationsList.innerHTML).toContain('No notifications for this client'); + }); + + test('should display empty message when schedules is null', () => { + notificationManager.displayNotifications(null); + + expect(notificationManager.notificationsList.innerHTML).toContain('No notifications for this client'); + }); + + test('should display notification items correctly', () => { + const mockSchedules = [ + { + schedule_id: 1, + team: 'Development', + cron_expr: '30 9 * * *', + active_days: 'Monday, Friday', + time_window: '08:00-17:00', + last_updated: '2025-01-15T10:00:00' + } + ]; + + notificationManager.displayNotifications(mockSchedules); + + expect(notificationManager.notificationsList.innerHTML).toContain('Development Team'); + expect(notificationManager.notificationsList.innerHTML).toContain('09:30'); + expect(notificationManager.notificationsList.innerHTML).toContain('Monday, Friday'); + }); + }); + + describe('saveNotification', () => { + beforeEach(() => { + notificationManager.currentClientId = 123; + notificationManager.teamSelect.value = 'Development'; + notificationManager.notificationTimeInput.value = '09:00'; + notificationManager.startTimeInput.value = '08:00'; + notificationManager.endTimeInput.value = '17:00'; + notificationManager.checkboxes[0].checked = true; + }); + + test('should save a new notification successfully', async () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ schedule_id: 1, message: 'Success' }) + }); + + await notificationManager.saveNotification(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://127.0.0.1:5000/schedule', + expect.objectContaining({ + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }) + ); + + loadNotificationsSpy.mockRestore(); + switchTabSpy.mockRestore(); + }); + + test('should update an existing notification', async () => { + notificationManager.currentEditId = 5; + + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ message: 'Updated' }) + }); + + await notificationManager.saveNotification(); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://127.0.0.1:5000/schedule/5', + expect.objectContaining({ + method: 'PUT' + }) + ); + + loadNotificationsSpy.mockRestore(); + switchTabSpy.mockRestore(); + }); + + test('should not save if form is invalid', async () => { + notificationManager.teamSelect.value = ''; + + await notificationManager.saveNotification(); + + expect(mockFetch).not.toHaveBeenCalled(); + }); + + test('should handle error response with error field', async () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: 'Bad Request', + json: async () => ({ error: 'Invalid data' }) + }); + + await notificationManager.saveNotification(); + + expect(notificationManager.statusMessage.textContent).toContain('Invalid data'); + loadNotificationsSpy.mockRestore(); + }); + + test('should handle error response with message field', async () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 400, + statusText: 'Bad Request', + json: async () => ({ message: 'Validation failed' }) + }); + + await notificationManager.saveNotification(); + + expect(notificationManager.statusMessage.textContent).toContain('Validation failed'); + loadNotificationsSpy.mockRestore(); + }); + + test('should handle error response without json', async () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: async () => { throw new Error('No JSON'); } + }); + + await notificationManager.saveNotification(); + + expect(notificationManager.statusMessage.textContent).toContain('500'); + loadNotificationsSpy.mockRestore(); + }); + + test('should handle network error', async () => { + mockFetch.mockRejectedValueOnce(new Error('Network failed')); + + await notificationManager.saveNotification(); + + expect(notificationManager.statusMessage.textContent).toContain('Network error'); + }); + + test('should re-enable save button after completion', async () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ schedule_id: 1 }) + }); + + await notificationManager.saveNotification(); + + expect(notificationManager.saveBtn.disabled).toBe(false); + + loadNotificationsSpy.mockRestore(); + switchTabSpy.mockRestore(); + }); + }); + + describe('deleteNotification', () => { + test('should delete notification after confirmation', async () => { + global.confirm = jest.fn(() => true); + + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ message: 'Deleted' }) + }); + + await notificationManager.deleteNotification(123); + + expect(mockFetch).toHaveBeenCalledWith( + 'http://127.0.0.1:5000/schedule/123', + expect.objectContaining({ method: 'DELETE' }) + ); + expect(loadNotificationsSpy).toHaveBeenCalled(); + + loadNotificationsSpy.mockRestore(); + }); + + test('should not delete if user cancels', async () => { + global.confirm = jest.fn(() => false); + + await notificationManager.deleteNotification(123); + + expect(mockFetch).not.toHaveBeenCalled(); + }); + + test('should handle delete error', async () => { + global.confirm = jest.fn(() => true); + + mockFetch.mockResolvedValueOnce({ + ok: false, + json: async () => ({ error: 'Not found' }) + }); + + await notificationManager.deleteNotification(123); + + expect(notificationManager.statusMessage.textContent).toContain('Not found'); + }); + + test('should handle delete network error', async () => { + global.confirm = jest.fn(() => true); + + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.deleteNotification(123); + + expect(notificationManager.statusMessage.textContent).toContain('Error connecting to server'); + }); + }); + + describe('formatCronTime', () => { + test('should format cron expression to time format', () => { + const formatted = notificationManager.formatCronTime('30 9 * * *'); + expect(formatted).toBe('09:30'); + }); + + test('should add leading zeros', () => { + const formatted = notificationManager.formatCronTime('5 8 * * *'); + expect(formatted).toBe('08:05'); + }); + + test('should return original expression if invalid', () => { + const formatted = notificationManager.formatCronTime('invalid'); + expect(formatted).toBe('invalid'); + }); + }); + + describe('formatDate', () => { + test('should format date correctly', () => { + const formatted = notificationManager.formatDate('2025-01-15T10:00:00'); + expect(formatted).toContain('2025'); + }); + + test('should return Unknown for invalid date', () => { + const formatted = notificationManager.formatDate(null); + expect(formatted).toBe('Unknown'); + }); + + test('should return Unknown for undefined', () => { + const formatted = notificationManager.formatDate(undefined); + expect(formatted).toBe('Unknown'); + }); + }); + + describe('showStatus', () => { + test('should display a status message', () => { + notificationManager.showStatus('Test message', 'success'); + + expect(notificationManager.statusMessage.textContent).toBe('Test message'); + expect(notificationManager.statusMessage.className).toContain('success'); + expect(notificationManager.statusMessage.style.display).toBe('block'); + }); + + test('should display error message', () => { + notificationManager.showStatus('Error message', 'error'); + + expect(notificationManager.statusMessage.textContent).toBe('Error message'); + expect(notificationManager.statusMessage.className).toContain('error'); + }); + + test('should display loading message', () => { + notificationManager.showStatus('Loading...', 'loading'); + + expect(notificationManager.statusMessage.textContent).toBe('Loading...'); + expect(notificationManager.statusMessage.className).toContain('loading'); + }); + + test('should hide success message after 3 seconds', (done) => { + jest.useFakeTimers(); + notificationManager.showStatus('Success', 'success'); + + jest.advanceTimersByTime(3000); + + expect(notificationManager.statusMessage.style.display).toBe('none'); + jest.useRealTimers(); + done(); + }); + + test('should not auto-hide error messages', (done) => { + jest.useFakeTimers(); + notificationManager.showStatus('Error', 'error'); + + jest.advanceTimersByTime(5000); + + expect(notificationManager.statusMessage.style.display).toBe('block'); + jest.useRealTimers(); + done(); + }); + }); + + describe('switchTab', () => { + test('should switch to the correct tab', () => { + const addBtn = document.querySelector('[data-tab="add"]'); + const listBtn = document.querySelector('[data-tab="list"]'); + + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + notificationManager.switchTab('list'); + + expect(listBtn.classList.contains('active')).toBe(true); + expect(addBtn.classList.contains('active')).toBe(false); + + loadNotificationsSpy.mockRestore(); + }); + + test('should load notifications when switching to the list tab', () => { + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + notificationManager.switchTab('list'); + + expect(loadNotificationsSpy).toHaveBeenCalled(); + loadNotificationsSpy.mockRestore(); + }); + + test('should reset form when switching to add tab if no active edit', () => { + const resetFormSpy = jest.spyOn(notificationManager, 'resetForm'); + notificationManager.currentEditId = null; + + notificationManager.switchTab('add'); + + expect(resetFormSpy).toHaveBeenCalled(); + }); + + test('should not reset form when switching to add tab during edit', () => { + const resetFormSpy = jest.spyOn(notificationManager, 'resetForm'); + notificationManager.currentEditId = 5; + + notificationManager.switchTab('add'); + + expect(resetFormSpy).not.toHaveBeenCalled(); + }); + }); + + describe('editNotification', () => { + test('should load notification for editing', async () => { + notificationManager.currentClientId = 123; + const mockSchedule = { + schedule_id: 1, + client_id: 123, + team: 'Development', + cron_expr: '30 9 * * *', + active_days: 'Monday, Friday', + time_window: '08:00-17:00' + }; + + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSchedule + }); + + await notificationManager.editNotification(1); + + expect(notificationManager.currentEditId).toBe(1); + expect(notificationManager.teamSelect.value).toBe('Development'); + expect(notificationManager.notificationTimeInput.value).toBe('09:30'); + + switchTabSpy.mockRestore(); + }); + + test('should show error if schedule belongs to another client', async () => { + notificationManager.currentClientId = 123; + const mockSchedule = { + schedule_id: 1, + client_id: 999, + team: 'Development' + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSchedule + }); + + const showStatusSpy = jest.spyOn(notificationManager, 'showStatus'); + + await notificationManager.editNotification(1); + + expect(showStatusSpy).toHaveBeenCalledWith( + expect.stringContaining('does not belong to client'), + 'error' + ); + }); + + test('should handle edit API error', async () => { + notificationManager.currentClientId = 123; + + mockFetch.mockResolvedValueOnce({ + ok: false, + json: async () => ({ error: 'Schedule not found' }) + }); + + await notificationManager.editNotification(1); + + expect(notificationManager.statusMessage.textContent).toContain('Schedule not found'); + }); + + test('should handle edit network error', async () => { + notificationManager.currentClientId = 123; + + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.editNotification(1); + + expect(notificationManager.statusMessage.textContent).toContain('Error connecting to server'); + }); + + test('should update form title and checkboxes correctly', async () => { + notificationManager.currentClientId = 123; + const mockSchedule = { + schedule_id: 5, + client_id: 123, + team: 'QA', + cron_expr: '15 14 * * *', + active_days: 'Tuesday, Thursday', + time_window: '09:00-18:00' + }; + + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockSchedule + }); + + await notificationManager.editNotification(5); + + expect(notificationManager.formTitle.textContent).toContain('Schedule ID: 5'); + expect(notificationManager.startTimeInput.value).toBe('09:00'); + expect(notificationManager.endTimeInput.value).toBe('18:00'); + + // Check that Tuesday and Thursday are checked + const tuesdayCheckbox = Array.from(notificationManager.checkboxes).find(cb => cb.value === 'Tuesday'); + const thursdayCheckbox = Array.from(notificationManager.checkboxes).find(cb => cb.value === 'Thursday'); + const mondayCheckbox = Array.from(notificationManager.checkboxes).find(cb => cb.value === 'Monday'); + + expect(tuesdayCheckbox.checked).toBe(true); + expect(thursdayCheckbox.checked).toBe(true); + expect(mondayCheckbox.checked).toBe(false); + + switchTabSpy.mockRestore(); + }); + }); + + describe('closePopup', () => { + test('should reset everything and close the popup', () => { + notificationManager.currentClientId = 123; + notificationManager.currentEditId = 5; + notificationManager.userClientIdInput.value = '123'; + + notificationManager.closePopup(); + + expect(notificationManager.popup.style.display).toBe('none'); + expect(notificationManager.currentClientId).toBeNull(); + expect(notificationManager.currentEditId).toBeNull(); + expect(notificationManager.userClientIdInput.value).toBe(''); + }); + + test('should call parent closeNotificationPopup if available', () => { + const mockCloseNotificationPopup = jest.fn(); + + // Save original window.parent + const originalParent = window.parent; + + // Mock window.parent with closeNotificationPopup function + Object.defineProperty(window, 'parent', { + writable: true, + value: { + closeNotificationPopup: mockCloseNotificationPopup + } + }); + + notificationManager.closePopup(); + + expect(mockCloseNotificationPopup).toHaveBeenCalled(); + + // Restore original window.parent + Object.defineProperty(window, 'parent', { + writable: true, + value: originalParent + }); + }); + + test('should not error if parent closeNotificationPopup is not available', () => { + global.window = { + parent: {} + }; + + expect(() => { + notificationManager.closePopup(); + }).not.toThrow(); + }); + }); + + describe('Tab Button Click Events', () => { + test('should switch tabs when tab buttons are clicked', () => { + const addBtn = document.querySelector('[data-tab="add"]'); + const listBtn = document.querySelector('[data-tab="list"]'); + + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + addBtn.click(); + expect(switchTabSpy).toHaveBeenCalledWith('add'); + + listBtn.click(); + expect(switchTabSpy).toHaveBeenCalledWith('list'); + + switchTabSpy.mockRestore(); + }); + }); + + describe('Edge Cases and Additional Coverage', () => { + test('should handle multiple days selected', () => { + notificationManager.checkboxes[0].checked = true; // Monday + notificationManager.checkboxes[2].checked = true; // Wednesday + notificationManager.checkboxes[4].checked = true; // Friday + notificationManager.checkboxes[6].checked = true; // Sunday + + const days = notificationManager.getSelectedDays(); + expect(days).toContain('Monday'); + expect(days).toContain('Wednesday'); + expect(days).toContain('Friday'); + expect(days).toContain('Sunday'); + }); + + test('should handle time window with different formats', () => { + notificationManager.startTimeInput.value = '06:30'; + notificationManager.endTimeInput.value = '22:45'; + + const timeWindow = notificationManager.getTimeWindow(); + expect(timeWindow).toBe('06:30-22:45'); + }); + + test('should generate cron for midnight', () => { + notificationManager.notificationTimeInput.value = '00:00'; + const cronExpr = notificationManager.generateCronExpression(); + + expect(cronExpr).toBe('0 0 * * *'); + }); + + test('should generate cron for noon', () => { + notificationManager.notificationTimeInput.value = '12:00'; + const cronExpr = notificationManager.generateCronExpression(); + + expect(cronExpr).toBe('0 12 * * *'); + }); + + test('should generate cron for late evening', () => { + notificationManager.notificationTimeInput.value = '23:59'; + const cronExpr = notificationManager.generateCronExpression(); + + expect(cronExpr).toBe('59 23 * * *'); + }); + }); + + describe('Console Logging', () => { + test('should log when loading notifications', async () => { + notificationManager.currentClientId = 123; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => [] + }); + + await notificationManager.loadNotifications(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Loading notifications')); + consoleSpy.mockRestore(); + }); + + test('should log response status', async () => { + notificationManager.currentClientId = 123; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => [] + }); + + await notificationManager.loadNotifications(); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Response status')); + consoleSpy.mockRestore(); + }); + + test('should log when editing notification', async () => { + notificationManager.currentClientId = 123; + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + schedule_id: 1, + client_id: 123, + team: 'Development', + cron_expr: '0 9 * * *', + active_days: 'Monday', + time_window: '08:00-17:00' + }) + }); + + const switchTabSpy = jest.spyOn(notificationManager, 'switchTab').mockImplementation(() => {}); + + await notificationManager.editNotification(1); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Editing notification')); + + consoleSpy.mockRestore(); + switchTabSpy.mockRestore(); + }); + + test('should log when deleting notification', async () => { + global.confirm = jest.fn(() => true); + const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + const loadNotificationsSpy = jest.spyOn(notificationManager, 'loadNotifications').mockImplementation(() => {}); + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ message: 'Deleted' }) + }); + + await notificationManager.deleteNotification(1); + + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Deleting notification')); + + consoleSpy.mockRestore(); + loadNotificationsSpy.mockRestore(); + }); + }); + + describe('Error Handling in Console', () => { + test('should log error when network fails in loadNotifications', async () => { + notificationManager.currentClientId = 123; + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.loadNotifications(); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Network error'), expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + + test('should log error when network fails in saveNotification', async () => { + notificationManager.currentClientId = 123; + notificationManager.teamSelect.value = 'Development'; + notificationManager.checkboxes[0].checked = true; + + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.saveNotification(); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Network error'), expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + + test('should log error when network fails in deleteNotification', async () => { + global.confirm = jest.fn(() => true); + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.deleteNotification(1); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error deleting notification'), expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + + test('should log error when network fails in editNotification', async () => { + notificationManager.currentClientId = 123; + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + await notificationManager.editNotification(1); + + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error loading notification'), expect.any(Error)); + + consoleErrorSpy.mockRestore(); + }); + }); + + describe('Browser Initialization', () => { + test('should initialize on DOMContentLoaded event', () => { + // This test verifies that the global initialization code runs + // The code at lines 426-430 initializes the notificationManager on DOMContentLoaded + + // Create a new document event + const event = new Event('DOMContentLoaded'); + + // The global code should be listening for this event + // We can't directly test the global scope code, but we can verify + // that the NotificationManager constructor works as expected + const testManager = new NotificationManager(); + + expect(testManager).toBeInstanceOf(NotificationManager); + expect(testManager.apiUrl).toBe('http://127.0.0.1:5000'); + }); + + test('should define openNotificationPopup function in window scope', () => { + // This tests that the global window.openNotificationPopup function exists + // The actual function is defined in the script, but we can test its behavior + + // Simulate what the function should do + const popup = document.getElementById('popupOverlay'); + if (popup) { + popup.style.display = 'flex'; + expect(popup.style.display).toBe('flex'); + } + }); + }); +}); \ No newline at end of file diff --git a/AgCloud/services/API-notifications/tests/test_app.py b/AgCloud/services/API-notifications/tests/test_app.py new file mode 100644 index 000000000..98c70184e --- /dev/null +++ b/AgCloud/services/API-notifications/tests/test_app.py @@ -0,0 +1,112 @@ +import pytest +from src.backend.app import app +from unittest.mock import patch, MagicMock +from datetime import datetime + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + +# Test: GET /schedules (Fetch all schedules) +@patch('src.backend.app.get_db_connection') +def test_get_schedules(mock_get_db_connection, client): + # Mocking the database connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_get_db_connection.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchall.return_value = [ + (1, 'client1', 'team1', 'cron1', 'Mon,Tue', '09:00-12:00', datetime(2025, 10, 20, 10, 0, 0)), + (2, 'client2', 'team2', 'cron2', 'Wed,Thu', '14:00-16:00', datetime(2025, 10, 19, 9, 0, 0)) + ] + + response = client.get('/schedules') + + assert response.status_code == 200 + data = response.get_json() + assert len(data) == 2 + assert data[0]['client_id'] == 'client1' + assert data[1]['team'] == 'team2' + +# Test: GET /schedule/{schedule_id} (Fetch a single schedule) +@patch('src.backend.app.get_db_connection') +def test_get_schedule(mock_get_db_connection, client): + # Mocking the database connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_get_db_connection.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchone.return_value = ( + 1, 'client1', 'team1', 'cron1', 'Mon,Tue', '09:00-12:00', datetime(2025, 10, 20, 10, 0, 0) + ) + + response = client.get('/schedule/1') + + assert response.status_code == 200 + data = response.get_json() + assert data['client_id'] == 'client1' + assert data['schedule_id'] == 1 + +# Test: POST /schedule (Add a new schedule) +@patch('src.backend.app.get_db_connection') +def test_add_schedule(mock_get_db_connection, client): + # Mocking the database connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_get_db_connection.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + mock_cursor.fetchone.return_value = (1,) + + schedule_data = { + 'client_id': 'client3', + 'team': 'team3', + 'cron_expr': 'cron3', + 'active_days': 'Mon,Wed,Fri', + 'time_window': '08:00-10:00' + } + + response = client.post('/schedule', json=schedule_data) + + assert response.status_code == 201 + data = response.get_json() + assert data['message'] == "Schedule added successfully" + assert data['schedule_id'] == 1 + +# Test: PUT /schedule/{schedule_id} (Update a schedule) +@patch('src.backend.app.get_db_connection') +def test_update_schedule(mock_get_db_connection, client): + # Mocking the database connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_get_db_connection.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + + update_data = { + 'client_id': 'client1', + 'team': 'team1_updated', + 'cron_expr': 'cron1_updated', + 'active_days': 'Mon,Wed', + 'time_window': '10:00-12:00' + } + + response = client.put('/schedule/1', json=update_data) + + assert response.status_code == 200 + data = response.get_json() + assert data['message'] == "Schedule updated successfully" + +# Test: DELETE /schedule/{schedule_id} (Delete a schedule) +@patch('src.backend.app.get_db_connection') +def test_delete_schedule(mock_get_db_connection, client): + # Mocking the database connection + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_get_db_connection.return_value = mock_conn + mock_conn.cursor.return_value = mock_cursor + + response = client.delete('/schedule/1') + + assert response.status_code == 200 + data = response.get_json() + assert data['message'] == "Schedule deleted successfully" \ No newline at end of file diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/.github/CODEOWNERS b/AgCloud/services/Cross-Sensor System-Level Anomalies/.github/CODEOWNERS new file mode 100644 index 000000000..3371c6fe8 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/.github/CODEOWNERS @@ -0,0 +1 @@ +* @KamaTechOrg @SaraShimon @hadasaGIT @tamarmar diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/.gitignore b/AgCloud/services/Cross-Sensor System-Level Anomalies/.gitignore new file mode 100644 index 000000000..80522fd97 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/.gitignore @@ -0,0 +1,47 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.so + +# Virtual envs +.venv/ +env/ +venv/ + +# IDE +.vscode/ +.idea/ + +# Outputs & plots +out/ +*.png +*.svg + +# Data files : +*.csv +*.parquet +*.feather + +# Allow specific data files +!data/ +!data/Crop_recommendationV2.csv + + +private_*.py +local_*.py +scratch_*.ipynb + +# files +analyze_dataset.py +anomalies.py +check_25_consistency.py +diff_anomalies.py +export_anomalies.py + +# Ignore model artifacts +*.pkl +*.joblib +models/*.pkl +models/*.joblib \ No newline at end of file diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/Dockerfile.flink b/AgCloud/services/Cross-Sensor System-Level Anomalies/Dockerfile.flink new file mode 100644 index 000000000..f836a9c79 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/Dockerfile.flink @@ -0,0 +1,61 @@ +# Dockerfile.flink +FROM flink:1.19.3-scala_2.12-java11 + +USER root + + +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 python3-venv python3-pip ca-certificates curl libgomp1 \ + && rm -rf /var/lib/apt/lists/* + + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + + +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" + + +RUN python -m pip install --no-cache-dir --upgrade pip setuptools wheel + +RUN apt-get update && apt-get install -y \ + build-essential \ + gfortran \ + libatlas-base-dev \ + && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends \ + netcat-openbsd \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --no-cache-dir --prefer-binary \ + apache-flink \ + pandas \ + scikit-learn \ + joblib + + + +RUN curl -fSL \ + https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +COPY conf/flink-conf.yaml /opt/flink/conf/flink-conf.yaml +WORKDIR /opt/app +COPY flink_job.py /opt/app/flink_job.py +COPY models/iforest_pca_artifacts.joblib /opt/models/iforest_pca_artifacts.joblib +COPY models/residuals_artifacts.joblib /opt/models/residuals_artifacts.joblib + + +ENV ART_IFOREST_PCA=/opt/models/iforest_pca_artifacts.joblib \ + ART_RESIDUALS=/opt/models/residuals_artifacts.joblib \ + KAFKA_BROKERS=kafka-single:9092 \ + IN_TOPIC=sensors \ + OUT_TOPIC=sensors_anomalies_modal \ + PYTHONPATH=/opt/app \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/README.md b/AgCloud/services/Cross-Sensor System-Level Anomalies/README.md new file mode 100644 index 000000000..e12181bc4 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/README.md @@ -0,0 +1,137 @@ +# Cross-Sensor System-Level Anomalies + +# Model Artifacts + +This folder stores the model artifacts required for anomaly detection. + +The binary model files (**.pkl** / **.joblib**) are **not stored in the Git repository**, as they are large binary files that should not be tracked by Git. + +Instead, you can download all required model files from Google Drive: + +🔗 **Google Drive folder (all model artifacts):** +https://drive.google.com/drive/folders/1QKRW5jTv3K-NmQLjMjfKqwgVu6JwrZsf?usp=drive_link + +## How to use + +1. Download all four model files from the Drive link. +2. Place them inside this `models/` directory. +3. The application will load them from here during runtime. + +## Notes + +- Do **not** upload model files back into Git. +- To prevent accidental uploads, ensure `.gitignore` includes: + + +## Overview + +Unsupervised anomaly detection for agricultural sensor data using three complementary methods: + +1. Isolation Forest (tree-based outlier model) +2. PCA Reconstruction Error (distance-from-low-rank structure) +3. Residual-per-Feature (OOF) with a robust HuberRegressor + +We combine them into a hybrid decision: + +* UNION (sensitive, higher recall) +* INTERSECTION (strict, higher precision) +* Majority (2-of-3) — good balance for high-confidence alerts. + +Now fully integrated with Apache Flink streaming, consuming sensor JSON data from Kafka topics and producing anomaly alerts in real-time. + +## Project Layout + +docker-compose.yml # Build & run Flink + Kafka + job +flink_job.py # Flink job wrapping IF + PCA + Residual pipeline +detect_iforest_pca.py # Stage 1: IF + PCA + basic plots, intermediate CSV (used in batch mode) +detect_residuals_and_hybrid.py # Stage 2: Residual OOF + Hybrid + 2-of-3 + hybrid plot (used in batch mode) +tests/ # Pytest-based tests (synthetic data fixtures included) +data/Crop_recommendationV2.csv # Dataset (kept in repo) +out/ # Outputs (ignored by git) +Dockerfile # Build & run both stages +requirements.txt # Python dependencies +README.txt # This file + +## Why these models? + +* Isolation Forest: robust to high-dimensional mixed features; detects "few and different." +* PCA Recon Error: catches samples that don't fit global low-rank structure; complementary to IF. +* Residual-per-Feature (OOF): per-target predictive errors using KFold with no data leakage; highlights physically inconsistent sensor relationships (e.g., high soil moisture with very low rainfall). + +## No-Leakage Residuals (critical) + +Residuals are computed Out-Of-Fold: + +* For each fold, we fit imputer + scaler + HuberRegressor on train only, then score the validation split. +* This prevents information leakage and over-optimistic errors. + +## Streaming with Kafka & Flink + +1. Start Kafka with Docker Compose: + + ```bash + docker compose -f docker-compose.yml up -d + ``` + +2. Build and start Flink + job environment: + + ```bash + docker compose up -d + ``` + +3. Open Flink Web UI at [http://localhost:8084/](http://localhost:8084/) to monitor jobs. + +4. Submit the Flink job: + + ```bash + docker compose exec jobmanager flink run -py /opt/app/flink_job.py + ``` + +5. Open two terminals for Kafka: + + * Producer (send JSON sensor data): + + ```bash + docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic sensors + ``` + * Consumer (receive anomaly results): + + ```bash + docker exec -it kafka kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic sensors_anomalies_modal --from-beginning + ``` + +6. Example JSON input to producer: + +```json +{"sid": "normal-01", "timestamp": 1690010100, "temperature": 24, "humidity": 55, "rainfall": 12, "soil_moisture": 45} +{"sid": "anomaly-01", "timestamp": 1690010150, "temperature": 100, "humidity": 0, "rainfall": 9999, "soil_moisture": 1000} +``` + +Consumer will output whether the sample is flagged as an anomaly or not. + +## Outputs + +out/dataset_with_iforest_pca.csv # IF + PCA features/flags +out/pca_iforest_anomalies.png # PCA scatter with IF anomalies +out/dataset_hybrid_iforest_pca_residual.csv # Final hybrid CSV with residual scores & flags +out/pca_hybrid_union.png # PCA scatter colored by hybrid union +out/top10_residual_rows.csv # Top-10 by residual_general_score (always created) + +## Interpreting Results + +* 'anomaly_union' is sensitive and best for broad monitoring. +* 'anomaly_2of3' is stricter and good as "high-confidence" alerting. +* Tune sensitivity via: + + * IsolationForest 'contamination' + * PCA reconstruction error quantile + * Residuals quantile (RES_Q) +* Choose thresholds to match your alert budget (~1–3% for 2-of-3 recommended). + +## Notes + +* Plots are saved headless (matplotlib Agg backend). +* The residual targets default to: soil_moisture, rainfall, temperature, humidity. +* top10_residual_rows.csv is always written (even if fewer than 10 rows exist). +* Streaming mode fully supports real-time detection via Flink and Kafka. +* Ensure Kafka is running before starting Flink jobs to avoid connectivity errors. diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/check_models.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/check_models.py new file mode 100644 index 000000000..db534a8cf --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/check_models.py @@ -0,0 +1,19 @@ +import joblib + +print("=== iforest_pca_artifacts.joblib ===") +try: + artifact_ifpca = joblib.load("models/iforest_pca_artifacts.joblib") + print("Model version:", artifact_ifpca.get("model_version")) + print("PCA threshold:", artifact_ifpca.get("pca_thr")) + print("Features:", artifact_ifpca.get("feature_cols", [])[:5], "...") +except Exception as e: + print("Error loading iforest_pca_artifacts.joblib:", e) + +print("\n=== residuals_artifacts.joblib ===") +try: + artifact_resid = joblib.load("models/residuals_artifacts.joblib") + print("Model version:", artifact_resid.get("model_version")) + print("Residual threshold:", artifact_resid.get("resid_thr")) + print("Targets:", list(artifact_resid.get("resid_models", {}).keys())) +except Exception as e: + print("Error loading residuals_artifacts.joblib:", e) diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml b/AgCloud/services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml new file mode 100644 index 000000000..18cf61e32 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml @@ -0,0 +1,15 @@ +blob.server.port: 6124 +taskmanager.memory.process.size: 4096m +taskmanager.bind-host: 0.0.0.0 +jobmanager.execution.failover-strategy: region +python.fn-execution.memory.managed: false +taskmanager.memory.task.off-heap.size: 512m +taskmanager.memory.jvm-metaspace.size: 256m +jobmanager.rpc.address: crosssensor-flink-jobmanager +jobmanager.memory.process.size: 2048m +jobmanager.rpc.port: 6123 +query.server.port: 6125 +jobmanager.bind-host: 0.0.0.0 +taskmanager.memory.framework.off-heap.size: 256m +parallelism.default: 4 +taskmanager.numberOfTaskSlots: 4 diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/convert_model.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/convert_model.py new file mode 100644 index 000000000..2cf8e5be4 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/convert_model.py @@ -0,0 +1,28 @@ +import joblib +import sys +from pathlib import Path + +def main(): + if len(sys.argv) < 2: + print("Usage: python convert_model.py ") + sys.exit(1) + + model_path = Path(sys.argv[1]) + if not model_path.exists(): + print(f"❌ File not found: {model_path}") + sys.exit(1) + + + print(f"📥 Loading model from {model_path} ...") + model = joblib.load(model_path) + + + new_path = model_path.with_name(model_path.stem + "_compat.pkl") + + + joblib.dump(model, new_path) + + print(f"✅ Model re-saved successfully as {new_path}") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/data/Crop_recommendationV2.csv b/AgCloud/services/Cross-Sensor System-Level Anomalies/data/Crop_recommendationV2.csv new file mode 100644 index 000000000..3de7ccc07 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/data/Crop_recommendationV2.csv @@ -0,0 +1,2201 @@ +N,P,K,temperature,humidity,ph,rainfall,label,soil_moisture,soil_type,sunlight_exposure,wind_speed,co2_concentration,organic_matter,irrigation_frequency,crop_density,pest_pressure,fertilizer_usage,growth_stage,urban_area_proximity,water_source_type,frost_risk,water_usage_efficiency +90,42,43,20.87974371,82.00274423,6.502985292,202.9355362,rice,29.44606392482905,2,8.67735526697563,10.109875244575115,435.61122566407204,3.121394502259622,4,11.743910096203432,57.60730813596583,188.19495775411988,1,2.7196142681382094,3,95.64998536684321,1.1932932982959272 +85,58,41,21.77046169,80.31964408,7.038096361,226.6555374,rice,12.851182636936997,3,5.754287955222303,12.048049798316917,401.45185974634256,2.142020928535281,4,16.797101236780506,74.73687900741903,70.96362942364794,1,4.714427327225068,2,77.26569365523096,1.7526716815799785 +60,55,44,23.00445915,82.3207629,7.840207144,263.9642476,rice,29.363912891054824,2,9.875230096038333,9.05134891346406,357.4179627074981,1.4749737247687815,1,12.654394579097698,1.034478009144435,191.97607728111194,1,30.431736475207316,2,18.192167864978813,3.035541019903109 +74,35,40,26.49109635,80.15836264,6.980400905,242.8640342,rice,26.20773239299878,3,8.023684684293785,7.963606057288741,363.6943055002588,8.393907171864402,1,10.864360184826305,24.091887934783962,55.76138848397649,3,10.861071276483274,3,82.81872017326596,1.2733406458545562 +78,42,42,20.13017482,81.60487287,7.628472891,262.7173405,rice,28.236236135835618,2,8.12051188272924,19.264133422858432,410.35645777701023,5.202285434682235,3,13.852910054226463,38.811481435085696,185.25970154082898,2,47.19077674711596,3,25.466498927055326,2.5786710849952157 +69,37,42,23.05804872,83.37011772,7.073453503,251.0549998,rice,23.61311519614218,3,10.873767569219407,2.728812124554161,428.72842969362614,5.958483178119839,5,13.478474458070083,82.07341331570409,135.92906584189868,3,42.15873477813557,3,43.42633430045652,1.7536156668614868 +69,55,38,22.70883798,82.63941394,5.70080568,271.3248604,rice,15.333693105308976,2,8.726839860577611,4.715023586814744,398.3170075705707,7.389915833434773,1,13.487610448270205,48.81462557502119,121.17365960158139,1,1.5921718254178319,3,50.01897066431516,3.171514124476316 +94,53,40,20.27774362,82.89408619,5.718627178,241.9741949,rice,20.835640482532128,3,10.719887142177978,6.627556793662457,379.8472815222341,1.2420726896565477,4,8.63078182173721,80.12783103951577,195.7361604567481,1,14.415279885864967,1,18.08996232939174,3.1795385786845385 +89,54,38,24.51588066,83.5352163,6.685346424,230.4462359,rice,26.64065576840069,2,11.600185637716663,7.309391590701411,446.3333843786954,8.821774607066256,2,11.260104812933562,89.87019212181914,73.44697633488323,3,48.13877175140959,3,98.37740877207062,1.5309800787118886 +68,58,38,23.22397386,83.03322691,6.336253525,221.2091958,rice,24.368852917432807,3,6.413153902771772,15.395845984336622,377.3990151601595,4.24170639150573,1,11.88543252673378,26.544257604904598,72.79170148098056,1,45.647152908162894,2,63.335751982038204,4.586856058247635 +91,53,40,26.52723513,81.41753846,5.386167788,264.6148697,rice,20.459145741082907,2,6.43288482293385,18.241282308646483,397.4717144170916,7.686282485746422,3,19.80376704152053,43.0149813897197,88.35389727182411,1,25.44276360101112,3,0.8030068386047917,1.4760491681677208 +90,46,42,23.97898217,81.45061596,7.50283396,250.0832336,rice,15.404600446017936,2,8.554866438403772,3.5335211917178566,387.838957847824,3.312374645893745,2,6.024622352933802,65.42929939559889,162.39579093341422,1,36.39975138070278,2,79.21176901091427,4.956311495518364 +78,58,44,26.80079604,80.88684822,5.108681786,284.4364567,rice,15.69649075752503,1,5.962473079366161,7.89643882264818,359.04279531520604,6.142806338720238,6,7.755604910749735,44.776936686283655,196.08297109004903,3,7.030797616012851,3,26.72412345028374,1.5690463286676493 +93,56,36,24.01497622,82.05687182,6.98435366,185.2773389,rice,18.102299560975805,3,10.509164776834663,4.249812524263021,439.9141828695301,3.208669383951548,2,7.552875738109826,80.6212931315953,83.06454514117044,1,40.4247179730376,2,12.45162810967173,1.0297367847931582 +94,50,37,25.66585205,80.66385045,6.94801983,209.5869708,rice,19.742522826826033,2,7.613462956316367,3.9259507614610967,431.68393229524645,1.1538330760013102,4,13.520434354100505,91.7188882485618,78.3083947509769,2,10.157776806077878,2,30.712435357097934,4.410523115565436 +60,48,39,24.28209415,80.30025587,7.042299069,231.0863347,rice,23.34511451117176,1,10.168697865057386,13.214305576708757,386.14283047459605,8.705518065479506,3,14.312229905712677,13.358098388005168,117.26676065919189,3,43.476465497635566,1,50.39889549176869,1.3745635802528167 +85,38,41,21.58711777,82.7883708,6.249050656,276.6552459,rice,12.287843010348427,1,10.748001538790213,18.997984889655246,435.4804751841295,8.55056918740796,1,15.303243639522,31.42938168793934,182.0508467996191,3,14.486082459985944,2,38.68300438556499,1.1359236150294585 +91,35,39,23.79391957,80.41817957,6.970859754,206.2611855,rice,20.505529046999705,2,8.914357978526676,5.605837157503199,362.0769125293712,5.083744266596145,5,6.886703068768259,14.483418321200748,74.4938501084522,3,35.14762403771211,3,77.8685812971585,1.085565894426281 +77,38,36,21.8652524,80.1923008,5.953933276,224.5550169,rice,19.86297771132577,3,9.604758001573376,15.037544987768992,449.09998238776404,3.287939301403511,6,8.66938933565357,50.16163399739286,75.75147164765943,2,4.852330346328937,1,83.16991783223679,2.290178559584014 +88,35,40,23.57943626,83.58760316,5.85393208,291.2986618,rice,25.75100657263861,1,5.156131899329995,11.966922691518535,387.84517226922674,8.826688210121965,6,13.41922724517027,21.42755696379507,87.37678676139276,2,24.83894964192667,2,49.834587199231606,1.3075942280167938 +89,45,36,21.32504158,80.47476396,6.442475375,185.4974732,rice,17.850955791197684,1,11.30220543702904,2.5680289175683435,386.91204857095914,8.137254880064633,2,13.331189009949806,90.87507422306251,145.35805048578587,2,43.275650169073835,1,3.070325598982515,1.4701086515910866 +76,40,43,25.15745531,83.11713476,5.070175667,231.3843163,rice,16.92544358289537,2,7.193711117792186,16.94274790637595,380.15457772902835,3.757578539131919,6,11.439381139697474,43.70821709513933,69.95604214677172,3,9.150663021420286,3,94.38189733419962,3.6723121619098236 +67,59,41,21.94766735,80.97384195,6.012632591,213.3560921,rice,13.9285897742515,3,8.401741241240257,17.444358494246778,427.32100344533956,4.9411167167386605,1,5.2613486650637284,37.08392899537516,177.95358783440952,3,23.13274544495867,3,15.47295955998289,2.4656465098439284 +83,41,43,21.0525355,82.67839517,6.254028451,233.1075816,rice,15.054079970805905,1,8.353858331438325,1.0286659731733572,422.16318039654476,5.1542050838430695,2,5.844103097052975,71.06154282829911,68.9503249773089,1,43.62347417344994,3,20.140472654441886,2.6868715107254615 +98,47,37,23.48381344,81.33265073,7.375482851,224.0581164,rice,21.665501586766386,3,11.400351466402661,7.1781252308063825,387.1073930580847,2.5247545824809117,3,7.674413593102753,66.68658019500681,56.29101402022205,3,33.55821259097579,2,77.15182533785733,2.6293142066193536 +66,53,41,25.0756354,80.52389148,7.778915154,257.0038865,rice,13.741746858175928,2,8.232858165322387,1.2902067748532486,434.99224295037806,7.417834013024693,2,13.420275630579985,66.96630853056874,161.99888092080016,1,21.855042906713884,1,75.12350816058637,3.9887219236789067 +97,59,43,26.35927159,84.04403589,6.286500176,271.3586137,rice,25.52548813642485,1,5.029698818479235,12.957018280144002,354.2222341724808,2.3172103907133668,2,10.720871310411214,89.94165562227481,123.91618268614569,1,15.840594303675386,3,80.56353348649273,2.7810452572861553 +97,50,41,24.52922681,80.54498576,7.070959995,260.2634026,rice,16.9004052999644,1,7.130601915609544,16.285840161183593,387.0109983219061,4.229277117008785,1,9.288550189993176,34.12657847522734,186.48685102113703,3,27.329056036699583,1,29.00824790733667,4.596199347451918 +60,49,44,20.77576147,84.49774397,6.244841491,240.0810647,rice,14.07087343441151,1,8.20556958897901,18.85925311258835,382.4049654362147,6.409720868261506,4,15.92940372453539,42.57121963780162,54.46000266994612,2,27.513657535428003,3,24.079101189899234,1.169038310400524 +84,51,35,22.30157427,80.64416466,6.043304899,197.9791215,rice,12.41872292580177,3,6.495707284772177,13.90703851581521,385.59068019596657,4.852641176937873,5,6.141819410846823,41.556331929529954,123.11369498702734,3,15.718680974054983,1,49.51670996926777,1.457174298152816 +73,57,41,21.44653958,84.94375962,5.824709117,272.2017204,rice,24.038531950479534,2,7.395417364492115,15.97704743199468,409.13507585559415,2.374012772166458,4,14.308048809199102,45.57980344027099,143.3450708523203,1,33.469149566509856,1,72.29219081889899,3.6672817146402275 +92,35,40,22.17931888,80.33127223,6.357389366,200.0882787,rice,11.78666410407552,2,7.827432567657639,19.205834810173457,449.75283667822123,1.1610994447862981,3,10.644278367228107,32.18107344470167,112.78792329339363,1,30.478580663185745,3,87.23012611655435,3.8583490632502904 +85,37,39,24.52783742,82.73685569,6.364134968,224.6757231,rice,18.580802806789414,1,10.681930603549745,3.936908658170577,440.59356041170236,4.719268902845018,6,15.78996607668547,5.000830207015228,73.55137622003463,1,22.972076616170384,2,11.030281219282845,1.2581960363365012 +98,53,38,20.26707606,81.63895217,5.01450727,270.4417274,rice,12.173006937040185,3,7.411662488078816,5.978189568747238,437.35228176667613,8.659881716344454,6,10.992251318755233,46.72542365743689,134.2455911192905,2,49.82165834015599,1,41.05329141913438,3.4668364446701974 +88,54,44,25.7354293,83.88266234,6.149410611,233.1321372,rice,23.33402741040184,3,11.435795412825094,1.123597347821832,428.43707637137913,5.1183852700980115,5,8.58871956139635,2.002447204415725,127.80651505647134,1,2.910249616931365,1,71.06733199806864,3.478428457663009 +95,55,42,26.79533926,82.1480873,5.950660556,193.3473987,rice,23.978828255346407,3,11.40265470663751,14.23982332784258,440.27705465686984,2.8606047983092937,6,19.940179865130624,18.592823085504907,148.6785351076055,1,29.808356985813404,3,87.44669457509315,4.937106996587655 +99,57,35,26.75754171,81.17734011,5.960370061,272.2999056,rice,20.463412227529027,1,6.2885136001290824,15.680680873219739,414.95432438786736,9.040013385591827,6,6.191903207615072,2.7200192410222424,185.9919749922708,2,2.1033043307092605,1,97.66742866354488,3.86799401207321 +95,39,36,23.86330467,83.15250801,5.561398642,285.2493645,rice,18.19548533667535,2,9.659394559081736,5.91500978313838,423.02488401737594,2.3715055511692955,1,7.624250507782482,62.71503007028074,57.9637403385222,3,10.362238021056097,3,6.8526759252996,1.049513577841978 +60,43,44,21.01944696,82.95221726,7.416245107,298.4018471,rice,19.47400022303122,3,10.173810986216726,5.6144207297556825,381.3204769233853,6.184190584365346,2,13.158529897373638,45.733507435124054,142.7926757284559,2,3.2353428707002307,2,21.146305191155513,2.208341569439201 +63,44,41,24.17298839,83.7287574,5.583370042,257.0343554,rice,10.470323050276075,3,6.112574760750782,11.560939120014613,446.1576903753747,5.1912928458118355,5,19.669808340988297,13.415560327065524,162.2816929151758,3,8.469410839964864,1,42.22708139720211,4.558693861518211 +62,42,36,22.78133816,82.06719137,6.430010215,248.7183228,rice,19.71000774484648,1,10.665139917708814,16.508163220734254,415.2461742627597,8.748923931901139,2,17.747714548222874,79.52545134432043,65.14449898572833,3,44.58429975024805,3,6.1138640539346145,3.6062501886256157 +64,45,43,25.62980105,83.52842314,5.534878156,209.9001977,rice,17.241898182189612,2,10.912255011011943,5.994164681549803,428.69964059252925,2.601890339800027,3,14.830346646255126,82.17204057073666,161.6658188784206,3,29.801272180384586,1,47.308195054078,1.0867395612698543 +83,60,36,25.59704938,80.14509262,6.903985986,200.834898,rice,23.532235559361865,3,8.76691037407468,8.119286466905507,401.04588920009286,8.410945628340926,1,5.545100233843027,86.37193496156841,74.7326110897238,1,6.334056913628555,1,88.2559796343018,4.937216070656682 +82,40,40,23.83067496,84.81360127,6.271478838,298.5601175,rice,22.691620214136808,1,6.60503715579402,15.553970905337561,437.10889660253844,7.030883615149051,2,7.031841761325804,43.38731405276066,195.8162786796658,1,3.5154381351317934,3,36.92147305256569,4.652537295264811 +85,52,45,26.31355498,82.36698992,7.224285503,265.5355937,rice,10.924939769484347,1,9.8176727910344,9.747403754434998,368.03569094118075,1.8143648596860777,6,14.080812219915186,90.09073038644414,181.7079578905352,1,22.939446748478254,3,15.530858697250661,2.8258265400350386 +91,35,38,24.8972823,80.52586088,6.13428721,183.6793207,rice,14.659362350623981,1,7.719460462143672,18.6770593044926,407.88658529231157,4.8469323795326815,6,8.775049596009735,98.9995425042727,130.2798842463123,2,21.9793870975708,2,10.359632793076035,3.257700872428641 +76,49,42,24.958779,84.47963372,5.206373153,196.9560008,rice,15.672750919557645,3,10.14681184399114,9.895260931811753,423.0962709665236,6.42222247707992,3,6.765426686979041,88.5524671721141,87.01920348090971,2,5.510014012877839,3,18.33425108498282,2.243055952362575 +74,39,38,23.24113501,84.59201843,7.782051313,233.0453455,rice,20.81748305905524,3,11.913349858198561,11.206828237810033,392.47297638412806,2.514791433556788,6,18.05209272255442,12.797245190699236,57.79366971753189,2,8.956987271527167,3,3.006486696932764,2.389991554637027 +79,43,39,21.66628296,80.70960551,7.062779015,210.8142087,rice,16.434935959029833,1,8.620858210010176,7.996159945564127,373.4682793057467,4.293714527293529,4,5.724663012476134,51.50982369776087,151.98828590293806,3,39.88277176919184,3,95.62224628259068,2.7800309771194263 +88,55,45,24.63544858,80.41363018,7.730367824,253.7202781,rice,14.83400005660129,3,7.0299315247365675,14.434281252135346,420.6499543076612,1.7059432979616953,6,15.951517619057757,11.010510353792712,98.02721357338355,1,17.10798930512149,2,11.032124827970403,4.338717263699402 +60,36,43,23.43121862,83.06310136,5.286203711,219.9048349,rice,24.8121987674652,1,7.1699310899608175,7.122173780010099,409.3179306793328,2.2645154872020052,6,14.054509318880132,41.56000215352197,90.25123166275117,1,4.335156741635321,3,40.45347159293695,1.179476422724584 +76,60,39,20.0454142,80.3477562,6.766240045,208.5810155,rice,10.907756040294556,2,9.518815468591033,4.538322562892398,446.2142258989603,9.711797807054227,2,12.277305467128615,98.53192199244982,126.16351024579018,3,6.659135697162805,1,41.29659021359268,3.3426584464698568 +93,56,42,23.85724032,82.22572988,7.382762603,195.0948311,rice,29.146041129266138,1,8.706770211778142,8.006397242835632,364.4324680916244,7.237875249463322,3,9.943805879583564,25.96809287969377,130.24024403885724,1,31.22242222634088,2,62.38411170401456,3.893675454174683 +65,60,43,21.97199397,81.89918197,5.658169482,227.3637009,rice,21.279364223543475,3,9.694557135285127,14.759346365481305,359.3430403515077,1.5614187930812968,3,15.134719809759043,70.62633951527407,101.3128211428695,1,10.19287102298922,3,49.998243880027594,4.331025133020088 +95,52,36,26.22916897,83.83625819,5.543360238,286.5083725,rice,16.404267961783816,1,9.506083980727048,9.96361333023617,410.9776457780935,3.7475816088492824,4,13.665584559067932,14.43284131608662,124.08871632013896,2,21.74750792119669,3,36.90025767066773,1.732768742010205 +75,38,39,23.44676801,84.79352417,6.215109715,283.9338466,rice,13.104904061824998,3,10.10608178157582,17.37441873678496,420.6999516017301,1.8355654130967771,2,11.00629674217113,99.43025731792928,141.51172728195272,1,14.240641354573762,2,78.3372724368921,3.6564828968883094 +74,54,38,25.65553461,83.47021081,7.120272972,217.3788583,rice,21.98743157620418,1,8.364997214765754,6.428312409954922,372.50119767131713,5.268051691122307,3,7.120670963023565,10.953229601268644,63.70842566808378,2,5.288644975579782,3,73.14725206396203,3.857388005841727 +91,36,45,24.44345477,82.45432595,5.950647577,267.9761948,rice,23.16205187173825,3,11.579201458855032,0.7639098737618966,414.95720791834634,3.5629031070467945,6,6.26214276716537,51.31985891047406,58.534785406313375,1,6.707805125271133,1,83.25806597493758,3.996544549895163 +71,46,40,20.2801937,82.1235421,7.236705436,191.9535738,rice,14.510480330649035,3,10.075854149612363,19.270908923947662,419.84281932107467,7.460449893967257,2,11.346599804821283,70.21030642246134,131.47925278847293,2,10.731507879482011,2,10.484498111926776,4.986762807502485 +99,55,35,21.7238313,80.2389895,6.501697816,277.9626192,rice,13.538539648190985,1,9.153951848687026,17.43376305411903,399.27840636225307,7.792042020666548,6,13.835956010995186,59.3651907576288,53.32331105162666,1,4.531077958351326,1,32.65593194168696,1.5620905056992695 +72,40,38,20.41447029,82.20802629,7.592490617,245.1511304,rice,14.592365270038037,3,10.994239280777903,1.789767905513986,393.2001556575748,3.9017389217755056,6,5.407176816711667,8.700531740345085,53.445356185109375,3,18.261436822772726,3,20.926994270124133,3.256415373141469 +83,58,45,25.75528612,83.51827127,5.875345751,245.6626799,rice,14.696973511495068,1,10.098612476063089,16.102923423795584,425.29909923701746,9.347480667753917,5,18.36459059319938,10.447536091915389,90.09117542879707,2,19.060004487984628,1,74.35069868011723,4.562012264508544 +93,58,38,20.61521424,83.77345559,6.932400225,279.5451717,rice,19.569122738360516,3,8.962662837776401,17.59924760989761,430.70688622839964,5.780798200037314,4,14.367410418916537,1.2100892407415187,67.82375808154919,3,7.734178463661928,2,30.764317142552855,1.1678093088284922 +70,36,42,21.84106875,80.72886384,6.946209881,202.3838319,rice,13.825855174688847,2,9.139950658853223,13.327711148386477,366.15574674408026,3.644763752764966,1,14.745989316097555,28.636245175283015,59.54664820922447,2,49.183980078509826,3,77.93952677708141,1.8753376905284456 +76,47,42,20.08369642,83.29114712,5.739175027,263.6372176,rice,12.798551225078885,1,8.493220515439155,14.793145743826779,361.02091567323566,1.985335032598622,4,7.303002128148399,13.445380845596954,134.0076579778979,2,42.93040374466491,3,66.24336098308976,3.4540275931594833 +99,41,36,24.45802087,82.74835604,6.738652179,182.5616319,rice,16.952351417059347,1,10.018050387435224,14.277774807927026,401.1430304659543,4.5256181184152835,3,11.81226410781256,47.84099194412511,170.64946473485531,3,33.346448676639675,1,23.656292621719523,3.625787958916628 +99,54,37,21.14347496,80.33502926,5.594819626,198.6730942,rice,15.393957784044227,3,9.361882068000092,2.6483106419485236,353.1164708535708,9.107231414495246,6,17.87947383561629,66.87910668138838,60.55799294468706,1,29.336491212143034,2,23.257368366471454,1.4600224768097783 +86,59,35,25.78720567,82.11124033,6.946636369,243.5120414,rice,10.308661233612547,1,7.191549350538006,8.510774774915603,401.4504766264109,1.241607294139976,4,7.32202807288316,23.521256255359802,71.62588155356148,2,33.87827329021119,2,1.9116639478860997,4.5372072686839875 +69,46,41,23.64124821,80.28597873,5.012139669,263.1103304,rice,18.880685233418696,2,10.831767020718933,15.844308334520589,428.42838833857763,8.275411802198768,6,7.0408597815728875,66.47197175806218,161.1511192985543,2,11.679116871575934,1,43.387930202188144,3.100221279464048 +91,56,37,23.43191632,80.56887849,6.363472208,269.5039162,rice,23.365152996616825,2,10.630830430978955,7.055707864747114,431.437947572399,4.595950250761159,4,13.757519351796471,32.29148179612109,139.16901748610525,1,40.37082457952749,3,58.590449975548765,2.538025948530431 +61,52,41,24.97669518,83.891805,6.880431223,204.8001847,rice,18.825945982287358,2,10.805946367333611,1.5682839911427404,426.82988928449737,2.775087902476876,5,15.476558419761076,41.72254045993387,188.18436225462816,2,6.203481960846608,3,75.91396560628976,4.2086307992241565 +67,45,38,22.72791041,82.1706881,7.300410836,260.8875056,rice,26.46537120009733,3,9.143613979505744,14.5416924741046,392.14531137505486,9.76133822014606,2,7.14944570440716,31.222512485403364,92.15260517357126,2,4.194985620642871,2,75.06507363965994,2.9699200803837047 +79,42,37,24.87300744,82.84022551,6.587918708,295.6094492,rice,28.479208747976088,1,9.982401859212954,16.820796568383592,352.6022202925,6.4963617277235715,6,11.314347532403602,85.20503231387296,108.03340693170699,2,29.125179044374285,2,52.85013703914488,3.748161739200655 +78,43,42,21.32376327,83.00320459,7.283736617,192.3197536,rice,26.958458153643253,3,11.513024035041527,13.171620444337286,405.7932062139446,8.959053347735628,6,5.5111304955159035,15.560573364494923,127.8709880901036,3,49.68373215866776,2,44.23937121236209,4.465167105403804 +75,54,36,26.29465461,84.56919326,7.023936392,257.4914906,rice,12.692405256703811,1,10.107386654563172,16.370263035927152,378.88278792276606,5.711948512456132,5,14.354384400048602,30.005963650270363,177.6444672214584,1,4.970428090272394,3,79.4798521508006,4.561682533023239 +97,36,45,22.2286982,81.85872947,6.939083505,278.0791793,rice,21.742934622322476,2,8.555142534523862,12.921630582099965,387.7311859883452,3.054852950064713,2,15.385554829755966,22.17683939975881,110.18419685355285,1,27.384108486113597,3,3.7155727391854687,4.975013598385738 +67,47,44,26.73072391,81.78596776,7.868474653,280.4044392,rice,11.75158593268405,2,7.738664534793937,8.817489638097593,416.6140621933331,7.950500850109946,6,11.479111183478434,47.623938328421666,104.4842425093012,1,6.983647225825362,1,62.49022737819724,3.767585951435315 +73,35,38,24.88921174,81.97927117,5.005306977,185.9461429,rice,16.512010148068555,3,10.916537773149614,2.8412736706849473,391.0619312352157,2.625693130482998,3,7.382253533992833,81.30788103914537,98.5371975997906,3,34.22210086121554,2,48.72180390853752,4.367224224978273 +77,36,37,26.88444878,81.46033732,6.136131869,194.5766559,rice,29.615404320651862,3,6.984510379676158,2.261604010333411,442.4042944499589,1.171969895212413,5,16.56948169978149,85.82209094624453,162.12832940612995,2,36.65283920631073,3,34.56122926492523,1.552999309360061 +81,41,38,22.67846116,83.72874389,7.524080076,200.9133156,rice,25.619096306119154,1,7.693171089062004,9.809661026988328,417.594528247409,5.675302324647516,4,18.79716620642527,20.122956874708365,198.32016072814443,3,38.47677764247125,3,91.77639502384389,2.5104863034093494 +68,57,43,26.08867875,80.37979919,5.706943251,182.9043504,rice,19.692628262369958,2,10.20137176731399,1.1079885820330926,424.7263248932942,4.2928318939608054,2,17.068146137334224,62.13732416015099,82.9523141465906,1,27.78524893870808,1,65.2888596700758,3.808248085280934 +72,45,35,25.42977518,82.94682591,5.758506323,195.3574542,rice,11.875026900296128,2,9.96686366052047,1.393476353571308,359.42617040101936,1.3295956163114628,2,7.40716422874189,74.64723652678093,135.7377885014937,3,36.01261617977819,1,33.10591165125892,3.181773323564408 +61,53,43,26.40323239,81.05635517,6.349606327,223.3671883,rice,28.761611146685205,1,10.212025418448317,5.067830179901323,387.74246291258265,9.60990064731802,4,17.63028251830553,6.947564227419035,94.50164703380148,3,31.017419993335356,1,57.30462164897895,4.556014599317104 +67,43,39,26.04371967,84.96907151,5.999969026,186.7536773,rice,16.7470551416293,1,11.322839349737604,3.009587967088847,365.87111715462606,2.3570732927293525,2,7.020516362405225,99.31161833505557,156.71569404413762,1,39.72124751888767,3,60.26723564198137,1.7535621686631453 +67,58,39,25.2827223,80.54372813,5.453592032,220.1156708,rice,12.612552024800824,1,8.168906404433727,16.111398131742526,361.39419335558557,1.400432280475421,3,8.36075029867662,5.256702169123928,192.22689965854005,2,31.935297187524963,1,75.56094196383658,4.843657322960674 +66,60,38,22.08576562,83.47038318,6.372576327,231.7364957,rice,28.298454436135017,2,9.381699483527832,17.497149089540137,371.36714979251565,1.978030141671313,5,17.25455607177455,12.726035399916547,80.39865991915731,3,17.068090813193482,1,62.23285165297112,1.7260055689391591 +82,43,38,23.28617173,81.43321641,5.105588355,242.3170629,rice,27.219011180701177,1,5.569152367667043,17.033311807471673,405.5040918139793,6.070042779991108,5,16.185987168945253,92.529345725829,194.8462494962094,2,32.031400995774945,1,63.603897970666736,4.69763303207872 +84,50,44,25.48591986,81.40633547,5.935344406,182.6549356,rice,17.8886838179578,2,8.449936908327008,10.593161540395338,388.47047426130456,3.940238365811812,1,5.183824357892965,79.06296965868911,197.88295846111782,1,30.201742560194123,3,79.19770372686067,3.741958327988631 +81,53,42,23.67575393,81.03569343,5.17782304,233.7034975,rice,25.949927940533676,2,11.85680927654324,6.485574932046092,415.95308242932083,3.2707015974798033,3,10.026937780528783,4.281976211538052,102.7245311499158,1,39.15509050264488,3,64.53883907891502,4.349212315669888 +91,50,40,20.82477109,84.1341879,6.462391607,230.2242223,rice,12.50657530419173,2,10.671771298160097,4.410430124774289,396.471249525371,6.972058538748989,5,11.432461995579867,45.20504156684125,136.02909931446464,2,2.854875355888659,2,52.18373864732552,1.1669671425606056 +93,53,38,26.92995077,81.91411159,7.069172227,290.6793783,rice,28.25910002250434,1,6.378760462518726,18.908676350006775,352.4409810980666,7.699751399011879,2,16.213774194747153,83.04973571425558,69.7809545775652,1,20.2378475395106,1,57.558778592883385,2.296473029385508 +90,44,38,23.83509503,83.88387074,7.473134377,241.2013513,rice,13.91157577786971,1,6.990586093261763,18.162174985921602,380.0805206318279,6.6768228384878165,4,17.141311350605257,3.976145351830307,186.9294298835475,2,24.12238200946591,1,98.19485078778193,2.5650189438392497 +81,45,35,26.52872817,80.12267476,6.158376967,218.9163567,rice,10.664596611430836,3,10.305123719250641,1.9802437947808715,383.25166153387386,5.786553038348169,2,12.463467033144376,21.71469305735925,165.79366141632315,1,12.37146533845328,2,12.092905072536308,2.5105887784096996 +78,40,38,26.46428311,83.85642678,7.549873681,248.2256491,rice,17.122876153973998,2,8.443883260881767,3.3772935322157904,399.3725140164827,2.710678978084017,4,14.859973248773601,89.47333635932176,163.60040788315183,3,41.684995362394204,1,1.9891436792416228,2.206593657295648 +60,51,36,22.69657794,82.81088865,6.028321558,256.9964761,rice,16.194472911082855,1,6.42389127289027,9.298748203306834,354.79099601781576,4.388515720640965,3,7.985093996001121,33.91551511398264,184.35492240009202,3,19.049281995863982,2,31.95272039870628,2.7106079870976036 +88,46,42,22.68319059,83.46358271,6.604993475,194.2651719,rice,23.81684773602596,1,5.122433172035966,13.975935842613731,357.58239368378196,4.551065766778282,1,7.284444179427627,88.79185465173238,118.59008039681912,3,29.93185195556929,2,48.16128032944297,2.4927184961230173 +93,47,37,21.53346343,82.14004101,6.500343222,295.9248796,rice,27.740009084178773,2,5.42947414856075,7.130239308961157,449.1218921098511,9.14554523917223,5,7.411669392147853,69.62919254848961,125.66994046168375,3,6.845270438197776,2,90.86432426533774,3.7703017173233935 +60,55,45,21.40865769,83.3293191,5.935745417,287.5766935,rice,11.523065573544693,3,6.462053281841766,18.371658193412344,392.9445046447296,1.2835124229095598,1,10.642500799949506,69.65347817072832,130.37475284044007,3,7.711555753164584,1,8.729263909014783,1.4643518692610047 +78,35,44,26.54348085,84.67353597,7.072655622,183.6222657,rice,14.777482361774299,1,6.99683917257488,14.834502651212185,388.5297715339604,2.054593533311688,3,15.945053506781143,5.211421652847859,172.4208991438148,1,24.699916131253584,2,19.9188175750872,2.7611641386973265 +65,37,40,23.35905428,83.59512273,5.333322606,188.413665,rice,28.968310478447773,1,10.339150689269,11.056683208838201,421.02356091318785,9.393009250160304,4,5.484538541649057,29.137157431355643,81.77991296914234,1,13.155469273392306,2,55.35620492885723,4.282289822503507 +71,54,16,22.61359953,63.69070564,5.749914421,87.75953857,maize,28.156114237523916,1,10.4576601616066,14.635145836663545,418.1292073546417,8.563344115067382,1,17.403104220998536,91.21045169141881,162.70904483875375,2,15.252876265844723,2,61.72390606857003,3.795894645249465 +61,44,17,26.10018422,71.57476937,6.931756558,102.2662445,maize,20.077735685013888,1,10.289859638018285,12.841988225487285,365.48489253268934,8.453087186021335,1,14.256065491378326,15.849372917478943,60.31843581473337,1,31.10022803850989,2,6.801556749727022,3.009500931401725 +80,43,16,23.55882094,71.59351368,6.657964753,66.71995467,maize,15.374348473394676,2,6.339511862572754,0.09573474609472443,418.97470876214027,1.0828255895455383,5,9.989453729079614,52.25777314483076,90.038070568537,2,29.017744501451453,3,77.9288728634444,4.85950927979083 +73,58,21,19.97215954,57.68272924,6.596060648,60.65171481,maize,27.030658271675122,2,6.344720978162365,16.29883251302163,409.4140461799825,5.0816083083204076,3,7.358802371161817,52.054374876759866,121.56979071519591,1,11.182231203060228,2,67.65339695294185,3.565479847440921 +61,38,20,18.47891261,62.69503871,5.970458434,65.43835393,maize,26.64690297120908,3,6.945164306014915,17.66803320023015,422.2249570742907,2.367882777802868,2,8.262650457055916,55.32698827597444,135.4694481586128,1,34.888152354961356,2,44.148870019065626,1.4113493815475193 +68,41,16,21.77689322,57.80840636,6.158830619,102.0861694,maize,16.916115717602835,3,9.282074793155324,5.5415496813857,427.29145013841145,7.9608652457013305,3,10.351651775804458,63.23212380945598,156.83349954557946,1,10.084132267556612,1,64.89237361237468,3.855729270734928 +93,41,17,25.6217169,66.50415474,6.047906679,105.4654703,maize,23.187377578327315,2,10.018629748924496,19.115083906627884,361.9329794791084,9.169777905327692,5,12.177475707156415,49.22027462850708,70.01148158526053,3,1.3197442252329161,2,2.8005041510896556,4.977450806470273 +89,60,19,25.19192419,66.6902901,5.913664501,78.06639649,maize,23.96198764939306,3,7.9743677613738235,12.406600507956444,375.04669574988264,6.035312524317499,4,13.496860600121634,62.613064678750376,69.1423489369196,3,1.8606160719431108,1,47.86273638760582,3.6201363654359566 +76,44,17,20.41683147,62.5542482,5.855442401,65.27798457,maize,25.43969221505493,1,8.128670502444741,11.342437973246067,365.60362795026225,8.104929796744013,3,5.778014622307045,73.55118791501147,127.23637324936807,2,48.569130915607374,2,13.021129364594408,2.9723159844104035 +67,60,25,24.92162194,66.78627406,5.750254943,109.2162279,maize,18.810809946739603,1,7.355747703474254,4.952370142135402,410.56049975561785,3.287584989466832,2,12.550304099257465,73.15099475398165,97.43424879026091,1,13.16491571416553,3,62.70727287874255,1.3781879213463348 +70,44,19,23.31689124,73.4541537,5.852607099,94.29712821,maize,18.35536638092298,3,9.938918604408634,10.706750345132791,394.4056601902954,3.2437906076985428,4,13.103252635674144,31.934164617380766,143.01517202938913,3,16.213898432091277,2,66.24277217211676,1.1993949698890676 +90,49,21,24.84016732,68.3584573,6.472523287,74.05474936,maize,15.475575411009304,3,11.285436917010383,13.773911528099836,366.49567072399606,1.03443993947727,5,8.319287108668323,45.22873452598076,130.07543366050697,1,31.587129294230742,1,31.914456082520182,1.210945364128046 +62,52,16,22.27526694,58.84015925,6.967057762,63.87020584,maize,28.818107434133015,1,9.36311331410015,6.868265821161971,362.51840622472565,4.311333351785014,6,12.549366890834857,46.382134147103194,156.5443354532003,2,4.844258560297576,3,17.86729936411161,4.357799119499675 +92,44,16,18.87751445,65.76816093,6.082973754,94.76189431,maize,16.51598934276396,3,9.541492150387434,5.342120631199774,411.56085829111623,3.6999827888842907,2,8.480648425783688,4.178685433798812,64.90979248812386,2,5.237154050854781,3,67.76069582065286,3.571591728432276 +66,54,21,25.19008683,60.2001687,5.919045532,72.12375573,maize,20.220159164922585,2,7.423576255500406,19.145718543100244,351.65914469014217,1.6325832025301086,2,14.146164946984477,70.05089554978348,163.91622978418593,3,5.792494299249879,1,52.42581981447745,4.794256796005639 +63,58,22,18.25405352,55.28220433,6.204747653,63.72358154,maize,29.564409215525536,2,11.192985136397244,6.4786868758872584,357.8076384307208,1.17251688037859,5,8.076160620007826,88.470864632309,83.2757211170948,1,10.756825058653646,1,14.994566171085177,3.8391866408203557 +70,47,17,24.6129118,70.4162444,6.600827017,104.1626147,maize,20.02109254320799,1,10.772228518270595,12.045762640050942,392.7139043478131,6.401624076069764,5,10.867535362615893,12.799899994777453,111.69765438364496,2,31.736820786813354,2,64.72764247946668,1.4137416188734484 +61,41,17,25.1420613,65.26185135,6.021902237,76.68456006,maize,29.564065781027193,1,9.571397394060487,12.421020602544797,395.6082094108026,4.8756386598676835,5,13.672334019978845,34.756039188882816,102.32510768412166,1,29.04560353566869,2,4.41334308885687,4.254663478706797 +66,53,19,23.09348056,60.1159381,6.033550195,65.49730729,maize,17.777308012949078,3,8.139861891424822,15.694184583565391,394.98459197401326,6.4197805860464054,5,17.545024846230945,25.570199686659855,166.57231522635095,3,33.10010699498457,2,57.63680063022102,2.4721631055805333 +74,55,19,18.05033737,62.89366992,6.28886807,84.23613484,maize,28.361883052679257,3,7.681525712895155,18.632775176054338,380.7963324132751,2.640846091740541,4,13.71699861924386,86.71488486432531,55.12069480853036,3,16.071264008579654,1,49.73906723092462,2.04522576754565 +77,57,21,24.9321581,73.80435276,6.550563823,79.74078719,maize,25.08406622790464,1,10.397123987862376,15.484854483085043,386.8619027811957,6.717954629661662,5,9.15477207376625,94.91474203223625,169.53659841875515,2,4.953312485140815,1,73.17456535822878,4.491385538631137 +99,50,15,18.14710054,71.09445342,5.573286437,88.07753741,maize,16.483831774064306,1,9.379188921868618,16.88292084900617,371.8112027959144,6.918472121701261,4,7.470003928218493,26.763543619603535,86.61070677800393,1,37.86246984239875,2,42.23117351554303,4.159630333864085 +74,56,22,18.28362235,66.65952796,6.829199275,80.97573281,maize,10.557327891943224,3,10.83192583282395,17.988080048208047,364.67934018627403,5.316890644844102,6,5.201145336322493,83.78298076978213,58.47081771157484,1,10.753839484866251,3,46.47527079661836,4.466445884409916 +83,45,21,18.83344471,58.75082029,5.716222912,79.7532896,maize,20.709097635329297,1,9.891744119864367,0.0028699831011058663,435.28434448948826,9.290833683244204,4,18.19873965724357,57.89451147131556,178.5840260359625,2,0.775878150027759,2,49.554728913830026,2.532466667747218 +100,48,16,25.71895816,67.22190688,5.54990242,74.51490791,maize,21.22619485890013,1,11.947405276289771,13.947580594371523,431.98059043394665,2.3667708969986765,1,13.41172579969479,40.98109660839617,179.69058134877503,2,44.03384055147534,3,82.44241544906274,1.492170729707151 +79,51,16,25.33797709,68.49835977,6.586244581,96.46380213,maize,26.550300867005102,1,11.9112554720343,12.725374709602653,417.6657456963728,8.410889148419427,2,9.752331183752151,72.02096613988199,128.99762556641122,3,37.18264427443312,1,72.44831092389418,2.498723847592176 +94,39,18,23.89114571,57.48775781,5.893093135,102.8301942,maize,19.783968908523512,3,10.335042866881452,14.65783855206299,414.8819147289122,3.3402567324689043,3,18.600387231393107,13.059975510986089,116.7154630883587,3,13.657851892941968,3,61.85552093917713,4.238195092373365 +75,49,15,21.53574127,71.50905983,5.918263801,102.4852929,maize,13.366998531307289,2,5.212174170651337,0.43516696816681577,393.0346068562641,4.107909745784703,6,15.237957415469394,9.183958155483774,109.0361329204643,1,3.224739326113818,3,9.174379745815454,2.4688512841863455 +78,48,22,23.08974909,63.10459626,5.588650585,70.43473609,maize,11.766911669745031,3,10.02693325768997,1.131060427028412,374.7561366651767,7.131052626309331,1,12.617373927469494,43.373526023124576,79.36313101485527,2,0.05100027656864681,1,84.8327609721675,4.434976689320607 +87,54,20,25.61707368,63.4711755,6.576418207,108.8303762,maize,20.70903958586095,1,11.925229223232812,0.7877098121617987,402.94879540520617,1.4514109657922438,2,10.481830006234032,62.5663983211316,194.46537178757615,2,27.61743818470897,2,52.211015030209495,3.333349557907917 +87,35,25,21.44526922,63.1621551,6.178056304,65.88951188,maize,12.024365136909857,2,7.025473918542891,9.636454275820574,437.66748810491094,9.09043841303276,1,5.136479448650119,1.4722843513942374,138.7770689068788,2,8.463016371367049,3,76.41745343467062,3.8996705146629873 +63,43,19,18.51816776,55.53128131,6.641906353,90.988051,maize,10.825667707107044,2,11.593728814565367,15.7705200275212,360.1340384618379,8.290345462926442,1,8.441099099484457,32.116045569709584,68.20600730927012,1,34.45421746666232,1,97.22599820831897,1.7256139508085835 +84,57,25,22.53510514,67.99257471,6.489040367,64.40866039,maize,10.31015108426444,3,5.321100794191025,12.331107898263793,393.38395943545436,7.908178584669154,2,12.315468996415266,68.69956434618702,62.52098834181867,2,42.75667705665569,1,20.84317515988414,1.9699483934582274 +64,35,23,23.02038334,61.89472002,5.680361038,63.03843397,maize,25.994938800495667,2,7.929225231230511,6.930290324954798,391.68983609791906,5.892073429989431,6,5.4727001471613175,72.31552202033662,98.300501869008,2,18.163902811084125,1,88.01318654695997,1.4725103509437498 +60,46,22,24.89364635,65.61418761,6.625404348,87.9298085,maize,16.11860233000442,2,9.35492288249931,5.655700625847202,390.82424230746005,9.110038220829544,3,7.909937165868556,15.725754102822108,138.24007836765517,1,28.89583402726419,2,46.756888703620945,2.3080612227485044 +98,44,21,25.77175115,74.089114,6.524478032,107.4931917,maize,16.18147559658525,2,7.139734066592972,15.377594807872253,380.3692118643177,7.4066962470237225,2,8.957046138169506,97.08582627962441,148.0560576055633,1,42.66952219581532,1,76.81332346451318,1.9610536963990355 +75,56,18,19.39851734,62.35750641,5.696205468,60.95197486,maize,15.509719611987599,3,5.693955811265353,11.440690741372029,403.2726882501837,9.441555378948523,4,13.591520198316868,38.869970227355196,181.35636770905035,1,49.07879005776542,1,97.46042299404664,1.3899369084084277 +86,55,21,21.54156232,59.64024162,6.803931519,109.7515385,maize,25.40648231124742,2,5.421761267587175,13.953796326708794,422.9332074015874,7.539780823844976,1,14.528664099794987,33.132219921217384,177.3899730581817,3,40.20983673008141,3,48.45018632155897,2.5564549267760683 +98,35,18,23.79746068,74.82913698,6.252797548,91.76337172,maize,22.6919053080773,1,7.98570892490657,5.310167614140995,431.1193253745016,6.160676196620697,1,19.932663074827303,53.36238643447064,175.05313499022634,1,9.796705667526378,1,1.292525891338303,4.450859980605886 +76,57,18,18.9802729,74.52600826,6.092725883,94.26249353,maize,28.784592006394575,3,10.550047413011114,10.823207722509274,449.69869557543313,1.5458254697100964,5,12.708877059990156,37.19198005428698,161.27349181695152,1,31.39389387190536,1,32.851751957607924,1.0800769088964608 +99,56,17,24.10859207,73.13112261,6.234330356,71.07562236,maize,14.69504496019837,3,8.677477727721634,12.647115269211902,428.6027601702945,9.333546192189281,3,7.805512640876092,76.31931037232788,79.44767278280503,2,27.400766660984655,1,93.1186238383711,4.454077982323488 +60,44,23,24.7947077,70.04556743,5.722579819,76.72860067,maize,17.626428704420114,2,10.166162207907812,6.967887724695856,366.41285411311605,8.579635315418768,4,7.04495444822971,0.5614210194318514,132.20877116174552,3,30.613258217909824,3,10.72321818421571,3.4127390044597345 +74,48,17,21.63162756,60.27766379,6.430616465,69.21803098,maize,25.58405607247876,3,8.843296433572885,3.353474748424652,424.67630974491885,3.747302982036841,2,9.30491297798995,0.03810333825923218,166.04727940670887,3,49.238614344595824,3,42.82098612293197,2.7049325457675977 +89,60,17,25.37548751,57.21025565,5.983952675,101.7004306,maize,24.3952801312897,1,5.585151224634395,12.193613817481113,397.44774475788194,1.1333539918115116,3,7.48417760834058,20.801617420111285,132.88521727386944,1,38.34958003502755,2,39.830284969298,4.356934287952017 +69,51,23,22.21738222,72.85462807,6.80163854,106.6213157,maize,22.423386416477808,1,11.840331199765423,7.247076081947183,420.94455947068315,1.4025280830112834,1,12.516766964632325,57.08770912648716,173.4000279177962,3,17.01691588992606,2,1.8351307727198174,2.7722565624488817 +96,46,22,20.58314011,69.00128641,6.499936446,66.29390357,maize,14.48966526248931,1,10.191230930589679,6.263832316038447,437.9304508700769,3.178371540879528,1,6.179459216687517,27.916454719985516,69.14680302954363,1,43.288909777026646,2,30.937605912470023,1.165076098841824 +61,60,15,24.87502824,68.74248334,6.265564338,91.26056654,maize,21.260414498467775,3,7.077418010925226,18.38791521505286,444.7979525227851,4.640238746835432,2,14.255651050360761,8.67008251978656,107.43116395731514,3,37.54157773631281,3,46.6022956628057,2.424669745784972 +74,58,18,20.03728219,56.35606753,6.727303282,109.024141,maize,25.00861027953608,1,8.508133170032366,11.605523824306518,417.0069817058546,4.610375789148433,1,14.898992302488635,79.4704144449591,115.02329779434895,1,19.150393686224916,1,60.18168394840999,2.6218159783552237 +74,43,23,25.95263264,61.89082199,6.325235159,99.57981207,maize,27.459627375694836,1,7.886227116144518,10.125153639385562,393.7830108957594,9.254137836951292,5,18.846196360555492,55.00597878693748,105.38261729102703,2,23.449073423157067,2,87.90359432778183,2.939576457583108 +63,43,17,19.28889933,65.47050802,6.807487794,71.3195307,maize,28.435890670149476,2,5.280586264089705,11.568013529157703,411.31808573902697,1.662285212306121,1,10.161683027589696,99.89974644462399,60.8791805192695,3,0.415997807100954,2,37.65267991942805,2.1206651641150205 +99,36,20,20.57981887,65.34583901,6.671085817,78.34604471,maize,25.405561379933715,1,9.201812197219205,17.266212873463843,352.76457613836914,4.567640039742477,6,15.44392517727786,35.86732171213733,64.62263325490517,3,7.016145355521291,3,25.793879587513313,2.0850432544323794 +77,36,23,24.71417533,56.73426469,6.648725327,88.45361858,maize,28.443769919769352,2,10.106009914388903,18.907173706193174,354.6669244953803,1.9579872652380221,2,10.690410886054764,62.755731142764446,128.23188523274808,2,12.888195788450934,2,8.233446456835347,4.422928323306293 +87,60,23,20.27317074,63.91281869,6.439071996,62.50351892,maize,17.177087805545685,3,5.817462300480618,8.715019816474074,430.7242209915556,5.375129377106358,5,17.843443008937925,99.8719349760149,149.42298760922466,2,9.70094513455853,3,76.39186277298457,1.8219530335234864 +60,38,17,18.41932981,64.23580251,6.474476516,76.41312437,maize,20.860039409441107,2,9.47085517222036,13.718946933245252,385.2710083445663,3.246225075939655,5,16.156766220071802,52.33152627394014,53.06463216807005,3,8.562627921297716,1,46.64749572651732,1.8470540287088064 +94,54,17,23.39128187,61.74427165,5.871647806,107.3198135,maize,20.17302945406609,1,5.311372444842171,2.1483431423102517,387.51908570009516,3.814075766569502,5,15.984332260680901,11.174649539939841,148.77804162927305,1,45.407355174881324,1,37.402087528159754,2.3117452723016947 +95,38,22,19.84939404,61.24500053,5.730617109,100.7689246,maize,27.24057149159221,1,10.281803884113703,17.994566879219242,422.457909913992,4.428488420270796,3,16.042398139731425,18.974444317620332,95.12348051277266,1,21.44507400630273,2,10.091052507469955,1.0804518903373794 +84,44,21,21.869274,61.91044947,5.850439831,107.2681929,maize,25.75189006255298,1,5.097418919103131,14.517392342677564,429.385195418356,7.232329331053059,5,17.830242001772373,20.01839654357567,151.19603335103704,2,6.918201163979182,1,49.486356806029406,3.1335152328474685 +77,58,19,22.8056033,56.50768935,5.791649933,101.5952794,maize,14.633805874356845,3,9.954512637879553,3.1753905233687707,421.1541811026011,1.2135070499086016,6,11.779800803995947,11.566829253446908,170.47018716051412,1,47.95011897171934,1,64.45366800148746,3.8524893551723816 +66,44,20,19.0781471,69.02298571,6.740000688,80.72515943,maize,18.419803551509652,2,10.19609968097122,14.580834811815995,395.02141470760546,4.5176373435578885,1,18.440338456991753,40.460341506329065,118.15717889321736,2,20.946101728117593,3,65.24624992163848,3.850412236324358 +63,35,16,22.02720976,65.35549924,6.272417541,83.73280082,maize,11.813093986606347,2,6.036377526282939,10.235403137206866,412.9796765255336,4.2463672030004735,3,6.7525777965247755,74.16172432657592,97.04732635043786,3,42.74847621343011,3,78.44525777731768,2.935125027092456 +79,45,20,23.80546189,59.24537979,5.715208817,89.9622014,maize,20.040852237917964,3,9.79578208333223,3.4866536347049926,361.78892261154516,1.9463361421270295,3,12.647647546034134,10.364412091450525,129.4272078394163,2,24.952145879349157,3,49.070343250123415,3.832164311714721 +72,60,25,18.52510753,69.0276233,5.773454729,88.10234397,maize,18.104902150279425,1,8.233334952892696,3.1383006712790062,436.45429971958566,8.915273023719175,6,12.357774060312721,14.65827099700151,111.92986900866882,1,3.287193052337367,2,79.35478134213156,1.0452981156980559 +67,51,24,23.50297882,61.32026065,5.584171461,64.77791424,maize,29.26467351986148,3,11.723327994690862,1.3117254962609337,370.47200441737675,8.769848378830542,3,17.65818882510448,50.94086544519703,185.46798667770727,2,7.825643121583337,3,17.055777258179205,1.7681207817849396 +86,36,24,26.54986394,72.89187265,5.787268394,73.33636055,maize,19.331725558632996,1,8.041045784160316,19.151375942437493,390.8523909704385,9.406392646116165,3,14.080907791274898,18.22983797970228,156.80611532643263,2,25.588450972813497,1,80.4747861467936,1.8969972900536152 +76,48,18,19.29563411,69.63481219,5.77597783,83.21030571,maize,16.30111470104408,2,10.958720364359795,17.95663201732216,363.0267580498317,2.0069585760048225,3,6.353349425990796,78.09352144477624,179.1024591202059,3,47.26280770116056,3,48.36953463874125,1.5303967710357687 +75,53,18,20.68899915,59.4375337,6.864793607,103.651438,maize,15.481196021626536,3,7.958415540105081,3.34396082120066,421.30261683249614,3.9099396124938948,3,15.249649775290163,98.6789299589765,96.76359102397306,1,5.056436652040841,1,67.94038247613823,2.3898966024658055 +81,45,23,19.32666088,68.034493,6.192360003,84.22969177,maize,26.570913509730723,3,5.187140972272441,6.686084000418142,377.58557225776354,8.673550763440826,5,6.289926997973194,83.34257056734201,74.37916665193858,3,48.975696363127156,1,60.998213770414736,1.9544220112424564 +73,45,21,24.60532218,73.58868502,6.636803223,96.59195302,maize,15.55441258248264,2,7.663928217899676,14.599401310253379,448.06664727103794,2.1835081036811514,3,7.028913355459454,81.54280710856095,156.22444744804625,1,17.633760302655915,3,18.153460725171733,3.5421068690387645 +71,35,24,22.27373646,59.52193158,5.826426917,67.96704792,maize,15.74570393886939,3,8.523533077749091,15.552504119160194,369.1809942625622,2.8698357838089,1,11.492621903101632,16.639564220213366,50.5137904119127,1,0.7452125730909365,2,7.521921937457732,1.1035824553247244 +96,54,22,25.70196694,61.33450447,6.960358276,83.20711308,maize,17.363971184414066,1,8.128524498155933,6.881733231566569,386.26761176327057,9.324581466058012,6,17.796992944319314,24.098825633487607,56.75320571605474,2,41.79832878736197,3,81.82954559295081,4.427178621334322 +99,39,18,19.20129357,68.30578978,6.11275104,87.85092352,maize,16.247215558153485,1,8.592736669086673,19.14323710780399,426.6477759505081,7.5340338706859304,4,8.19155341180029,39.94142728060136,80.9585165361021,3,34.24992127187122,3,21.849380643025608,2.6918938402807577 +62,48,20,21.70181447,60.47470519,6.708446922,95.71388473,maize,18.873230593947397,2,6.74855112668991,17.675737752571116,403.7226708414982,9.308117935977796,4,5.694703592205469,70.61923112928065,86.32779065484485,1,43.396310706341254,1,49.3664607137875,1.3242316175244104 +86,37,16,20.51716779,59.21235483,5.561510732,67.61013737,maize,24.77973159794391,2,11.122159845105017,14.859952978688689,366.9931264906183,8.784357098673777,1,12.021268024084389,47.362185695141804,125.67340631304897,2,45.26653983050325,1,7.374460104874214,1.192419163819559 +94,50,19,23.30355338,73.62548442,5.873242491,97.59081274,maize,15.79416857957826,3,6.683881117699223,3.9544340610647244,435.56820597269405,8.371270672689072,4,19.810718774191947,62.30045966805792,83.55117940975367,2,0.32283832237851584,2,95.28385816711362,2.244192422886826 +76,39,24,24.2547451,55.64709899,6.995843776,64.23845455,maize,15.119546282543407,2,8.456038632738139,4.804740482390574,424.6786165885767,4.043798906552058,4,11.716946760922799,51.11855813356543,72.83044951894448,3,41.52588147021503,2,55.53001222798426,2.039435540550364 +77,52,17,24.86374934,65.7420046,5.714799723,75.82270467,maize,11.787229925774788,3,5.89279330798145,12.592767775125523,448.923313693228,3.1897094180513594,6,10.358573006493405,10.030387975043597,118.75556175231566,2,46.38342263149758,2,39.942503331754274,4.068601268951715 +74,39,23,22.6265115,65.77472881,6.78073637,88.17251033,maize,27.32199653303682,3,11.193790191540508,7.629118354377753,352.904880652595,7.310568444434592,1,11.181807936951836,57.01158612337706,72.75606820523552,2,22.767171653057094,2,8.06113152367054,2.567881446027299 +81,49,20,18.04185513,60.61494304,5.513697923,104.2321615,maize,13.140268766996403,2,10.196607975259784,3.4832115534787733,397.4356174963996,3.2782065640579092,2,5.414770671610774,44.746384473151004,88.40605515252025,2,46.19669037166359,2,38.31235820233824,1.1168902416254172 +63,42,21,23.26237612,72.33125523,5.798423908,67.10225139,maize,17.31523561761122,1,7.849243177135962,4.095620810916083,365.4540671963635,1.9559260646112966,6,9.164329483559474,5.081291415735745,111.11511016724113,3,18.00706004626559,3,16.36278637811406,4.103625650038794 +99,38,21,22.88330922,71.59722446,6.352471866,67.72777298,maize,25.152751993732778,2,8.371245021323062,14.481232333635486,415.7635745919353,4.22405638094979,5,14.381723879647334,96.47137923862667,183.83601593606326,1,28.07849199312776,3,3.130629430080556,4.09170459542183 +90,52,25,25.97482359,69.36385721,6.822586546,103.2234212,maize,21.989640068600735,1,10.342484070593438,16.583664875326335,442.5406299173002,4.599840117844138,2,7.9111987154144074,50.71018392194593,117.77919195353641,3,42.68537362266174,3,89.51313080277625,2.874749671453941 +68,40,19,26.14384005,66.20569924,6.655426355,107.2361366,maize,24.61074581078945,3,6.174129143559844,9.881363275835515,418.1281007802853,4.2311166570450895,4,5.461923401636004,45.54954262915827,132.00618789090888,3,7.3265732997072615,2,4.535363379647251,3.0036833968613936 +60,57,24,18.66116213,61.55327249,6.121294041,75.03247667,maize,23.856300168189968,1,7.266250392248363,3.060514275280659,406.4379210866088,7.947931521547467,2,5.850574917101877,43.67301931020937,153.60052412097752,2,21.19415053899066,3,11.761352922430223,4.855731659520144 +71,52,18,25.10787449,55.97732754,5.790770203,78.16077693,maize,24.418857520447084,3,10.551891892896808,12.403388216696632,366.0808225771917,4.257854167941089,2,5.796895313422025,57.41985879079804,109.84339982647128,3,42.497834861926925,1,89.56139640297094,3.704657757419396 +61,59,17,23.33844615,59.24580604,6.47444292,105.0083144,maize,22.223938666202645,2,9.928580901950046,14.578745903626064,387.5294136828265,7.0157063581796315,5,16.94774083646296,4.494926318313763,191.48394745399847,3,10.23664028612304,3,68.86234029016124,2.7172288909959996 +88,38,15,25.08239719,65.92195844,6.455116637,62.49190812,maize,14.870988392069222,1,8.714014557173432,13.271228463631628,422.059478644224,3.5768913557038435,2,6.4368910550950975,94.98265612730185,132.18604929157254,2,0.9075737494833092,3,99.1862892798888,1.5964223622074538 +65,60,22,25.36768364,72.52054555,6.606984086,107.9124111,maize,15.390627022479427,1,11.342605622109431,13.228410890052553,408.2629918504056,7.741397000539391,2,16.914679824672614,83.86720481900898,163.20158796758875,2,43.69142349568697,1,31.129592541073205,4.86315181451786 +78,37,22,25.34217103,63.31801994,6.330554389,74.52082026,maize,25.52928577439126,2,8.27430762483107,16.000444339875692,449.11370290856553,4.996667305023241,6,13.233219736724235,31.699481285984543,184.61170512474547,1,49.47928111598519,3,33.96147441613348,2.8233235864546296 +78,58,15,25.00933355,67.816568,6.528631266,62.91359494,maize,21.418969534149536,2,7.381552115301711,14.920389000514367,382.73875023411034,9.32175524470922,5,8.082137765739834,2.4411191940279298,119.70615759111739,3,22.81370437253355,3,36.59740104019218,2.2958042811275843 +92,60,23,18.66746724,71.516474,5.721667141,69.93293255,maize,29.79614445339352,1,9.157163231052252,8.015793974322424,370.5752005160799,7.763379453224758,6,19.390152421220254,56.43846684781507,113.78725148276587,1,26.338107092766073,1,68.43907762157919,1.4017927449612841 +79,59,17,20.37999665,63.73849998,6.644205485,108.5054416,maize,22.522206330197566,2,9.02671098190191,11.163760408298602,377.1710347331981,6.831260208833757,2,15.069717386741758,76.4686880037692,79.49257075458524,3,24.74026417240806,2,44.73949871069311,3.245678594622081 +91,55,15,18.09300227,72.61024172,6.376651091,78.96159541,maize,22.160503048245175,2,9.293838829229468,1.4510100140192295,429.06159045266776,4.086805039836934,4,12.19923480815148,95.38151251706745,69.08408655433956,2,18.874662198070002,1,67.90228913508464,3.0462904527202275 +76,51,18,26.16985907,71.96246617,6.247040422,79.84925393,maize,25.203148573763222,2,6.433505948475184,15.85709417488098,357.1869299069623,7.055220948922785,2,11.609298480327528,73.88308625300832,51.708005261829,2,25.516598302408084,1,10.017235240924972,4.376680365359048 +87,48,25,18.65396672,61.37879671,6.656730008,93.62039175,maize,10.829340209454173,3,11.10019077910031,17.25430693483319,418.16223693941504,2.519905146189566,4,17.489674137213534,64.99441265306486,107.80064147950878,2,37.45337343765176,2,16.907763631837504,3.565962259806854 +71,60,22,26.07470121,59.37147589,6.2048017,85.75692395,maize,26.51320154509817,1,11.597738092839382,14.285398465332989,424.2189782376373,7.035000808853337,6,13.335002940254167,43.88541612350818,168.15983659460858,1,28.23930788314811,3,34.16548714568012,2.6256857111032224 +90,57,24,18.92851916,72.80086137,6.158860284,82.34162918,maize,14.204733068318305,1,7.511533173691788,2.905776711170591,357.474461591779,9.399769903758651,6,9.965544067494417,47.73270601351744,157.48038245153631,3,25.107784176170966,3,31.024511334985693,2.6750850648405997 +67,35,22,23.30546753,63.24648023,6.385684214,108.7603001,maize,14.644122612068678,2,5.673489469340444,5.600581230132036,442.15246295058324,1.9791372467954105,4,8.06972771146671,32.76954018189521,87.94052548675873,1,6.1797061831319,3,40.395851596587086,1.1305515437416358 +60,54,19,18.74826712,62.49878458,6.417820493,70.23401597,maize,27.443328277090266,1,7.0227758283696,19.398892142744344,427.49133042636936,8.568352691783055,3,5.1944866892726616,99.42325184651747,129.96462966860045,1,9.082351746830069,1,45.15901144503276,2.778748669210386 +83,58,23,19.74213321,59.66263104,6.381201909,65.50861389,maize,23.877505842772578,3,8.630778875689666,18.8342416418148,357.11508072541375,5.080115727781574,2,14.17929793257815,27.71774119138405,191.1797472721646,2,8.449766810802368,1,91.71148311263607,3.641138981661048 +83,57,19,25.73044432,70.74739256,6.877869005,98.73771338,maize,11.318500163338104,2,11.435626370270699,7.367576322865088,356.09461966300614,2.8785687512696008,6,6.310274384944956,99.38119141134656,155.84674722568252,1,21.194100477537358,1,25.961516341020385,4.1695748950379015 +40,72,77,17.02498456,16.98861173,7.485996067,88.55123143,chickpea,17.883769647178593,1,11.450264999979893,4.008740762379432,395.28055616597123,4.175852139267892,3,19.538137648105852,84.21808845378636,62.12247787492086,3,4.404796040752751,3,88.59923610206668,4.254476758311473 +23,72,84,19.02061277,17.13159126,6.920251378,79.92698081,chickpea,27.05509317878045,3,11.878308835344855,2.815813462829515,416.79872503934456,3.5492663410517467,6,9.493220478777129,33.115625927157,59.70228819804632,3,38.376003590536605,3,10.03302386589996,3.3074601310830722 +39,58,85,17.88776475,15.40589717,5.996932037,68.54932919,chickpea,10.333280500159196,3,9.041096449529492,18.263061222799116,386.13435748404925,9.379368392579806,3,17.48414845121379,38.18364849343664,144.33491944585325,2,41.70058183062172,3,81.63991031531191,3.851439578709019 +22,72,85,18.86805647,15.65809214,6.391173589,88.51048983,chickpea,19.18716957882736,1,10.862606230986676,12.997827664757295,351.4241212028633,3.1866846001678084,3,14.222679441745441,95.48733256486143,168.05913500315523,2,15.505807553254819,2,21.00468699185747,2.2100386315009573 +36,67,77,18.36952567,19.56381041,7.152811172,79.26357665,chickpea,17.398324281444683,3,8.922137133747439,5.893234491062076,358.1794686531044,1.791547617798237,4,6.416859995791264,91.48492904898315,50.23297973155482,2,34.14144683496081,2,90.47890613120232,4.345380240659991 +32,73,81,20.45078582,15.40312102,5.988992796,92.68373702,chickpea,17.39105459619854,3,7.319758147818643,12.36564398197296,409.70989206310054,9.925365521257197,4,19.202486716654903,64.85282445111888,83.42295509022486,2,26.06788480960312,1,18.190323031668797,2.2424091079834914 +58,70,84,20.6543203,16.60820843,6.231049028,74.6631118,chickpea,11.86449934520388,1,8.081814024118177,14.878447975920647,370.4081608733405,4.925810774280869,5,19.776496152204473,44.93988022978135,97.3670638513376,3,44.804873553847926,2,38.356629199866575,2.0813051934295324 +59,70,84,17.3348681,18.74926979,7.550808267,82.61734721,chickpea,20.80520694710316,1,6.061224857756229,9.744177629074498,410.0611703463969,4.286936211416223,4,7.529905341424893,66.08846416181758,129.0487393455678,1,8.3351726033142,3,75.23177871738217,1.495584903540391 +42,62,75,18.17912258,18.90426935,7.010570541,81.84997529,chickpea,16.855989834990762,2,10.898630488599764,18.717924563063054,363.1426364063716,8.710450464452705,5,12.162733880735614,27.82289426330574,124.79094402149043,3,25.919149052211477,3,76.09638378851825,4.29157406907057 +28,74,81,18.01272266,18.30968112,8.753795334,81.98568791,chickpea,28.194534550199293,1,11.020217163366166,0.7799968800970647,447.0634882386164,6.056725288855371,1,14.811510345687008,8.822399594300768,173.177902240913,2,43.72478954001057,2,53.22983104157473,2.4906675296205503 +58,66,79,20.99373558,19.33470387,8.718192847,93.55280105,chickpea,16.377314990448518,3,7.635890092464875,9.437445560045468,425.5362394391332,7.386713056513713,1,11.378194844023954,69.61771392718562,65.75033912259543,3,15.156948915941621,3,81.55634329958858,1.8551940428244382 +43,66,79,19.46233971,15.22538951,7.976607593,74.58565097,chickpea,18.649005552034154,3,8.678865601523844,7.768842035716236,392.423243434558,5.365131182907307,5,5.705618739932875,25.220063562570328,60.04976133364728,2,40.5491739950942,2,45.7308997454234,4.71097589643278 +58,63,81,19.81344531,14.69765308,6.515499549,78.96514709,chickpea,16.226574044304257,2,7.22876068290824,7.443739948013577,410.8708708885899,9.552020013630946,5,6.047475623132928,88.80300646686628,62.91307477345761,3,39.73238481231317,2,37.463930604593074,2.1273331264862088 +23,62,85,18.97424756,19.5161216,8.490127142,80.7108745,chickpea,25.460091273127905,3,8.790221039706333,0.8594327021561554,350.6622023844562,8.630685339710386,5,14.114808216871875,47.11808907845859,124.47594178376303,1,47.65292702206749,1,4.679212126598964,3.1638571195329495 +27,62,77,18.19737048,14.71070537,6.576415562,70.18185181,chickpea,16.66349434113963,3,11.798227277721587,16.23998215243586,447.03713568310536,3.5209287062451082,6,9.98946651264458,74.21731936393104,186.9701868718644,2,26.09824113735863,2,63.82922923090456,3.304724950605886 +28,72,84,18.72963144,19.18197264,6.481783043,71.58010169,chickpea,25.13860270697137,2,6.405853888337862,2.0205707301865927,391.68579272168597,3.6945398232044355,1,19.192294390421086,29.547742012218293,181.73522221021398,1,18.598374367275373,1,1.2339438196844466,3.373610863005012 +50,56,76,20.99502153,19.8601304,7.966605025,73.50734019,chickpea,18.08944050167041,3,5.381545148145534,6.6143837410652235,417.65177523123464,7.204964914929031,2,15.415367684821728,1.6583647777020705,198.7702532837622,1,30.831104140378862,2,3.007494111957776,1.275783348432701 +39,71,84,20.28155898,16.39535215,8.140825437,82.52339655,chickpea,17.54654752624201,2,10.421650684226343,19.693778168217243,398.7713767979736,8.300831404175232,6,10.466471737214551,66.88799366818279,119.37304155990783,3,5.598804003963193,1,49.23509262359846,1.0235188723538826 +25,78,76,17.48042641,15.7559405,7.228963452,66.96980581,chickpea,10.381990876237381,1,8.906908007224743,3.502025459572713,391.4503141600682,5.4842219398852965,1,5.84610629395161,73.59436066405578,195.96949710013843,1,32.76586513338042,3,55.02836477689708,1.2074488799675613 +31,70,77,20.88818675,14.32313811,6.492546046,90.46228334,chickpea,26.23268612268895,1,6.773495330284018,2.4256667455555636,360.0131126772707,8.593841230060736,6,19.87231327063354,94.37990031520002,133.98925020705144,3,24.055575755674575,3,3.0274411275390767,3.55293876431195 +26,80,83,17.08498521,16.14565756,7.528599957,71.31007253,chickpea,21.293774850946086,1,11.451315272126987,9.412980646594814,449.1563024127249,5.542487017792919,6,17.465459830598448,91.76380435918469,148.4426525548937,2,25.571018422622526,3,48.01280467311895,4.158550148423668 +25,68,77,20.09340593,15.11279612,7.701446446,85.74904898,chickpea,21.98104519816333,1,8.018334118522745,10.399693722488198,361.5517261115133,4.94166353804454,3,15.437716441205566,80.56162600819718,80.62196063019674,3,19.915956391241274,2,33.31988911005192,4.5617193924396435 +31,78,76,17.57212145,14.99927489,8.519975748,89.31050665,chickpea,10.139559488060783,2,10.554765223975345,18.263015405896326,363.8349607590321,6.153485072967357,5,10.991956128483594,15.333488382127069,142.25218145025667,3,1.6576727114613332,3,18.863385517796804,1.2909300856064343 +60,68,83,19.12065218,18.43475844,6.620900869,85.52950164,chickpea,21.162223655975843,2,10.407197816131974,6.778676684972607,427.21444324032865,4.967753295665488,5,12.368024962474003,54.18300583042846,114.0648347834285,1,23.72703730082816,2,1.933926945659592,1.8985416627632659 +59,62,83,18.57665902,19.22008229,8.104396058,72.94940441,chickpea,29.24766608564906,2,5.3058959378719965,15.467749192548176,363.2005448225677,9.283973810705183,5,18.047567243311452,74.5183297311113,83.18937561577343,1,9.501954565086946,2,37.49928518284125,4.26523446281083 +22,67,78,17.16606398,14.42457525,6.204090835,72.32667516,chickpea,12.749978307351926,2,5.253313913059135,8.10095786697239,435.3218480028378,7.738915752519131,4,13.63673071192061,78.92375477593997,174.6626620899322,2,37.322627004007934,3,45.04181595380484,2.7486067592081973 +36,65,80,18.2872007,16.67921616,6.051091339,74.87445574,chickpea,19.94660520390211,1,11.595418776858759,17.22929785325301,416.1453279400823,1.5847319028873834,5,19.860237805264497,41.021404124268976,73.87188460178672,1,12.33402567788746,1,60.3362807784382,1.9280267901203714 +59,60,84,19.03025305,18.66725565,7.690962338,94.70992037,chickpea,28.161800463685136,1,5.35860840343292,11.400663377093297,438.4098402351785,1.2909767978774576,1,13.401840125739605,62.45447661167408,68.57463046394965,1,48.66431634249764,1,74.93251760419156,1.550002163350534 +54,77,85,17.1418614,17.0662427,7.829211144,83.74606679,chickpea,14.94566795676447,1,6.3237446349480475,6.741236943789364,350.99621521094826,9.448286237607052,1,16.62088061236865,26.402047907932015,64.467382468398,1,41.23216857144729,2,73.9864136818539,4.242441075863542 +43,68,81,17.47809436,17.93253975,6.761599706,78.92060234,chickpea,29.54751947015624,1,10.923211331694134,3.7618393865345467,405.73129251034396,9.97077940130214,2,14.158485156909475,30.606500410386218,169.62259056532866,3,38.19704692810651,2,99.51482648398853,1.107302768652128 +28,76,82,20.56601874,14.25803981,6.654425315,83.75937135,chickpea,25.714360682572632,1,10.15340488219801,6.063440970586131,381.3442113540241,2.3394109563618617,5,7.560792781918695,1.0479509166304357,93.7207325793955,3,31.941322136311452,1,0.05491132086610229,4.100617267436344 +42,79,85,17.22385224,15.82069268,6.129533877,76.57580954,chickpea,18.753296686393078,3,6.570016163496534,6.712612112472782,367.359147468591,2.8232673945691253,1,18.263335641553503,46.804244810117545,152.1965332537667,1,45.03297141381157,3,64.5887153801255,3.4038757953687373 +32,60,83,19.69141713,19.44225438,8.829273328,91.76071648,chickpea,19.77662735497598,2,10.750109900367274,4.695382677631397,389.42411117733184,5.054603037725947,5,15.443946159831247,32.47549497326977,124.81861532958483,1,30.408181559796088,2,53.25354332039801,1.1421687978299002 +22,78,76,17.84851658,19.09172907,8.621662982,76.32470713,chickpea,25.01219432511382,2,10.001646028101856,11.70782119875091,379.8787571834583,4.137592729944506,6,5.138515867656392,28.750413183602387,150.64694806976306,2,19.35536419441675,3,68.83013440351641,4.543012802391944 +31,79,75,18.8202251,16.1074793,8.204862075,89.73119396,chickpea,23.165716293534224,2,7.453287233131013,8.154860995882178,401.0335580904291,7.705392048729591,5,11.862147575474378,39.970107618720796,84.67342716065369,2,18.849226113348692,3,67.16875836034355,2.978148693555786 +28,58,81,17.47500984,16.54314829,6.18042747,93.35034262,chickpea,12.154437978074892,1,6.789587059964622,16.672068071226292,396.13297148696523,4.5995278560711546,3,15.010946080969953,53.567358759232555,64.51951608323938,3,49.7797569179059,2,64.45630231612026,3.41867786828623 +57,58,77,18.72649425,17.58406365,7.978996755,81.20176515,chickpea,11.480270533958736,2,8.983284085491416,6.388832413443726,436.5363175124824,1.3076643952658884,5,12.527163233589338,63.317287181715955,101.3488154533488,2,34.82675934639328,3,89.18276045752143,1.0702891185252104 +49,55,78,18.65580107,16.17772668,7.863113671,81.70769297,chickpea,27.452257018012133,3,8.888531866010904,14.39143305071456,418.85534874282257,5.062467373820615,5,8.694667989288202,57.74020376998982,115.37056598476032,1,6.4189904008074175,1,33.508788569727535,3.8552569435433797 +46,76,77,18.2356751,19.68538502,6.967843048,83.74879344,chickpea,13.26077622047605,2,8.617777166746158,3.83402629385754,402.0965807659653,9.022721388055322,3,18.517849301719927,52.28593901846396,199.66489031281287,1,9.876464874029601,2,84.68987868234443,1.4904177729925872 +54,61,77,18.81198127,15.21618225,6.206582193,77.5429424,chickpea,24.21622413859022,2,6.709050051320754,17.374977039241777,414.3662135720034,5.12771682691161,1,17.202816202051224,57.26210228964287,156.58038863375015,2,32.81091640804333,3,77.2448468258024,1.8303128510112563 +38,60,76,18.65054116,17.80852431,8.868741443,77.92798682,chickpea,29.994929058198117,1,11.580893894371329,8.083998947591962,381.4319783584996,3.970762390066988,6,17.31392837257514,19.28984363934344,189.12405210963172,2,19.855658392753533,2,34.910097682929894,3.951398292021683 +59,55,79,20.36720401,16.89574311,8.766128654,82.2545577,chickpea,27.082363240405464,3,9.78106760211675,10.683573288033912,412.4025684073879,4.068531778380055,6,19.781030483746207,94.17638438111074,142.9774075782269,3,11.978986265868796,2,83.35936421613933,4.662246593692281 +36,76,75,18.38120357,16.63805158,8.736337905,70.52056697,chickpea,20.394642863959994,1,10.543237515375498,2.6340826916566296,427.44508984548213,4.618156634145274,4,12.253886695549047,28.592842017516396,59.36474248096073,2,4.523720464597847,2,4.330072294778153,1.0527331983028447 +57,68,81,17.17012591,17.30457712,8.081095263,72.78624223,chickpea,22.790242154768656,3,7.622832633130679,13.277217812653122,370.41757936505854,8.342449188934314,1,5.9478421724018,40.11809546844381,71.38914955068967,1,34.272257114039775,2,47.479253011405916,4.555196851556014 +35,66,81,19.37101121,15.77458129,6.138243973,85.24819851,chickpea,18.080024986404176,3,11.885275629711519,3.2854104016273955,432.5100928196487,7.338427891294119,5,9.945728890602242,23.07550038345002,194.7114229527087,3,36.803754601197554,3,98.951339317668,2.656517606693801 +35,64,78,17.92845928,14.27327988,7.496645259,85.37378769,chickpea,22.78968891335932,3,8.819669834648794,12.09481971203267,386.61326036121034,6.9422815465434295,6,18.49712625938062,85.52825243705855,139.34980036646454,1,6.893873320551536,2,2.0558238807251272,3.3481021864237444 +52,60,79,19.45339934,18.23490739,8.380185271,75.6317566,chickpea,14.998251504511178,3,5.1044124309370424,10.161872166177151,360.4512676755268,4.937593045300533,1,8.021766292066356,87.1750003899084,54.331370934796176,2,39.49411297448684,2,93.85456717991644,1.616745132642412 +27,76,83,19.12829388,14.92241479,6.289614016,89.61857826,chickpea,21.541871460057635,3,9.293249155490695,14.585715958846176,384.25766453051637,5.067315382080652,5,19.16398949622991,56.0094689089333,89.09424079466251,2,32.06278950828626,2,30.338527964256834,4.430763756113667 +57,60,84,19.1034283,17.26184541,6.586777189,75.49101167,chickpea,26.772502737496232,2,10.74922772371636,19.822042342362703,408.3103542305387,4.7958284134777225,1,11.52580421177343,6.868434212639296,96.524692677145,1,12.114823267848374,1,63.088957441034566,4.574147963115351 +52,68,78,17.48504075,16.96070581,6.89655198,86.05078037,chickpea,24.623966249910847,2,8.109775352355642,9.509722211659684,424.1117933015513,7.346606373702374,3,6.1778462020034,0.9029841632646973,109.26400411388622,3,40.950019979554895,3,35.022759272092884,2.6365560044084133 +43,79,79,19.40751744,18.98030507,7.806747656,80.25064637,chickpea,26.38601641316534,3,6.450882076451939,1.1549250180758164,442.7906871686305,7.273742513464672,6,17.188720352450822,30.841273427081596,86.06635344157867,2,17.987291880327476,1,85.10389333549568,3.8818378743160453 +44,74,85,20.18649426,19.63719995,7.150681303,78.26039559,chickpea,19.628462685667692,1,11.976878669190004,17.700936230060265,414.3377381232037,7.636493633259607,5,8.572042500914904,22.691659831675693,73.09052931264073,2,24.663364657435803,2,4.442606007810479,2.9406317173914562 +24,55,78,17.30287885,15.15405941,6.64919573,75.57790384,chickpea,18.531786438595475,1,6.809877242010023,4.945121310852776,430.11625966861294,6.108061461286077,2,8.156431144484284,48.72939718403902,189.5251187999463,1,34.726262939185936,1,4.51277406671724,1.7132365773489244 +29,77,75,17.50361137,15.48083156,7.778591618,72.9446671,chickpea,21.73801419770495,1,6.0010352531995945,13.047696607894215,396.7579187759338,9.068331969319035,3,16.136981326205238,73.04698140420949,150.56251841857284,1,15.717363309940907,1,94.3862133483835,1.233230609621645 +20,60,78,18.17234999,14.70085967,6.358740355,90.7760707,chickpea,22.03614471986157,3,11.560346563847705,8.216987459771072,395.71468852391325,5.0304498265967466,6,8.09064454173304,59.805365613884966,197.42921699390277,1,18.879050223833687,3,62.230132218132674,4.9076588257096505 +56,67,78,17.57445618,16.71826572,8.255450758,77.81891424,chickpea,21.059925818094012,2,9.25351685429045,3.1646692989827807,423.9537522666036,4.359266400005853,2,10.437174184557133,39.973371102215474,84.70099219371035,2,22.934935262785388,2,20.173265540442088,3.177390512037714 +37,66,85,20.93175255,18.91295403,6.456148474,78.06910795,chickpea,24.822017865932946,2,6.279108826428956,13.937731639229218,402.3813582419315,4.40382497948817,1,6.998926575830079,44.61665719266937,80.68377651171865,2,14.80201568672489,3,72.36117605525541,2.2865142751285603 +49,71,76,19.71098332,17.63879418,6.613072145,85.57925437,chickpea,29.943211443255056,1,5.552101442312158,15.357823322293207,404.2258598551211,3.590525303563381,3,6.247451740513538,19.667948104339295,59.70007420660832,2,29.683939677412525,1,86.22439988452804,2.9245945426638635 +59,69,80,19.07937684,17.86754927,8.165359297,69.40619137,chickpea,29.0241727125213,1,9.49797946975448,5.1699533029442435,387.4626467983097,1.2967160895827083,6,14.53689580252676,75.79448728289555,119.74433700694674,1,38.53400849279675,2,72.59603353048021,2.9765829537138937 +20,79,77,18.54988627,16.02542689,7.64867466,76.32565249,chickpea,11.667579744366584,3,8.362777405376711,13.021733879508693,399.64971030590135,7.2086334904446545,5,12.056074007669194,24.32184327179201,123.37947873472778,3,17.14592330665935,2,39.243362091305066,3.381918499902772 +24,56,85,18.19903647,17.41333199,6.545888558,80.6405403,chickpea,21.800074137833455,2,9.975782300155924,6.182987465415881,400.3889374474538,3.6579556972777723,4,17.908771661114983,77.86972155525973,54.30901306223228,1,31.181639561501207,3,45.34785000785653,3.7107508224793837 +51,72,75,18.88852533,14.99451145,7.104224797,80.1113384,chickpea,18.067476026225933,1,9.113804472904881,16.189198537386485,388.0564188988351,5.797137148817807,5,19.032606613544026,37.86435988844825,175.87754398196898,1,15.524798271401712,3,95.35553531909812,4.73147263985435 +57,73,85,18.49311205,14.72115044,7.358099622,91.94595352,chickpea,24.037268835277033,3,7.3850957630104395,15.690749108817098,353.548727718767,1.0715680112887727,5,15.045096317237084,33.676846936167216,117.32934165021186,3,44.44932814010678,2,39.925280914533005,2.56867121154816 +22,64,82,19.48974337,17.17260319,6.4740245,87.51312796,chickpea,14.23907603538938,2,7.166337739065072,9.035475584384248,364.1550525156102,8.82953222632951,1,9.284375156753711,31.635749428267868,190.95066355422324,3,16.85821321468648,3,72.85928563426684,1.9816934582464287 +52,73,79,17.25769499,18.74943955,7.840339389,94.00287214,chickpea,11.8285937485254,3,10.377411334696063,4.345740564364067,439.90367424049504,2.6967908007056756,6,15.066569698868319,56.10461154252149,93.50844436839998,1,24.028511899407256,2,17.89926919969985,4.935058237322844 +29,75,75,19.62416326,18.71483156,7.064790365,88.4585692,chickpea,15.897071974750855,3,10.692051704546593,2.5148352147937936,444.8943044003912,2.1716721951593843,4,13.800550372816897,1.381714488530561,98.16274814973391,3,32.50785587732725,1,79.35040092523936,1.536636292259733 +44,59,78,20.67526473,19.85388984,7.599033472,84.78344008,chickpea,19.947621318367467,1,5.06580304569626,12.103503343547743,444.81532222968514,8.745087331215563,5,6.385149119274406,44.32343257649044,162.31865569220687,2,3.716450900550805,2,12.423317014506342,4.244842165443473 +41,69,82,20.02381489,16.63294455,6.715587232,68.97806542,chickpea,28.250741125292,2,6.918300610057977,7.1339305723449975,371.7952683514761,9.644479873220309,2,11.276553952503424,39.822183980201906,143.98815300871024,2,35.647373133767516,1,23.948880802495054,3.3085353644119397 +52,56,85,20.1187446,14.44228303,6.81712422,88.68168643,chickpea,11.656547185356438,2,10.4822757686118,15.711767524414558,364.31288270478854,3.9234126298946763,5,14.953278737396838,79.01579291080839,197.01967181900739,2,45.74301912923368,3,48.79919369825747,4.46227898298532 +34,76,80,20.65691793,15.84572566,7.985417393,65.23811143,chickpea,29.979965983368174,3,5.4670364657330985,14.213162516176608,394.8182140165517,1.1038859824349625,3,16.95776187219184,62.38827838095349,126.45468590001254,3,43.39608271486477,1,94.73458253173692,1.375606727669584 +42,74,83,19.2582557,14.2804191,7.545258424,65.78042032,chickpea,27.393446589245826,1,9.602328608798254,12.273020487978396,415.5909006385842,2.3968835784240206,3,7.188379502947527,71.19900806439094,53.935291120351756,3,37.32316481398094,1,56.84241005350885,1.3948924059825334 +34,71,79,17.927806,15.85622899,7.728998197,74.63872762,chickpea,26.478581969419828,2,8.966390791268909,5.328551803833593,392.708684942077,6.682422762463133,5,9.211589209886675,47.37313922383727,116.55764357221682,3,23.826744858387872,3,29.534592474233257,4.895861790931178 +27,73,79,19.16288268,15.83500655,7.354973451,82.69766829,chickpea,26.67850111240156,3,8.181205351753285,19.147162490050594,353.8579363574177,7.07900220686989,4,11.416063437046578,51.2371668641043,160.1714048413134,2,32.62732094034127,2,61.1928752093302,3.2962393628241973 +30,70,79,20.26942271,19.96978871,7.313122235,69.64449182,chickpea,19.738529986535294,1,10.81564759099324,0.038107266674445306,378.4954209806302,8.556879435010451,2,19.380378214359432,43.66419712766961,82.32626339444275,2,22.19490124653591,2,88.28464864146562,3.166157637840389 +57,57,75,17.09104223,18.25142068,7.785039076,87.27444866,chickpea,28.383699425159037,3,10.277683329522995,14.727418553203044,389.14465669767526,5.528464580441527,5,12.911504026779463,29.07413498105963,171.68021322194417,2,37.3084693850182,2,15.831080804176045,3.0490942624768955 +27,79,82,17.06579293,17.54024066,6.307004923,70.87150577,chickpea,13.357899296620008,1,6.742360603458354,10.577598122345044,400.56072957554636,6.218240962356194,4,7.798792878871041,68.90982568302124,111.93346662648949,3,20.32570088026412,1,9.496897998917964,3.3623404822215175 +32,71,85,20.62767492,14.44008871,6.403982316,92.06630306,chickpea,26.94225128596082,2,5.60519369766758,19.356695033037187,441.32254461885776,4.011070489191036,2,14.578949129705629,28.675876886304152,63.16795283211216,2,28.45687245436266,3,97.54006089629037,2.1973555173970656 +31,76,82,20.8248451,17.85057083,7.599279991,79.20509212,chickpea,12.77954673106138,3,9.99751534602387,4.7095459202198775,421.8805448358208,3.9352307160006337,4,12.575815285817331,61.59077512151969,135.55733176017026,1,46.837568688207064,1,53.07589838369149,4.160453588353617 +33,75,84,19.46210401,18.72831993,7.217018459,68.81405149,chickpea,27.096972090022707,2,11.139780759827515,4.29079893966585,372.89853208340304,4.729604373858946,2,19.622742721327086,67.0398326585035,161.1763131651524,3,47.10939508790569,3,95.0298103848115,2.7379722699329965 +47,80,77,17.18248372,16.42891834,7.561108006,72.85017344,chickpea,28.901180479380105,3,6.194122276807066,7.28659022012176,407.5843319159114,1.734952506599186,5,9.867624322237404,44.71999901031345,72.60430392587723,2,19.914144095388707,2,71.01680187825711,3.2574921904080605 +54,62,80,17.48911699,16.39055394,7.489545074,79.45758333,chickpea,13.320788058022774,3,8.28679838193019,17.709909465858225,365.2646258124796,4.593647019650798,5,13.142181692018003,96.80566279680207,174.13136626885782,1,12.411896697916841,3,44.19784943857603,3.5850183919945056 +47,79,78,17.48395377,14.76014523,6.609696734,65.11365631,chickpea,27.295604427348373,2,9.313509896804556,5.512746680377834,441.1628244809847,4.040683196425894,5,19.0899986927827,21.188772063302576,136.6628479939916,2,49.25603412813157,3,69.32193499628833,1.4752834240243193 +35,57,83,19.48316794,17.44534641,7.476800943,80.4986291,chickpea,12.970284474954587,1,7.748139292188885,9.13463713950448,436.44334394010855,7.870700526982329,6,19.383521302963025,81.95542228250365,50.954417880407,3,21.239167833863743,3,16.360922408510092,1.6731246778214448 +53,73,77,19.71359733,18.09665739,7.325451279,73.64476535,chickpea,28.483221209014367,3,10.16746638856756,18.910283740944738,383.31553500517555,5.746119021266241,3,8.756709673821002,21.636779426444242,78.15760186642788,3,28.895495531649168,1,65.18168545370106,2.7846347274877954 +45,61,78,19.48649305,16.06240074,6.489389282,81.5284269,chickpea,22.84477059004342,3,8.671086425745088,15.886427846506027,421.39652925228285,2.2161530325290575,5,18.066052688847243,24.98202532059447,72.44552108567827,3,19.88342270384856,3,9.489527802817188,2.3725706962308113 +37,78,79,19.95264829,14.82633099,7.786366322,88.6810311,chickpea,14.90137643538507,1,8.870745425614995,16.311546794430157,353.41805003064263,2.17605794976177,2,13.7367972884811,64.77856576813026,127.87082240192697,3,13.662100147944123,2,27.078443637245396,4.539683666397325 +30,75,81,19.41789736,16.80472243,6.408437886,68.4951189,chickpea,11.284287837910371,2,6.692229384622463,15.073056700417588,416.2721851459184,7.725692891917048,3,6.7737566418159485,14.865382099531022,117.70349254221927,1,18.472479201934906,2,75.26848304306128,4.295481286566759 +37,55,82,19.45591848,18.02235902,8.423873703,78.44910564,chickpea,16.865705857598286,2,5.453685934157896,14.22018801676639,366.7393557548683,1.7952618037915802,5,10.331295895379824,85.77149912167982,55.61326172153046,3,13.148044024296695,2,82.91241152596598,3.024963024012242 +53,65,76,20.19137759,16.41998269,8.719960893,77.33795356,chickpea,13.24948353816419,2,6.27764189651712,16.129956469425334,449.2043745512914,5.148700455292939,5,8.814358560087827,88.8643409824953,108.9088756920103,3,18.8893402706219,3,34.64980437045393,3.2243447722358236 +22,60,85,18.8392908,14.74071856,7.811997977,94.78189594,chickpea,11.531019543181118,2,8.719907736566812,10.95381276885077,385.68783350805097,9.357082804345456,6,15.466187795103231,55.69802681900854,169.08993056819628,1,4.12780122100111,3,45.01060714218049,1.9311992547581731 +60,61,78,20.71219282,19.83643308,6.317153205,94.03659867,chickpea,11.107673771458952,3,11.329621805341105,13.37876285161759,377.5738891198305,7.662949766429506,3,16.09784138955446,85.66741800375823,95.44522793873355,2,19.848735896650698,1,51.764659087844244,2.9986718985926384 +42,67,77,18.99424448,15.9362937,7.114405288,78.69707199,chickpea,15.17605479052179,1,6.507387069401224,12.092064585428178,372.1267453975246,1.0857357981999942,3,5.572470770081403,67.64023822917551,188.77076259945255,2,47.45741402286928,3,30.920927474762582,3.1895135713595177 +39,76,76,19.96837462,15.57324389,8.135900726,69.15759062,chickpea,22.35165870933058,2,6.705466281529003,6.854969554048944,379.66509306607804,9.950583145807352,1,6.083519936165048,78.48669415073385,168.93573909544585,3,35.96179276276575,2,95.71311968311385,2.411262909731253 +35,63,76,17.81564548,17.60756635,7.714153038,90.82097601,chickpea,22.86506219543244,2,8.562131836120892,12.37534192699465,445.5145991858032,7.242635841534607,5,19.386381920212447,41.908834460254354,132.28741642007822,1,8.148822504837966,1,44.14971996991248,2.165712295561095 +30,65,82,20.71424384,15.27824066,7.103798069,76.77888672,chickpea,10.755513474047405,3,5.103020707188653,6.329701225232349,417.7462599925155,2.5486233514651806,3,11.994878715822388,93.44835440267599,152.22147269372917,1,27.044044326745155,1,37.58653143248838,1.6682542867978989 +57,56,78,17.34150229,18.75626255,8.861479668,67.9545435,chickpea,24.775945985932008,3,8.89286478561668,6.6709417640984,406.9590440688067,6.803641433200147,3,5.884098929968645,58.06263400753764,79.10303559723077,2,32.54091331001322,2,24.148037816860757,3.517111668376066 +48,65,78,17.43732714,14.33847406,7.861128148,73.0926704,chickpea,26.67336072523073,2,7.532076310431481,14.435938323228658,416.76434959592757,3.8317839119759354,6,13.83142161773423,55.81753262004493,95.00626343603635,3,39.65051744082343,1,57.718758648235244,3.9843006645407133 +36,56,83,18.89780215,19.76182946,7.4526709,69.09512477,chickpea,13.838533161313347,2,11.090525679529073,9.135163915924757,385.37795310339203,1.0370879489545077,5,7.109190760200254,15.466718504380594,87.8917147529627,2,23.040592225554747,1,97.91862091978543,4.887664829804454 +40,58,75,18.59190771,14.77959596,7.168096055,89.60982451,chickpea,16.847427570162445,3,8.95693572801621,13.813972834958118,386.4246741195086,9.736556900052769,2,16.236014291055145,38.96876783618588,170.19914880503742,2,33.17205453163092,2,47.78046586734811,3.0548790014330405 +49,69,82,18.31561493,15.36143547,7.26311855,81.78710463,chickpea,11.817008069604121,2,9.983741398127764,3.72551643090566,351.00278533495595,6.853570360196736,4,8.056156524932218,0.42233403530306246,97.46521532639953,2,37.93690685575367,1,40.623162030357584,2.9743256010311767 +13,60,25,17.13692774,20.59541693,5.68597166,128.256862,kidneybeans,27.512468581866077,1,9.51763865893584,5.079438167852535,426.6788016098191,8.67648678749785,2,12.053498579678289,69.17307590236025,111.48571649144034,1,22.48715713675571,3,0.8898600636219167,3.6430758467098956 +25,70,16,19.63474332,18.90705639,5.759237003,106.3598183,kidneybeans,24.96345299589259,2,6.231957785769042,19.755397300613357,352.227210564526,8.216576162766309,5,8.925164988687051,61.752278833210305,196.43177752092893,2,43.89646255403537,2,97.72894797864235,2.6654565724375163 +31,55,22,22.91350245,21.33953114,5.873171894,109.225556,kidneybeans,17.819731660719924,2,9.820474927772086,4.074698874551961,409.8125315782006,8.212299479078375,2,7.166826458341379,39.75861810136381,144.96347686198936,2,1.4951797795991206,2,76.76771874469912,2.501529534706752 +40,64,16,16.43340342,24.24045875,5.926676985,140.3717815,kidneybeans,25.651885974975315,2,7.690638875975539,9.566861977978707,396.43427029396656,9.52141924306254,1,8.559228963108206,12.564163735655033,148.3329136887229,2,9.757608685232338,1,93.89634676888019,4.3244265535518505 +2,61,20,22.13974653,23.02251117,5.95561668,76.64128258,kidneybeans,26.504125760169867,1,6.882398075708998,5.003979276946405,353.2220330655377,8.099101928366357,5,19.513415420315063,77.16387143755196,129.83547481040168,2,48.81378737722203,3,71.97717208510502,3.745214112505995 +26,65,22,17.84806561,18.77621951,5.949949081,143.0984171,kidneybeans,10.02426042785048,2,7.985858574754115,2.334916088242671,426.15200265550516,7.491014159146264,2,7.416228745136397,79.62204538303598,72.4624461797907,3,40.62007937803436,2,44.00618661019382,3.246431419554556 +17,57,21,19.88394011,20.31564139,5.789214289,60.91974792,kidneybeans,16.541197372535187,2,9.801117924182222,7.021644099906704,350.23251557818054,7.0002545963959095,4,9.439151631851555,59.98134784433788,77.50585042481956,1,5.96468717447281,2,42.747793877435534,3.306133142094209 +26,80,18,19.32509638,23.3334788,5.581021521,104.7783947,kidneybeans,20.603631039841375,3,8.10385608287084,18.202389887865394,428.9131703124481,9.016377741120369,2,13.225161982995917,5.3734039376570175,93.15715494443177,1,19.110674185156682,1,96.61824407150141,4.514289968223743 +17,59,17,18.4167001,23.42829938,5.689858133,132.9801054,kidneybeans,26.774617745085003,3,8.691696276342428,16.658179973693628,423.05915011134505,5.527089099084947,1,15.600299101848439,88.8448622957402,187.89880840317812,3,29.071251220413853,3,32.915348262842905,2.097447946257074 +27,59,22,21.81167649,23.20591245,5.794158504,130.0608093,kidneybeans,18.425932510115622,1,10.147254544800163,14.871133659263414,377.45792000289845,8.151395236938576,4,5.710080187805279,42.16774630447002,54.301752600177146,3,49.608117307012435,2,8.095278372948478,3.4583032103799356 +28,58,24,19.72702528,18.28173015,5.748190463,143.7630894,kidneybeans,24.964031578852484,2,6.647306472633382,3.5852700220927747,391.48191593438673,8.313992460509617,4,15.097344293602976,94.57030281686973,121.03178339074496,3,46.65186634596271,1,91.68471637793031,2.7420847301608986 +25,57,19,17.15432954,19.87070659,5.566522896,87.99669731,kidneybeans,13.665492297792216,3,6.863102874634539,14.360847004647791,436.6565185777047,1.5731094840819644,1,19.18412348205738,57.365928519711176,58.408861770979684,3,49.518164909454086,2,4.432372028135855,1.4123090099577538 +28,80,17,19.62207826,18.67170854,5.809419584,144.1567454,kidneybeans,28.832605636216627,3,7.29836308144607,19.37008974873278,387.735687781983,9.656784904016089,4,6.792739945280974,5.951117689842144,158.52983957528153,3,24.958203789485207,3,23.278540959691295,1.8968957370157251 +25,60,22,21.63149148,21.17919701,5.887263027,134.3649948,kidneybeans,16.42780693223697,3,11.425776339303894,0.7916443983891464,441.78424862329877,5.855922741927069,5,15.6555585516641,94.73907299687858,156.23339403126585,2,9.673273360125595,3,24.383215188259122,3.6788000179508273 +12,78,23,16.06522754,18.72479695,5.99812453,88.06638775,kidneybeans,14.433044818296704,1,6.712534135317046,14.866213728011783,398.26558862864454,6.248739520014743,3,19.314035332101977,11.947188569120959,98.62938788443682,2,37.65504860400382,2,18.36143055853744,2.0519498471598614 +6,77,25,20.61162204,24.36314135,5.792744849,69.63833855,kidneybeans,10.805769979411888,2,6.061166628155409,8.841607170248885,389.2394648345179,2.7059173765699454,3,7.374789279495772,57.81940592659522,50.515415908905894,1,9.03447217479702,1,40.423921328160496,4.932472004767453 +22,79,17,21.42451099,20.39659714,5.912289889,116.5206923,kidneybeans,14.151682493405776,2,10.600961754558448,10.000207584868823,377.14977679742304,6.221452094595055,2,8.411626508001552,75.99078301754139,95.35114280105623,1,21.194788657693593,3,5.399964479848263,1.2255621921110782 +27,80,15,19.07096165,21.21092266,5.788386951,86.21917578,kidneybeans,28.75197472595325,2,5.356433858095645,5.549910977295733,376.7975400024399,3.6643912617873555,4,14.212065037215803,16.411257491631627,118.48322037922809,1,37.323905260776236,3,25.75968849566118,4.241539636932313 +10,55,23,21.18853178,19.63438599,5.728233081,137.1948633,kidneybeans,16.017119950826903,3,8.115663155614389,2.531698604005108,354.05840249070866,4.863938863798946,5,11.871470998866153,18.626052095979485,131.59770333561914,1,8.496116953152983,1,36.688059027811036,2.9989138583474153 +23,65,20,23.0429097,22.42610972,5.833940084,108.3684316,kidneybeans,10.080568134375081,2,6.738603884521762,4.940401335009799,430.5793671322704,3.5709955694792055,2,9.560659067789214,83.51429876538478,186.9055514633362,3,18.53726217511485,2,15.639886946926119,4.439428815447364 +19,78,16,20.65375833,23.10538637,5.967533236,67.71768947,kidneybeans,13.94424908706781,1,9.270579755233387,10.270693106869755,365.23049751585535,2.4339983989857856,5,11.863002920794692,92.6823007422312,154.4853158709322,2,37.19654155776618,2,13.310989529387662,1.5112111381733788 +19,65,25,18.09551014,18.29318436,5.625096446,144.7902323,kidneybeans,22.913729556605283,2,11.927801913708018,1.5346433068650955,431.9775546455339,7.715514386878776,6,17.08054552055806,85.899408088922,128.89512448422803,3,6.190504312857447,3,82.77388663332894,4.0267064384994455 +22,70,19,18.23775702,21.07643273,5.515615023,69.44951585,kidneybeans,16.616754025087637,3,6.240834048179343,7.235828929446784,415.3513486583844,2.8243276018225867,3,15.813808058464408,12.254951181438955,184.06290632078964,2,12.264436171628285,2,78.95900113990369,3.643342239305256 +37,64,22,17.48189735,18.8251973,5.954665349,121.9401369,kidneybeans,20.707059976696492,1,9.764585715432307,12.934515143375682,366.0321174549998,9.457081057289917,2,16.359284555590477,40.60090224835089,133.62653435192448,3,22.8115884369572,1,18.702851334627525,1.7485326177693472 +11,71,17,19.9191786,21.47324158,5.74644777,82.68554379,kidneybeans,26.759698719054498,3,10.907205820924217,8.139994230300463,401.235705432997,1.1819446803398446,5,8.618731230429862,50.396694908526605,179.133721699248,3,22.482809968992463,1,17.956282883717634,3.9406779454059317 +18,79,20,20.27514686,23.2353604,5.877347515,139.7521543,kidneybeans,14.442414784704404,3,5.105040889933038,5.096240153482126,441.0449488566787,4.082531108788829,2,5.181299304564206,81.10348433700665,96.02627961201023,3,49.367963802748974,2,68.89636947831053,2.2596444420468567 +21,63,17,15.77370214,19.2303162,5.979973965,108.3441414,kidneybeans,28.342690862912963,2,6.085685308283894,16.77901980162781,385.1189359205954,2.1408435114124127,5,16.839072419253437,43.559413613401965,188.2341126004427,3,29.9238499644166,3,69.2043942410171,2.87299513222695 +24,80,22,16.71170642,19.17651433,5.635993966,96.77285817,kidneybeans,28.269491819788726,2,8.055430218961114,11.862198674733776,444.16253713527817,3.5882861117206803,3,7.805995280349702,58.07847311485015,60.38948498563418,1,47.10502676905361,2,75.44260769888038,3.676829917311401 +34,60,22,17.66148158,18.15302753,5.635231778,100.6711761,kidneybeans,12.453555536647421,2,10.28226541678447,15.087531056652978,442.1475950829363,5.623478932786681,4,13.02975300041788,96.1328131608065,137.96694805741024,1,27.24226209630482,2,90.87213735548153,1.0461858704080402 +16,75,21,18.50692825,23.61670065,5.679224346,87.0513289,kidneybeans,26.089262704590052,3,10.895625782799513,11.639765432489167,412.5373434104419,7.420667728414605,2,8.478721409080494,23.38495369648017,71.51163528374308,2,30.054726175321644,2,45.09125890593118,3.3366405545116526 +17,77,23,24.51324787,20.81527638,5.670062975,64.19497947,kidneybeans,29.59891522667612,3,6.881596136998308,5.829400501126997,396.7110919595346,4.747525614127909,4,8.556008376552274,3.3658861848583688,183.56553503648823,1,25.52351568110463,2,55.23343305698195,3.971081543496998 +37,72,18,18.87614998,24.54038287,5.724242065,105.4120514,kidneybeans,16.071083939229037,2,6.714515918607777,4.48156033975417,389.5834665160461,5.407496563621576,2,8.804789560002252,22.211874805638477,187.63634955184077,3,31.724008517560854,2,71.67767282108495,2.8514146818898793 +40,73,20,21.59343016,20.31871249,5.811314232,61.13872036,kidneybeans,21.10292381552731,1,6.669653225795148,10.08302622842595,391.1300221480855,6.451714999786911,2,5.88138290212627,31.651001931689905,126.3408696871014,2,33.00920481317985,1,2.004239979225042,3.832683679290506 +9,77,17,20.12373284,24.45202552,5.783425416,106.158201,kidneybeans,25.961731636860627,1,6.425579999300101,11.985915798707246,441.10994482999047,7.202816295930529,1,6.146769357555797,68.81630537634918,197.95740865981153,1,24.328602403715742,2,93.73064470111461,1.648076659747952 +1,62,23,15.43546065,18.37477907,5.607808432,139.0302034,kidneybeans,22.59439814917546,1,9.322978023907536,18.01900333294743,416.10350550420264,6.815284278482407,3,11.453718416591212,29.420409469046895,56.04815367115822,1,24.202406867462017,2,83.84656799400054,4.65990656822642 +33,59,22,22.64236876,21.59396123,5.946999529,122.3886015,kidneybeans,11.968096013573625,2,8.692602329140563,17.740115393218737,407.67702215315853,5.972307285777767,6,16.840705606211028,48.70216635798267,197.85529415491678,1,31.09104827543325,3,90.1951639349122,1.6579424927215287 +23,59,19,21.98560799,24.87304788,5.852046999,129.5650601,kidneybeans,29.11433231879194,3,11.431351510873498,2.717596056797844,396.05084688819414,7.2402019480030075,4,17.28044392319866,23.51005810995619,146.87665706891502,1,47.586735810474714,1,84.6166227041781,2.1117362819285264 +6,62,22,20.53052663,18.09224048,5.824090984,120.4509288,kidneybeans,15.118545496371134,1,10.690606846019747,12.314004532827248,406.71985990295485,8.548523896934508,1,15.164022803636726,11.375687885418595,89.51464708720701,1,45.2743383074135,1,80.07291499408315,3.2279843128492924 +25,63,20,15.78601387,21.14544088,5.502999119,95.17028129,kidneybeans,24.583990699880474,3,5.358735290398668,3.3944447821310075,443.66468812859944,9.249272351087152,3,12.901841361786635,54.90289908324527,57.4368363577773,2,15.076130068407096,2,69.72955767330372,4.010324404174373 +7,79,23,19.6365349,19.68751084,5.821649914,96.65888933,kidneybeans,16.475552973043193,2,11.603880994249158,10.582564556514315,387.9772769234488,5.378594805130065,4,10.914143727925147,16.97532337852016,137.4364929841551,3,14.19923380612394,1,8.720474941506895,1.2348328463976554 +8,72,17,20.57341244,19.7520218,5.711439256,87.87869161,kidneybeans,16.247993900123042,3,6.986280007841963,19.831162465397224,412.1377493919506,9.579262264464043,3,18.72389696365461,65.56989324619167,104.9010500758297,3,7.180117472143505,2,73.0591027911533,2.0174111864995075 +27,64,15,20.16080524,24.84207559,5.514927264,138.2362122,kidneybeans,29.87351889473596,2,7.703079497809844,4.785757731167129,354.3321177945103,5.274333424001637,3,7.504839696234772,62.8978922310107,79.82206258496839,3,44.283197846937945,1,72.95802840371158,3.0254208741628363 +28,66,23,21.53989176,24.25386207,5.99616119,120.6913038,kidneybeans,19.825321903378196,3,11.744196656530406,14.534320826123587,400.61991106646667,7.097685324427192,5,17.41933497260741,95.82016815133184,52.911512518917974,3,19.296164090344803,3,33.81372900755658,4.839321921772445 +32,57,18,15.53834801,23.75560241,5.695422863,107.3850593,kidneybeans,26.760166422245693,3,9.445503943479054,11.954373418771802,442.9431444410603,7.564046966222799,3,10.715206281214044,38.82550121377331,176.805006972234,3,47.79051041700166,2,75.29313209920417,1.0523971783484702 +27,56,22,19.91853092,20.70099804,5.833010958,108.6434544,kidneybeans,27.762002719534,3,9.717147211681972,3.1421353116979134,445.15764537349696,5.036793551793523,3,18.77016609480195,50.563510147349,171.5160235737656,3,19.444039032078763,1,29.56478874541294,2.5575673532700502 +17,77,24,20.76952209,18.93146941,5.568456899,109.0193712,kidneybeans,26.19833580529604,2,11.375126126669134,12.809806399520713,350.0013849350592,6.44599870645958,4,18.562612146315303,77.09097344279195,88.04408120752419,3,18.63601255972646,1,0.4831164426460144,3.027068012867183 +0,65,15,23.46168338,23.22197648,5.645435626,95.84253438,kidneybeans,20.472434517738375,2,9.20042010966154,0.9658739835947983,356.9808787859431,6.595151563930153,3,18.35964206670819,77.76104344095715,67.7232105892359,2,45.89245799828209,1,8.416721483964562,2.938920604708963 +13,72,21,24.32116642,21.0278674,5.821194486,60.27552528,kidneybeans,16.07083494731713,2,8.022550311769695,17.413288349219194,381.77534065605784,7.4633591120692735,6,14.100064728811102,78.77068236299164,180.01543751970692,1,12.45522379982284,1,10.102844384145527,2.785245165206519 +34,60,23,20.12574053,24.96969858,5.659254981,100.0497183,kidneybeans,15.691226228148736,1,7.049113752307047,3.4139689070875012,415.672638312396,1.4675158454856896,4,11.178304280691236,89.59746357536899,168.04943957666626,2,47.32805580812385,1,45.22754995092894,2.287396791333346 +9,80,19,21.80619564,18.57086554,5.945465949,125.0972687,kidneybeans,21.35430384488906,1,5.267650819517367,6.949317734578752,351.8146417893083,2.203888833790767,3,9.01374591323204,4.572489845082927,160.57205317923018,3,38.33140444602739,2,84.96883342732872,1.4003088916288635 +11,72,20,19.52226241,24.92607153,5.951177452,113.334026,kidneybeans,12.028981062864597,3,6.435084233850315,3.492145542937428,352.64004784354523,4.645756932635414,4,8.05097137814689,17.686227862486003,110.54581969752701,3,30.279356766834265,3,74.12291396698114,1.0114976704307521 +3,67,24,17.00067625,19.90790546,5.520880014,103.2926407,kidneybeans,22.44296685810167,3,9.048272961709976,5.497398880365458,445.49376041465916,3.0673513539678803,5,16.81385190100513,98.77575323835335,59.881271064007976,2,16.026777621528666,1,60.66309937631712,3.0036341556557775 +35,69,23,16.78791503,24.96881755,5.578410206,75.45328039,kidneybeans,18.43342367784564,1,8.160651296465543,6.47679244513343,412.06765807503984,2.854228612996849,1,19.40137663852155,20.224677558739323,175.89477094436978,2,40.16055340946384,2,12.578947344743629,3.348624425576983 +3,77,25,24.84906168,22.89464642,5.608165195,62.21292186,kidneybeans,13.942790975596775,3,7.763281390362714,17.689362511002997,386.2634944140779,6.720948590811908,5,8.75103530592644,70.09988989452744,82.90189296034364,2,0.7782541988605507,1,79.252279971044,4.722406545361399 +23,62,19,16.51783455,20.4555596,5.609435128,98.77794225,kidneybeans,17.946604234832,3,11.837854966891822,13.398142341938552,419.6882018006906,8.643223232367767,1,16.14970421084289,29.736994967634654,138.02610245594045,1,13.148375072726848,1,80.72789538020935,3.7715164059409947 +22,71,17,18.15300153,19.38602098,5.509295379,107.6907964,kidneybeans,18.922862175409264,1,6.7599504219788695,15.797934809524543,399.7032078540656,2.452806052527011,1,18.572712697507516,4.001347049031201,68.99984517438678,1,43.932144340009884,2,36.57274253048194,3.97612537240174 +31,79,25,23.18864385,22.3104551,5.902033406,63.38208822,kidneybeans,10.971919704895674,2,5.285055627683736,11.373709564668504,435.43268551919294,7.072003085363502,6,11.554714840801967,6.779701711148323,147.26429655008639,3,47.74448602994492,2,17.00948274452522,4.360350780667897 +34,59,18,23.38002569,21.98879437,5.744117663,87.66898664,kidneybeans,26.566190855190783,3,5.0334836976033825,10.918413419379977,383.90896346854214,2.6518020059753207,1,16.37905881130497,27.494888037222744,104.78459305413753,3,2.8673555452168706,2,56.499965792646144,4.83469925557122 +12,63,17,18.358923,19.37703396,5.717143397,138.414764,kidneybeans,17.618195055144753,1,8.27509103137341,1.5843899009709372,398.7879534502884,1.9597093087420334,3,10.8124817090585,25.393216768000425,80.36856868382287,1,26.79289810693883,2,88.12283542803235,4.439533903101626 +27,56,20,19.25975367,20.51346956,5.542690119,94.9533526,kidneybeans,21.357883024408736,1,10.562753713053196,13.063349479867332,438.0898894077818,9.114575768465874,6,5.630244658003872,4.357310345089138,101.67694295009825,3,26.490806597263955,3,93.19343936986819,3.168137457139605 +7,63,24,22.95458237,24.03553105,5.858617867,107.7315386,kidneybeans,23.469396541671507,1,7.931817148431566,2.33222581890844,371.47719054216145,4.37062844026503,2,9.5062520021397,69.21266795930427,72.6506806558766,2,19.99749579955548,2,96.18029441319787,4.70471159185284 +24,67,22,20.120043,22.89845607,5.618844277,104.6252153,kidneybeans,15.453491204089453,1,11.132326674919137,1.9230573394385209,449.17927631874073,7.603244919449663,5,13.787007625259815,47.694445873545114,138.30996178062065,2,10.055123204346161,2,80.40645151796045,4.506973483327021 +11,71,24,21.14011423,22.7182355,5.606620346,141.6056722,kidneybeans,11.895561105974524,1,10.697693358897613,5.960178799407753,405.9554037454089,5.167021690906626,1,8.611566612612085,92.29896433024781,75.31009670732925,3,10.020322211924288,1,59.79990542444554,3.5817709609315482 +37,74,15,24.92360104,18.22590825,5.582178402,62.7089169,kidneybeans,24.542975289438644,3,5.853525152135354,5.588424938449474,404.9567011101793,4.748465461046923,2,14.269320540813183,78.44089014797953,159.124328827684,2,8.306636352988994,3,73.22274651076542,4.646217535156312 +25,76,24,15.33042636,24.91506728,5.56503533,135.3315583,kidneybeans,24.83407740527442,1,5.968727018086067,14.397991486433137,403.14446353786127,3.2039414465264375,5,7.906735945985541,37.930902755544324,58.227375897868285,3,46.49031613814027,3,6.607224692221047,3.0595804666286126 +34,66,17,18.81097271,21.27833035,5.889614577,125.084915,kidneybeans,21.176332352021156,1,7.2642973931918124,1.1293190705373624,433.016893024206,1.4149376823810327,6,12.201239962790456,74.82528068512129,176.0496171642922,1,41.606188129780584,2,41.40991446208571,1.656397914563951 +20,69,15,23.44260668,22.77255917,5.934136378,107.4137246,kidneybeans,17.773925549896163,1,8.685607542022456,17.343762495977863,406.4316161124042,8.006953864635022,4,15.679991354213312,41.045785239140805,129.26515867212646,3,6.912874168257998,1,82.92744834894685,2.081737847568799 +37,65,16,22.8352024,18.97267518,5.683548308,63.59276673,kidneybeans,29.080227525230203,1,11.014997565466466,1.4463870187915084,389.3519046276468,1.5807999947027942,3,8.201883700109864,39.655889756130506,158.4992311323428,2,40.31552348030828,1,74.94155737855436,4.109441367781832 +18,74,15,24.9035819,22.27512704,5.70836603,146.4727237,kidneybeans,20.43591067668373,1,8.338879033346226,8.516548164422177,389.361521438987,1.6981848557850816,1,15.99855131578282,71.60335532776163,146.770955929652,1,19.3079181428013,1,98.44962547170866,1.7489216461742902 +4,67,25,23.78709569,24.35679348,5.948164454,119.6404412,kidneybeans,29.676942328809936,3,10.509278509472324,3.8787404725077335,389.02435820707836,1.0135505223405297,6,11.474576084093766,77.29511142154314,162.8530689133645,1,43.602770616752764,3,13.95272581532926,1.3349461335052468 +37,56,25,22.05592283,19.60379304,5.774755144,126.7265372,kidneybeans,23.90900735942316,2,5.428770197081969,3.6180762253200682,447.21548140747075,4.879931019660453,1,6.7620859445853245,64.12721170918346,99.7459900077683,2,49.01607983396606,1,66.68808550176198,4.914414585360598 +5,59,15,18.87492997,20.18238348,5.97229163,134.1811718,kidneybeans,19.895010310467995,2,11.6946613214166,5.1647461795277305,439.32038435214804,6.819827787827926,2,6.294081900032555,79.91657299699317,107.1857264023904,2,12.074966430745354,3,41.55648973293344,1.556984740100693 +11,61,21,18.62328774,23.02410338,5.532100554,135.3378033,kidneybeans,16.61647633429648,2,8.375584900252374,9.636081326372107,403.95165539027016,3.9835159651068155,2,14.620061090521169,76.47276369138355,172.03400089093364,1,1.7143222452754014,1,86.75590314217587,4.749139633370902 +22,80,20,23.00884744,18.86880997,5.669560726,100.118612,kidneybeans,28.899088824927887,2,11.154081719571295,14.46081753888895,393.27395618303916,6.081203706903107,1,12.926192563211444,12.043661665399707,129.19948973563672,3,47.939695487588814,3,47.4563482959665,1.2825986904022488 +12,61,19,19.33162606,24.13995025,5.655726817,68.51253427,kidneybeans,26.16962610505743,1,6.982162159023001,19.247599469574197,409.59307893018126,5.3615686838808925,3,16.91393983978083,45.38817595937845,112.14150979645015,3,15.859537839345606,1,87.1132603855511,4.822698153434703 +5,74,21,16.24469193,21.35793891,5.591704014,66.97053257,kidneybeans,18.355384003808155,3,5.000725850236799,15.055427935662674,355.4159957619112,2.1553313603573248,3,10.873326455519212,34.30874484948383,173.95436604816672,2,46.90156379109069,3,99.75567664097788,1.1698906169496444 +27,69,22,17.91652287,24.90814655,5.932323085,69.14681022,kidneybeans,29.2824667028517,3,6.854594622738157,13.506307890508804,427.0074417677744,4.759096931684615,6,6.340360695231755,7.128296960017211,189.1480747453213,1,36.5113734662266,1,66.49305536786451,1.8514384214421185 +31,75,18,15.46789263,21.43780702,5.824208309,88.88796102,kidneybeans,11.983118511669431,2,9.988099591802975,14.35285215735943,374.1204491289247,6.485329273851615,1,10.251884962150505,29.48813406533667,91.87792496729833,3,14.30571068121353,1,73.65782956760837,3.7572478636543485 +36,68,20,17.06104474,23.77201471,5.86442953,81.83420522,kidneybeans,17.906917426643606,2,5.329953219128357,5.658148571421286,418.1176724431083,6.638963800079786,2,5.450547799159866,18.582171647334565,101.15413851521564,1,26.427502976983362,1,55.06134486163885,1.5611786785707222 +5,65,16,21.32776028,18.48522915,5.866744372,109.1013261,kidneybeans,21.161898388489256,2,7.246797566094891,14.64186372444648,443.5440481589047,4.549911215008955,5,19.212214052172943,47.85163722224974,122.69279795628792,3,25.38821718785312,1,32.73359236832509,3.925440588826343 +32,79,15,23.90910104,20.74619325,5.706198621,81.60211243,kidneybeans,15.353073018423906,3,10.440015464412266,7.027301726426465,369.13558617232957,1.1727777058013777,6,17.041749674219773,53.31245666476552,181.44932599857034,1,34.56300370597923,2,98.19956898094607,4.213788332985267 +11,78,22,23.89756791,22.74378977,5.940546818,112.6616435,kidneybeans,13.788061540765955,2,5.632876165931489,11.630294914966253,354.36124844902946,9.569986385359094,4,17.73022931203207,85.09178423681351,120.7772764451618,1,23.78805872925786,2,17.322648469327383,1.0402899641251597 +0,55,22,22.98666928,20.57940608,5.916779289,143.8584938,kidneybeans,13.23421114798425,3,9.760573082298034,12.638096848495337,411.4834894324374,1.4730410377284466,5,9.34183498398556,76.62537786474877,144.47395894144205,2,26.676636757298517,2,0.612337234127891,4.648640697100481 +14,59,15,21.35135729,22.91244883,5.779090476,146.4548645,kidneybeans,21.737838617637927,3,7.649346895739051,2.8637608644166623,448.37513233511953,4.601718065136092,3,16.289113950657825,82.47892105508534,89.89334344973517,2,26.131549290800198,2,59.35995424611284,4.110168261719495 +29,68,23,24.1638445,19.27907819,5.82738029,116.7324324,kidneybeans,27.837638914081946,2,7.4304370938729765,5.281232217596992,373.55857408771715,9.896542001003837,6,8.829067521805195,93.26372329595117,142.11954544072432,1,8.128292136048126,2,63.79884929046368,4.963950522114844 +32,68,19,24.62835037,18.18325169,5.514234138,149.7441028,kidneybeans,18.011115897496307,1,9.720050882247104,18.734104278816705,357.82446247788414,3.026468411111301,1,11.282366519872767,60.632709640735825,176.05158674655735,1,30.151309536018857,3,91.31826410600164,1.08953703769266 +17,64,17,21.02213209,24.93896255,5.662699104,124.6118471,kidneybeans,20.7434885365372,3,9.696834018376462,18.54707422969308,393.6097922469893,3.8530730811625773,4,18.17690295985238,59.22137252517938,175.9838657764697,2,8.785741397922047,1,88.06156804992304,4.776668259849876 +13,69,19,17.30844532,20.01730914,5.86390397,115.199245,kidneybeans,28.133786676387416,1,9.227475552676326,17.0261057723399,449.40552768902785,1.5568310895493447,6,14.700492063805418,57.58339565124082,173.96416813631942,3,3.069025657830421,2,78.28115052750786,1.9614577682870213 +14,67,22,23.82576704,24.75485098,5.624690248,84.64143632,kidneybeans,12.722751429431252,2,10.8598614900021,4.411364744465427,425.52968341222504,8.048016731838128,3,8.698478608437254,53.95645729278493,126.71016693926957,3,25.755343668025212,2,34.38465248860507,4.602850525310867 +9,69,20,19.30607278,23.96362799,5.591560999,129.3449326,kidneybeans,12.531124637144437,2,8.334571782263083,2.54302796395921,404.3312315270977,6.595306769687665,2,5.899801342925024,0.9705246730235317,147.12779155679016,3,1.016687733033772,2,40.70191178109409,4.292105924382222 +20,73,22,16.03768615,22.33195853,5.976312538,130.3900798,kidneybeans,14.977243556855885,2,7.323366715199722,10.853044055455772,368.38581329988375,5.591420214879972,6,19.19381032008985,35.904759395639275,78.0997368708477,2,37.09422070429346,1,67.49091165657603,2.3290332264746034 +40,78,20,19.18572809,20.83398341,5.669236258,80.15293435,kidneybeans,26.038360544951527,2,8.069194135841734,14.68859661918306,398.2563054665093,9.3520986282443,3,17.64626014503473,4.342274444716232,167.21439823918263,1,48.26408267157923,1,7.083514310218897,3.9275961534017676 +27,72,23,19.92889503,21.79992115,5.961934481,64.02640797,kidneybeans,29.00728658906845,3,5.42506449360833,16.233653377779248,378.2673886036657,4.3157693757291655,2,13.942214232591114,66.32758467602986,74.1463654486272,2,39.52265052248517,1,84.74650386257538,3.521859537716317 +14,67,15,19.56376468,24.67385131,5.690065688,139.2921004,kidneybeans,11.316366737501761,2,9.18836543832641,2.318661248744014,385.6304607509318,4.13418039626902,5,18.51273832612462,6.246486557294828,88.78776333617608,1,18.10532844300554,1,48.02184632660337,4.241862125373659 +7,56,18,18.31357543,24.32991649,5.698371311,76.14153904,kidneybeans,23.291332348908178,1,5.975012823243761,18.602537348829486,420.96259210666835,6.880011552668389,3,19.8188008399775,77.5561200793116,87.18404174212755,2,7.330710201161517,2,66.73513113586725,2.319390123827271 +27,65,18,20.10993761,23.22323766,5.59503163,73.36386477,kidneybeans,17.946714843631362,2,11.575180659985921,13.58353809171186,351.4397463546144,2.3410663943182817,6,11.130512570962232,86.33861906258188,121.69200601433182,2,36.2031883908621,2,76.36693963708645,3.408384399884358 +30,63,16,23.60506572,21.90539577,5.525904526,100.5978728,kidneybeans,19.490939662428342,2,9.767113769969189,3.9400134935414055,350.5283921341889,2.4811536288473786,1,13.96529364473711,27.778012203589732,80.73659140489772,3,42.96442053501875,3,49.7198841237377,3.5033412983840684 +37,70,25,19.73136909,24.89487354,5.819403771,84.06354115,kidneybeans,11.653781873535564,3,5.739923237503676,0.6941571304388505,381.7360655651895,5.982346450055076,1,19.18676189867034,22.343136772083383,126.76347682996811,1,31.925982141161914,1,28.616658063607737,1.6314936158736382 +27,63,19,20.93409877,21.1893007,5.562201934,133.1914419,kidneybeans,24.230694654991012,3,8.44226665788819,17.122584967274516,367.69538638535363,7.984621115435975,6,8.361600321296338,25.699094022192092,95.96285687867028,3,7.0184602173603725,1,86.3256713601544,2.254813111723486 +22,60,24,18.78226261,20.24768314,5.630664753,104.2570723,kidneybeans,13.799752224936125,2,8.61189739966737,0.8378713950750472,413.45329994220447,6.063635782609917,6,18.817036846960388,56.97130643330688,148.88355226995742,1,31.785218060311955,1,1.4820518232845248,2.5423063669422707 +3,72,24,36.51268371,57.92887167,6.03160778,122.6539694,pigeonpeas,12.300796996218914,1,5.072280482284679,2.659512966080706,413.16670948071965,5.483977498461362,3,13.235678121410562,97.17837194001999,120.16398859301589,1,48.71899841386766,3,0.41189132428613995,2.1496913420145503 +40,59,23,36.89163721,62.73178224,5.269084669,163.7266551,pigeonpeas,21.690469967780327,2,6.163348097011641,8.34682011670828,412.90178212033385,1.5244348057249075,5,5.140515336551182,18.34559698798426,64.91873963859064,2,25.440294381307783,3,13.313148957864108,4.117013945719067 +33,73,23,29.23540524,59.38967583,5.985792703,103.3301803,pigeonpeas,27.20553611295348,1,8.038033585965547,2.4515935611989037,414.23830180088663,4.317063119118515,5,12.060118750313956,28.44797223233897,185.91206953466622,3,6.886015294294895,1,10.26302906189539,4.6495955556186 +27,57,24,27.33534897,43.35795962,6.09186275,142.3303677,pigeonpeas,11.836692557738575,1,8.876881648355596,9.850233002657415,361.7105540134821,8.276828900517847,5,15.236328991418173,70.15409225544983,52.31099106975566,1,16.97113869873069,3,8.27716646724056,2.1474045145462966 +10,79,18,21.0643684,55.46985938,5.624731338,184.6226709,pigeonpeas,11.474456310853924,1,7.730261399553752,2.9450354718127603,449.3885939127531,2.1318046566072923,6,5.878450604102734,10.395513720240036,52.41205256248171,1,4.861199048302972,2,73.210630391249,4.519996524081275 +30,75,25,30.33276599,42.35249879,6.446091759,149.299952,pigeonpeas,19.681494496747476,3,11.34773021909681,12.72710136458599,448.44665949010806,1.310700463359314,4,19.780737692967666,77.8945482891502,187.30077377825896,2,15.490396191410976,1,87.66939276800923,1.7075408499207363 +40,70,20,31.80130272,45.03186173,5.623490043,147.0361442,pigeonpeas,29.61732802823861,2,5.995560155416908,15.704537450868761,413.9293889846838,8.100617380177466,1,9.611654468814745,80.12034151154982,97.90348653238536,1,24.041173423751623,3,79.95442886654696,2.357028590264465 +38,55,19,33.18184225,38.23184742,5.864623352,198.8298806,pigeonpeas,28.149057634325928,2,5.62112730643268,3.663909856913674,443.9609740825335,4.631461339409707,3,8.100185418690428,10.888280121314708,114.96499227927268,3,7.359083751337575,2,96.4754676198541,3.538008426745571 +35,58,20,29.38538562,63.47742011,5.761702519,90.05422663,pigeonpeas,20.519347500263585,3,6.077312315215153,7.809820325771961,372.2651173075719,2.6176980025806276,1,16.61623398660741,8.904757695427346,94.53547347950274,2,36.55597204748761,2,87.23955669756691,2.4147997789373328 +38,61,21,30.27374995,67.38680755,4.696518678,127.7767134,pigeonpeas,13.226911105408599,2,8.188188959517042,11.324020218155717,425.2274442646335,8.662691299114757,2,11.935930253141262,39.27257354479104,153.1361601927435,1,1.7743125354371225,3,91.48932095551018,3.36359767193341 +33,58,24,35.45790488,68.75810535,5.269504214,108.6333046,pigeonpeas,17.039146933469503,1,5.530338685780148,9.503660301602304,370.2057266169742,2.3416562938024716,1,19.86099004315194,86.50986440270209,133.1536054131301,1,8.126186964400468,3,76.27668208192968,3.4294185966076105 +16,56,17,33.80020039,40.03262418,7.445444883,176.6165894,pigeonpeas,17.316644609244523,3,9.201795373500994,18.40535993748196,383.70481440793264,4.804550490024798,3,10.17226863052781,54.73951940882075,155.05788916752206,2,4.995733608304359,3,5.308778924174639,3.963376983028888 +31,72,17,28.69180475,49.47225353,5.833031708,96.36222901,pigeonpeas,18.977267616760244,2,11.370115045005678,3.307362590781109,378.28710444374553,1.4123175373500523,3,10.013186934416403,45.21671170869025,116.73738171867416,2,18.880781320723678,2,72.35378543457223,2.535596847973458 +16,80,20,31.24021696,56.67369054,7.339320929,122.0146733,pigeonpeas,24.374317867416302,3,8.735461617204365,2.8315727434383398,411.6613736257922,6.670853814245111,3,10.546189785730615,17.968902100386998,134.62327371890927,1,23.84427908894929,3,69.72657334370606,4.40380454655578 +27,72,17,28.98039357,57.23265151,6.347929353,120.7435664,pigeonpeas,10.111576199163583,3,11.753818102239538,7.109740549153887,411.71548563925785,1.9913254038628068,4,10.05307601794615,31.724552549025333,94.68018892581722,3,31.66732342307509,3,83.07433132610342,2.5582569374392405 +40,62,19,27.32198928,34.13737127,4.697750704,96.51524028,pigeonpeas,23.844591844386525,3,11.438588318263863,17.255331676702507,408.9138087464971,8.601798474665546,5,5.932516771174222,56.08911013858695,172.92472883249303,2,21.5153548614977,1,64.89035655476414,3.250517506845388 +18,58,16,21.47607807,38.80023714,4.962661422,180.382234,pigeonpeas,22.34487535165203,3,5.168262963127516,17.59422221825715,357.0436191605813,7.809306664385367,3,9.520180638504346,21.378697493509748,53.67693480360279,2,14.966757594915459,1,11.293445145295477,3.093144725683383 +3,68,16,18.31910448,34.69776639,4.964887857,107.4721605,pigeonpeas,10.839354431342027,2,11.422137829218888,13.888238606828725,397.62468140443957,1.961767823821801,3,5.378781001946435,71.78760706149521,82.24074584436177,2,42.442439739775025,3,18.50400233818218,4.851682854767699 +26,67,24,36.97794384,37.73992903,5.642813116,161.4812963,pigeonpeas,19.804437899305963,2,7.156592165400795,16.710165326170618,370.3365544913563,7.485819466577953,2,6.174961410389539,19.956112935827807,175.59666308434575,3,49.91377799311904,1,35.91753981796405,2.6608782397173907 +16,70,20,24.80467592,40.1242747,5.6093956,121.5639121,pigeonpeas,11.925177213490823,3,9.527850976326082,3.7572139290019346,427.8957422164799,7.204210577236569,5,16.767951697113823,84.24498480283349,60.906004522160714,3,1.368782417851766,2,83.25191997588729,3.168463851337414 +24,63,19,19.3479443,55.96805489,4.681576043,194.5921148,pigeonpeas,28.00339268681403,2,10.004633732433332,15.897510555527086,350.94953865817297,4.360636825269207,6,5.815317245626514,8.527237301545565,109.74874634292209,2,38.5010703107897,1,39.35298665706474,3.395942307588764 +9,76,25,28.88302142,50.12323801,5.70951224,179.2155874,pigeonpeas,20.609035288675578,1,5.319241146575369,6.206983710762577,404.49385777773483,8.025651540584414,4,12.789747683035639,88.72074093780833,81.37968953619989,2,35.63461842158595,2,42.13124771871316,3.3282030391228474 +16,55,19,19.54314136,47.19188279,6.413543781,192.4372194,pigeonpeas,14.99847935562,1,9.27555930354837,1.5251866173448847,377.71192766408916,8.435835337019693,3,11.989082443820147,85.12975746457009,183.49783908910868,2,3.2649478670135346,1,14.316911284432642,2.281112115420574 +28,75,21,24.7741949,50.54621094,6.007508163,114.2821387,pigeonpeas,29.463761862908953,2,11.506963685652265,12.392273778633154,371.9672260576834,3.026658083918364,4,18.076204639668944,6.6853148920502425,60.91366339051772,2,1.780732152807124,1,77.19050703617971,3.1527112472026957 +16,71,24,18.33124824,38.40975482,4.946369874,139.6483317,pigeonpeas,19.443914261989892,2,7.529945537998149,1.5557834235688661,358.4602276146244,1.8381674603942597,1,16.189187170871826,10.855966717103849,50.75259534219755,1,5.923400342060265,1,54.053633232994414,3.149294542734077 +24,70,21,19.14729038,45.3733757,5.517208078,132.7748215,pigeonpeas,29.57541025793995,3,8.783325774824105,18.31970408849659,439.3641422672258,1.5471113692062564,3,12.970047432419507,4.367640379548998,119.46996553954534,2,8.66123043183331,3,25.20127038312533,1.5711820862484251 +38,72,21,28.23416057,49.4421345,5.902103172,186.5008581,pigeonpeas,25.240901136770265,2,10.867076022950577,10.492769370697143,400.12472136951146,9.46906696224253,6,16.865596940910073,78.33861177790578,198.3465759008838,3,37.47477621622803,3,90.57576170429331,3.2757742217841948 +9,66,21,30.11812084,34.13307843,5.719889876,157.0858232,pigeonpeas,26.39936213598953,2,9.339718440640421,12.99272072519333,421.853941992299,7.786182018755541,3,7.404729205775199,82.30129443371555,65.69228814761644,2,32.19032887819103,3,95.71294713580234,1.8594587371505042 +34,56,17,33.4126864,35.42910045,4.548202098,139.6702541,pigeonpeas,28.960571249626923,2,5.279736296523776,9.947231008364717,429.47480661781594,8.679242146316861,1,13.496605589077785,0.7068301657443321,194.8081603016334,1,0.9650302384279719,3,57.48047774101077,3.5068726859579082 +1,76,19,24.18553163,46.68746847,6.669529416,177.3377996,pigeonpeas,12.42564520814211,1,8.331382035209916,1.6188481905875185,354.4122650440536,4.604244357318449,1,19.445782112222997,44.709602929505074,174.1325321968036,1,17.878096579777054,1,28.66960751257035,4.670057744410638 +6,69,19,26.88630675,41.69617915,4.750929218,94.46748008,pigeonpeas,23.85391520298036,3,10.91420755325355,6.582210559600574,383.6558901266551,5.141911660547071,1,12.013021075576333,55.28812326117611,71.14635494827195,1,13.838677336257653,1,82.1843321215376,4.846333105606398 +26,73,21,31.33170829,57.97429171,4.946263888,161.7820226,pigeonpeas,18.17458205825503,1,5.033648254900967,12.626350332662742,359.5798391529733,4.774294233544552,4,7.408240969762502,20.49833041058693,197.01038588191165,1,47.99200068890505,1,76.63054423243177,2.1472757581855224 +27,61,18,33.30711818,67.07780816,5.266227032,108.5090168,pigeonpeas,27.239878784955625,3,7.713445487347235,17.957789480430502,424.56985927529104,2.640648368077306,3,11.077811208845704,1.3882783884024752,190.9958625616827,1,32.70715018540253,2,29.943009023240997,2.9319944311222508 +27,71,23,23.45379018,46.48714759,7.10959773,150.8712202,pigeonpeas,10.823293035205078,2,11.591083633008159,13.642393198423452,435.68704004957453,7.075779050429993,5,7.260945902198273,96.47567243717313,169.96024929949516,3,0.3017229802193766,3,93.134871817826,1.3093597087944175 +36,61,21,34.53823889,39.04468913,5.617008201,168.5948318,pigeonpeas,17.175231257937416,2,5.17609379355275,15.794662985790032,381.14090935232247,4.64800613414549,3,12.858562908312667,64.13971403172899,140.8788773030001,3,14.957078827033865,2,69.05478630822344,1.8626027002045924 +17,73,18,19.50112224,34.51086611,5.632353113,197.3752649,pigeonpeas,12.198484002327145,2,9.375924752895653,5.314282487556521,431.4329239524566,9.257502766645407,2,11.43564662120366,85.03701968702,77.22720703090002,2,9.915977615153459,3,23.202766540167563,1.5661748855474125 +26,72,22,28.76794904,37.57792132,4.674941549,91.72084869,pigeonpeas,21.93074253051045,2,9.338218913966742,15.894693175649234,417.82015129290187,2.9999146111389887,1,18.83050325218484,95.76192763091353,143.38875839244855,2,30.88120133036454,1,17.357185947998268,3.6086044752922533 +17,64,16,30.97758716,32.24914235,7.161797643,180.716828,pigeonpeas,25.401393509519277,2,9.84026577450224,11.0558953227463,421.37878404022405,4.467365023408394,4,7.775680962849811,22.122644926632905,62.69417794902066,1,38.97688611275558,1,37.44007681099788,4.429394728511054 +14,74,19,18.39759147,36.82639309,6.624966131,93.12330644,pigeonpeas,17.550465980619173,2,6.314859914012473,14.217256882778148,445.6284929002302,4.812143582589716,5,6.683822556925545,36.933918934960964,63.76699357254688,3,47.88056179107724,2,24.826013565545313,4.434869732725675 +39,60,15,35.09357419,30.98685456,5.004074624,116.9106908,pigeonpeas,22.273735686005956,3,5.166187890567446,9.00335836068982,430.1271861166221,2.1967021658099943,4,15.861791557723802,23.66893910420017,133.03865745152572,1,39.64311406859103,2,33.71741488288642,2.24516546428673 +6,66,15,34.93174223,30.40046769,6.345806011,159.2649827,pigeonpeas,13.104249501878993,3,5.219498400600568,7.472620708452613,412.6576256157811,5.922931530035967,5,15.820638927473995,12.793221672129961,114.03875781083809,3,21.259370770858197,3,3.148601577688337,2.849916652467987 +8,59,18,29.50523036,35.72032498,6.216814453,187.8961851,pigeonpeas,28.543974679479618,1,5.749932659220722,14.844315656536777,419.2572572546425,7.723628885147478,4,7.293167876359295,20.997756096630393,122.5474627948831,1,6.753621989008024,3,21.735943778693933,1.2429482971234576 +2,67,18,34.51934775,47.52980027,5.921666758,129.0064612,pigeonpeas,24.124161352916854,3,8.713087617708426,7.927732702496828,380.78784264011296,2.135078374743898,2,13.113106191625395,10.783436678327462,178.01525136395938,1,6.265937847329239,1,67.93701249328508,1.6069438208698967 +1,76,17,28.43430726,52.10010827,6.012719118,147.0414824,pigeonpeas,10.322210948495435,3,7.059986014422503,13.331968197923521,400.16958727229434,4.776103425362379,2,11.832189626695703,93.52162764747895,151.3597257104543,3,12.17537629126369,1,33.422666661530634,1.7682827063529336 +16,73,19,18.41645629,34.80541039,4.684079249,163.2747473,pigeonpeas,17.63277578044524,2,11.686471231031726,17.271656472868653,429.1787060464478,8.252554374619912,1,13.382907333907259,50.241013265608416,130.2794538965753,3,27.940574978709492,2,31.461077411969065,3.3914565384880424 +23,75,25,31.07508973,47.19847683,7.077170002,91.31256412,pigeonpeas,21.787629446120242,2,7.203048917853369,16.364504322227166,438.7993854986912,4.605787243763082,1,17.111194461001013,92.44687083929341,89.82692143137432,2,4.83770581399714,3,66.79908576974834,3.498975266547826 +32,70,20,20.89342749,46.24856523,6.208843215,195.5697875,pigeonpeas,16.034651062413694,2,7.597466618249813,18.09252224578785,444.4346372200027,7.4887836337328935,4,17.358501503782144,54.96703957988509,56.246533803522006,2,30.48635653692184,1,7.968255405879954,4.676194814949666 +28,59,22,30.90607799,52.79913039,7.05181629,170.9919828,pigeonpeas,18.134095039370962,2,8.048709457538177,18.90537272130784,389.1588407432497,3.605138801450135,2,12.660742406619894,74.66357885263196,108.75597526214341,3,20.76251157572252,2,7.519390059380438,1.8207839209388963 +5,62,23,27.9348279,66.45457122,4.722222454,145.3728801,pigeonpeas,20.869700420788323,1,11.27873115329217,11.289289540689381,413.7417849851939,1.6066560905842264,4,9.84355084929923,76.06371706901697,72.79471468140349,3,16.019638661857034,1,29.794629948175956,1.2732306639202835 +36,67,25,35.95176642,36.52780776,6.418062652,136.0456753,pigeonpeas,17.446672583396285,1,6.58572281301667,15.862044290844464,430.8558823579834,1.2531178287238056,1,12.954481426575164,46.106829434966926,191.0977066975583,1,42.038291533204465,3,50.03302997550292,4.472393623152479 +1,66,23,19.54317155,56.92831399,4.803564468,173.1686574,pigeonpeas,20.8171208872756,1,7.202590149079941,19.57669843664285,431.8073292562947,9.127032084325588,4,8.615274134613157,19.48018064140652,126.89525398683388,3,25.428614916510373,2,32.5183729591195,1.6908366413663511 +24,73,20,19.63736208,32.31528909,4.608695247,176.4134092,pigeonpeas,22.31779011810577,2,9.738809435603242,5.283260675237964,416.87115733929676,8.618869860363159,1,17.582753674460637,14.167041069206675,142.36918200156327,2,11.960797835605552,1,87.35628684273347,4.7744718468283285 +17,67,18,31.2192752,56.46868874,5.611510977,129.2028653,pigeonpeas,28.584563960000423,1,10.665537972205673,7.90816136550053,449.9733068383365,7.890303208779609,6,11.063283449677371,88.478642925792,158.88715414329522,3,27.7935243892657,2,18.574318282464553,1.9606372188415873 +5,55,18,33.50876355,45.70976142,7.322097972,126.6738117,pigeonpeas,26.79830410905741,2,8.064716971424067,2.894781351395226,389.7996774800895,4.357972695646012,3,16.179015307276543,63.002023901417836,196.08957382808669,2,18.919240268302406,1,83.59295911078948,3.785333202508487 +5,56,24,24.80710166,45.01110015,5.023115055,188.4928637,pigeonpeas,14.076418862577299,2,9.586080669881008,4.541020922155594,434.6159287482174,1.180529729765198,4,7.194281438495965,92.07260258801698,81.20702457685772,3,46.2466479291619,2,47.1327434243671,2.9088834859272543 +37,77,17,36.20970524,31.94550613,5.617122801,191.0658531,pigeonpeas,23.447386608015286,1,6.942108449147423,12.744894614392637,440.6651640687926,8.530714437243448,5,8.554081150991994,72.18538639341287,77.03901767446263,2,35.63747536716709,1,87.11738363584031,3.0798116927372012 +13,73,20,30.50420876,35.48885969,5.391560418,162.5927723,pigeonpeas,24.55603787299254,3,7.928709300776811,17.28371263208555,430.55113994573094,4.7101012390866,4,15.877132841039673,36.83027364192885,79.52207931110493,2,38.34990097381099,1,37.78509354317292,3.574421346075017 +6,63,23,26.01630259,49.94704718,5.906596905,160.3337447,pigeonpeas,23.026259523357034,3,11.97776724743307,3.047720747184952,358.1394668153906,4.953195631470677,1,16.953700794551743,11.35763771398871,185.09557951286303,3,44.26430368781096,2,58.31548680823973,4.696484639886428 +16,77,22,31.48469278,35.6395615,6.574209678,100.546816,pigeonpeas,13.503443492069124,2,8.956100256436343,9.960515797690979,399.2849832833393,7.6608800139634,4,8.910023849945752,24.05886596717296,156.7035367746551,3,47.0830384392201,1,46.47125693275819,1.0819200898178427 +25,64,20,33.15122581,32.45974539,4.807776749,105.0380275,pigeonpeas,11.14442455105382,2,7.7323394286812315,12.418490607253945,371.64069736070303,8.471443713043529,4,7.370538842074502,96.82422518944526,177.58432507769277,2,30.505387034970067,2,80.38203716999492,1.5086974737716083 +34,75,24,23.50222822,51.29019509,4.760038039,192.3023991,pigeonpeas,10.861290327140908,1,7.987065028235585,4.231017063218074,412.5181946462031,3.3636210970702605,1,15.18864854160398,64.34425166024353,193.08952705065823,3,42.794490340410974,1,11.337315969987538,3.5986807495202764 +20,77,23,34.87248659,38.83786012,5.180271502,148.2502786,pigeonpeas,19.13500094602272,3,7.570002919107106,0.07464850035170612,419.39940794905766,8.411286778320967,4,11.465456695130468,13.776235633911782,73.0235131968403,1,43.02771042008818,3,94.36784942325365,4.306364157586762 +35,80,25,28.09269012,44.93322042,4.895927306,197.1144011,pigeonpeas,21.01508297367535,3,6.768425907113386,9.176459109960591,449.69232699515175,7.076989479594747,6,17.75523553220434,19.468981782557083,103.31310870234513,2,44.83432482297855,3,59.968533694020245,4.000572971014442 +14,75,24,24.54757829,57.3414485,6.436160044,118.3606557,pigeonpeas,16.436343351656625,3,6.522948074585463,16.700265064937216,411.36756159413864,5.54518954498311,4,7.815381712558966,2.2772659267670026,190.3058554769988,2,48.64671607512711,2,75.10612756838738,2.7556172750462204 +36,80,21,33.64769646,48.41490082,7.066087261,100.4673278,pigeonpeas,20.27335682184806,3,10.909934358866781,2.838905560239753,354.6416284368445,9.65905136443818,5,17.967089587263132,26.90585215624942,140.79218594993338,3,47.38884126310429,1,39.18615519181036,1.699460049023381 +7,77,18,20.5591255,60.54880693,6.655918078,191.0895109,pigeonpeas,18.77264635408146,1,6.184451405558974,1.6341812203629846,436.4578901391969,9.835521220839937,4,5.575001624572456,20.416296545625023,87.47150540588083,1,0.2636957763911929,2,43.23573745712835,1.7124943520961442 +29,78,25,19.95991719,59.33157782,5.982854523,195.787103,pigeonpeas,29.57944517574392,2,11.51368180516924,19.164810724013684,360.36853014280257,5.197559446172972,2,8.641981225225265,45.60880989017204,189.57989571154047,2,17.280263379839646,1,51.97521725244837,2.4190333319256627 +30,60,21,28.87667593,62.4901206,5.457871273,182.2688175,pigeonpeas,15.639245965487605,1,9.900521595873464,7.846074262170555,369.9352933596119,6.501549941814209,3,16.21358945528566,18.80517726955916,141.99022831768212,3,4.307914033775656,3,36.17365135573761,4.159844989748362 +20,74,16,36.04353699,43.61444121,4.759490199,159.8938645,pigeonpeas,16.830723829140045,2,8.064456103666004,14.995786516977063,402.82825699768955,2.988959952750694,2,13.25523101909122,51.96817848874,90.76441502130152,2,45.35493942118649,2,43.351612328802126,4.092615972442488 +19,57,23,23.6734328,47.2879691,7.342409555,141.1250722,pigeonpeas,24.70216545099768,3,5.038039970731716,6.990036156706598,355.5609497016683,8.029519026462346,5,13.501224634817897,13.998744811462306,143.4011115741675,2,18.446681063608082,2,47.07600971836663,2.6667848452473466 +3,60,19,25.74679443,40.7192594,4.820788186,100.7791633,pigeonpeas,22.86168392964897,2,8.234082262241294,9.89160977209436,382.7144182317677,7.9811985892574935,2,5.949172376027609,54.144344513121844,139.15722806348896,3,36.29989532340239,2,71.78533469383129,4.075655368623673 +5,77,19,31.08564994,66.68832981,6.242052013,175.9303271,pigeonpeas,28.352043418526467,2,6.335672726847935,10.356971283633287,432.4522558730647,6.7628158819666435,2,12.225756717426703,7.194244291984974,198.9523482048007,1,25.692980204792782,2,43.14961762999783,2.9985365943971094 +5,68,20,18.72987676,61.33186249,5.001038726,139.8710041,pigeonpeas,17.990852893646625,1,11.778976062056287,14.043927356604819,446.24268330153006,2.289837129881322,3,8.797810485292596,45.8892218288413,56.359224984718416,1,24.52575928744191,3,14.782124127234065,1.03974778899105 +37,73,21,29.50304807,63.46513414,5.560224583,189.5208915,pigeonpeas,16.244048200372607,1,5.22703179719752,18.672271589428895,406.2782932567569,3.959330534853915,5,17.36801145167252,77.78734720869814,110.35238385794995,1,39.60405652491491,1,5.988386194081663,3.492891658754013 +9,59,24,20.43517772,39.37252634,4.747352458,137.2279662,pigeonpeas,24.11837034334575,3,7.102052572222014,0.5626128041366352,430.9584213685999,8.345998421077478,6,9.078079062885616,52.150800484097715,139.96582279338912,1,22.076318465169432,1,26.803205066647994,2.608225250711364 +20,72,15,36.00415838,56.01334416,7.313517308,134.8596466,pigeonpeas,22.7039820593924,3,10.274899268364786,16.330875299203804,433.6716738575575,1.8557467592590502,2,7.703686401967665,6.418773604403083,82.76020645587563,1,9.879507351223287,1,92.15642855353462,4.943149790517502 +31,56,23,31.46846241,35.39454002,5.661826398,174.5723999,pigeonpeas,15.507253104127388,2,7.2470653756572005,10.040611032245842,390.4098123071718,9.65620240561114,5,19.95366295593367,4.524194101428525,186.50269846897785,1,36.47228811737156,2,33.079290146763306,3.6378899378599066 +0,70,21,36.30049702,56.03021253,4.672437054,101.6073988,pigeonpeas,17.245111131009196,1,8.165665517651437,8.146610467999624,419.5958786727825,6.55541426532938,5,14.58621345194688,56.25917128842318,169.9866401927882,3,12.784041583855805,2,18.494558229034354,2.422677311419732 +21,74,15,29.49096726,67.10604388,6.471862118,153.2504506,pigeonpeas,14.239629393797188,2,5.930180890564917,0.32484887205410873,365.33510349048817,9.241203703454989,4,14.985989553566508,7.08997334441298,152.61891423472179,2,30.12338303044293,3,69.92512908037837,4.359632779679584 +13,67,18,30.5753044,34.75591197,5.384762927,177.5764304,pigeonpeas,22.880546308604025,2,10.942878186363426,11.94264163464223,377.9078667834244,4.319519021245091,1,5.808709625178518,10.447275840691262,164.89638779262273,2,47.36814043081315,1,54.498712379347936,4.155442907950057 +27,74,20,24.69487673,59.96669215,5.859813416,91.95792434,pigeonpeas,13.651598067000839,2,5.6186189868997465,13.443017448466225,417.7584060263794,2.3631330629197542,5,10.684600526814688,22.685509955830096,100.93672057870154,1,19.134547378924243,1,92.25768325692162,1.0725564482426457 +29,72,24,23.17409556,36.67847052,6.962386495,162.5931264,pigeonpeas,27.40851176067615,1,8.759670980455972,11.85254992025726,375.45628263398305,9.07797299695461,6,19.27734323436708,32.49741574149822,182.59012436242682,1,49.99204584876098,2,57.48010280073774,2.839896353124249 +5,68,20,19.04380471,33.10695144,6.12166671,155.3705624,pigeonpeas,10.044198588370408,2,6.377857918574554,3.218027096446643,446.6280797311101,2.637351437067509,4,11.72858308485769,36.900872725195924,135.44257158631348,2,7.623885438312506,3,99.47926374120914,4.0160719335812285 +39,57,19,29.32379604,45.93248374,6.421748487,165.4113371,pigeonpeas,22.228344391082473,2,11.059069571589205,0.8361363364353247,434.4837652511268,4.856109122076338,6,5.835193338661812,95.82393263241204,129.86446070526964,1,27.985681979488046,2,26.736174420734415,3.5114350324178925 +22,62,16,34.6455408,54.32342534,4.828936119,180.9009998,pigeonpeas,14.745659861885434,1,7.799059646503258,16.392485963873817,383.4980328160074,2.885249036019889,4,19.017045553917093,94.78785917646299,70.48868534896987,3,46.021443014509984,1,92.10351653688119,3.2821039336808853 +18,55,23,21.9989826,56.31006755,6.98571967,136.8274312,pigeonpeas,14.667143323270043,1,7.980615549339858,19.375729406783698,399.1437576006446,5.367992171646964,3,14.535549974353454,29.90132929887428,90.34594853642264,1,11.247155565094152,2,37.70033450106457,4.291225983403853 +39,77,21,22.99774444,60.24218572,4.603563116,159.689339,pigeonpeas,29.525719559895617,3,7.761819565680114,13.384859539509122,394.2821505911681,2.0453140144429676,1,6.455915043750408,44.78452809132213,77.95457635828373,2,15.685464265680038,2,81.01769731017642,4.6962984511536 +13,75,20,30.55992394,35.29006485,6.979540061,178.8998611,pigeonpeas,11.99450787833964,2,9.72952489364852,10.082901755397781,378.2325483923352,4.519431539600638,2,13.24111529061029,76.03519712258641,104.24454508478487,3,46.416719927214785,1,31.291135751010167,2.0864030250017445 +27,71,24,31.46417866,48.17631461,7.064973419,165.405354,pigeonpeas,16.833088612854667,1,9.719712245085152,9.207350550530762,386.8054235053354,6.415453096399448,2,10.810086805519596,7.8634616682689895,79.9700454526329,1,16.15480066483172,2,44.317114820190284,2.104668243475422 +26,64,22,25.95058595,40.58227261,5.16516459,109.1821183,pigeonpeas,24.337247163015938,2,10.219375584757374,15.565935564870815,433.5296174197556,1.3092660917145955,4,8.62793459163246,16.60671766059567,138.38235925906838,2,22.816533471634827,3,85.12215475327716,2.5847038450746616 +23,55,16,21.01142393,69.69141302,5.111488821,185.2039114,pigeonpeas,16.046434214997014,3,7.916642599503468,4.124644356944147,419.7175571666999,8.183942202302532,6,11.023266218165512,10.228139129492131,74.41770991767802,3,24.758953684551503,2,60.62679941083644,1.5024540826707704 +4,69,19,19.25100056,47.70351758,5.374358869,149.063196,pigeonpeas,25.892655539610168,1,9.452703344933475,18.47207847725167,447.0072833307881,7.461516330190708,5,18.598596923480102,13.034343145125737,148.0183145382528,1,35.58946778266804,1,56.25389543240924,1.2198366650308894 +20,67,19,19.24462755,50.54495302,5.671419084,180.6465282,pigeonpeas,14.831856249968645,1,7.841199551983227,14.694132912947095,422.6342923059257,6.620653760993394,5,16.097174434644288,15.733637607200935,114.74736875201229,2,13.659378242123916,3,79.09846833731524,1.4890945348193356 +7,74,17,22.47253208,62.56532471,5.667419697,96.74706956,pigeonpeas,29.64027199573485,1,11.529502570846837,1.8110511924700123,422.38957962672725,4.903695084345505,5,16.58398431298572,38.58947571034705,74.88790175286982,1,29.066355612598223,2,83.29311073762507,3.2489262863574075 +17,64,18,36.75087487,58.25799145,6.07938452,124.6028153,pigeonpeas,13.452076748075058,2,10.431872591375136,9.009049731989796,362.067983606727,5.062420687881043,5,15.528392114933308,97.4830464490651,109.06767149358619,3,8.510428342517063,3,89.59571333514829,2.867079515996433 +35,71,17,29.89286629,66.35375127,6.931924963,198.1403003,pigeonpeas,19.729388147239106,1,7.796798422648668,19.78073427327339,372.69953369554355,2.580833583452166,3,17.939963291311606,82.34839540424883,167.53660060263786,1,26.267010197691675,1,7.047671970007851,3.158771072163165 +11,72,22,29.37735586,44.82294584,6.842744374,172.40168,pigeonpeas,11.165825145073999,1,6.4850700400982495,2.729360502483018,413.44592099441434,2.0423619297680156,3,8.332613524276688,85.29931509617745,140.4591296447267,3,43.30828590240738,1,8.4472925798584,4.18667506021106 +20,60,22,29.65052947,42.89833235,6.876572503,186.9226052,pigeonpeas,28.194653190695508,1,10.291790920882903,15.893567200147881,427.7824063629527,2.03832234107195,3,10.744098444982082,36.073363415089176,87.06233455607287,3,26.990178094741662,1,83.289455549128,3.872240998797074 +10,71,18,19.54284889,66.34777265,6.151029296,173.1106982,pigeonpeas,27.86323769357574,3,7.377153245928853,17.50576299965807,379.6630131111904,9.312911851010073,4,11.671045945311208,0.14174937034495683,94.12727903547056,3,48.37757608848062,2,71.37189488648616,3.75470248949503 +33,61,24,20.04611791,48.93905624,4.567446499,122.4564203,pigeonpeas,27.798513633552275,3,8.392958698758978,17.853521968635,379.1836424720473,2.892880014908596,1,10.909462399410742,82.96361104411935,93.98281928793581,3,2.334894659604736,2,4.0827511909582,3.3577250720615424 +3,49,18,27.91095209,64.70930606,3.692863601,32.67891866,mothbeans,17.573282397210757,2,5.100458492857327,11.15411359530355,430.48643418154285,2.3409097832477466,4,17.554036447785077,72.33332389924287,195.60733541490322,3,7.289069138381132,2,56.52394268103913,1.0041728913278836 +22,59,23,27.32220619,51.27868781,4.371745575,36.5037914,mothbeans,26.01405623923391,1,8.263482514111107,18.151300671961287,404.87018952624555,8.791931589783106,6,5.727882794717728,69.08412850817253,150.2109306161392,2,8.486648988490703,3,29.337332671099546,2.548836781386438 +36,58,25,28.66024187,59.3189118,8.399135958,36.92629678,mothbeans,17.12723207972995,2,9.417667784971666,10.874538512061804,390.1517519482007,2.802286344947758,2,11.969687372725883,25.72878538527653,113.97905735875419,1,40.09185576982474,3,49.66884655041955,1.7731919365727995 +4,43,18,29.02955344,61.09387478,8.840656256,72.98016599,mothbeans,12.606522573557639,3,11.897958796269345,18.416210459047264,391.6395984632415,7.170747632275008,6,17.616900358745152,51.60750952933475,64.9602113199584,1,33.01257553599826,3,8.328396702255592,2.3145428733280866 +29,54,16,27.78031515,54.65030015,8.153022903,32.05025323,mothbeans,11.615198698087507,1,10.079705165529358,8.35703555840556,420.83048848028176,2.221138935208067,2,9.654483998218366,29.88387503818357,85.5268868908761,1,4.095248605924878,3,53.0151193876688,3.9389088536600694 +32,43,22,31.99928579,54.1077461,5.270749441,71.6266696,mothbeans,24.83904661075843,2,8.708023125843345,5.205510666994682,367.3763248550832,7.568199070044663,4,5.890130803357259,99.98384949755518,94.41574136216603,3,11.064872913830525,3,7.0491093325495635,3.437298942377883 +14,55,15,27.33580911,55.27755933,8.050304395,73.44775287,mothbeans,11.544266452178187,3,9.932868173350284,11.378222792285902,351.38413662193847,2.3693736209211034,5,17.150529007031782,21.51548190093091,134.85684466631554,1,18.277132306151984,3,46.19984620355084,1.6239782461563799 +5,35,20,28.92952635,53.57014709,9.679240873,66.35634104,mothbeans,26.908255648224603,1,8.359809256671983,11.163971075003092,430.0530549212115,7.571832587639098,2,15.72303360949804,21.287054494381074,121.75759784343404,1,49.006532909798636,2,9.004401448160804,2.8596900172266317 +25,57,24,27.65472156,58.59986279,6.974978386,36.94255012,mothbeans,21.07517038249238,1,7.07358176142308,1.2832465192822728,351.04684736989157,1.0663580567255357,1,10.705276851302322,98.89310716277177,192.64825248339085,1,20.21146731720027,3,35.23664690550723,1.2659463906501132 +11,53,24,28.52396666,55.77264351,7.39389918,61.32935611,mothbeans,18.250433434583204,3,8.108059038188838,11.803329126516642,360.0404156804542,6.708921807696146,6,17.42723603484213,87.84479566748094,66.36210819149159,3,49.97218409725636,2,20.13563572535022,2.869790596189484 +40,49,17,31.02215872,45.89239456,6.68727523,53.56783314,mothbeans,17.048585962513297,3,10.457466239240961,9.826256579033817,389.8110347339485,2.1359791206643526,6,18.856408257009438,20.75972524963624,148.17511127804087,2,7.776370569015578,2,25.65163721490693,4.72144708002484 +38,56,25,25.74095321,45.38497051,7.88118645,67.43488235,mothbeans,24.86856130849398,2,7.748566972417489,2.4189211863841553,403.1371435476497,5.111341365333063,3,10.98509066560553,64.76523037957898,103.2289550857089,3,1.833968966270244,3,80.77708642281046,3.0397958781830168 +27,43,23,31.70447482,56.85420099,5.875333778,44.94317432,mothbeans,27.806659233164098,1,6.806429734997205,12.728098015724791,417.4658934608411,2.230219092953507,6,16.878948078837034,83.00983292599187,64.04657307232945,1,48.44172253704666,3,14.033001317224658,3.0178178831134757 +24,38,22,24.47876451,58.51663927,8.202706015,34.96933295,mothbeans,23.257199051088868,2,11.925440595643387,16.63706870324745,436.7602167316314,5.584262872685959,6,18.5611307663104,15.756452795115816,73.34907317305174,2,9.879132063479679,1,0.19860598790878425,3.6967140828682776 +23,45,21,31.46511256,51.79939437,8.985348193,74.44330654,mothbeans,18.118653454203823,1,10.105891297208963,16.353869999939285,419.7411383060934,1.386371826519444,2,13.963984470745284,15.77207477645276,75.54763806100813,2,25.2781903703591,1,21.95350472597738,3.4616155314618786 +29,57,20,25.60973447,50.7330069,5.87707519,53.39249517,mothbeans,29.169742921783513,2,6.9757150372072125,6.323442573401032,392.053457650311,3.6516115462137133,5,11.292955818708386,6.343705855618975,53.501604773783804,2,10.945680895513998,2,88.44335936984376,2.358239129325595 +31,35,23,30.30260453,47.18283631,7.707595055,68.04039813,mothbeans,25.352515979929674,2,6.214647539275902,12.568578522885785,392.9769192949655,7.613266290138664,2,19.228413774271587,56.85547128625378,87.89114469318281,1,18.593788525755823,3,68.86566465514994,2.9557487234328796 +0,55,25,28.17489437,43.6672299,4.524171562,45.78172762,mothbeans,28.095512411051537,2,6.0274706818872446,7.0286695283335305,431.9242703409816,9.636186259991717,5,15.564519479022396,43.605291317769044,150.05138747500177,1,29.944929924524637,1,9.007733755308378,2.721114664884179 +7,45,22,25.50634557,44.8302551,9.926212291,74.32635105,mothbeans,10.926084651987418,3,7.77694540534142,3.0999190140538935,420.0316152391806,9.33482433215969,2,5.413359727042585,57.97611468762815,103.58740173572481,3,1.6246786537615687,3,39.941586058805555,1.5166004477830044 +17,58,25,31.12896766,43.58788762,6.455592696,32.76742894,mothbeans,24.125689888251497,2,7.324768177641001,1.2827930135375865,359.04148202237513,7.8060160305005475,4,8.174547729689085,41.96842317375565,62.77071764540508,3,6.187008780024467,3,37.505849605641586,2.855860833156846 +11,44,17,26.34043268,55.59160391,8.016210782,35.1051197,mothbeans,11.573780353481702,1,10.293850266691656,4.911955140908153,437.3320463764,6.123651614868954,2,5.1249251282197275,85.8472732342378,171.97510511970557,2,31.892995885411512,2,91.36980617485028,3.2526238568614763 +22,49,22,28.23494706,61.5620517,3.71105919,72.66666443,mothbeans,11.265376754776792,3,10.511776176755053,0.46316321001582006,420.296959627764,3.885654009085365,6,18.779876016925407,3.9592397402004154,135.8560201864664,2,11.201699148310606,3,7.454777151682301,4.424937921347251 +9,51,19,27.04453473,49.32609633,5.49091063,48.25207759,mothbeans,26.027072914641117,3,11.75673661873803,5.4303329158951685,381.33213636614124,9.901878199868321,6,17.242728883824885,58.89888208111651,80.1082714377829,2,5.802295833218373,3,17.794526587390635,2.5315101021249875 +28,48,15,25.16125354,55.25435777,9.254089438,40.89732789,mothbeans,13.525332836338746,1,7.442243451515088,8.21897640721923,355.24233711908437,1.0103587987767262,6,9.234221762442726,27.906047528164535,159.27389151368942,1,13.669812043066331,2,78.18973453295385,3.1617821912077426 +26,50,19,27.3179125,51.66921088,6.005242945,32.55919573,mothbeans,19.018076572832353,1,7.933340132247877,18.42835587199551,422.60729324840133,9.261260354457281,1,13.44969647878644,52.39873541599045,70.07485656858276,3,33.94955012853916,3,66.3630567879059,2.0856150709311345 +36,56,20,25.4123765,49.66474269,7.437078236,31.87416982,mothbeans,19.393259064605587,3,8.765863699907554,14.727728728378931,384.04013458836425,9.770668477688606,2,17.684822873699073,66.09380200029064,55.34380260488707,1,6.79093563889327,2,0.7280946602128302,1.2605293273887348 +8,60,18,31.21629982,46.01868196,3.808429173,53.1205277,mothbeans,13.46856493102478,1,8.402865778594041,12.981089351329178,362.81672630061126,9.695467732223031,4,9.643269153563654,93.79909141966884,65.69833986214387,1,21.328050402116855,1,81.39506209494223,2.6847596999925734 +24,37,21,30.573999,58.22686794,5.818219385,62.74803826,mothbeans,25.303038280668773,2,6.462734607821044,9.123552992143704,388.27063177431825,2.0028904941047276,6,5.201428788189604,8.8648315856092,193.50009456192606,1,8.245701996937205,2,58.607770465678335,4.591634792174229 +22,43,24,25.42517036,53.2208266,4.52363558,46.19374559,mothbeans,10.384722746040309,1,5.152436642254749,4.127735518442794,364.06424413236863,4.428483981852416,2,7.713901087386951,36.081525421016046,104.4500431856076,1,29.292794786311948,1,66.58225813490337,1.9051116824687737 +36,43,24,27.09400578,43.65305437,3.510404312,41.53749535,mothbeans,29.5591348015403,2,8.345169287671482,9.928861498601332,351.17310417679784,6.646557313664252,6,16.001342304717742,14.368457070070416,192.13503225182203,2,27.75195358578649,2,7.377297652827364,3.717284958526059 +22,44,24,24.30935081,56.32938343,6.030447288,58.99536268,mothbeans,27.88877649247166,2,9.097579365623632,9.44038129615925,437.6713263004832,1.175085103251879,4,10.179467325429528,70.48314313755496,169.3729531513219,3,41.95254529341952,2,42.32199988983304,2.5341215700713353 +17,43,22,30.06142622,45.90067655,5.498340808,41.0550915,mothbeans,11.445407233793619,1,10.380958989041844,14.150382441612681,414.6402456940078,1.0482189720748183,4,13.108703325371783,94.10709396744404,133.98597976313476,3,33.375535746664255,2,67.53866777645185,3.7857122472684845 +8,45,15,28.09568993,60.9835384,4.61136408,33.84110759,mothbeans,24.504910635508402,3,7.11329500181969,1.1143357012464472,417.95193169259676,7.5492789463865435,1,5.9612437187659015,3.653843785149502,92.52929790018439,3,13.03319791861331,2,76.9798757571411,4.44995117029985 +7,56,23,26.33908791,40.00933429,5.545219232,55.50429227,mothbeans,20.44935177539218,1,7.707057416892088,8.59736011517884,428.3681702350199,4.154018483836744,1,16.872167145116116,62.103506744888435,97.96817829051771,2,3.6839144098085255,1,26.30196540979203,3.0540979160295216 +36,57,16,28.61409059,57.14218792,8.292875734,57.02891698,mothbeans,18.46843246408917,3,11.887092101877393,4.1763188447803845,433.4767292280572,9.501114988650997,1,14.494483330958834,72.94605548142832,144.12419207177956,1,1.7580950527365913,2,43.60769997944701,2.5855622341698066 +11,45,19,28.70012137,44.359648,3.828031463,44.11622138,mothbeans,19.20084965700184,2,10.18595844508863,2.864367921471036,431.9444276746626,5.445899020895337,3,19.284462888624027,2.253652286860619,76.50941211221219,3,29.711834607273385,3,11.07045571800629,4.561022255165051 +6,36,22,24.21610338,59.79236306,8.869532817,42.24783476,mothbeans,24.409597837236525,1,7.8509425494344125,1.590128333107852,368.04832867634144,6.2501029305235605,5,7.148719312243441,72.28773596599592,63.019140869967686,2,49.82560446808189,3,95.79393039327874,2.7431530677730804 +17,57,20,28.50677929,45.20094476,3.793575185,66.1761456,mothbeans,27.57426251263818,2,10.755912510707798,7.463916927219911,404.4048518098975,9.144692149246444,6,19.441137990364034,10.368168190678972,186.93661598195618,3,37.181942755236044,3,14.765475560231977,4.498958863951639 +4,47,20,25.97948991,64.95585424,4.193189124,72.19245835,mothbeans,12.48625697800941,1,6.888997965955616,4.055158152103395,377.1448462822919,4.11430305005092,3,14.388854835968619,63.21820813301468,70.98280936652947,3,16.42266572446785,1,32.173329493429435,1.1341619918524493 +9,49,16,30.88482722,41.36561835,7.661537348,55.053805,mothbeans,24.93190490364716,1,11.023257661847211,5.506013210643623,401.77160438701816,1.7119211169722952,5,7.0026889049812135,50.62880046296776,136.72146349939436,1,0.6054499468604202,3,10.401699466046676,1.7235228422721587 +25,51,24,25.5042419,61.66852372,9.392694614,65.07981523,mothbeans,23.430910355174166,3,5.4761975243937995,4.202151055088914,382.680430336379,4.223864135743031,5,14.698367051670084,71.51866914165048,72.11757015862318,1,18.17645519276208,1,93.58967974215268,2.801284532121414 +36,44,21,25.12528913,51.33189406,4.516154055,38.48678973,mothbeans,14.113912935636186,2,11.688404534076582,0.8382972158191615,432.37075333914214,9.152760416666991,2,6.36204527369882,94.55457597577266,134.54231677349554,3,1.7830325527044888,3,71.2902252473865,1.6608205131010876 +21,38,20,27.10508014,63.56791363,5.794289715,62.20279647,mothbeans,21.213342045202147,1,7.565801683677528,15.997406020985059,446.36414313890407,2.647659583408006,5,14.774861131802611,66.14267710057045,109.27265748940968,3,4.020877711638598,2,13.876496959401807,2.7449521485987938 +37,57,20,31.1006247,44.82069159,7.354286985,70.79934452,mothbeans,14.26583797542585,3,7.118495163740955,5.476392028317223,430.1141064496881,9.98903778133353,1,19.14292163946303,35.597760018114485,156.266629819775,2,22.82035408784942,2,10.498451236084104,3.3087370385685486 +32,48,18,26.45707778,56.40226277,5.993513566,64.16167699,mothbeans,25.227846210353015,3,9.181847866561107,3.1267821585822198,362.4001194434914,9.134486155608695,5,8.092848671021642,79.73240359564373,69.916176229547,1,46.28593905428339,2,32.31527518586568,4.524122583910098 +29,44,20,30.04132304,63.56222995,8.620107545,31.83192392,mothbeans,19.356322071679983,1,10.538869813624853,19.427562759684747,433.08163265632635,3.6507575910304486,2,12.35642361704728,97.55643544605765,81.88184770662065,2,30.229827589306318,3,83.76342830669155,1.7699315091087597 +25,51,18,27.77799528,54.82130787,9.45949344,50.28438729,mothbeans,12.141741565541352,1,6.08755579213822,8.926745765566771,421.63354182281824,9.685597516954667,3,7.17052391030479,41.786773298656655,109.12063987880083,3,14.593600827653669,2,18.82304361846331,1.3803628798257823 +10,44,24,30.99256944,43.02151392,8.0344125,58.27600682,mothbeans,20.60356975186675,3,5.198721371433642,6.734449417842585,421.466521744887,2.3733056267770674,6,15.793631203378162,94.01252710877459,179.5541255861399,1,41.08195017079291,2,96.01972839772706,2.342447833046625 +23,35,18,26.4908332,47.36534833,5.414492777,36.99362831,mothbeans,10.17966022997495,2,7.41990657347036,0.3520412928731842,374.77182950436526,2.865425093617887,3,17.280469009255008,66.95747692121297,70.72553267929365,2,39.519403067992705,1,86.2072700690447,1.7814119769699106 +9,60,23,31.96987867,57.17377029,6.276004336,64.25520357,mothbeans,13.79313101036887,3,9.58564940116704,3.72048370173369,359.8508739022864,2.7268210496492054,1,11.444869033740234,28.929101204641782,169.0991222838419,2,12.543930656570279,3,51.18254203249173,4.652752716874536 +3,58,21,25.36140526,46.82652785,9.160691747,55.60523179,mothbeans,16.426411491519072,1,8.094934602230964,10.607060194366191,410.7766900630419,5.506651969878731,3,12.073077050973263,86.77419076748542,120.10039387658452,3,26.43983204921637,2,36.55805684308399,2.7232063881458193 +22,42,22,25.54249137,56.96640758,7.887658711,48.46797044,mothbeans,26.033191890241227,1,8.233225800182886,9.302279965579409,368.44765859667945,5.821035325996104,2,14.716642409607793,55.17728597685514,183.8644627091056,1,37.183745032062156,3,95.52024469602831,4.26017790347494 +12,39,21,28.99319096,62.85948245,8.183844843,70.4713043,mothbeans,19.10396294030041,2,5.519090137193511,9.176797866627528,401.04183079711186,6.99922502348349,6,15.048268716724387,60.21005850141776,108.0819451111833,3,27.729661652341253,2,65.1256786870742,4.768111346853152 +39,36,22,29.34317422,60.50320928,9.072011412,34.03335472,mothbeans,23.7481119491935,2,6.942578069837338,18.188073487694695,373.1951168256593,6.522792197681198,3,13.748538803232183,63.325220592495526,196.73922781149773,3,14.052703827279894,2,30.08389196604795,4.97176643447683 +32,41,16,28.63618921,61.39451307,7.702287236,68.54877876,mothbeans,25.256174771256287,3,11.350258278013676,8.99448400086142,417.202182771941,5.1479472015672,6,14.537185600385692,35.891957087380156,161.97563041305943,2,39.75883986656911,1,98.0636716590419,1.9752259311759577 +30,41,15,24.83206631,44.17085032,5.88509677,52.0810886,mothbeans,15.573493072138046,2,7.46173487080748,0.24224056835894237,361.0616733642571,2.688074360685721,6,16.114580894617685,50.84103894466,183.4788858193433,2,22.39112816062599,2,28.31343965322757,1.500033649118056 +19,36,22,25.44689075,58.55363573,6.16496284,57.04826619,mothbeans,12.472760939716386,2,5.0865336027157,12.715974185103157,354.359080894786,6.43622664059352,3,11.482433909632238,51.29112054684109,166.81359138339207,1,48.68269014388886,3,61.58924421605306,2.917055250346679 +4,46,15,31.01274943,62.40392519,3.504752314,63.77192383,mothbeans,15.321627455529681,1,6.743617593126242,10.630288587974457,395.68833379405237,8.967959669057652,6,11.55860070150078,91.42943478850735,170.48577827146218,1,1.9145197915639622,3,45.595205861225864,3.8101407317778846 +21,39,20,27.06179658,52.3003173,7.388007483,60.74583498,mothbeans,19.13251841260769,3,5.6354975044578,7.464662211915554,413.41454684217604,5.412942836205235,3,5.28313780163118,42.218035025222676,140.37095176599914,3,28.0869786332413,2,52.915298472868685,3.545946938263496 +35,57,25,27.0956288,42.26206161,8.340398059,71.1271039,mothbeans,23.56947337146422,2,9.033133507510382,5.90323133755575,441.9474144587645,9.757850622065066,3,12.566995439532201,99.03135775148685,170.27179532513938,3,23.52476165963116,2,55.96148132094464,2.5024541391159563 +22,55,24,28.56800579,57.30636014,8.66077954,64.53027638,mothbeans,22.12598950239208,1,7.827600064598506,18.74376192640716,444.2638825265699,8.978364104809648,6,10.361250367717158,70.95905140423659,113.80370464769744,3,18.061600558928575,2,53.41398364952218,2.893315053847993 +35,51,17,28.79929247,49.84213387,3.558822825,40.85534718,mothbeans,27.58410011038416,2,6.76856305182862,9.298069441303314,439.21675617804203,3.0231065186472112,3,15.977537266532568,91.9824794406158,173.20601767433058,3,1.0669375779377877,1,81.25760071668577,4.227681612575604 +17,56,17,27.94293692,45.41393636,5.9565851,69.66289997,mothbeans,13.351745529739055,1,7.160445134450459,19.010130309102703,424.595452083842,7.951117757939804,2,16.721682918005015,46.59621541754919,197.86721849164235,1,20.44994482077998,2,31.727080435722186,3.2075655691378375 +28,57,17,30.47757686,61.58245338,9.416003106,61.86633917,mothbeans,11.737787437312106,1,11.121880747799414,6.969204722991755,418.64734462121606,8.571635824099062,4,8.622915328121376,40.14150557522558,159.72780159724152,2,20.443456199193633,2,20.286454207902903,3.6536123231119455 +22,36,16,30.58139475,50.77148138,8.18422855,64.58559639,mothbeans,16.499630482975018,1,6.7845779851311985,0.04434389045487208,373.8366819183951,2.9827260301467606,2,16.30010543808924,65.63109649177721,66.68024230037793,1,17.231306164293734,2,65.28986182078896,3.983436048324634 +11,41,19,26.85911286,41.81420849,5.131779302,44.13827124,mothbeans,29.17208879732577,2,6.7232256902208425,1.055976623356858,446.67846155749515,4.613717835618829,4,12.003692901279635,90.96242189660954,131.77956199612916,2,26.46554464155918,2,4.506975405767277,3.6548503113166952 +38,38,18,26.31051759,61.18749126,6.294130313,35.73403813,mothbeans,23.306985941513478,2,11.190040088456186,1.5101350407348346,374.084498936934,5.300930895718105,3,17.211387004749415,24.382088361289544,167.58019555749286,3,2.8292550355828503,1,49.52509258039326,4.894155611601693 +23,37,24,28.77833449,44.2252605,7.991902443,33.95825723,mothbeans,23.609538094119905,3,8.163641409624773,11.864364287784184,382.642480433989,1.7831109394897955,3,7.4292105941398034,85.56782899705304,87.5650796516407,1,29.094534872773963,3,31.46687187248417,4.603689600456786 +25,35,20,28.90245417,43.35365671,8.923095695,71.90018566,mothbeans,16.33277227942275,1,7.181924260227771,8.882671494141158,439.54834674652045,9.084841755288542,4,10.482235667986723,9.120261008995934,120.62498938190116,2,38.59900510129025,1,14.26571987382178,1.9946041239517305 +40,45,20,29.37687468,57.69622912,6.878498176,38.34303462,mothbeans,26.6498226819409,2,8.716148169141324,3.0422280078013997,382.0353649013901,8.642639870580592,3,9.847961214232349,52.11400095137779,180.83173704297073,2,35.538894391052054,1,52.15619989734045,1.9822284053132555 +23,58,19,24.17093241,58.25204566,5.243634849,59.18953429,mothbeans,29.471734752490722,2,8.928022244467222,15.795774316266826,362.0508627409824,6.121328423947078,6,19.127774952209766,86.21805374639372,117.80240379451624,2,30.84832746837372,2,23.34502627561551,2.719254884659376 +2,56,23,26.65333029,59.79023382,7.550090941,36.91852635,mothbeans,21.905366371713423,3,11.34175364351724,14.617363768866827,409.7672770565939,5.350897653031954,2,17.492823543939195,99.86540536898106,151.11576396874545,3,47.815470337081116,1,89.68950029693,4.718480531299372 +3,56,17,28.19912143,53.50567601,8.709291688,52.13580529,mothbeans,19.47215996496614,2,6.292275716190937,3.2088426309882445,359.45750187260586,5.799886960720507,4,10.191922205821024,74.98404631778031,148.88882305980925,3,6.669174268873595,1,12.130973613426866,1.559721103489307 +26,51,25,28.76488954,52.62741529,7.792508068,55.21606732,mothbeans,10.12783374743764,3,6.090710998443355,19.61449260009768,421.9934207024994,1.0547983975989674,2,12.80984755073947,18.17862636547416,184.20178071374147,2,16.726443286442542,3,11.441041217834302,4.114247028694821 +39,42,20,29.3499706,61.25353851,8.055908858,40.82840673,mothbeans,23.75561074964405,2,8.956207885682948,19.229078077496162,442.07756872320806,7.776503450647754,3,18.115890936466954,55.74187844546535,117.35881103358966,1,44.29208440798587,2,97.50584753088542,4.747570543314918 +27,59,20,28.00937423,52.60950014,4.397698806,36.01203025,mothbeans,19.800941871362454,1,7.5421254276168295,10.531910061996665,352.02010659378413,5.387893738578066,1,7.566411846588919,68.53483893330966,109.6933759824006,3,18.131837395046325,2,17.912191934681076,3.6498401030859715 +24,45,19,26.85851927,48.8246387,5.952384957,34.7426459,mothbeans,17.931702557056486,3,7.949974595168191,14.739774601959727,440.9807227665831,3.0715451539152316,5,6.817708312442658,32.07310474632433,101.59075383220369,3,41.0319500554062,3,75.55631868520798,1.147114139708056 +7,40,17,31.2123945,40.92604945,8.532078733,53.78769958,mothbeans,17.69495169682302,2,10.086239746582745,11.552296876402464,405.94158883996624,2.5917640833510633,1,17.976091007509233,49.7830802187736,100.9275705422738,1,31.09959910432035,2,60.587880462409686,1.9355313013043793 +15,45,23,24.20422636,61.43378674,7.224193642,46.0203959,mothbeans,17.042527658699484,2,6.717720909784783,15.82100629896809,367.82676836859576,7.877867227609075,3,9.83573360069213,83.72802182258143,184.20765856674902,3,18.14979430194726,2,66.1657712765903,4.721977067820061 +26,52,23,29.98835437,49.60384796,4.931890506,52.92929636,mothbeans,29.206922038473262,1,5.31334104987321,7.187470199539783,374.9419587184337,6.391610485124865,3,12.982212953762922,46.98706044200727,135.78244246209266,1,28.118690713561207,1,98.00355393347631,4.333622504210846 +20,45,16,29.93964907,54.61813464,4.626212446,45.43669946,mothbeans,26.74203318602375,2,11.563319189513368,7.045532781697674,436.06270984534893,4.828964833623635,4,14.486563402533129,57.06282847095188,111.58437942852538,2,41.17808626492882,1,45.713400726745114,4.908421322722964 +34,54,24,31.2119298,41.55934359,5.026003659,68.80141783,mothbeans,16.997986847340627,1,11.209331467418256,5.468808989125929,380.88402553408497,9.87852468370304,3,7.635770248975149,8.84601342805068,143.71975808146686,2,12.7539628395388,1,19.57365632992686,4.800636017704436 +19,51,25,26.80474415,48.23991436,3.5253661,43.87801983,mothbeans,15.86987574961903,1,10.717175118935199,8.995700363949508,376.2277612213674,7.581773117719541,1,18.850929499924778,81.64308005371697,149.18123915120128,1,11.872132615699005,3,49.67048177136802,3.3468590536246583 +29,41,21,31.49398069,62.84916863,8.86979671,64.56807592,mothbeans,19.320533704879914,2,6.777285228228739,3.5776155072399307,374.30339710781766,7.363523664420048,3,10.184035609423443,18.321796658126665,106.23942590188344,2,40.534922585949204,3,7.688647976454521,2.7451606252428746 +20,50,22,30.99694676,46.42693735,9.406887533,38.31597852,mothbeans,13.716790171536932,1,10.035565017218836,2.515736010238594,362.9002321598973,3.4735865852206858,5,7.928117288694386,17.92540415695585,199.88157956474925,1,23.022589736537295,3,47.616348824871245,3.466980435204032 +11,40,23,29.61253065,63.04749127,5.80428611,50.1978269,mothbeans,19.412205965510825,1,6.985668316688893,3.2118740050442818,368.1737350305575,9.855034058441223,1,10.788851790042859,99.9544740812911,167.92490009510308,1,29.210513154939292,1,32.81785985592629,3.628752267563799 +15,54,15,29.97604322,57.03184356,8.35495812,44.86052932,mothbeans,17.703594353008477,3,6.443115865208169,0.8687224153222006,423.19690754624264,6.2513514609607554,5,6.707538382234297,91.08972100916837,69.92985117485986,3,29.583167016363728,3,0.10604033766569154,1.4392762964837358 +35,55,22,30.88883074,52.62696801,8.634929739,55.51932414,mothbeans,13.85272937259489,3,6.961355247745702,12.48794711201346,396.4955374918728,6.034790470180624,2,18.687169545982364,21.356160194469275,123.44539325149393,3,31.24001691476784,3,38.33126182604909,2.7523582242649245 +9,59,25,30.39321309,60.16299493,7.699200949,35.37493212,mothbeans,26.940246492019146,3,7.955322528467683,15.886315502984408,408.4719043023054,4.391306634185138,1,18.201428992834984,32.26800792901487,197.40290968781514,1,10.903785003218125,1,78.21228661788498,2.985123950276039 +40,45,18,30.43683729,55.20522037,5.261285926,30.92014047,mothbeans,19.393894373235998,1,10.022925324337127,9.285440757433179,398.1252042508194,9.4481696822315,5,15.057927248184331,9.31917070587529,124.14983543912705,2,12.474276575836225,1,38.91909890284446,4.586244829588393 +35,38,19,25.32688786,63.18180319,9.112771682,32.71129281,mothbeans,22.96719134004259,1,9.625613039190146,6.689293060449448,438.3475448255558,5.45619173586532,2,15.517459772085239,22.00828139351647,129.53025310412784,2,48.62739037929152,2,96.26738094262012,1.9746241554929198 +14,58,17,30.53684308,59.96664731,4.605700542,33.48919022,mothbeans,15.968682046160998,2,9.119783473363455,11.688890105473508,374.62305277735123,3.7148188149785217,6,11.925584636842029,87.58754643150517,50.77746022122046,3,13.028816628360683,1,34.59828724315891,4.172763065451807 +40,55,18,30.38257873,40.5926071,7.115994051,47.95406479,mothbeans,17.320335490978174,3,6.946792724968129,1.7140021728947397,406.8029869464216,1.4443338642609844,4,6.952435041653713,66.3833974926405,82.82202308488746,1,7.181177761786339,2,72.26627503808855,4.020712576187473 +18,36,23,24.01825377,53.76623369,7.214078621,35.03404425,mothbeans,29.819795908558532,3,10.387338933188126,4.57958565761611,434.83295003546436,7.2817122156223055,4,15.317472025375803,41.30414879586007,88.772189629152,3,16.015876191914636,3,23.92179139305052,3.5972557182502425 +35,52,15,28.69841277,61.14754363,9.93509073,65.67591794,mothbeans,19.303856543481743,1,11.363374010473088,2.049268639463313,366.5838567268781,2.812278359766552,5,9.13770678300827,65.06141840971337,79.4819471700776,2,47.58167127477713,1,89.6266359440786,2.7501506781657574 +4,59,22,29.33743412,49.00323081,8.914074888,42.44054315,mothbeans,21.830246552536494,1,5.653256833364102,19.512036881582947,410.01866619549253,8.617960888934881,6,15.869387861210383,47.79923600498152,75.93574860573187,2,46.019758702744916,3,43.861868275188996,1.7007394457423732 +22,51,16,27.96583691,61.34900107,8.639586199,70.10472076,mothbeans,21.695397466069636,3,9.137806338165984,3.2261642681137204,373.25938438534473,9.183517130861276,2,9.979624257400147,71.06205915353328,63.24601529947475,2,38.38877450895896,1,19.43897553475873,3.584369341588516 +33,47,17,24.86803974,48.27531965,8.621514073,63.9187654,mothbeans,20.206656248325164,1,5.121638277919372,10.915303456085038,428.3757230373896,5.731886623813362,1,17.677743582821243,10.627325286040712,116.41148859303775,2,26.349401318020753,2,25.45985028123895,4.453402796249366 +2,51,17,25.87682261,45.96341933,5.838508699,38.53254678,mothbeans,26.22282843549394,1,10.992233592727008,1.1336075354859876,405.5673852414353,8.666025086148043,4,5.617356749166893,39.62000388653322,128.1851338244163,3,36.751678995778754,1,25.863336292175752,3.9885793804588556 +16,51,21,31.01963639,49.9767522,3.532008668,32.81296548,mothbeans,16.28911808895467,2,10.107060232501807,12.44540366209665,432.64891541117294,9.792096309984684,6,8.75227758613493,24.32815581113552,178.82944878902032,3,6.2384998604379955,1,23.54312163673624,3.4213674350145737 +19,55,20,27.43329405,87.80507732,7.18530147,54.73367631,mungbean,18.500345321383342,3,10.063063870844449,13.423075517640351,386.28011862144,3.9603879007253537,6,12.057229630946686,13.634623686314573,194.12622174805483,1,44.801307412690036,3,64.98613402529895,3.784947840954921 +8,54,20,28.3340432,80.77275974,7.034214276,38.7976407,mungbean,25.994381319405548,3,6.134352317376838,9.482870918275214,447.4761784027138,4.641116979602568,1,17.76495474325639,56.79527288515983,55.191308811407545,1,9.717869411293345,2,16.52377092647399,2.9255415520570587 +36,55,20,27.01470397,84.34262707,6.635968698,55.296354,mungbean,20.33728970498565,3,6.256440245190361,11.097232518118165,377.6764343117186,8.438937607669763,6,14.039189900563237,10.905778617738381,90.32702492013723,2,12.604456611684256,3,48.11109997150289,4.976944122869359 +10,56,16,28.17432665,81.04554836,6.828187499,36.35720652,mungbean,16.400390779250376,2,7.976017540364233,13.042821709949655,441.19486725614286,9.592362938734425,5,15.847544662418427,55.31077916779801,104.174370102864,2,36.22287462048497,3,10.199504255258185,4.447998306232603 +22,56,17,29.87888063,87.32761241,6.89077995,44.75215854,mungbean,14.824138686204268,1,8.482151947927859,19.409273166159956,385.6941516327896,1.1567672278962604,1,10.171353094929994,66.43121073134148,197.5500081937698,1,39.209461826653836,1,76.56486017669998,2.8916095363475294 +9,57,24,29.89232778,89.71503316,7.165121109,42.99498978,mungbean,14.933378149473928,1,10.853306693383168,5.482904342379231,368.1231600081201,7.487938255205838,5,19.165326424697987,7.663946366588448,60.20959851793643,2,9.909156547937014,2,63.21849750374864,4.451575322231065 +34,59,23,28.56212158,83.24855855,6.935804256,56.48265193,mungbean,23.467826869882202,1,10.874354105994058,4.259003457389081,401.97749336218465,2.75805852779861,1,18.887037508553426,95.82161749524474,166.60087881602348,1,25.69269867132838,2,99.93927158624639,4.598664157540444 +31,51,25,27.53592929,85.5701901,7.196774236,53.01899249,mungbean,22.266488664004527,1,5.250812780274657,12.170761842794985,358.55102376603145,1.1365635331451938,3,16.834600896161973,87.34646676279631,103.1965213955612,1,9.310323033646007,3,38.54262535099685,2.2009101934158286 +0,49,18,29.68361658,87.93598094,6.990095452,41.82490236,mungbean,15.221686107949463,1,10.04335523058245,11.297550322606277,403.1459617278921,8.119511928465847,6,14.738475118870456,35.71652959075706,191.4140284485437,2,3.1538141216433893,2,91.52597075144597,2.509883975598188 +21,39,20,28.14448546,82.1193047,7.064782138,46.75690086,mungbean,27.039414754591323,3,8.596122918340084,15.597733798303079,352.8317846282003,5.157938844329821,1,6.190927683243376,93.8264169466639,105.89513780179215,1,7.007456561076847,1,79.29263335641022,2.3655095041128216 +28,35,22,29.53037621,86.73346018,7.156563094,59.87232071,mungbean,25.769266961700097,3,5.099860624713038,7.181087210080939,369.51975520466436,3.507004954965545,3,10.869309117453891,53.75771670227362,112.85844642073681,2,40.849611142149016,2,61.49783307300958,4.5615315437302035 +17,52,17,27.88352946,86.45147631,6.364967184,44.64407105,mungbean,29.020144582765425,3,10.949254216586194,10.985005635274659,417.2333981027039,7.343624061381707,5,16.353134375170036,97.9101957951615,184.85533095768466,2,13.502848696573421,3,61.550834432575954,4.23218000990313 +24,42,23,28.22471276,82.35916228,6.428054409,44.01206619,mungbean,10.327596406110278,3,7.252080915746541,10.228046766815686,393.7271182430995,3.722230189667338,2,15.406300199156826,37.23787209507024,186.01945319442794,3,39.712442369939595,3,6.297676106232297,4.075168781608625 +28,46,16,29.008124,84.96089355,6.664187809,45.91011391,mungbean,16.482409020425827,1,7.8627145975910535,10.84826662550043,441.40606670730034,7.894192161073087,2,7.401107980363115,68.3048787679651,185.6652874634005,2,45.09895493657137,2,18.241900210310302,3.99507882740395 +21,38,21,29.75538903,86.45193297,6.637677489,37.54602719,mungbean,25.53461711457743,1,8.101776362903312,9.213701642397398,368.037007278404,2.328197402775652,2,15.868741651347541,64.9713998489903,167.30579177622758,3,40.99007073280062,2,75.59416996361217,1.413953970959795 +34,60,25,29.78416743,85.16906976,6.79385576,40.77872823,mungbean,27.268251662364392,3,11.663386611961513,5.41262583218397,382.99152548704126,7.249114539791023,3,18.32874131866759,62.72165409250524,72.91822191097563,3,2.534770053091484,1,27.330365283942694,4.975011394221128 +19,53,22,27.8640132,80.4513142,6.852884643,42.83053902,mungbean,19.957376005563702,2,9.633042533123344,6.55950191062761,425.03862650937754,1.7263230114163943,3,8.362095885260166,16.392675947257363,164.09135101732903,1,6.759563076506714,3,14.883474121746353,3.310672610506021 +31,58,15,27.11026483,84.96771717,7.121571293,51.52617423,mungbean,22.276990477441192,3,10.13112763951517,12.958154284784808,407.1910650329088,9.30588822745528,5,18.197886415887275,57.65089423464222,184.31341879493667,3,15.658971277531252,2,25.694598106583467,1.2558002851781311 +19,35,24,27.11030369,83.64274107,6.883308033,49.11964582,mungbean,21.75351818873004,2,7.555508947956852,16.376118997552325,413.5326487511478,9.17880646380281,5,8.365489928178285,18.212925427423,64.58607772384984,1,12.662472158242055,3,25.282714292784714,2.1613209757500207 +24,53,17,28.95451232,89.07866095,6.421271178,57.65901369,mungbean,26.871985878654193,2,6.158158582843827,13.170955756833639,397.9251953960482,3.5790124732432576,6,14.838176766504517,31.804907974028986,158.01638692250532,3,8.730742092257831,3,49.150871986453936,1.8909756012423364 +13,47,20,29.21780035,87.93724219,6.54450214,43.1386631,mungbean,29.172405963292835,1,6.526118384957407,2.129719468842377,408.5862296026756,4.714071798484921,5,17.62368755325734,80.99674876743215,100.16792920777236,3,26.2299804405692,2,98.75075790639319,2.4793388249741577 +31,53,16,28.7420098,85.81675947,6.452006451,48.54598575,mungbean,25.25480283780734,3,9.250816834225386,12.976777852789763,438.29359167295695,5.5875800897059476,5,6.3792584986495005,33.79512887063417,75.4008561756873,3,13.055427705211798,3,99.01226344721353,3.913538209968443 +28,45,23,29.65021184,80.29868321,6.489259136,56.76278363,mungbean,13.895573037979101,1,7.3562304741102436,18.161495904516446,445.84657970344284,8.070870695467832,3,10.393496010443384,83.88977965524886,50.97205047104805,2,41.999036393771156,3,82.18647318299853,2.4511041197084946 +31,37,21,27.23924995,86.404241,6.713410626,37.31236904,mungbean,24.38779630113764,1,5.365232708100329,7.539806865431437,410.4670240035616,3.9413545681230846,5,8.624616941496363,21.987847829984332,160.4323649637663,1,2.190211023804012,2,86.96029693351545,3.0737056419770705 +33,60,15,28.95172351,81.67085323,6.510840928,56.51103293,mungbean,23.06373632739978,1,7.312838473724796,15.997895726222646,396.765204180705,8.13338610409476,1,7.534527378946198,82.26439213562536,178.8524442643163,1,8.308775439438577,3,35.25407485096024,3.1265811018397076 +34,45,21,28.18837136,82.60629652,6.287380117,37.01110438,mungbean,10.77252582021304,3,11.567611690122881,8.152309371772192,372.08028447089674,7.696929150821766,2,14.345131576386109,35.37943431984395,54.50653012316506,2,22.910995786199408,1,58.735612789192736,3.3673702638166607 +13,57,25,28.30041493,86.20681554,6.86308576,50.47333854,mungbean,12.35207772263321,2,7.838039082366695,19.02126377829011,376.24138181821274,7.03801243440927,1,15.43034851608366,70.77372230442333,154.2956162824938,1,31.84578020050019,3,37.42692228739166,3.753834460429225 +33,57,17,27.89636126,88.71782287,6.78415271,57.79863368,mungbean,24.226219456379617,3,9.028249261393054,5.093254843308943,406.37923290224285,8.43296945919506,6,8.576779103512461,47.00759237518491,90.36997909549743,3,3.635956750207081,3,14.179006534016326,4.761542857710241 +32,57,22,28.6899851,87.50436797,6.769415888,44.56598352,mungbean,13.945082022622453,1,8.239420615667893,12.593550389313021,395.4618699462327,3.0793326027742793,5,8.697352706596046,2.3998457990038413,103.59936007380685,3,14.420076994353781,3,67.32921722076702,3.3903924787398094 +23,59,25,27.8262623,88.73100226,6.320768488,56.68833819,mungbean,23.679371355101424,1,9.505779897911111,0.8767517812283465,442.60632769514064,7.009360662542389,6,15.584459489122171,90.98484779672437,126.18548013083372,1,46.09156993192308,3,4.89307519662373,3.115523335925235 +35,41,18,28.70562673,81.59200689,6.705008504,59.87065439,mungbean,10.806164626739998,1,11.945840787338916,5.472917621791797,431.8522263570635,6.421474607999091,6,10.989572203564148,81.43505856200527,98.43863613543245,2,33.83200512188769,1,21.05220992386935,3.6513214462752837 +6,48,24,28.6362812,84.61431076,6.790736339,48.48319335,mungbean,23.88330695629805,2,6.171385077639604,1.823532636950047,441.69055164982615,2.9014634468780036,1,14.3805066954009,34.62350964471283,133.12688793792137,3,20.22361528589853,2,90.09583642536232,3.9925361169539855 +29,36,25,28.28511547,88.4393979,7.130278657,48.56690235,mungbean,25.720469344916552,3,7.582908792029447,12.027501768132112,428.1406827787274,9.826969284904811,4,9.552308763181077,32.815172445879234,162.69555002293913,1,12.553191192971369,1,78.47661169737316,4.406168467194886 +4,36,22,27.60887393,86.13316408,7.012740397,43.80041104,mungbean,16.45921434868169,1,6.2076552045069615,0.9253124507556221,407.31598174320334,8.891255748517732,6,7.476394966668346,59.306181273523116,183.32867367820438,1,16.388658966697694,2,12.966578148841034,3.077457453669723 +10,59,22,28.60901145,86.99495766,7.155685016,36.94616965,mungbean,23.568710994370687,2,10.3484262869818,4.380225304884271,379.59080681946796,5.40692368180777,6,14.756259768138417,59.42479727163396,182.7848544321559,1,24.10088448475548,1,65.72745549332076,4.52680920571774 +14,48,21,29.24598976,84.80084105,6.991242362,53.43228915,mungbean,19.57807918709891,1,10.914421376849694,19.65807546294472,357.8642892123429,4.632358651620411,1,5.741850218873673,36.62305958108599,153.95309928980754,1,43.47985417292708,2,72.74584219996268,4.082057289838618 +8,50,21,28.62911222,89.1148059,6.218923893,50.49913241,mungbean,27.901971035126266,3,7.3170820904911755,9.307720648884494,432.61987456955086,4.980442967575685,6,14.574604330265416,3.5892979557944016,68.27353251553252,2,20.96397205186617,1,0.10110417863760102,1.4081027159381767 +20,40,15,29.57329479,88.07505524,7.199495368,45.04467075,mungbean,24.79759330670052,2,5.523702989529167,17.43083548490106,448.6540013717253,3.1207596661021193,1,13.95196419363388,49.51991816670791,112.96638527262525,3,9.44212132999388,2,19.691295885084948,4.000570550685719 +36,43,22,27.82684262,87.16679147,6.389882166,58.37249772,mungbean,21.60431480630391,2,11.401596661617024,4.378503750797417,403.0980153059028,6.243353215931169,4,8.975357742971749,98.08252395892698,101.81546469652605,3,27.210340981600567,3,66.63057951671674,3.302660309180787 +14,57,15,29.8757015,83.14796296,6.623438282,40.12044158,mungbean,22.5801603262697,2,5.025547869988459,8.100873807047721,416.7675209413258,5.522106363309658,2,15.987857721154445,59.03383567581408,157.988831977907,2,26.67278554558684,1,76.25061192971717,4.673019202613975 +11,60,23,27.33684386,88.50229102,7.033012777,51.09802625,mungbean,16.993429954823718,2,9.895989418026428,12.357501399148472,366.05096457463696,7.105378195397041,6,13.303504772930207,62.516098268209085,178.86914690043935,3,41.95858344665193,3,23.42928900309569,2.954418827793757 +10,59,15,29.83040388,89.30428305,6.32400451,58.86687093,mungbean,18.34078535944638,2,5.134806685277974,10.82294034301275,399.47427214786944,2.426137392646901,1,5.352734210300366,17.985366253138167,56.42574427700407,2,23.4552584868176,2,64.07716683225043,4.904711635175548 +7,60,25,28.2753171,82.76020821,6.397636709,56.04995423,mungbean,15.504278100088829,1,10.436229739205057,14.929279588177707,444.5969776083614,8.626819416077272,4,7.072675977795985,92.84157233221461,194.70705710667463,1,23.68048377266269,2,26.234548066505514,2.9348547523692776 +2,47,15,29.86860065,85.99127934,6.401455706,58.41394143,mungbean,28.466005448070174,3,10.872042143082611,3.8401013109308013,423.6103633595251,2.142934390529188,6,17.03666641706841,61.559491888741945,76.97382344171658,3,39.773042948784784,2,18.705148926966665,2.248472219643614 +20,45,22,29.5888162,89.9939693,6.904587016,54.96121262,mungbean,26.972461723523185,1,6.063556432511746,2.7840333900816994,428.93059514246585,3.5918656231442765,4,11.936660450557929,11.920920206413854,50.42472403517268,2,27.120854464832316,2,44.772869528343065,1.705449382025931 +2,39,15,28.07219563,82.9116472,6.478557136,49.61865305,mungbean,23.263032045833988,2,10.114437607355836,6.1213007925658065,394.58503889237136,9.917378998527436,1,17.668136974908577,58.61933544616019,84.44065673348354,2,27.243453932592082,3,12.359811077369809,2.4810491707349294 +27,40,24,27.84026517,89.99615558,7.063022095,52.84626009,mungbean,27.779577050348035,1,8.026831395105756,6.146631510110295,436.299443944315,1.8548389583460048,1,7.168881959560198,33.08342578107543,53.97530155063081,2,16.583109723706784,2,46.700413258832455,2.6734670274434427 +35,48,15,27.10818093,87.4512669,6.981758362,55.03723979,mungbean,10.898518512426204,2,11.298400392382495,0.8664790556160606,391.11092849988313,3.860637799914421,6,19.925955611981546,58.698851525095975,100.68136184336748,2,6.158774755379448,2,41.42199040525014,1.7461563264743805 +4,59,25,27.68515114,81.94268594,6.227134139,54.62243308,mungbean,23.293865898642444,2,7.987059038012418,8.505165616009776,414.72842334402077,7.926112389978731,5,19.067890134532277,71.12434142606327,78.241680550386,3,19.06835774200267,1,83.06274285044593,3.6623129025872605 +1,48,24,29.34594634,85.60472562,6.232836962,59.03629954,mungbean,20.34366154221962,1,10.225356610738617,11.412656638306649,437.4206367078022,7.336082733543491,1,11.226824645245827,96.16504191722007,59.76557434005949,1,1.3359412336788945,3,7.671267364170797,2.3502390538878126 +36,43,21,28.36319404,84.8593608,7.140437859,52.93031105,mungbean,22.685406022692995,1,10.81159905558441,13.593329496571258,398.2784843873901,7.938921330620739,4,16.525094492489245,81.15299291975569,113.07828278245151,3,10.680369806268935,3,26.294333993845775,4.950568938645809 +11,46,24,27.65280218,89.80650642,6.459252023,56.52558045,mungbean,21.884680624706466,1,8.416456178994569,6.898478040163449,433.750841732157,3.0795177966550678,4,9.108311989771785,87.99000818331743,94.59863293422805,1,25.836296186041707,2,65.66749088926352,1.8510770664296765 +34,47,19,27.31372793,85.44815232,6.568795404,53.15223123,mungbean,23.200537277837306,3,10.467041548754,3.6152265108094483,363.92079258180985,6.24735977626606,2,15.389462584959894,82.5825368119248,156.46814223935624,3,39.02472488589031,1,29.02594628493478,3.559221918606901 +21,44,18,27.06909959,86.89934108,7.12851089,50.46746116,mungbean,19.1248653473817,2,6.358961617572832,16.406411186796976,406.8840093840905,4.6970565098277515,3,17.4933012696765,19.04615805088483,65.76244156394868,2,1.7186714186944507,3,14.46580349418628,2.3312390105968923 +17,58,20,28.06642822,85.91625451,6.42937879,39.23831035,mungbean,20.51109414201811,2,10.142010747452062,14.756608327990406,392.71257604477006,2.9001200135356258,4,15.94759287197125,90.21528272029573,149.31122732827652,3,15.388351975067954,1,35.52413389529308,4.844738223643443 +25,40,21,27.73329078,81.13903037,6.248900919,44.17580911,mungbean,22.222386708197135,1,10.679585790922411,6.872896393209798,350.92491197662963,9.40556477988647,4,18.889230715194124,79.53418458051524,160.31776082560293,1,20.646927756586848,1,9.000340723490607,2.437512876784151 +2,38,18,27.53632932,89.92908171,6.619891498,45.48591922,mungbean,22.575228491367987,1,7.443793519035861,8.666745176496258,400.77043779962077,5.221483058041268,1,14.854601615602014,37.480953526467644,154.56542616742786,1,7.129856308733756,2,29.235572164910593,3.298679928538759 +9,48,20,29.66461594,84.28187572,6.377568542,56.09542002,mungbean,24.936893411311598,3,7.50171975498957,4.064010903653966,443.7708184395928,6.231629077284575,5,5.367593787991231,68.08824220566248,148.21394882632706,3,47.4019837847058,1,50.09446897877239,2.7545356943792236 +37,49,25,29.9145443,85.85384444,6.415459592,41.39081525,mungbean,19.162143277756837,1,10.73964675235138,14.960746236835682,416.9776633168782,9.346502114619847,3,10.3701312040501,51.666657553156114,120.18236790224748,2,33.89136361203388,1,25.843721635757653,3.8113346263089785 +36,38,15,28.36363858,87.59810657,6.320662012,57.99524359,mungbean,10.419889465405209,2,6.884132627238562,13.266390532950725,383.6512922648041,2.7280096516060537,3,14.828588800967479,84.23016970287026,152.79123219406466,1,20.259843710324333,3,71.05751756121816,4.128275988692238 +40,58,15,29.46416042,87.60890009,6.978400282,43.15411472,mungbean,13.532386053203107,2,11.990356848583282,7.136626679973044,389.53527721157224,8.13439464369839,4,16.707119630014994,1.814341481660553,195.5193840649691,3,14.192814050823682,3,29.20576037489552,4.62812297046926 +30,44,16,29.73013036,82.89166381,6.442335593,50.91511275,mungbean,14.701166856771303,1,8.214284695587256,1.4504565578124295,435.73329802316005,7.961942493640746,4,18.202671102131582,54.38874549445904,131.92867480533897,3,4.995897681185657,3,80.5788544351989,3.6111676327306474 +1,59,23,27.46852989,87.17649,7.184398832,43.78420984,mungbean,26.905932728273402,3,11.226191732257401,8.599382339575566,354.0976472218162,5.188935254713487,6,13.930657876331313,67.09443765171088,75.37124859615275,2,28.55642697960447,2,42.17533794655307,1.002106323932241 +9,48,22,27.77076285,87.09979549,6.402926221,49.50812624,mungbean,25.49440692183775,1,9.742414481339951,7.826374104067371,406.5249893272392,7.3174679087956305,3,8.058816799601235,35.68583590696458,78.32041495933046,2,17.460285209265574,3,20.31212812983124,1.3884052850005073 +14,41,17,29.12939524,88.48312598,7.085982325,36.45012824,mungbean,24.7320869147292,2,5.899465873826669,16.224448134473654,440.84165289687746,5.5608183970397915,1,11.903803860926331,97.62030142459099,124.35302289956334,2,40.256400288139695,1,9.315582959203006,1.5241443388292537 +35,52,19,27.10606808,89.89593328,6.698574085,37.45680611,mungbean,22.484433924046137,3,7.578688814992074,16.89303352234699,390.3559587884441,1.9813016477587713,1,19.800345571464,83.43551365499751,89.8798796510059,2,18.14165939754372,3,12.949566920544587,3.8947093771980272 +31,48,17,28.88078945,86.94206817,6.594739424,53.79732545,mungbean,11.402539356998794,3,11.355889337197498,8.03394412668489,420.58124228902943,3.1194214845378494,4,6.629019063650249,69.56757292010822,162.2115142975116,2,47.660237812236915,2,69.76822623845096,3.177945054129654 +4,41,20,28.14720892,83.8001509,6.647965508,37.44800463,mungbean,22.08148010791099,2,7.607656468366766,7.500012894699619,417.97009189654216,2.6718209508368536,4,11.492481457265804,74.36620510948036,156.27800171407574,2,25.33488584850645,3,77.70959981791007,3.4422690291564737 +30,37,25,29.89129144,80.14487166,7.120032489,54.7960127,mungbean,26.23302146648161,3,5.788779725272575,6.978795568065697,400.88392117006043,6.123245865448629,6,13.347020969872116,13.018482086353222,179.90972750152187,2,19.810814951772688,1,56.64198625918559,2.0620392609098546 +9,35,20,27.41503453,80.98004661,6.91380932,40.53173216,mungbean,18.871151704650686,1,5.576141179640385,6.399982121585717,390.4579538812476,7.678046358918431,3,17.269830118669212,39.50075116604344,72.1137879378363,1,39.35917653338595,2,24.396029438174494,3.247032048103397 +20,41,20,29.27308605,89.4875022,7.073048264,50.9246554,mungbean,28.665088563274722,3,8.17095365937584,14.457716861200986,383.58961854031287,4.693911681664237,4,5.6694268404667545,87.5219839422208,179.09836786234132,2,37.82974880609078,2,17.46964102767873,4.115820278043751 +37,50,23,29.65296893,88.48587386,6.5304707,56.01913159,mungbean,17.12070475802118,1,7.827264304211853,0.6640737862093449,356.5887368100393,5.944345804867277,5,5.981973777214476,40.68538241032837,136.37629868408635,1,29.530348486189073,2,63.90464640008714,2.6982724138544323 +34,35,21,28.44524991,82.67639542,6.684381357,58.18713162,mungbean,24.94473910818272,2,9.65907455744158,11.340469340089967,380.6628519737677,1.1226346124607014,2,17.088616038557497,62.36845018467784,181.37260550499826,2,5.428551936316007,2,44.429849528196954,3.8240825600409885 +14,37,15,27.96235681,83.97586797,6.581351374,48.9366954,mungbean,24.404918822352524,3,7.121041597292145,18.21332158201602,384.344735927687,2.878720402127975,4,8.180664789164245,67.24167478095501,109.80672690326148,2,41.336118664569035,3,6.580660894974011,4.800520508152797 +23,39,22,29.25649321,81.97952224,6.86483915,42.02483277,mungbean,23.550068739343324,2,8.95437910855697,19.41290050266285,446.0539623082047,1.6968176693227155,3,17.446376455638,80.31078401220695,72.08074640097868,3,29.903513615604183,1,88.19131559366141,4.506149649585761 +5,45,21,28.36291385,88.00989267,6.487124217,43.05130077,mungbean,27.361352207263565,3,9.510164739539354,1.0869656059702848,360.6948344284867,4.4055428515449035,1,14.58284157754409,10.165241920978518,136.87788570547661,2,38.0633226592188,2,6.158653855694862,1.4956908918261074 +22,37,20,27.62749466,86.49366929,6.605733068,39.26137642,mungbean,23.959931245701316,2,7.069340577545434,0.5024926251262563,393.11236674053134,2.2295612394182265,3,17.99531880882654,22.770902481251156,108.40347174624213,1,31.773629362778777,3,63.88369419627394,1.3384841759576198 +40,51,17,28.66086349,86.12194568,6.860602782,50.01534317,mungbean,29.66750757185857,3,5.668087676421662,3.8554870115677375,358.116839944377,9.731649307706565,2,5.120032545129725,57.9808071471353,136.67465270054322,2,4.472954345654462,1,11.738676227159683,3.932479788599695 +27,56,20,29.2114218,87.11497805,6.41874299,51.53848218,mungbean,15.13176545749669,1,6.628777207590542,14.365636813455547,426.6984771305092,4.909971703262595,3,9.146224006178745,17.305225248719825,184.44222143919737,3,29.452261325630047,3,82.51507701289633,3.8425618867109925 +31,40,22,29.40889385,86.16063492,6.365513634,53.35486977,mungbean,29.609828086557236,3,9.142173471891418,2.644329589550991,432.0031917172272,2.102557598817891,1,12.351444940587857,74.00184641274879,83.59398800199217,1,19.78896068199664,3,68.48798617979502,3.178233401378305 +38,36,21,28.02952623,84.8845732,6.556372966,36.12042927,mungbean,10.50661295510137,3,11.577044996407075,2.8271035116278176,434.0139525335403,2.8088038386097525,3,7.456285996613666,57.44318648744772,107.43748095346953,3,4.371497947856751,1,3.7754254483178373,1.6857164721718711 +6,37,17,28.08657178,80.35005927,6.760694228,38.14476781,mungbean,29.354737294445616,3,11.087007992721738,9.664957262331917,415.82741129992985,1.3176951734269078,1,8.173172837667805,54.55277276325282,133.9913064665919,2,40.87672355012186,3,85.86355701417385,4.369280660848471 +6,47,18,29.16174608,80.28038146,6.715276663,40.16545979,mungbean,27.609144629312254,1,11.177068350624445,15.470662880917192,384.72509406532134,6.5774494095024965,6,13.182658035537527,31.629052933350955,108.88304447587058,2,48.110109093871074,1,4.398369653856282,1.0098793158669315 +24,44,17,29.8596912,80.03499648,6.666380512,50.66487502,mungbean,29.146111076779214,1,11.150097755717367,16.73248106535018,376.885585353395,5.69317678348879,3,5.493859357593379,31.02748591627984,95.19039742906912,2,46.217059729561946,1,67.08881550406021,4.696934795491084 +25,59,19,29.06631494,83.6869203,6.626629798,43.95183726,mungbean,14.239136974517791,1,8.79932911515087,0.3331277200397187,418.6148778863097,6.95444629128521,1,6.013455775344818,58.65137527891166,155.6591529376839,1,16.3777903965291,2,72.42722090829294,2.096714133819357 +32,56,21,27.38538997,88.66663953,6.702772465,58.29933073,mungbean,23.440529568659336,2,10.838772958420417,15.604833561379559,419.51603557459754,4.090801775363449,4,13.937258424004748,34.74370718983179,120.83857556141179,1,3.4529150989251525,1,69.57312618613682,2.629191817435851 +8,45,18,27.93034941,85.42058715,7.011030515,43.25095608,mungbean,11.661358078018417,2,8.46482226627843,8.342840531929983,355.91937784908026,3.985567798864174,4,13.10106079882591,72.9129551464377,63.50617900278353,1,30.331131678550477,1,52.684374306350904,2.0271950430709293 +19,39,17,29.2808618,81.8009244,6.890156495,44.47427436,mungbean,22.98077834388595,1,5.494881706440911,18.12106918722083,428.29799611559343,2.3792889546190534,4,8.860870006789508,68.01657074272896,183.9586213001541,3,12.921559522761761,2,47.20460820659053,4.9492891053459385 +39,37,15,28.9973145,83.78911515,6.821747052,59.84499208,mungbean,29.033398635116285,2,10.29776739900633,13.880357530818639,367.2667739677519,5.893271831813116,2,11.824000668712628,90.9847461249218,150.9985254746033,2,8.660981948697316,3,28.939611908199335,4.068456607549175 +33,37,19,27.92678579,86.5543196,7.183189922,43.4826194,mungbean,15.314463414163031,2,9.144012168297472,8.212375813589968,433.6612806133312,2.7744470428400043,6,10.816641749979667,0.06568998616534039,146.98071359501517,3,19.26838285506016,1,69.68357850833551,3.1040338209845038 +26,54,17,28.5474135,88.9570454,6.27258822,49.4897245,mungbean,20.287319496496032,2,5.207686992138332,6.123845778343291,370.51473408627623,2.7017666772937403,1,7.572633422768209,5.993289318553108,188.60076191114655,3,11.965474155610966,1,12.561058257546964,1.7644157574681802 +21,51,15,29.36488409,89.1886954,6.679127482,48.30159325,mungbean,17.7157768054615,3,10.568648970003217,17.734310667496317,355.7771604475385,7.686820021283655,3,9.160521424211055,27.81764204367283,126.42838700104328,2,31.99566210277028,1,40.336887588204384,2.6723407591101647 +22,54,20,28.56149805,83.63802195,6.689825155,41.013132,mungbean,25.66663034078251,3,11.261724208850609,1.0439915076258832,413.6910122503096,7.053076400775706,1,13.672741208290503,56.80896239661492,193.65551558083132,1,30.396229943253577,2,45.8749524534074,4.84095195822511 +29,45,16,28.43683487,87.91332682,6.583381939,43.12063289,mungbean,14.257501799405535,1,10.872232836095066,8.842491454079886,366.8034163839278,6.0595358504597785,1,13.266938023573456,45.21026160524742,195.9146550892993,2,47.253156414833,1,49.40789866524785,1.9132490032789486 +4,40,21,28.79728147,80.45744422,6.725551062,44.30070517,mungbean,11.924927051281731,2,7.458857033604382,0.33769495846467823,389.25602897098446,5.678531006017132,2,8.246649742764491,20.16684905938988,187.46655981055525,3,28.134555127802486,3,72.68841219983062,4.125197586424832 +10,37,22,28.7275267,89.12760359,7.069747814,58.52974279,mungbean,17.84930633635421,2,5.7746495699956455,13.134142324486206,368.9867389589185,3.6755056138436597,4,7.422017192863086,57.2374599523225,160.44535006661815,3,2.9697062309190447,2,44.60379052623678,4.55253872477149 +4,44,19,27.95639663,83.52706038,6.921993878,43.25726752,mungbean,28.69797587119391,3,5.203966330062462,13.644098701343179,409.00298624952575,9.684244699772307,5,18.256759712218937,53.15579662752336,88.43146522804989,2,16.618845892799193,3,44.483237210317995,2.7430766915677096 +20,45,17,28.17458662,83.69659318,6.770955317,37.2464655,mungbean,12.364684035734651,1,5.0805159091736085,6.892584945428717,395.2920185066278,1.3090863802542554,2,13.059993440389267,90.86756132157745,101.72538685372791,2,21.35276716450583,2,28.564027550934547,2.252382713795479 +23,45,23,28.77653519,86.69133979,6.983130466,56.12443206,mungbean,26.963498847263175,3,6.358876678063082,8.737910097302361,397.7465295272182,1.8892128620285542,6,17.070357510114743,7.996987706132952,160.16413606534178,1,0.5905335264858425,2,12.960066683772197,1.1692504775718313 +25,48,21,28.438097,83.48991368,6.267684328,52.55469976,mungbean,18.612922244354976,2,10.030710261274812,11.36428209901161,406.8325454466031,4.205619608621003,1,11.273380826937686,53.505984706106865,113.34548329158793,1,11.700079038499617,1,80.96126386478558,2.5448077798919084 +56,79,15,29.48439992,63.19915325,7.454532137,71.89090748,blackgram,18.922578028891927,1,9.191656129359494,19.270504094265544,385.63827938346793,8.036079677972126,4,11.014390246584512,19.4391788287283,104.53137484517814,3,8.054422566324938,1,71.5197743887691,4.647243657494869 +25,62,21,26.73433965,68.13999721,7.040056094,67.15096376,blackgram,26.467277437375525,1,7.756742701577019,13.51039294794251,367.0861571014446,5.945108745690581,6,14.332923662893455,54.390863460683,65.71082491127477,2,47.63017708599011,2,40.60210142402953,2.476589509542112 +42,61,22,26.27274407,62.28814857,7.418650668,70.23207557,blackgram,17.847875103559108,1,5.606756913903757,6.503058374370316,423.6303973199391,7.0983445547414945,2,11.783802400828563,17.83712989970797,73.98434354427917,1,46.225342853627836,2,90.85330432584583,3.788237438066084 +42,73,25,34.03679184,67.21113844,6.501869314,73.23573601,blackgram,20.578134570744396,2,10.983093574913802,10.894159173350124,385.38856870926645,3.585402074437731,4,10.571228796753875,11.011768602955541,85.18682216924896,3,11.901989767291699,2,10.255749061908448,2.934417933282647 +44,58,18,28.03644051,65.06601664,6.814410928,72.49507741,blackgram,26.779319044777264,2,8.359980431894265,17.83873384580256,434.33865685516537,8.73997196714627,6,14.869845294232885,43.65510480127962,163.2946577927375,3,8.583264440551192,3,16.000826665074964,1.143063319549087 +50,55,16,28.81460716,65.33538112,7.581442888,62.26242533,blackgram,23.292722904671827,1,7.902764878274361,8.76272601854196,433.5408732228823,9.98112927450739,3,6.5630738097496915,58.80673471963901,120.56714336454503,3,39.20003347338712,3,17.67328714554499,4.916220737424393 +35,72,21,34.03619494,64.28791388,7.741418772,66.85510868,blackgram,16.532717945506896,2,10.586124760950323,11.907816206457477,373.3767105932916,1.5814922959725026,1,16.602604870814893,61.289488462023556,105.42243131698444,1,16.534011649124146,3,49.176820732934026,1.255564253971042 +30,64,20,33.8642935,61.57072498,6.573531614,68.02199825,blackgram,15.649079354772613,3,8.951445002897177,10.835165555130764,423.2790686481394,1.3143019150317239,4,17.943273125636075,22.003132379392387,156.5596174595184,2,20.356786302791757,3,37.118052401570125,3.036800573401638 +27,64,21,32.84213012,68.68401492,7.543804223,73.67166182,blackgram,11.107117322247353,2,5.839071196449529,16.872362276403955,368.5777097526811,3.440820108626049,6,14.13166551941277,8.948641311090311,185.96738434884242,2,7.932424734322319,3,68.68180563429213,2.613095409144007 +50,74,17,27.10053268,63.36085585,6.5408208,73.84949872,blackgram,15.369051103041095,1,9.591871830187703,2.631042772258354,399.3824738253012,1.5399400895889173,6,11.229805851617144,30.086582644205507,144.0582826092894,1,46.14562639181224,2,44.09536957821069,1.6797021112964434 +39,73,24,25.65842532,61.18235808,7.22405917,69.28607828,blackgram,27.244386378816042,2,5.094887532590085,11.072496779059122,381.59436520298124,7.667592901456899,2,18.487593495454583,46.90151239749854,102.29354150771127,3,24.074377811001085,3,68.08082239262372,2.7601284371660624 +57,67,25,32.34744009,66.61452812,7.551364319,64.55882254,blackgram,24.558808594651648,3,10.482074099653111,6.115096971387535,444.31519047686163,7.017896366273345,1,5.067141847034413,59.84297395396661,60.67619902218952,3,20.589680906323228,2,70.72101713838917,1.5939381511223925 +52,63,19,29.58949031,68.32176769,6.928898659,67.53021213,blackgram,27.22840461276248,1,10.106772690724112,3.8284566241290996,412.1433191624531,3.5672895611202486,2,13.02598944030411,8.112501635995928,50.63697165072813,1,21.884875797250185,1,67.07301448895791,4.665151071808281 +55,66,22,30.91219459,68.79427388,7.747775263,66.63830637,blackgram,29.868738998853313,2,8.323945459513116,12.443540151971284,404.89619555145595,5.456156936257829,6,11.933347329514683,13.289574023746143,182.27065004196652,2,8.207947153728574,2,93.97683731601482,4.76246135702345 +51,56,18,28.12787838,64.2097765,6.706505915,70.86340755,blackgram,14.431461476970997,1,11.879486791349617,15.638596817760888,373.88087632035445,3.9893332770158203,3,6.396930807088294,46.97023043664984,106.28959531394025,2,24.042336638436883,2,89.80774063003423,2.1026904531596187 +36,66,15,30.08545364,69.34811988,6.668238556,67.1367443,blackgram,19.13805170446003,1,9.151017234400271,19.650616903898136,390.2999405655276,3.8384874454582665,4,18.075295812636618,29.06502517451164,183.16726339663998,3,46.38267302789619,2,13.557431288892897,4.041723822909636 +59,55,19,31.74379487,62.51007687,7.332375138,68.97097538,blackgram,18.436854100084386,2,7.677021777254691,11.882356108654575,397.8454410520954,9.667150129250674,1,16.54380697601012,93.15954948785581,198.0416735829436,1,45.58404609641952,2,80.3856593430933,3.3845534485317645 +50,58,23,27.81326852,62.50460464,7.596802025,69.75555541,blackgram,25.602886753243176,1,8.610037248469956,17.03407316788102,425.0895161388173,5.8220460614289475,2,15.131312056873576,84.30791684304533,89.94207353374462,1,39.22212709465782,1,18.100603140909378,3.644557505548892 +30,65,25,32.88733849,64.59457409,7.70650895,71.50569456,blackgram,10.0829206182682,3,5.230110114746603,17.250480152451843,446.05177456412235,5.564829835684749,5,16.88894961002032,61.06565122082299,83.35247686197148,1,21.690136791549712,2,35.75136815052045,3.2505096470318056 +20,62,18,29.36358721,64.98742947,7.366542647,61.91208707,blackgram,13.646635296163916,3,7.659623155515862,13.315719808468895,372.2885485015453,7.800092388246922,6,15.313625395428563,12.908864585610969,67.37642970029091,2,38.12598646279954,1,16.758696167969656,2.805022294190884 +58,71,15,27.82592799,67.5861883,6.919243702,74.01229707,blackgram,16.51250342830847,2,9.884864880985255,10.49667546625081,432.54919675328813,9.957045678380066,4,11.530076813913775,30.137838389737936,116.53077378215482,2,28.72234941497001,1,52.420987351669226,2.358748143984829 +25,71,24,28.49538735,60.44848407,7.187721818,74.91559514,blackgram,13.610894518514336,1,8.142420579924092,13.920700459505486,426.0770330202066,1.4652667351086182,6,5.580136797569292,79.20061359973344,159.8400644661903,2,18.913827270592716,3,87.74772588296493,4.239861925813785 +52,71,16,27.74274761,68.53997144,7.075886472,71.78615328,blackgram,18.31880842937396,2,6.22900597212474,0.8646567970618957,384.9804843474358,4.661984963631134,3,18.832002649101177,10.037123760801181,163.2871170261023,2,32.69033921414921,2,21.154919470070965,3.988968507760309 +40,63,18,30.41588462,67.66323804,6.74441168,63.02473185,blackgram,17.61211365012349,3,7.091492010589074,1.6163234138594196,388.32486446821684,1.554756978652595,5,15.291371419512183,86.88072197332394,74.62377489951865,1,23.130979425338023,3,36.95263177708023,2.3836151916073516 +20,60,25,27.3254209,69.09047809,6.726469088,61.19250859,blackgram,18.27169062729123,2,10.698454693740754,17.240716658312486,362.7167176916475,1.6624323928132727,4,9.354547601254986,72.41392762200877,174.7781740882922,3,28.15696283327158,1,51.977556842911035,4.655028844108863 +48,61,21,30.28496619,61.69295127,6.628264883,65.62859526,blackgram,16.04396540629365,1,8.604601630588135,8.921026970440927,430.29829044669066,7.254118372763572,3,9.295367708738567,19.5638592658686,150.66977606930715,2,36.702852988603105,3,99.46941011799687,1.9130487705279302 +49,68,22,28.56840626,61.53278622,7.127064207,63.49726331,blackgram,25.999899765748083,1,7.348115112245079,15.547477955538733,396.8493593775919,2.545208556174425,2,19.07183959024545,51.77765954214306,127.91789266095192,2,29.844599029812287,2,62.36810919499791,2.9514103102568146 +48,62,15,25.36586097,66.6379724,7.538631462,65.81655892,blackgram,26.835018838945256,3,6.891373786623266,11.187818181563959,421.795439658548,7.7892620144999585,2,15.319779973347385,48.16151629502522,180.60572692020085,3,27.16903152774283,1,14.902698534094982,2.171385332459931 +32,66,17,34.9466155,65.26774011,7.162357641,70.1415139,blackgram,22.106012685551185,3,8.208097998451727,5.439752819005874,387.0913481904231,8.055024166004337,6,11.03150677420312,67.17379583940122,73.37568349208142,3,40.71251087468033,3,18.11935145051259,2.3992154977540703 +21,63,22,25.09737391,67.72837887,6.859409487,74.61649888,blackgram,28.138703794532923,1,9.700013822286145,5.114374244334914,381.8898108578714,5.820814209109331,2,11.79263096702799,31.54671766558558,194.63639907381616,2,40.94074662667639,2,17.142840297218733,3.7638233813163082 +20,72,19,32.47648301,64.34848735,7.397190844,65.820457,blackgram,27.019000199708113,3,5.58106823009286,13.86790981177418,370.02839397746476,8.670186216139424,1,14.971766342736522,42.169377601415384,190.61647353307185,3,30.3075911296129,3,64.69790666351692,4.346772568122207 +25,65,21,33.86351172,68.59232289,6.880245789,69.24464096,blackgram,27.00848098599995,1,10.010991701609614,3.514824827075702,393.6301309896302,9.99280222597621,3,5.445247487247115,71.78558733971589,62.41911573572013,1,3.448945717649571,3,15.402950131366588,1.7805124782882427 +41,78,21,25.19857725,60.37332688,6.581313137,70.88787207,blackgram,21.523216255559326,2,9.2803712536085,6.976037703455125,356.64838192542436,9.33059421892278,4,12.19956712937601,54.21980040702646,94.75979929375829,3,34.21305422496039,2,65.79232810285924,3.2645393629008788 +53,67,17,31.77681682,69.01852894,7.296972161,61.46892873,blackgram,29.874288781108582,2,6.719868002891305,9.319220434306745,424.0250764453127,9.706732538324761,1,9.40767573320064,98.98309142043887,146.37218810389942,1,45.35882501911984,1,69.85981230898142,1.1966515983905053 +39,60,21,34.89814946,63.59948557,6.97297656,64.72797143,blackgram,28.892264485924578,2,9.138024139989016,14.134481884888437,390.1494257319296,2.2608344849229685,6,7.733598997635678,12.941719439253784,134.77764423891185,3,22.29846422108111,2,11.91074642503126,4.456775851376639 +25,76,17,31.74105409,68.63525428,7.241148507,62.3061735,blackgram,20.256072078972384,2,8.513747305566397,0.16976103228671713,394.31611513682884,9.814070819436676,3,6.156999801710866,94.84780304150358,88.03898210198298,3,47.31342086158274,1,91.11396800842421,4.974024833956182 +21,78,19,27.16159076,66.76017239,6.92009048,69.85112265,blackgram,24.231881594151655,2,10.906575074255421,8.1231291839304,382.24545212753367,2.1797677223210026,6,15.389327730105213,59.660783177695635,95.12893874147937,3,11.711689038415585,3,67.19822226664515,2.953830870591314 +57,60,17,26.23773129,67.88521396,7.504608385,73.58663968,blackgram,22.36151771380224,2,5.511941232022479,1.1031463431507516,408.9912229992451,6.970765885562602,1,10.177759178982638,5.958802156942133,187.95604143250077,1,16.94040727265625,3,89.32009609411783,3.408121227158564 +56,75,15,30.20157245,60.06534859,7.152272256,66.37171179,blackgram,26.451794927584427,1,11.553600695920732,12.85503646964965,430.7252040753533,1.7367079316017526,2,15.317315947509522,22.80717886938336,151.8923373571551,2,36.881734388226555,3,83.3247121944187,1.3429864802452842 +49,72,15,31.55846339,67.83563765,7.137004749,74.86960831,blackgram,10.992659009788897,1,11.5249214718563,3.0194590402103083,439.2044213693608,3.0701632443847835,3,16.098441137695083,34.54561499679749,72.6382077363155,3,10.40247184547572,3,63.5229989096202,1.1827672489687373 +24,80,19,29.67892453,69.0854554,6.808041722,65.66436565,blackgram,27.821200046958005,1,10.81188343557905,19.591857181532546,388.7133963215799,2.5841719012644924,6,6.510280534957897,59.20285623028199,64.29046029603937,3,9.183066681075214,1,63.81996924079986,3.250484355243185 +49,76,18,27.05365239,67.7017527,7.393631868,60.4693835,blackgram,14.855370604313354,1,11.210130935672609,3.6532890740787427,436.00999734239286,1.2065562640553598,6,12.832847243513505,25.243916832515755,153.79970077928374,3,4.8878977777853825,1,34.997315130536286,4.8990724616807695 +28,68,19,34.63880966,61.38597868,7.69950698,72.43169115,blackgram,24.909939462273396,2,10.347051184277149,12.124319587535714,376.8338152346977,3.4137835856805663,5,12.83650918642269,56.880961018634544,154.0690890069302,1,47.52729839925541,3,84.11374911034714,1.6574569658489704 +55,78,21,33.39438752,62.93692886,6.602888249,63.57445989,blackgram,28.736961880967943,1,5.341972385235391,0.9046086986397794,408.0909955635647,9.07679090438356,6,18.189473191011295,66.80565816912744,89.11704199613982,3,28.06850860333114,3,37.404781748553084,1.8455052345589027 +50,64,25,28.84079155,63.37230676,6.734447425,70.25496749,blackgram,29.887280240029916,1,5.297057520782795,10.263674146180069,428.3073343843985,5.613081019408456,2,16.860066715683935,79.95587222935924,129.9587894841057,2,13.340950518327638,1,39.23377227967955,3.8748187219622015 +34,80,19,31.49338309,63.0563645,6.521217963,71.48327008,blackgram,27.53323428378236,2,8.051484276634657,6.650263710223907,356.6769822529189,8.78874097701579,5,15.87346912943228,44.09882530660242,111.88469883792585,2,12.145016671015107,1,33.03312049852708,4.506014112938477 +20,68,23,25.54960633,63.95425534,7.707332484,63.1830529,blackgram,17.8144256944369,1,11.260861092009911,16.025316198575187,374.7967661847132,2.571329901487574,5,5.121105741765044,40.38815006835029,86.03300245354444,2,49.030418678745654,1,61.730052872688645,1.9981416705778612 +55,67,16,34.37329112,69.69366426,6.596719015,70.27184748,blackgram,25.06094807513915,2,10.62770979144067,7.663871227056315,409.055467938469,6.003239505117515,4,9.90236847942932,17.370140302455006,156.08010202144814,2,13.469121041077543,2,95.79365135392925,4.84773923508363 +23,70,15,34.6008247,63.11296779,7.403623355,60.41790253,blackgram,29.620836117410114,3,8.574941324379031,8.587600156410272,368.09462700637414,2.335926174600433,5,16.64985238429464,41.88805544084497,194.61257684940682,3,41.34875031190699,1,62.05724224044224,3.9470631659220303 +53,74,15,29.43463808,64.94329356,7.517097,72.17818157,blackgram,14.841228761081371,3,7.844482610827926,19.983758249450595,406.50058218602555,3.4349949448147683,4,10.360370877233603,22.125455925882065,172.06074618132618,1,26.51671296879651,2,69.8060518326831,2.791499520284755 +26,67,16,29.10713092,67.90577375,7.17620823,67.83345933,blackgram,18.16695378725479,3,6.511672498757356,14.016686824307751,427.5098872002775,9.783091025739253,6,8.136597354541186,37.22295437522365,117.74159336186368,1,39.999592899020946,2,30.389216009717323,4.556876122173283 +33,80,22,28.57006111,65.71765781,6.593961761,70.0866434,blackgram,12.108626796795905,1,7.399729392584067,5.143324051794529,395.39340251821477,6.81792384978133,2,9.03377900695986,52.28123252866618,146.0076261603397,1,49.236285008666435,1,53.67983877633936,4.819772402201583 +37,79,19,27.54384835,69.3478631,7.143942758,69.40878198,blackgram,12.920400303176496,1,10.147753883442158,16.973603743548352,421.64726987301185,8.890250732296316,3,10.971969631797958,16.169267952289445,55.509855090416806,3,38.40741565870805,2,78.73427324139745,1.3385393565269643 +33,75,21,33.04687968,68.93875631,6.690655045,62.30278274,blackgram,12.644838590168145,1,10.770374927879171,19.344046055152127,363.7065451285466,7.978737149951488,5,15.221322943397007,6.363183448273757,145.85744220412107,2,19.011270952029307,2,78.06274051764883,2.1861297971506404 +22,55,20,33.95309131,69.96100028,7.423530351,61.16350463,blackgram,10.796257502339472,1,11.594514260569767,12.020205106268536,399.95630657729066,5.686842209432004,2,11.105251884576656,76.34389733946388,140.2290349013113,2,48.06501708621049,3,34.32365238280769,1.6279709547624002 +20,68,17,30.11873003,60.11680815,6.578714843,71.72980375,blackgram,10.818319611405244,3,8.928754651783434,17.29532928267083,408.9807492558236,6.796996860963593,2,6.302929277219317,0.4659333647148767,127.69643411605914,3,2.5253346198019444,2,49.923812485473654,3.8905839032977774 +43,68,20,29.57812712,66.17587668,7.497469256,69.43895491,blackgram,10.092302479295224,1,5.3769081564223535,15.77020990141536,405.9589692194056,3.000780450470375,2,18.091770263572528,66.65577871446716,99.63635367225574,1,30.54158151462662,2,63.27436801635835,2.9829235719861202 +44,76,22,27.26458947,68.01232937,7.775306272,68.91754359,blackgram,17.977476906476454,2,8.69791843703997,6.177301643398218,369.20628365146297,3.346744793363857,6,14.959958457850945,8.325817911822197,194.73841426068023,1,21.329861066557505,3,5.921960171661289,4.781444598333863 +34,60,16,31.35730791,64.24992106,7.322555223,63.85668948,blackgram,22.9996416547392,2,7.824448877768872,7.424200339667539,389.1864655376613,7.471551355793187,4,8.378453168178808,95.00709364141632,191.11147527632264,1,10.150818325071553,2,46.31510329987676,3.099273055923887 +21,72,17,31.52104732,66.55723677,7.580527339,61.71111448,blackgram,27.7827388314278,1,11.764151140927705,6.314121103564023,370.91323616286013,5.158967527913977,6,15.623239732805603,5.973358138236895,124.28650757682946,3,30.53018111668426,2,88.43142782628448,2.7074430870741106 +25,68,19,29.39982732,64.25510719,7.108450121,67.47677295,blackgram,18.26070172110823,1,6.781280908845157,9.559016403089839,444.36665929263415,2.8410066857905307,5,12.104943995977468,33.11682171143,75.06525515108467,3,12.644526969690217,2,65.8144512106195,2.476153478214783 +41,62,15,29.38400259,64.14928485,7.358974541,65.24194361,blackgram,26.2709344099806,3,5.694865036669737,0.18072600661302785,379.2132127928223,4.698816359205969,6,7.334254571900827,83.76175987488237,110.30103782164042,3,3.9809682508280653,1,30.389776745561626,4.778841273241292 +28,65,23,28.38686534,61.88871127,7.405176138,74.24459122,blackgram,22.94600596496226,1,7.378597506846579,18.590630674987306,384.3919174963336,8.825775089728321,4,19.474085239815928,39.80771504628736,50.209990366842156,2,18.73129367804544,1,62.234785767363654,1.2005054953406193 +35,64,15,28.47442276,63.53604453,6.500144962,69.5274407,blackgram,20.113293079539623,1,5.063887239785832,3.8789626237204122,371.29923172842973,7.598530598745731,6,12.643175681344019,24.72035450108878,135.49984369999714,3,46.23666004424658,2,57.241222571620156,2.7098829578931527 +52,58,16,30.64095781,61.14508627,7.167435834,71.36947525,blackgram,29.209071571315924,1,7.051620689415581,1.872791280049364,431.3003551939999,4.146056028363027,1,19.520129228580032,95.35511296721987,130.10517342022558,1,16.878671291779668,1,85.4888846980033,4.723537303246935 +58,75,25,25.25596239,61.36669662,7.261791753,68.64685069,blackgram,23.699623410142078,3,11.799487714054543,1.3828388462876484,361.84700841356874,1.7304186406525564,4,16.329316861528618,10.423496231446949,144.72862041740888,3,21.424004593279072,2,64.21834523537355,2.9964404045364597 +34,66,19,32.97030511,60.18122078,7.586642101,73.44678678,blackgram,29.784502645239744,1,9.530569812064545,4.669588993648468,434.9618214861572,3.164770027185559,1,6.5260159269638995,0.6390887070379714,127.71224823901218,1,18.885532720592273,3,48.236477578324376,3.1804356774603937 +52,70,16,33.66855394,66.60416867,7.534811833,67.32520551,blackgram,17.29969334991618,3,6.448743837863282,16.57287258546842,377.2860584857802,1.4736359362421823,4,7.909298531878886,35.74725669250686,71.14582367300697,1,12.31703372260466,1,1.6128539119043217,1.433932147750339 +23,57,19,32.83963757,67.99803573,7.251000789,73.40452716,blackgram,25.145010558349448,3,5.1068419596869905,16.379127462388666,418.99747816661477,1.5203979373611851,2,17.442167991559455,20.13914721550394,114.68805617232346,3,26.78917988695879,3,46.4971940770443,2.3569248468643305 +42,58,25,27.45853567,62.90020977,6.513620918,69.46020927,blackgram,11.114582318756037,2,5.837217962483831,11.389073514300998,395.26621984641685,4.670532084776915,4,14.067756854706236,17.707709113916504,53.69999432609255,2,23.43365999859892,1,96.75594181928477,4.412940503819076 +37,62,17,25.68576704,69.84354028,7.121254928,74.62068748,blackgram,20.951075117301148,1,9.075074750605662,14.961016299953304,433.3421811734861,8.012997123792033,4,9.340832653703146,79.46337659585481,199.25068069960747,1,22.367383168793044,3,82.755005274053,2.1699689484594473 +44,75,22,30.0328403,64.14800537,7.574561547,71.21006868,blackgram,22.07938122392231,3,5.413115962476269,2.5056741486178713,404.4674685265983,6.751890788767515,6,14.497523356095062,92.4287460284115,119.50298824045485,1,5.665279434640796,3,42.81375323562152,2.4265028096320984 +21,80,20,28.20667264,68.27085245,7.350869792,64.32887142,blackgram,21.490728974630017,2,11.164256760020859,14.68219879459089,410.84202350449766,4.452900535511214,2,7.558927444694694,16.211175587809613,97.32467280795538,3,39.55789724380659,3,92.55059076302004,1.012690556010968 +56,76,16,28.27265858,61.18956161,7.513151076,63.29900785,blackgram,23.69212809400546,1,11.494125703307342,4.132629667025885,406.61388084726906,3.7528111029693316,4,11.642250662882411,31.41998747971848,155.85935528792442,2,45.90979430963916,1,49.25854292423458,1.0399064943927474 +29,76,15,28.5417236,64.2020154,7.025607706,69.68862306,blackgram,15.509799224738156,1,6.926161579754062,13.406375032226086,367.58822048679724,7.82880080217106,3,12.157540370032384,16.66223058973879,123.06528174361713,1,5.423462007267404,3,98.77054885161797,1.3171530104024685 +43,61,20,26.87187036,61.61367264,6.804253866,63.51822045,blackgram,16.694120968506276,2,5.832731402971343,10.774680800355716,442.4324842912114,3.1009939844083334,5,12.251138053974115,88.59112310691435,67.9321834390274,1,17.21659492700586,2,93.49342720795008,3.996515781327503 +55,60,15,32.79766751,68.77994074,7.163043872,64.11411069,blackgram,23.34752238847974,3,9.744000018907236,0.16289928088857097,427.2086500596179,6.5876954379564205,1,12.451458546683178,87.62649462582064,70.58021496314488,1,28.767564527650986,2,45.649973270195346,1.9836150212274015 +44,63,15,26.42333018,64.51136845,7.338929556,63.46546487,blackgram,26.689688838112808,1,11.659486229061596,12.649925297790478,434.08156528776925,1.9380396799265824,5,19.28704101401827,81.96716991785846,154.85654775776175,3,41.01847324395841,1,26.6225757924494,4.04620561178568 +29,67,21,29.79181107,63.38789228,6.621323612,63.02169909,blackgram,18.995601685542827,2,7.1343653346836655,2.107717191849432,382.9038238817102,3.0121941005710884,1,5.88551590633209,20.902777569587915,58.89684581873291,3,30.318446748089894,3,94.30269801082511,1.4602024382062906 +47,63,16,27.44003279,67.10464369,6.661870999,72.50669768,blackgram,25.808793266961022,2,10.51790295984596,17.650773074589207,421.3095140869937,4.805309922062655,5,17.96337479595145,60.24211492581443,73.18220250888163,2,31.087077769093476,3,23.71197042376647,2.45412717182445 +40,68,17,34.1262979,65.14877461,7.733149554,70.40795007,blackgram,20.17573526040703,2,7.247727998529076,17.469223904990855,419.6920891571865,3.6659894001058353,5,6.570140418756661,10.66701848482059,133.50837263268656,1,30.43659098033905,2,75.69386453735095,4.0556940899494105 +58,61,15,30.94908189,64.23364112,7.402891666,62.78730907,blackgram,18.00302441264779,1,8.123899715709292,19.303742988110283,383.79171057066,5.575175369809854,5,15.155013476277489,35.5879515083131,151.60388893327962,2,34.564512034583686,1,89.40534726268645,2.2912961047360763 +41,74,18,28.75751783,61.02701476,6.599147298,73.37686831,blackgram,26.56135993667578,2,7.415097265202078,13.83666246027648,403.76676405671844,1.298272119330001,3,5.205741193830994,37.31328583105372,162.5997835211911,2,46.353364632526315,3,6.661267958280048,4.089198495647053 +58,79,17,27.24766491,66.10123083,7.04174124,62.31842057,blackgram,13.283022799234775,2,11.502263054774286,14.777581418333607,356.91933436668324,9.528817072091545,1,12.406994067188442,87.44810857361375,54.14399328550846,3,10.334829300308085,1,62.557409225875396,4.166732718480864 +27,62,24,28.63005477,66.770943,7.353876754,62.2737345,blackgram,22.58134340424461,3,7.599815801414351,9.691422887456005,422.9710236570852,3.293886166008625,5,6.233908928608468,64.07349102064501,83.3625975461365,2,39.52727734043711,1,4.2681271028335965,3.942374802650465 +27,60,17,26.41768321,63.64698302,7.026795359,64.42177127,blackgram,21.53987789309604,2,7.070355788341972,6.784330891449217,388.8233796148441,4.939745401220387,5,12.208292793144125,21.361820984209402,118.89215353500873,1,1.2989234870194155,3,42.26914454241661,4.487605389826317 +52,65,20,32.81705216,66.15665137,6.814301033,68.83924882,blackgram,11.259541097797426,1,7.187574479802873,19.20381219893769,350.45905509785274,1.003337922269687,2,17.829485071760562,56.31352757581802,186.88821455381273,2,37.47350177678026,1,65.5643894813797,4.1454971257333355 +44,55,25,29.6321052,65.91359954,7.42160832,71.16331975,blackgram,14.58953920099858,1,9.115713463016295,19.082734298553653,388.5958613109717,5.984215910537722,5,18.62057429983328,9.055361942485629,156.83633674580958,3,45.366545344227035,3,29.30011154983575,1.9330139211139716 +21,62,24,33.49077065,62.73317204,6.847382891,65.45328463,blackgram,25.62278487311488,1,8.323090798171133,1.1723024031364515,444.31972665309075,9.06822527203353,5,12.805859274442241,38.3637985495601,80.5046332109429,2,24.44809592514939,1,97.96254470424319,1.279736957580777 +60,59,22,31.86847286,66.74217464,7.191522601,74.22238583,blackgram,10.437046480821193,1,7.795197220412568,13.185676615846226,369.4455646119876,5.723811028374692,2,14.939623841101811,79.81820739939762,72.995371326739,2,34.609086210934194,3,32.06297543101091,1.1827082142205625 +33,77,21,30.32992227,65.62971858,7.01285529,71.64631281,blackgram,27.96460644982269,1,5.034601212324363,18.652597432915154,410.1282057967608,3.444600193721331,2,17.66666405214337,98.71621334583291,71.4078706574277,2,27.108369144656542,1,14.527354618976718,1.0053175385519495 +59,58,17,28.546224,66.31394098,7.368318809,62.83469851,blackgram,27.280009662050613,2,8.145861163325478,16.12653639295363,361.8935983115696,1.2612962159319563,2,17.997585864689952,20.902829287318326,146.76750770069083,3,37.10629113887449,1,22.596087517644225,1.6268254069890529 +29,63,17,30.02629908,67.88811637,7.26154329,66.47264636,blackgram,22.4997111867399,2,9.23367905065711,14.454980874029841,359.10811662351466,9.332691474228236,2,8.114087235392383,73.4088616475051,93.71657364838555,3,5.631798173528252,3,65.31320578863922,2.0584458424187217 +59,63,18,31.65531175,60.13263713,6.52669158,66.69096751,blackgram,13.425150343119519,2,9.16521886885483,17.39233958985225,446.92776572592686,1.3036294844823653,6,6.273989964143322,5.856722915637147,99.75117476329007,3,11.358650862743374,1,43.239706864786406,1.481283471991123 +29,70,15,30.33499674,63.54718862,6.872594461,74.16699119,blackgram,12.641802593529977,2,9.08596974341158,9.036001343794222,426.1124007747379,9.725134715758282,3,8.106026994288186,91.09218086984188,147.3650968859624,3,49.68972998528319,2,53.12913532685073,2.285841856923615 +58,73,16,33.36984395,65.67718163,6.874142175,64.89517488,blackgram,27.92743382889407,1,8.045813170613412,4.596843033736104,437.71713193430276,7.118478301673552,1,10.064723772412728,6.702505032170791,98.8434458998906,3,20.386840440942837,3,95.4328586284362,4.7000041677112385 +55,77,22,31.4345059,62.99303471,7.76061831,64.77651469,blackgram,21.925867768493475,3,8.591762668504018,0.5018563557163569,404.6724396796241,7.114980253269617,2,9.559749753370513,61.07592178820267,152.8117827755376,3,29.4882690666738,3,13.225173500543496,3.0299246218877687 +42,79,23,27.71678273,63.29103387,6.781841984,68.56507978,blackgram,22.86476639253589,1,7.823688077135046,10.042828630982964,400.4204948323379,7.178226476036309,6,9.110924137466132,84.85260998467434,87.89464619059336,2,30.85724309238017,3,75.28169110914124,3.340848900190254 +44,77,21,32.63918668,61.3009051,7.326980454,61.83876146,blackgram,28.13724493810058,3,8.226536460412332,3.226825568773477,433.26961572140266,5.912718579305849,1,10.038085614160039,50.087253630099745,156.38451463515426,1,49.11793312758296,2,88.48464557276321,4.394297137277207 +38,62,25,32.7477393,67.77954584,7.453975408,63.37784443,blackgram,17.498770371575933,3,10.08220063485057,12.385414792962127,427.7359311169656,9.77061463527822,1,8.258629274564207,82.44182352938914,80.3320131567748,1,48.162697429234015,1,75.61933765785858,2.822553828415736 +32,76,15,28.05153602,63.49802189,7.604110177,43.35795377,lentil,19.27678302496176,3,11.33885373357628,15.008295632342442,429.2802551955656,6.46232154001593,3,13.496643537670344,52.362972805916606,182.4505183242789,3,45.95448423139486,1,24.884930026718887,3.5042943703293523 +13,61,22,19.44084326,63.27771461,7.728832424,46.83130119,lentil,18.438227419464383,2,9.937594279335745,9.129900283092717,424.60994377089946,5.018341503913297,1,6.099576085751946,82.13979551698755,109.50067254914393,1,47.24587657730934,3,97.75552054259823,2.310804589700542 +38,60,20,29.84823072,60.63872613,7.491217102,46.80452595,lentil,25.04034799814584,1,11.328454084375423,7.20789948844581,360.0309193619573,2.9231737408738847,2,15.73311624579651,4.739794714120071,69.97518015217105,3,40.223670775374785,3,50.15182236368132,4.0685573249955045 +11,74,17,21.36383757,69.92375891,6.633864582,46.6352865,lentil,23.18270053667158,1,7.455691856518834,6.629620933102245,368.76991631661934,5.991943934763735,2,13.686589460527586,77.77151779511888,91.90201542793761,3,44.72237053114457,2,65.41840102109762,1.122066032189974 +37,71,16,26.28663931,68.51966729,7.324863481,46.13833007,lentil,14.45817986156172,3,9.626370641021136,17.55861119160324,411.58704287651585,4.772328614297107,5,5.886138622888223,68.65614250772512,95.91380501974997,1,22.547897579239063,1,62.37843674736327,1.2991179532158545 +29,71,18,22.17499963,62.13873825,6.410441476,53.46622584,lentil,16.837691677132028,3,9.620154653125123,7.910947278204485,449.6635804608351,7.612303978595492,3,8.69086732756665,68.81675954423258,146.3702009082898,2,0.8843807577655294,1,86.68687056283959,3.001104050965304 +2,72,18,26.57597546,60.97876599,7.836719564,50.89110726,lentil,17.894337990722356,3,6.487111245714061,9.764371961158762,412.58377696237983,6.48257464676545,4,19.609272427998484,90.36287087809495,152.41668215557087,3,41.68530793287651,2,34.663865847429044,3.478139945834692 +6,59,21,26.58972517,66.14007674,6.139215944,50.90994463,lentil,10.236719226201533,2,8.656703397912683,15.430076214408611,393.51725429745863,9.906356957512898,4,12.537050497598052,80.3887350055627,54.023510964660375,2,3.5021582396895843,1,63.899945608529976,2.8012177933299593 +13,64,20,19.1345771,62.57526895,6.590571088,36.46946971,lentil,26.287518898382206,3,8.816027960722252,17.29904297523991,402.1956578346373,7.681152597712535,2,12.803219739282223,9.388962427804026,187.89553618285592,1,44.01972478632954,1,58.94788076396742,3.5048195924739747 +8,58,17,28.75273118,69.15640149,7.286049978,35.15426171,lentil,20.957280266813378,2,8.378705176732355,9.031564218333648,430.85572840377216,1.230903918036667,1,8.415045614090296,81.95126369016734,105.91613314063801,1,2.5264928579438406,2,19.265041023560237,2.157693672907198 +6,77,20,25.78746268,60.2816298,6.058306161,49.14337177,lentil,20.748192638913565,2,11.276729438981565,1.2865924206754698,361.3512048600494,3.60616371774761,6,8.659463024683705,31.576805770407436,112.13425567540412,3,3.8393354365707344,1,94.69239078103128,1.9851376230357078 +2,75,22,23.89271875,61.78779413,6.658605362,52.55730112,lentil,28.726548183184818,2,7.00398495931532,0.5866523481491925,410.979891204902,3.42341749073929,1,13.835772955356319,87.63887499618836,179.03630022435976,2,20.638041099528504,2,49.17617357712925,1.6250570190764937 +3,69,23,28.67408774,63.18832976,7.299360767,42.96018627,lentil,12.730847055470505,1,9.619756849717362,11.365544549286348,425.0166592382505,6.14063048592668,5,9.152563191495503,55.716383062708,176.05472949422534,2,37.91878465080736,3,38.57234137226557,2.9817288877634285 +27,80,24,28.42062847,61.77336343,7.815210661,49.02366803,lentil,13.462866142379825,3,8.125371069066718,9.898828072028572,441.50801141535413,9.623038817054168,5,12.16263917451695,66.92070845786286,142.07962565693092,2,7.253230278776035,2,37.02352784079864,2.396001717247454 +39,78,15,21.35499456,62.60136323,5.925391795,41.78219834,lentil,13.663389299811406,1,7.679533826019791,7.854718512313699,433.75249989747294,1.220703542092924,6,15.671333037601716,97.75591542346523,196.65335693721997,2,34.636055457826174,2,85.6977911304507,1.5358365932272755 +40,79,17,21.12695586,63.18738532,6.403683619,38.71834464,lentil,18.49230232182243,1,10.00278861587699,13.56269634844844,423.73460285777196,7.387340726592608,4,13.095067675199239,76.77591920593886,164.9275724718721,2,26.712135205444334,1,35.32776859431765,2.947503898953036 +37,62,22,24.02037872,61.62313345,7.397546271,49.78102578,lentil,16.884037140910706,2,11.900976572377242,10.542452075709933,366.9985005732283,9.56837597585988,4,9.631474456386652,5.023994159038569,180.65125468786547,3,16.17752875029704,1,52.78029645572961,2.796238204118609 +31,60,24,25.40474421,65.8567539,7.722335992,51.92057267,lentil,22.002019183940455,1,7.766478696570674,13.929083422155097,358.53661260329045,9.415375957931225,6,11.410312902456944,91.85756925589224,80.53423027896366,1,34.070265613356874,3,23.988822249400943,3.0808566533582473 +22,67,22,29.03017561,64.49166566,7.475926645,54.9393771,lentil,28.95694615481085,1,11.451804426249694,14.683642448418725,388.0698280728713,5.045427694048343,1,8.731687773360413,97.83375635640893,139.55052821065215,3,16.28086075941118,3,82.96005324564412,2.146180083019831 +3,78,18,20.21368219,68.65257685,6.887130053,50.89732989,lentil,27.85663558691468,1,9.436186763520624,2.6631401841352775,401.3579812573595,3.9440249650749495,4,15.993041878439268,68.24932342435335,192.71602737962226,1,22.243249723939336,2,66.78094398730433,4.269529976277018 +4,80,16,29.19585548,68.01965728,7.441976825,44.93261911,lentil,12.825285071494225,1,6.55384272637031,1.8631396836044911,380.96784299648306,2.8821566691499907,3,12.591736408355327,90.02019656713342,193.00337185971227,3,33.04401510236746,3,67.73461567337063,1.6967040137215377 +13,61,24,18.29783597,69.6897615,7.629910253,49.39111479,lentil,15.167263794397696,3,11.941165303381856,16.626420400079418,358.25627282760365,2.5792250599552817,3,18.337205549786685,87.71966777358324,121.60021550260196,1,41.82999785970061,2,9.295081044467102,1.5401563097373465 +12,66,20,27.41434987,63.41785982,7.336117221,44.43177543,lentil,26.188213083498283,1,9.862899428897524,14.22386350166076,396.5281637554764,3.832925210937656,2,13.539747660732495,36.836968536178375,51.05472640846486,1,48.83186979892773,1,39.56579738002974,2.0901914698106303 +4,61,21,24.84063998,60.09116626,6.75020529,48.77790371,lentil,22.399836623372344,3,9.916116978825903,7.087892294011757,389.66563298406436,3.375452176327615,1,13.762275594990523,26.537487609830855,118.82675084403276,1,16.802287876322925,3,18.229750827767788,2.230370003989106 +9,60,21,29.94413861,67.31323084,7.52178027,40.37113729,lentil,22.94051785690319,1,5.690374644978047,2.9660956689907403,404.3270446066069,8.54902951580384,5,5.61381978366978,21.652407132402153,102.30853153222665,3,10.729445852207236,2,34.0569832797955,3.739215696412038 +18,66,22,25.87990287,67.55109024,6.347379185,47.89645224,lentil,20.91114093424476,2,7.744262254749348,16.732080641813322,359.25528858152137,9.605752222904433,2,11.911153453403193,95.40023529375566,120.62724331766951,1,39.21285245968853,2,78.1171023446337,4.252812624022077 +32,56,18,20.0467711,65.84395319,7.135251532,46.05333124,lentil,20.359243797782497,2,6.3192162267518786,12.60041912253296,370.0136231255031,6.503828377311431,5,19.386535334945343,59.13109437631534,86.89098958851878,3,21.039614409417922,2,69.11455827605741,2.7539423294032126 +6,72,15,22.99451999,66.70897237,7.670178119,54.49044154,lentil,24.864879055825213,3,5.807752675700151,14.74449809137582,415.46808257544694,2.7167636081389457,3,11.311253001262102,15.224678626164811,94.98826067190492,2,16.061782553470888,3,82.81533838713784,1.6707014397067965 +15,77,20,25.13163619,66.92642362,7.399749291,49.04015558,lentil,21.47557123265734,1,10.040287015448866,15.45898234200077,400.87796778506515,9.438511266559809,4,17.243312537519028,43.82413772019298,175.73687771843703,1,16.919145065631696,1,71.45152025547326,2.981503190174741 +0,65,24,28.49584395,62.44616219,7.841496029,53.14531023,lentil,20.545202213880586,3,11.247289334267847,3.6327324507315417,448.8130409912659,1.4646507931219666,4,7.222196032575084,3.7007045470598743,159.44951613947921,3,29.98514498569301,3,50.218513129547674,2.7482267033091916 +30,79,22,18.28766124,69.48515056,6.254216611,48.60449438,lentil,16.20968329584451,2,8.436000761536494,12.340733181986396,361.27339326022417,9.090789724056382,3,11.872016789150674,19.57497777052133,118.1412619908518,2,8.084026512388931,2,79.71969105200336,1.9455433789884853 +3,63,16,24.38041875,61.18458224,6.868881708,53.13946695,lentil,29.04553646469292,2,7.115118395999351,3.2624980560728734,448.91234136571495,6.209932244437648,3,18.862295897439125,6.994867744293476,95.34627692635584,2,41.64568702145529,1,40.189520858328706,4.6850636108381565 +2,78,23,21.31852148,66.43934593,7.320514721,45.42616802,lentil,21.199323146876043,1,10.213355052607668,6.911864212126977,377.0020688629428,7.5321686236661165,6,12.097382189624634,51.562079227875444,181.99635360652044,3,32.411865423952854,1,9.685291886637737,3.7866438662796593 +10,78,18,18.54141834,62.70637578,6.296976913,44.07819743,lentil,11.619596564199043,2,6.901399450119246,1.2319557546458237,427.20438676187115,7.343510048139393,5,17.357632210629852,35.99340273357855,92.2156358266072,2,13.91828346241214,1,14.064688357293075,1.3959887328666332 +14,67,25,25.28710601,60.85993533,7.241151936,49.37369982,lentil,10.936884055517533,3,11.836996552694592,17.590336730852762,376.0742126187407,2.3748477860454753,1,9.095816997348496,18.250282662502492,148.34399646506313,3,40.02143710031867,3,29.06435336871851,2.8610551131141415 +39,65,23,25.43459777,69.12613376,7.685959305,41.02682925,lentil,10.668559319782968,3,6.391909834526754,7.958084854078242,420.24264650324176,1.743894977004242,6,10.232154749531146,95.80187666395142,97.79828414472311,3,39.00088379504362,1,45.98289268382856,1.673029754855631 +19,72,15,28.83600962,69.76112921,6.890760124,44.08562546,lentil,10.332752976245239,3,8.294363761536525,4.027588885127853,404.2713496280691,5.110229548141154,4,19.178325353203356,38.254918699692475,93.99043405695397,2,9.009656450559195,3,80.40516323082772,3.1771731611179157 +18,57,21,27.37659643,63.93927841,6.155915975,49.47371773,lentil,26.331670808980753,1,8.3511594270818,11.609303734369401,388.10945127226273,1.586407904011544,5,12.301091477402878,19.469523617499995,148.70915148085135,1,25.106384489821327,2,67.77301236402458,1.4793824668129307 +31,58,15,28.31886863,60.19461399,6.167855382,45.36521251,lentil,27.595820067402826,3,10.25567147508117,13.926134424198091,384.7273064495869,6.493201871906988,2,15.930731191698463,61.205595350981554,87.4501510324937,1,37.1792411011761,3,29.18394508171467,4.192587246829369 +28,58,25,27.4818649,62.04814951,6.861640036,37.81123974,lentil,17.6698208866501,1,6.889631896279932,3.1065174494573955,421.1294979696148,4.124305131155101,5,6.749504431581244,5.311328221908296,125.24542034036091,2,22.217327334339114,2,95.26081494538393,3.00261012746715 +5,65,19,18.28072173,68.10365387,6.978361689,48.80253285,lentil,19.90072248328306,1,7.761889469687644,14.67160153890947,378.0569602031333,1.0564141783000287,5,7.384101129670398,28.929658408895754,191.64101104801014,1,15.031763808923998,3,37.976421509693544,4.4180338407896675 +16,65,19,27.61204997,69.29786244,7.043160241,42.72374404,lentil,29.119181021801737,3,9.062038550207928,14.02925739398908,439.84937544736624,6.99520526570196,3,14.37338581561564,46.688699728067384,117.30729812273795,2,47.66305801347107,1,8.226413311805258,2.8466493418336225 +34,65,19,23.43974653,63.22011726,5.94239222,45.40277297,lentil,10.785040291831237,2,7.357999257745636,1.4465349876511358,424.57373771280726,3.6976918985047096,6,10.649972759834867,2.6070591207796423,88.36887333658018,2,6.754367051941496,3,0.629364367600993,1.4033804348983039 +14,69,19,20.95628486,63.68128841,7.239455147,52.39881209,lentil,14.95942230328832,1,10.682835114312631,17.312042169596047,373.12798609879303,8.298089778649647,5,5.402425966924702,37.37428476715756,101.07652864776117,2,28.967885602940406,1,14.654915478788766,4.492107231229698 +22,55,16,23.7937153,68.03209183,6.516317561,49.73922097,lentil,27.073735859090377,3,5.054747724762896,11.431874066738546,400.88081487476285,2.200394323565268,6,17.816464903062418,4.447605238355445,159.7020166179442,1,32.4601671800041,3,64.22073678411876,3.0317056621894505 +24,61,17,22.6371424,65.44544859,6.233269045,38.30411077,lentil,18.081055205437472,2,6.304728401782241,3.7354977625526953,393.9346484297704,6.3716461428768785,4,5.300027266792263,88.72919194341164,120.0753439618402,2,28.407064801676,2,29.970289700460164,3.671251275863861 +2,79,15,21.53577883,65.47227704,7.505283615,35.75107592,lentil,19.227672453883155,2,7.820464503866872,5.530977828947972,382.47923203170006,7.122700167611304,4,8.76288709788691,70.70320479592385,91.74046195047956,2,0.38507576285265466,3,52.84407589467291,4.9251846020066985 +26,63,17,29.87854588,65.73085206,6.950300686,44.95654782,lentil,24.736942539024028,2,7.498385966808504,18.7866728339011,396.9546742232212,7.023729731116315,1,13.345261125097057,8.04590438527878,55.69251692863946,1,48.354878668016696,3,97.5191859054308,3.464898415794676 +27,61,15,25.2653291,67.10004577,6.958054839,48.33941188,lentil,28.64486522684999,3,6.137018525462221,15.974127827031843,367.67017660078216,2.8954213388843226,2,11.819256917378803,36.9554874980362,75.0219092753126,2,32.902542709848504,3,74.07777773893307,3.5284204272470885 +24,70,16,25.17885316,68.93307305,6.54803469,35.03484812,lentil,14.345952234830875,1,9.77899953280404,10.596461673613467,415.4995492412156,4.09934833693325,6,16.789980656020912,2.2743437479968764,164.83459344987793,3,25.266777642718523,1,38.48476774483601,2.1151785850653724 +13,74,25,24.12192608,61.09533545,6.461618577,44.23629285,lentil,26.962441006957377,1,10.712694067392874,14.066382631068109,361.3229095724389,3.7407758494007712,2,5.051375583343425,72.97108931382394,53.2330702961511,1,5.039669238199762,1,59.493879877469034,1.880089209091727 +6,64,23,23.33565221,67.40460704,7.065264073,36.18678721,lentil,22.03811390384275,2,5.669890526083501,13.156988843374398,431.522831368868,1.1670529693550002,6,9.092995030783623,62.982786275086866,97.9659991903941,2,38.02906610726529,2,72.1528698741107,4.681176112910903 +12,58,23,21.74600081,63.39503184,6.765091462,50.43306085,lentil,24.89054517424656,1,5.69655648384773,4.059310743829371,449.12414548903587,6.118478342895687,4,8.441222802212597,44.583280550703776,190.56710446391864,2,28.258862798617685,2,61.3173822263886,4.680997266645102 +32,79,22,27.60195453,63.46170674,5.91645379,54.37814199,lentil,15.708845879449388,3,5.945760731909819,17.61623053743278,447.89180038916084,6.266976310580807,6,8.332525369685673,18.008088330203176,110.47108043166624,1,25.48596351364297,3,51.43406180629421,2.3564520313836734 +6,68,18,24.388717,62.50453062,6.711341147,47.26052494,lentil,28.348238943330266,2,7.124887638232765,17.759181717624518,402.6386194379623,9.377079677854862,5,12.211252769786213,38.55566511027472,117.30724455676926,1,22.4678921273072,3,63.376262585810586,4.178196401125927 +10,79,20,24.98287462,66.895409,6.379881442,38.21370568,lentil,23.202399721730934,3,11.861477380099046,6.781859808946191,406.4933693505076,6.182380090370091,1,16.015969346411424,26.47516825013311,163.27810036766954,2,2.5770707415240626,2,13.73378312935577,2.188480191495183 +38,77,22,28.234829,69.3159965,6.313284268,35.36831423,lentil,27.962340127888282,1,7.148028603347068,14.55011107983852,448.4794083381553,7.270209975982189,3,10.978467893336326,20.975602105443603,153.25370991857812,1,15.239527814175824,3,7.569973547662256,2.7964471477769233 +17,74,17,26.03026959,69.55863145,7.393210848,37.11395801,lentil,27.880726849596336,2,6.871373522409962,8.715293172561339,394.63279722001573,1.6623864127392145,3,16.974563530723856,85.53687742505383,140.3822080773886,3,9.931552525473652,2,74.66630263870651,4.411342686800024 +26,68,24,28.04849594,64.07691942,7.504930973,37.15824966,lentil,14.641443659577863,2,9.237731698910501,0.15506563320055733,374.07235697004734,6.265930772398644,4,6.885387349924699,12.53051187257549,166.63596843628682,2,41.081097126263636,2,29.05474926870926,3.5463757274504446 +23,75,17,24.87425505,64.00213929,7.198076286,48.28137482,lentil,27.017716233737296,3,5.513668358304629,4.213603198624785,396.4343685685683,6.431280058274633,3,11.451320274116753,34.63207907787281,134.5919794451369,1,6.604137137384475,1,59.513878043347,1.326306285743469 +32,78,22,23.97081395,62.35557553,7.007037515,53.40906048,lentil,22.618112514277907,1,9.304473950112495,13.06476411981668,398.43322172548886,2.886499308278208,1,8.339240910824124,38.70730457812781,91.361134262089,3,18.322703542686092,1,36.936743581037234,1.1829075053158067 +19,79,19,20.06003985,67.76252583,6.677262562,42.89509057,lentil,18.329149855486456,1,9.880709175279915,8.480549175136563,417.29174767279846,3.983581654526077,5,12.37093831520438,24.758554240807207,75.32923724020878,2,47.70789376799374,3,48.30141247495157,1.543963804763277 +22,60,18,19.59221047,61.28633405,6.74398035,41.7704893,lentil,11.908288521970295,3,11.87892454932975,3.160515995604345,409.5818997164872,1.1813482525994417,3,15.15959043217057,43.510860473123714,113.72324073670846,3,3.7967571561129674,2,25.390709257806254,4.56975029805194 +28,69,16,29.77013109,66.29327012,6.547361618,35.69674138,lentil,27.093874300113924,2,9.875342098314057,1.335826471093513,437.1446180832128,8.832227883615587,3,16.706238759488983,18.660956862827728,123.08791682710324,3,42.341301366570676,1,84.2370450757781,3.321662161799542 +1,67,21,27.52135365,60.53657684,6.551577598,48.06491307,lentil,20.10713785979723,1,11.79303773006647,16.064069697831023,371.88394650003477,5.6856838406539625,4,16.783901863612854,16.594426910489744,174.76189519589886,3,34.818104138956926,2,82.43951231269595,3.9031547477059214 +12,67,23,25.62896213,63.14909763,6.585020303,45.49683991,lentil,14.617376998249076,1,5.875797263042631,1.180002656854564,399.0970044237872,4.749962158465656,2,15.239111513465748,78.64516984607066,196.58796532887774,1,0.7816359790176253,2,44.2051744851492,4.174632323836173 +36,67,20,20.39078312,60.47528931,6.924042372,53.31508572,lentil,20.09578928612813,1,7.897613057558718,9.858719108757159,364.60267090105225,2.279296351926116,1,19.804129363872715,30.04899292872899,168.1958611998441,2,30.913615285413997,2,25.598524550507108,4.17440372679706 +28,70,21,25.39038396,60.4989659,7.437373666,39.18374505,lentil,29.618639154480586,3,10.262345909301697,3.870646132242692,365.4839651736186,2.1107097813758906,5,10.713264914906455,53.05652802309554,167.55163719360405,1,22.439777024035767,1,70.04856309766483,1.771372524515718 +12,71,19,24.91079638,60.71367427,7.142611056,42.19740397,lentil,23.367817290240918,1,10.167946684827523,17.433949753435222,359.81608835945536,3.19349397168311,3,10.987763765003198,88.56496673304649,132.09347303471415,1,12.621257373056732,3,63.933575183434755,4.197837154451969 +22,68,16,27.70496805,63.20915034,7.74672376,37.46160727,lentil,14.568782426502285,3,6.691179995148137,16.585933712118273,376.33384459989674,3.0761146445909877,5,15.38626120664413,65.51434926241406,144.8199745948752,3,49.966588688193,3,54.757311191407354,4.040052790930984 +26,66,22,18.06486101,65.1034354,6.300479414,51.54922825,lentil,16.70909070992215,2,6.329312056580471,13.77580785489586,409.99273939245677,1.4536083030102742,4,14.82536904733736,71.4881409713671,89.31210914127567,2,42.96153583612492,1,58.73846493687744,2.7015081822804365 +16,65,16,18.13027797,62.45851612,6.078724107,50.6128521,lentil,19.347354404761397,1,7.594171588144832,8.702869440392933,404.8072014034098,1.7275573518216776,1,7.542420166716921,5.335543427749556,163.6520288102647,1,45.086950263044784,1,57.63335150363893,2.3615979720169147 +14,59,22,23.82723528,67.89815262,6.76660668,46.90725077,lentil,12.580143977034561,3,5.640233975690396,9.272559366739133,396.2863056159336,1.995847090284123,1,7.270986292908939,12.116362071525998,59.967085749731545,1,36.059081687586186,2,68.03778561239437,3.940962900095362 +33,59,19,23.19305333,62.74710773,7.641024177,49.55213308,lentil,25.503149422228482,2,11.572300653496455,12.227823773458757,384.79135919037645,7.893359729994192,4,15.839673043583383,29.35492705782943,176.17768692245426,3,40.15672639081654,2,68.34540989360012,2.3194648057620255 +21,63,17,25.08966129,68.17543102,6.559681838,41.45486619,lentil,24.35983833596474,1,11.676934803634056,19.07973181633846,371.37627765656043,3.187829296101506,4,17.730521057733213,24.76429292631056,100.63237033264744,3,43.07950506433505,1,28.008464560688928,1.637794069390457 +0,69,21,25.86928193,61.88321072,7.072923306,36.68284038,lentil,20.5287131393018,2,8.290499811772033,3.989482862201663,405.1792635422988,3.980204474980871,4,5.368694876503923,18.296648745872012,60.037905735770174,3,22.092876505252956,1,7.232765455408597,2.0156475683399493 +10,75,17,18.43966037,68.05394959,7.732194788,39.00992137,lentil,10.273651368619861,2,8.474102929447048,2.715976683101875,372.44306423985284,6.013797166523013,2,15.723457893185449,82.07842976595529,117.79350826302277,1,36.501071460514304,1,73.93491461744216,3.270412719818772 +30,61,18,27.14911056,67.02664337,6.157782589,52.50812701,lentil,16.86115814260713,3,9.636014556966886,7.536925250971455,440.0246880479634,8.73373455899779,5,5.309411245547835,87.167564959173,187.0996702339672,3,13.319585444892345,1,58.780479549872034,1.5954873621414327 +0,74,17,23.33375853,64.50515776,7.240988401,47.01510708,lentil,11.911879522102387,2,7.914023130622338,11.94318729709434,354.50570134084677,7.680509515846121,2,12.683016127147482,42.68945995710794,73.73495527144723,1,47.131885286021685,2,16.07976342466276,3.276836937163461 +35,74,22,26.7230014,62.96841833,6.898905799,42.87274897,lentil,18.27494295425739,2,11.613911921738687,4.11477578824557,443.24173022827716,9.96243899295224,4,7.503654771329578,97.22310741572548,176.4944206941241,2,37.84536070969103,1,98.0792279815589,2.8737564558815176 +7,63,24,19.55750776,64.45268309,6.818681086,53.04669416,lentil,28.703481602002075,1,6.989636769606996,18.27946227694952,384.88349336278617,2.670690451669968,6,6.679976796214723,79.41792450521413,157.9013663391306,1,44.861678989915674,3,30.402394503279695,1.0848313235463038 +9,56,17,26.13708256,66.7729209,6.261937875,46.48280681,lentil,26.640420031657005,1,9.827287079356793,17.44561099467664,387.25084103019685,5.226436901308074,5,17.355183462200653,77.46893186117966,108.44596160421976,1,5.258451529725644,3,44.42394872558376,4.55553690488122 +14,74,15,27.99990346,65.57653373,6.493036868,49.94043064,lentil,24.29132104227174,3,6.848778050243904,17.753936946189427,409.0844997064032,6.765523037418564,3,6.1899160302164,75.759091363561,198.45304675823485,1,31.911345169979004,1,40.62044426107527,4.814154330682637 +14,76,20,29.05941162,62.10652364,7.042474679,36.5011366,lentil,14.965284817188142,1,7.196692431707119,9.415952248757637,426.0575308213743,6.936689181790468,6,9.994170882994322,88.78100281668921,89.60474845952584,2,28.917184423742853,1,9.356713934737105,4.46005079839197 +36,65,16,25.71269843,64.1123333,7.692013657,50.17067771,lentil,23.127137141911934,3,7.325446429469261,4.232665860292803,432.3041374084792,7.211427927417553,4,8.604609848622736,67.0785943863858,154.934642056815,3,25.365652625450824,2,4.663569390422406,2.529130508862883 +28,67,21,21.79792649,63.73086065,6.250994223,46.62370222,lentil,19.052286849157873,2,6.067945755608558,13.90833760268739,447.1535936419785,5.561190995086831,5,18.013855696487443,85.86586274487388,126.09323181397222,1,30.48507407714437,1,78.87429136630153,4.6583495318507495 +28,79,16,24.70626432,60.26854183,6.052184881,53.12442925,lentil,27.421001812254996,1,7.097979566016889,4.367215994237026,400.38171340982797,5.5307925257107176,6,6.510458165350672,92.51355976215574,113.18112791386608,1,30.71919138969269,1,85.89521782365122,1.6431166956141783 +40,61,22,20.94981756,65.8108757,7.002216044,44.23913012,lentil,26.567203951506727,2,10.410089478318362,18.30405744613611,414.01934528835034,5.083791828018195,6,9.702007236333653,89.86790352292105,199.98216584521697,2,19.299275594049192,1,99.4983286850011,3.6189134109851104 +10,70,19,24.84918386,68.98088448,7.272427638,41.61080544,lentil,16.224592754313896,3,7.7345059863568,12.45897742131061,359.6796474264641,3.181755889680254,1,13.263157700100418,30.021212620449933,106.57211441033523,2,28.731652182002055,2,80.96799379261705,1.6652679053172594 +12,80,19,21.91041045,65.21662467,5.962001484,36.10211371,lentil,15.741550620497598,2,10.023150441252197,6.4792907144254785,399.08402621682814,7.062958727067285,4,7.833186790196846,28.590868323658093,81.81545760654961,2,11.093004080134271,2,92.01907247463677,3.503932626653242 +37,77,20,25.93381964,68.70533022,7.080506001,51.02372773,lentil,27.843123662888544,1,10.02309930107644,13.088094293245415,410.0845438938006,2.977444233364575,2,14.23491008657956,74.80553720234431,145.3109905266887,2,48.028206038465164,1,83.20455667238541,4.844698022416765 +0,67,22,29.82112112,69.4073209,6.593798387,51.56461082,lentil,23.621614234869263,1,8.094505143791043,7.279192672821262,393.7340646290788,7.419163032875788,3,11.68965317624134,39.82833238735511,104.98692431810102,3,49.02557208181798,1,89.18026765214034,4.1537795111306455 +7,73,25,27.52185591,63.13215259,7.28805662,45.20841071,lentil,19.08616281257064,2,11.067205293135189,0.36647455041529664,392.6489769162514,7.374535241385539,2,15.807682408544945,71.18245871306873,187.44019395595274,2,19.72114292744743,3,11.448965436396975,2.885738777246149 +10,56,18,27.99627907,68.6428593,7.32710972,46.10585191,lentil,28.963953824655274,3,11.980394329176537,16.637197652092986,409.5988106177418,7.090399879050168,3,11.02172474329563,33.56750120687965,121.27967822228166,1,23.514765182000385,1,39.090588689564775,1.1143340797659702 +39,70,15,20.76774783,63.90164154,6.366355781,47.9271552,lentil,14.622800795816357,2,11.783512368771714,15.159914943360171,364.41869639648803,8.374056204502335,5,6.431010647859985,94.2030909202964,69.5250543637903,3,41.14122916828145,2,51.916412055031614,3.171390200893986 +26,56,22,23.05276444,60.424786,7.011121216,52.60285259,lentil,23.139438594218518,3,11.118981359251332,3.254042724435857,423.95901923397565,3.5616146697169304,3,18.173097313156013,96.93239327895921,147.45909433889156,1,29.138211158644765,1,39.280768621221476,4.620146393687778 +9,77,17,21.65845777,63.58337146,6.280725549,38.07659414,lentil,14.924898117590468,1,6.712965179303653,15.559355250182247,350.623168984658,8.830728960574737,2,18.932097039352662,40.35355917750029,135.40246335772966,1,34.72027668961286,2,5.626124369170904,4.790774302077372 +4,59,19,26.25070298,67.62779652,7.621494566,40.8106299,lentil,23.98906903439705,1,11.839940350240234,19.77469078527322,429.82565359421,2.575532115910115,2,19.514213831710844,78.7989173527513,70.29259414435741,3,15.752540477982556,2,75.48529089324909,2.7661226508034376 +34,73,15,20.97195263,63.83179889,7.630424083,53.10207889,lentil,20.245398484872055,3,10.175526795219522,6.452467603018146,432.01195193665023,8.504917297828268,2,19.457186779090673,90.0395984795438,151.69375527092274,1,12.444479126511048,2,92.38531724048363,1.6044679422712096 +33,77,15,23.89736406,66.32102048,7.802212437,40.74536757,lentil,16.843666120790946,3,11.634115816262156,4.964019382638066,426.9932412151866,4.104704264912611,5,9.372103560946316,76.54885794351736,109.46735374335651,3,25.16698073628958,2,38.23503718670073,2.129619540343404 +2,24,38,24.55981624,91.63536236,5.922935513,111.9684622,pomegranate,29.82073358067431,1,5.229433445769335,11.642977760924872,358.9806938418626,2.056651290642166,6,6.456733572143127,74.80624577650414,182.63103067288174,3,1.257897127552754,2,88.36356341169642,4.978824136865915 +6,18,37,19.65690085,89.93701023,5.937649578,108.0458926,pomegranate,26.191276152311584,3,6.708153128475681,4.668144003312782,381.05759021427787,8.955802673708687,3,17.723249768831252,62.16619975440017,198.18831931961984,3,49.176200538614,3,11.883734881760466,2.7365924903723675 +8,26,36,18.78359608,87.4024767,6.804781106,102.5184759,pomegranate,16.176486379938588,3,10.550730709631335,2.435822334766331,415.5251824017887,1.4933273454210279,6,9.599499002296529,88.58182814068985,133.8168623728943,2,30.038182661245955,3,75.05481149204861,1.6887381853661445 +37,18,39,24.1469628,94.5110662,6.424670614,110.2316633,pomegranate,29.69958033164991,1,10.044658469828775,17.15953709817065,411.6391028501411,6.352370792037157,4,8.879352206050186,72.68772870707213,187.58838882556282,1,31.20721700325994,2,65.0172479281433,2.91295167821925 +0,27,38,22.44581266,89.90147027,6.738016221,109.3905998,pomegranate,22.676006405774526,1,5.4308831263030815,17.38533549339872,398.1071022352494,2.506324756457042,5,10.812225299099897,90.37057458127191,195.59519923855683,2,21.61443209411101,3,35.7233904175992,3.1051786868366325 +31,25,38,24.96273236,92.40501423,6.497366677,109.4169192,pomegranate,12.177924300529003,1,5.56924058366493,1.07657231560766,353.5336932534243,7.138210997826638,2,19.39904803255098,14.743381338790574,92.09802794361752,1,12.479189491000808,3,56.178089490775626,1.872612412709247 +21,21,38,22.5526059,89.3259486,6.327673765,104.8955643,pomegranate,26.280214762278696,2,5.59983721105651,9.046386825360333,414.8612719277506,2.939434078672868,3,9.14566342417358,99.90675719545892,171.23370171870343,3,45.53685171482309,2,74.9722448178763,1.7638096985495046 +6,30,40,22.77035608,91.45498527,6.36137446,106.9659201,pomegranate,23.707311279600493,1,6.816272993056142,2.6982620417635705,423.713665115704,9.349070545091745,4,15.413516080924513,25.097291847709823,103.63097631738657,3,21.914961023549136,1,32.98964904938344,2.686251157761785 +25,27,41,19.20090378,94.27659596,6.923509371,108.0423555,pomegranate,10.806668033177386,2,11.307580854722428,5.7003841919991505,430.7276096887616,1.4463218319134223,4,15.288416974091387,70.99911747321036,158.98328408349303,3,47.66348831544285,3,71.93431802691461,1.4421661877375143 +15,11,38,23.12808226,92.68328358,6.630646083,109.3930157,pomegranate,24.403204180156607,3,9.799509196981468,3.709087459930225,350.1055289700729,5.433465769545952,4,10.770290321930183,83.31810965619054,112.44735959212738,3,1.023681288866768,2,60.06218848385101,2.497284417749725 +14,5,36,24.92639065,85.19098079,5.832525853,104.7693804,pomegranate,16.854470545846816,2,11.167383123987264,11.028140519927302,385.6321643750148,7.127845484957718,3,10.384903996642228,87.21853212357598,171.12560398866833,2,39.50639505756428,1,89.95940094542547,1.1043151026415963 +16,10,41,24.77464458,85.63608688,6.738993954,105.7595811,pomegranate,25.1540447810393,1,9.373111386124847,9.193857154094324,357.9660609025812,9.908473573708982,5,16.537403171444417,78.8109377959032,142.4198261231162,3,49.13710266867884,2,18.982348429753195,3.106634561334538 +36,7,37,19.8671184,86.35590206,5.782435567,108.3168858,pomegranate,27.753003309689646,2,5.472438994375602,1.0479295912050746,390.97699839213504,9.041922104418413,4,12.88164060530626,83.36006389015498,58.519688137933905,2,6.499175693601201,2,3.694218979187691,1.9307413107201703 +4,20,41,24.26601316,93.7974061,6.537042717,104.5375109,pomegranate,15.01205584314594,1,6.6216509286468215,19.78311657292187,395.14610009109407,7.056214001901947,4,9.503020333741148,88.06765418632077,132.32370121208743,3,2.7518908751832774,1,93.94034782626714,2.9639190330408467 +29,22,40,23.62600218,89.73266695,6.145104401,107.6836871,pomegranate,26.838771128325185,2,9.53382864057151,14.73810601866404,438.21700997949915,2.888362245534815,2,11.03547491070291,53.77803970521513,168.47286975292036,2,2.9172959776212926,3,10.722033358024408,3.9456994372554406 +16,15,42,19.67832052,89.08935702,6.890784045,108.5468633,pomegranate,15.443780035055942,1,7.429194036074655,5.020634964148374,449.24408988062424,4.170761220736455,5,13.771334380840875,63.46835412030083,169.73686727741472,3,24.927888255362213,2,12.033072647245625,4.650349448050031 +18,27,41,22.36509395,92.30882391,7.175344328,104.8216333,pomegranate,24.322284268605454,2,9.285023044041834,0.39083125830263077,388.71216009790413,6.426294756440868,4,14.01919524875444,64.92722665699185,149.01800314887296,3,1.048151784356105,1,77.43179254354618,4.69277734106482 +11,18,42,21.57936934,94.88267728,5.938528744,102.8593382,pomegranate,18.96694772570151,1,11.292019764770568,0.13398758194617333,438.84541434540364,5.10418823591812,1,13.239059233340388,4.163888210012834,134.98556772081304,3,33.67471150955671,1,91.02142201659989,1.4670729532676092 +5,15,38,18.26233221,88.16779129,5.709380472,108.0756727,pomegranate,28.76925310088035,1,8.863933726865742,6.841955039179679,390.32692975903734,8.552554423415442,6,11.923768395299893,95.70099383744692,94.0555619916651,2,20.60353807311775,3,58.77072467084334,2.113860098063629 +18,23,44,23.71028128,89.61794165,6.184400085,105.6499907,pomegranate,13.125473693909079,3,7.0116316854513805,14.512991656540423,411.49213648002575,6.0740190360412045,2,12.669168385554382,86.56522641623332,112.38070935767104,1,4.33634603185149,3,15.229306369325057,1.6476738826772217 +9,8,40,22.48720144,89.9224883,6.553509673,111.6631582,pomegranate,11.74268614956109,3,5.265638783236493,5.687718549226686,364.69659923546897,9.773097176584038,4,18.96030604185022,61.06804863785102,187.56723975298007,3,40.33473055869141,2,38.26410271219438,1.639145330735337 +40,27,45,21.6602498,94.79397419,5.885638185,112.4349689,pomegranate,25.62410154531993,2,10.121272660655007,4.5289396189883435,387.9696001982271,8.697286392109516,3,12.274019298673615,4.249559593759578,98.4246107750133,1,38.42244388288514,2,39.52935306914658,1.4227309705744444 +22,23,44,20.13037175,89.31505137,6.143874691,107.3416913,pomegranate,16.552014653998782,2,10.155982672641443,4.816263169059458,411.9858223870076,8.04043282762039,2,13.501408400663353,27.49589642025012,148.31944962193256,2,45.09289149012348,3,12.069954006087524,2.6927007455307472 +9,16,39,18.41164435,91.11927248,6.101198974,105.1834976,pomegranate,28.93421486775686,3,6.322785560610438,14.414364340577674,435.18749857137516,3.8321816795298393,2,9.14679203104739,40.420992573585714,87.58039411789743,2,26.256622509427146,1,19.687302114786597,2.4599989144117607 +12,29,40,19.68291173,89.75272999,6.594037135,111.2818551,pomegranate,12.737816445976943,1,8.4689973210167,13.924068805665616,384.9787071312857,2.0247917903587362,3,16.481452964060203,6.453921700720411,113.05110286089462,2,17.604713622365814,1,9.965941695605618,1.736492664774103 +0,17,42,23.20242586,91.19442671,6.859840821,109.0946323,pomegranate,10.607368638078409,2,7.824450744898426,8.166221517062999,396.78962817190177,6.950395145057668,2,14.935347197786873,45.763668056646566,74.89969577858119,1,14.496601118974878,3,70.17285506049159,3.0049957297612515 +2,21,44,18.92157197,87.31290342,6.56893406,102.8013275,pomegranate,26.339483590651508,2,10.929224976254497,8.221959908193151,355.32018779994036,9.269256213550257,6,15.750148904222502,55.339318108100635,108.28549828102351,1,8.124372002402808,2,23.338099182564985,1.5362183834227205 +28,6,40,22.10621387,91.34039616,6.769855664,106.8704803,pomegranate,18.54841639924345,3,6.9996712996742225,2.3406667127598335,398.07216830653715,2.434757805727848,3,5.828097621736742,43.690074722144146,78.50648007611984,2,24.17602788764796,3,27.987555544343902,2.669138773880433 +8,23,44,18.47412402,89.68919664,7.130837931,108.4758509,pomegranate,28.376525535257734,2,10.60103736978654,10.38742760956574,389.4663649409971,7.861572589606427,3,13.978932879893112,2.8975525752417797,118.97258350847514,1,38.42402743665473,3,32.233184252300454,2.2966041233209102 +29,16,36,19.81069447,88.92944254,5.740338002,102.860084,pomegranate,29.068625112908336,3,7.1254342516536635,0.8406887632323112,382.093102278883,8.665868569799166,3,9.664440731854631,52.18932540905763,78.51227358684415,2,33.4805760801006,1,84.31627510362809,3.2034352631764222 +17,18,43,24.4880844,90.83687246,5.843005428,103.1969341,pomegranate,16.997505665694476,1,8.786358178221835,6.646637452485431,437.3837976929168,2.7024158948205397,4,9.118294856898508,74.98763731620348,97.23084059358806,2,8.426748307004118,1,27.698265945996514,2.4365727769827767 +34,21,42,18.75927679,89.93457597,6.648687274,111.0196744,pomegranate,16.968321508148083,2,11.19892371271047,6.055952837532312,360.76012639635314,5.408698897826524,4,11.86889178793739,6.949340298689643,106.16638401385879,2,2.119843779269359,2,5.737829186309873,3.806563948343017 +21,23,42,19.54128063,90.29751796,6.902751061,104.3739878,pomegranate,27.213230446197805,3,10.34634338527339,3.4699578687445243,430.23898899386785,2.697424405634942,4,14.461817529043229,99.08578762433316,198.24606166941552,1,24.912561852531578,3,69.3568714144565,1.488973699832692 +25,17,40,18.91251245,87.74938524,6.608023872,111.2800516,pomegranate,19.277553432727444,3,10.455379519800749,19.909062478913597,367.43522021863686,9.273580107102832,5,11.046264832933225,75.951484160126,102.30621896907198,3,15.639574763437986,1,82.93454702234945,4.328754772513261 +8,25,36,19.91330523,94.95031368,6.828522375,104.0277061,pomegranate,19.605703128350104,3,6.23364843631577,3.4504469408929395,396.3132428209828,5.4150591831721995,4,7.416357107502619,66.20191569959634,88.5096312055496,2,17.777664681518505,1,63.863728165609814,4.763350611791864 +26,18,42,19.72620525,89.64934166,6.910374919,108.2287276,pomegranate,21.13809726435067,2,7.451686902975808,14.23040341233432,409.4623218476303,3.184524687448451,4,18.396618151539286,34.08218891391923,171.28128766778366,3,27.03707640099599,3,1.6954239386019476,1.5300638866434206 +4,19,42,23.83185873,87.84034604,6.306605528,111.2232716,pomegranate,26.392065523250196,1,11.16461244479341,12.380545028997679,362.1367930300196,6.182512638054062,5,10.436827130332048,77.31043944604944,191.92938931615672,1,19.48620561188595,1,40.83775354148334,4.981574245277226 +36,24,41,24.94467632,94.25702672,7.009180374,103.8799347,pomegranate,12.673266312138056,3,11.04213085356158,15.34863623382023,370.15837925534436,4.284580359126355,5,13.513503607348419,78.08255510386356,182.88175156487472,2,34.591465818118174,2,54.628725429295265,1.6320339978378238 +5,24,40,24.692258,93.87030088,6.297907579,104.6735454,pomegranate,16.004622811291533,1,5.25695217713196,16.31151716937383,425.2094635588337,9.508041913375557,1,5.4682902558123185,95.7241103523145,74.85659030924579,2,31.00262565966419,3,42.53895010478236,1.4749541756129805 +19,17,39,24.72485577,85.56083187,6.728599215,111.2787584,pomegranate,27.332540299795355,2,10.660257780277465,5.07017514774553,382.2356271411589,8.041345495744707,3,14.060831694863086,97.94910784594936,177.89276826503374,2,43.16793559458734,2,8.510561665648419,4.218148001181021 +39,30,38,20.12644921,87.59629625,6.965156738,108.065579,pomegranate,11.195846847011126,2,5.163682757185528,13.654797832479018,404.00710134170856,1.2457596488975902,4,16.105745475948513,60.22777152888254,173.2837914673207,1,25.861313796731316,1,28.010829549208992,1.1193121968793514 +5,29,44,21.02432943,93.0569505,5.578095745,104.7847006,pomegranate,15.817099084796723,3,8.648771053138201,16.246556136034844,363.5694695561259,6.70681604919974,1,7.264586235692451,34.88139033110579,163.19587042124698,1,3.343288758961638,1,40.09420152014509,1.388146564628066 +4,24,43,22.40423537,88.1508343,7.199504273,109.8695196,pomegranate,12.88791272173219,1,5.655148991198279,6.615044793374394,350.39816964245364,1.4591231395037292,3,5.084484877617874,21.898949829189117,191.56001905721564,1,3.9417950763689538,1,25.488753888616433,4.850005903892116 +38,21,35,20.33691147,89.38003827,5.841367187,110.9653137,pomegranate,21.02573587462075,3,8.430523937575625,17.196682550178167,401.64241475363696,2.41081046728446,2,17.19873043012587,83.18572594263303,143.66925639260648,3,27.065267197973707,2,26.82033907290321,4.501351571183664 +37,11,36,24.24779615,85.56033312,6.710143266,106.9216033,pomegranate,19.65705878605352,1,8.560609316815263,8.27440938467526,368.51337763714287,8.14828106310998,2,6.694865066391495,38.262427115976806,160.510896792121,2,33.68967707029298,2,36.17516052530153,4.693488953455045 +9,25,41,24.81530144,91.90842992,5.972714857,109.2853418,pomegranate,22.060361526689416,2,7.356054698209346,10.162682657682517,369.3762766719512,8.223448560837326,1,14.95329352497929,58.45779471096733,91.77330336082127,1,31.749801819655083,3,21.035330176216526,4.44599635007159 +29,22,43,19.66329768,87.95158129,5.561851831,106.0380805,pomegranate,29.19763352450463,1,5.264431242061461,8.323869112918743,408.0781043227347,4.195284082618885,3,7.400757690572969,66.8140846231147,83.04145140397361,2,21.06778293898841,2,61.06000211952305,3.5623000204991913 +5,21,38,22.43377991,90.3396556,6.107054808,112.459697,pomegranate,16.154835652418242,3,6.936039666888001,2.52393210400929,421.9432921767643,7.251225433788187,3,13.037458987800013,60.990876333896196,143.7552328149077,2,17.606156744293166,2,40.96530341642731,1.9114200062269084 +22,26,38,22.92052307,85.12912161,6.988035315,110.2437841,pomegranate,19.36511424713501,1,7.511500458718205,15.384845287667591,410.94528279095033,5.863683741506208,3,7.193982875010971,56.39115145938959,122.6251744623977,1,42.83504395917244,1,0.6711863464431533,1.6575200647197739 +4,18,37,22.91843172,85.40695044,7.13147457,106.2817706,pomegranate,29.614070801998576,2,5.790408853180449,18.0672892283866,449.3679214774031,2.621242218992858,3,16.42105576408848,98.93380607385063,169.74581834789421,1,15.959487781162801,1,99.03723392530051,4.807359963877918 +21,6,41,24.88244467,89.39686219,7.086947687,107.1951707,pomegranate,20.976618138030744,3,9.69337756637413,14.837554144064136,423.1374788575144,2.0598431181630055,4,19.58063400694489,95.15637861517483,142.360576497977,3,30.207205190688246,3,22.684571074479464,4.314911993790776 +29,21,45,23.40981539,93.13277,6.749260456,105.2240743,pomegranate,15.94913001881664,1,5.174529735836245,9.564344165850613,420.701310276496,5.431874500655034,3,11.509850948424722,53.31855314689234,165.72081081707876,3,36.497562598657076,2,20.031144033719407,2.350646622702507 +23,5,44,21.20725375,94.26304717,7.16300467,107.5660804,pomegranate,28.910970975883966,2,6.502625260398327,10.785676658922046,444.6682499234743,6.4337634759114035,4,5.9270930847093,26.626827961338982,79.84532811480098,2,10.709648306397796,3,13.09087172735033,4.596748480002578 +13,7,43,18.20230419,91.12282162,7.013481515,109.6623974,pomegranate,28.24014920359668,3,8.9506654612049,19.82310106507206,386.23734884669756,9.8347622556956,6,14.60904638505167,2.5936299700129006,199.38919346177227,3,29.338067546017893,1,28.92887181123228,2.5198454263672923 +5,13,37,22.34375696,89.7870345,5.648243649,103.3183074,pomegranate,11.442336684911844,1,7.3601797071140425,10.427818577162293,353.53652362048416,4.927833957471779,5,9.258559204402381,99.94570244673973,159.78335427298515,2,15.502997018505093,3,81.73559359754438,4.724939119339368 +27,24,41,24.32770134,90.88292835,6.610251186,110.4606459,pomegranate,20.375739681026285,2,11.563781374089327,6.437053249595399,369.51112145817143,4.9618986618781795,6,19.478730822827657,1.488883089532167,131.12639719611798,1,44.024537937792246,2,98.4608641902676,2.8839897959638328 +7,23,35,19.75088482,88.71691157,7.054313823,102.5538035,pomegranate,18.843079670742284,1,11.092604125141976,6.947505305185954,396.9251895746729,8.942793901246446,5,9.662298263096144,88.90534364943925,123.44943016901335,1,43.715078787708116,3,75.40749005777026,3.0558927471892328 +12,20,39,19.86173586,86.19740917,6.026999326,111.0217929,pomegranate,19.679920824644235,2,6.3233267358718965,16.442019652120322,405.1330777653061,6.858904794367301,3,10.278835704867513,92.28329676089052,161.2377189490994,3,12.681232810623777,3,31.95788225627343,2.4113815120326536 +4,19,43,18.07132963,93.14554876,5.779427402,106.3602023,pomegranate,26.15873615091607,1,10.319647646111868,2.373590925495974,360.78748704074684,8.229213652997597,4,17.856948997319897,18.658139911746517,186.13765990389976,2,8.689984493995805,2,29.307550303091812,3.6502765520118534 +3,9,45,23.89162561,89.61850203,6.535244251,104.617522,pomegranate,19.872746998326967,1,9.61421688801782,11.418204127586389,419.27810595569315,3.3321579655159055,3,9.155164483401924,29.47993716060585,176.2155671139116,1,28.69233807347351,1,58.0879042749414,2.7476449977089237 +1,27,36,23.98598756,93.34236582,5.684995235,104.991282,pomegranate,28.65626092875943,2,9.142501828732783,15.391525782167086,437.9228767298965,9.924951777926776,4,13.773738903765036,6.171702411391133,63.98318110736047,1,46.047228006244794,1,27.973358277861593,3.0338913306873785 +23,30,44,20.93892916,85.42912869,6.12476108,103.0295938,pomegranate,25.649589032234665,3,5.811108729394996,7.604685167603586,421.5269151472393,1.9383719004123459,1,16.344591827856142,35.52647463854295,75.75725763658251,3,24.922898993069055,2,59.02002762709249,4.064790866794923 +24,21,42,20.82210727,87.22815682,6.999014379,109.4429934,pomegranate,29.851948868154214,1,6.279204436884408,6.41919561159029,424.061301017349,2.7561361267997526,1,19.984020697709134,38.81481129230203,104.46759744959553,2,10.61193878653674,1,65.97583651562601,2.6789792180585374 +13,30,37,20.86474944,91.61793636,6.277148771,106.8685636,pomegranate,25.6333268407937,2,6.101170766980701,11.59386730442649,372.94096849107393,9.223834826876638,5,8.350423748825772,5.4761833915802915,53.2835793139323,1,13.200756384985768,1,75.29389404931351,1.2112763199817511 +40,11,44,24.45840036,86.10874614,6.322396027,111.3779693,pomegranate,29.707505847967465,2,11.528118763165931,19.883380790228696,419.79513875794976,6.831286017551865,1,5.070293203318788,90.50050298716059,72.53311159524988,2,32.52537313293238,1,33.427486907500025,1.0431216299534607 +21,9,40,24.51147697,90.64498715,5.956401828,105.6209954,pomegranate,22.522662700012916,2,5.2520025247478275,12.8178881100399,362.59605666334664,2.074794672021953,2,16.736078169058114,84.84877789414753,192.859235646241,3,42.62932595995023,1,6.60456410189939,1.502133853096094 +3,27,44,24.56811204,92.03009222,6.591302797,110.9633894,pomegranate,25.945411146318563,3,5.154807859453156,14.12323121660728,371.3988125150133,9.839765672676334,5,17.25378196566404,95.56669581782653,191.37822611046167,2,43.14189227355051,3,87.0300022819573,1.1810451494905632 +40,29,42,24.63228709,89.01574455,7.104094929,110.6956184,pomegranate,29.04661627663448,3,6.579925721150853,18.260451055040406,391.40359502420404,2.537508284503658,5,5.273845335687605,8.604832777843407,62.00159685734603,1,10.010534401084842,1,53.09516717409305,1.106465328121585 +14,25,40,20.07386547,90.97819712,6.407872061,103.7084055,pomegranate,21.925453917561953,2,11.12948331431135,3.6382443681219723,429.83778197385993,2.966346475883461,3,11.109942443819286,24.673872986830002,148.7010535358363,1,10.220379410290198,1,87.65264422201172,1.922033703874702 +38,14,37,21.80523051,94.63612858,6.658402594,102.6488846,pomegranate,18.328247890624652,1,6.840531290274212,6.236316021398888,376.9000822177975,6.064131615259199,1,9.089793102028162,1.8523847945511651,172.94818176868122,1,26.444989842964727,3,14.78670304904821,2.3906918821461205 +34,9,36,22.8122645,86.34233767,6.276038961,110.4432293,pomegranate,12.558982611490364,3,9.578071246714503,15.401444041233802,378.19356445309614,6.930698556045506,6,18.92551932826589,43.84489119090678,144.29637134442208,1,36.28078659687494,3,26.204863864523954,1.6120979025998525 +32,14,37,22.73031253,88.48567856,6.825256236,104.6843243,pomegranate,17.099713366928857,2,8.62107815709467,1.2867164656501662,412.2146229615919,1.5786048400228232,1,18.04950857157175,22.340219290259878,146.48202857459376,2,2.2960246088567504,1,69.49096922280833,1.8356024922443286 +18,21,35,23.2801227,94.94330457,6.368560522,111.1382096,pomegranate,22.5984525465593,2,6.971033442504762,14.012332504150828,430.8181701393216,1.3804447930054118,4,18.229010346185262,73.96699686754609,89.53170937255086,2,33.81461205325007,1,23.24873575888352,1.743232615753267 +8,23,38,19.30106297,87.1775172,7.005410734,105.4766591,pomegranate,24.990104035192857,3,10.614046595086132,9.936062852646259,432.0440730311121,9.354324482009925,3,15.65937814162079,82.40748785389783,191.92344392817174,1,19.018081369481166,2,82.0986942080776,3.581248272750314 +15,6,41,19.0087067,88.83768149,6.897368477,108.6793978,pomegranate,14.519284558567707,3,6.449944713407401,8.595677737237118,360.84384279381334,1.061488285898968,4,16.16790636602586,23.849539716591273,184.11407044715554,1,14.432614971069702,1,63.76780162940261,2.8009396727579086 +0,5,36,24.35193812,90.88612388,6.152906502,105.529185,pomegranate,11.56419431173771,2,9.735640518588152,7.702485639858178,371.1079938008926,4.267526339095509,6,7.629371158931173,69.02426740678733,179.47937353077538,1,43.94395777445992,1,45.841605737953884,3.2376751878933607 +22,9,44,24.72235539,88.87651295,5.744361602,112.1926517,pomegranate,10.792855126483271,2,6.7016076185346805,12.076782215101051,358.3628533565575,3.6813012731424504,4,7.33727334924099,75.30656884785947,66.37212078439187,1,43.065202766816974,2,48.062903328967444,1.6630433761270407 +14,8,43,21.92513945,94.46485312,7.051654924,111.7162016,pomegranate,24.560330833216412,2,5.449703854999377,15.00652577093619,425.03480919866854,1.3646291908135129,4,13.71347913221117,26.847734468288476,123.85575672362899,3,48.51869626951365,2,45.05589178223855,2.646806424163198 +31,11,45,24.83954414,86.88738076,6.034612928,107.6435771,pomegranate,15.581675035547706,2,11.135635758447401,1.5401783753993614,359.8985605479082,6.178929217705724,3,18.803502326636277,39.74296817049174,96.04580067095773,1,0.7264491056705347,1,73.21625426649929,2.3269298276801593 +39,17,45,18.09691127,90.42177379,6.924490731,104.88189,pomegranate,24.7255773805313,2,9.713818943400861,14.599876188340502,437.75714463324897,3.1239727335283933,6,19.184757463800903,21.280650759763,87.47214750948181,3,30.054004296764074,1,86.03219848576765,1.0626256928772828 +10,5,42,20.24104904,91.08706822,6.887005997,109.2537734,pomegranate,22.169247906066875,3,8.149083032265233,3.815287447205351,434.81812933671006,7.191391591935223,2,18.330253058217046,59.5555455410242,137.22047165016235,1,44.43138353018881,3,28.534513479081813,2.5808837098755864 +8,28,38,23.22594,94.42971362,6.8444019,105.6917856,pomegranate,24.305979237070282,1,5.736067602335522,3.022388521648167,422.4382097037606,4.3780414407331065,4,5.7868017740297635,80.90943605466082,155.75860658256528,1,32.964133183880286,2,89.90464182807905,2.388040277390816 +32,13,42,23.50128217,92.97527546,5.786058032,106.61905,pomegranate,29.978702331721447,1,6.406945447321342,19.702727270258883,363.09522458065027,2.941953890435432,2,11.002591840545193,43.71246986512895,193.90270283857663,3,23.806421990245052,2,98.4758512044287,2.438682411825818 +18,9,40,19.44623085,89.02127045,5.627186257,106.1606833,pomegranate,24.11949009499051,2,9.498764379065737,14.043222586820189,416.4018595298999,9.769024484976974,4,19.81844174298983,95.71343617191745,154.53103676689994,1,9.236759992220255,2,68.34581561351727,3.667144303701168 +20,27,41,20.51343484,92.51675903,5.700088663,110.5764023,pomegranate,28.944356438977977,3,10.033740227501823,18.859839558296446,404.80343824147894,5.827118590714913,5,10.650083990046834,13.048374516159356,198.73414152438573,1,16.86816936963768,1,46.52652613722735,4.3467356422876975 +39,25,36,18.90223032,94.99897537,5.567805185,107.6103211,pomegranate,10.925763227969833,3,10.709548863952525,18.341960538106875,424.716501150514,4.598171295251497,4,14.300004655362109,13.731712070600043,175.19968712731446,1,15.660287801545314,1,26.107885887734874,3.354237223619951 +20,7,45,18.90592319,89.24126808,6.077886012,112.4750941,pomegranate,26.91403297348575,1,11.264828453137671,10.455824425120461,445.9992597473871,5.020543554857289,1,5.877847599425198,38.36357473227289,164.7096716691027,3,46.36573660092475,3,69.07568175678655,3.4326289238361802 +11,10,45,22.63045168,88.45577158,6.397995609,109.0357597,pomegranate,24.16718845143553,3,6.3518987252534975,4.214766994323103,379.952452126036,4.090074620843957,6,18.50155519787145,51.78617405301475,84.75000582500161,3,16.531062631179605,3,41.536774673184816,2.0631736456242393 +40,18,43,19.38603815,86.79058496,5.767372539,109.9130984,pomegranate,26.693873208227995,1,10.568684008478677,6.502643765512854,423.19564172804564,2.980341530813273,2,11.585158774892342,34.83612659692766,81.73447500534702,1,10.379766122699808,1,80.17128038911436,1.862196183757928 +3,26,39,24.38318965,91.19431555,7.079973241,103.6012114,pomegranate,17.852454053949444,1,10.093531912900424,10.297795128371925,441.2744866745445,1.8357442620812883,2,5.291855032584359,92.79665883219313,186.70206817100558,2,24.922197984666028,1,85.13783650658317,2.9856169769062486 +9,16,36,23.77989026,92.93386903,5.893332378,106.977723,pomegranate,19.468467810458545,1,6.937846510971145,15.633098516806479,376.352780540804,8.533257433131435,3,12.102652069067265,40.75739482899292,166.95904670957788,2,26.500729956660628,2,63.50618664784429,4.929816009450487 +30,20,38,22.59890174,93.16343942,7.058222596,110.0932899,pomegranate,28.200468303177807,2,7.949219313735513,4.478250958611425,360.5996920027907,7.962657276686905,4,10.119523525457872,36.81586793492953,90.98925686543431,2,17.192061472415315,3,37.970707119990564,2.3206819533459075 +40,9,41,24.37766782,85.4017118,5.78270695,106.128338,pomegranate,10.429540377920024,3,10.01602388609432,15.993900320416385,389.2446168753697,8.2230605000939,3,13.444873018602962,62.96852601184318,91.48132388761478,3,43.519935225455704,2,37.741890848545,2.8277917527110135 +40,30,35,20.89273273,91.07776977,6.269663963,104.4407083,pomegranate,11.45359823331303,2,9.448771812000277,15.85535137982409,354.81145772349373,2.2243540688799346,4,9.420084035140118,80.87369167918159,114.81555461561844,1,24.599623835710478,2,25.33262373780517,3.6811299113879272 +32,25,35,18.09903225,85.70786282,5.892913826,107.0050976,pomegranate,12.358802990493395,3,5.3371336555318605,9.115429650457536,375.7433179226366,6.693698181731581,4,12.597770960172092,33.57254711309762,189.50176376909363,1,2.9083262135413293,1,39.3054086431507,3.8381575107869 +33,23,45,20.00218987,85.83618191,7.116538883,112.337046,pomegranate,28.83400218978235,3,11.378909982042412,18.742899809578304,405.89842378246186,5.599919189589377,1,14.536901818208296,24.445940854869875,77.79460668512309,2,10.76132110859755,1,44.505996897925485,4.330322544034911 +4,14,41,19.85139326,89.80732335,6.430163481,102.8186358,pomegranate,20.662950993546588,2,7.037216999939602,16.196263854123487,401.4095761669354,4.483171717945397,2,17.173627332291556,82.52167444268808,161.20814345370042,2,4.452168951636715,3,67.85394957840779,2.098904310554943 +13,17,45,21.25433607,92.65058936,7.159520979,106.2784673,pomegranate,24.475654701845393,3,5.7372457146868925,11.839194211757407,408.2012911348687,3.568954441082209,3,13.761880164848712,54.27662274503401,188.88897787321326,2,17.98378726176476,2,68.0414926115095,2.6553980712940715 +39,24,39,23.65374106,93.32657504,6.431265737,109.8076178,pomegranate,13.01299691329733,2,5.6967943680021165,19.012762426657048,441.0813902074959,4.458731055602319,5,19.78620215228434,29.67879324016599,152.9955973904517,1,0.9862809751114143,1,62.47553565117836,1.9396273488883122 +8,28,37,23.88404783,86.20613842,6.082571701,108.3121789,pomegranate,28.495770395351386,2,10.837918687585873,7.216049492131933,421.99608322961467,6.21325366863047,2,11.227101569907948,41.993814912312544,92.79412051831957,2,16.536691195498797,3,35.62414988897257,2.127919890949294 +91,94,46,29.36792366,76.24900101,6.149934034,92.82840911,banana,12.926622977414898,3,5.0406616673263125,13.003000836200593,420.34157412462525,5.333925296728927,3,19.920234449727026,51.08689910025191,137.38599652401876,2,23.36132557756145,2,59.483626823407334,4.792121736179208 +105,95,50,27.33368994,83.67675197,5.849076099,101.0494791,banana,23.035940691958224,2,10.569577736800543,0.8001305000049364,430.1163170600121,7.271328271850457,1,7.144636176223656,73.3728708044459,77.51230591761626,1,31.90308866982247,3,15.533399106499402,1.3234981798957648 +108,92,53,27.40053601,82.96221306,6.276800323,104.9378,banana,12.524361694819397,1,9.881711108679161,15.012190790508622,431.56556248737644,2.38034516432312,4,12.757510911841779,3.8580496404138853,129.51005311597203,2,47.3800394053659,1,86.85914379132925,4.25626916327424 +86,76,54,29.3159075,80.11585705,5.926824754,90.10978128,banana,22.513203908267013,3,5.188812393683553,16.73651997504425,408.5252416814535,7.918439594485177,2,13.951293198325324,61.00939867666849,187.5306309203067,3,0.6973995760514939,2,5.0265539995445145,4.64182116701836 +80,77,49,26.05433004,79.39654531,5.519088423,113.2297373,banana,29.777288344130767,3,7.790229825246227,18.39579482003864,388.424864898947,7.241953737271924,6,5.813607725040022,71.36484852226477,97.705825018495,1,2.4031058474914877,1,77.2293203446676,3.0408959428707867 +93,94,53,25.86632408,84.42379269,6.079178788,114.5357503,banana,16.775126349262365,2,7.070961032190277,14.857725675668416,414.9204459945005,7.386698908572576,6,18.52970350683104,87.48582567366311,94.29559554033244,3,30.68086425097022,2,3.4519973331019527,1.374842861209046 +90,92,55,27.00932084,80.18546798,6.13465588,97.32531705,banana,18.149825091205173,2,5.619849328226572,8.409839638544403,389.3993462924736,1.272330613332756,2,11.73113688063436,4.589745420585711,110.69171816953703,1,18.49202368582154,3,72.24625534452422,2.249791116913073 +108,89,53,29.55054817,78.06762846,5.808497604,99.34482238,banana,10.052477463262905,3,5.709020799271084,13.446311241670351,394.9186016581508,9.408521934996944,6,16.106318931704585,64.22810995738644,89.61214012869137,3,2.4508080296692047,3,61.18810570261915,2.9567270958826595 +108,88,55,26.28845991,83.390039,5.891458107,113.8729798,banana,13.938658325440155,2,6.202810781836392,17.513247589983102,350.7000511818338,5.116649990091593,5,12.69127655699791,62.566772860138386,127.77659063440505,3,23.25453909578706,1,48.78311561511849,1.889993430885759 +105,77,52,29.16226551,76.16151562,5.816622479,100.0075679,banana,11.707138610625297,2,10.162597273699845,6.358750306238625,406.59845745360366,4.384394726502066,1,15.536159990450109,70.29119766505491,51.11617165795382,2,19.017799687761368,1,21.61345939390672,2.7955586937937813 +118,88,52,28.65003945,82.68752542,5.843163161,98.75084366,banana,13.357367325075874,3,11.51857078403277,19.991293973622305,388.4971371790509,4.633586885293936,6,14.419306349821642,61.71534225074568,197.21994231502353,1,3.5278297409456263,2,59.903897466094705,3.2776381566383743 +101,87,54,29.07311132,76.50045221,6.376756633,100.1692639,banana,24.934679426813197,1,9.31215859929856,1.6604839494408052,388.27358624603266,7.463935004455667,1,19.3983908371509,43.749918756964554,177.8561845062281,1,16.370637088146157,1,21.46145844514107,3.7561652648311794 +95,75,50,28.08166093,75.26429821,5.623615687,118.2761894,banana,16.534598306057312,1,9.463407442063787,5.4011667664317,435.9291904710045,2.399819997925447,2,17.95836540713945,60.92001050806009,123.60563924023643,3,13.675491050790223,3,34.1684314009776,3.8345997594243864 +106,85,53,27.1994597,78.8086068,5.91505509,99.72430835,banana,25.53862575487019,3,9.596316490921488,11.00243255962782,421.13145643211203,1.6891462610302876,1,14.51239382164546,32.34660641571684,58.587838804379984,2,14.1150550402927,3,20.756182841633407,2.191669318183087 +86,95,49,28.05484146,78.04602887,6.458714879,108.3957179,banana,11.7319678187691,3,6.926496409845076,12.013230472741533,403.56956437933445,9.439411608783812,6,13.570464146983959,10.058564514541835,51.753279586206,3,33.68659979188914,1,74.13086855297291,3.877184822707213 +83,79,55,25.14748006,83.34688193,5.565028635,98.66679427,banana,14.839741894819547,3,8.229272115930936,8.447847791888147,398.40373300846505,5.865943543700374,4,12.451451190437556,33.02576539189007,89.3992975284932,1,32.199225982384434,1,96.26877197099833,2.7865279125394364 +85,95,47,25.94019018,78.3422098,6.211833161,119.84797,banana,27.518603511688376,3,6.531625356232604,2.425486443119098,407.34526179314685,7.459031045433743,1,12.322578874135328,69.3040563424938,153.76497951273336,2,12.708021714042022,1,62.55921928632674,4.725838354795833 +109,79,45,27.66752761,79.68542782,6.490074429,108.66464,banana,15.314945120564765,1,10.389157922342278,1.1702085374226012,398.4342604765606,2.4866857239335314,3,17.098896523829143,35.15057049279255,65.83655439898922,2,41.17340335222284,2,89.90241919427632,2.3182148208546227 +100,76,45,25.56703012,75.94067692,5.590236025,102.7867717,banana,28.759852898029557,3,7.151245689840065,15.539511953758305,388.72366135996816,5.5022715245822615,5,16.459640461889858,93.74981890355659,145.9956272682502,1,23.712924510219572,1,35.785934383999965,3.137749568514842 +117,86,48,28.6956201,82.54195839,6.225225239,116.1616839,banana,15.90121818968138,3,8.10181375136421,18.493177746157844,382.4990238811133,9.418732172400192,2,17.009024862016155,16.464097955123457,142.8194488043616,3,32.97119291197221,3,20.171545617663277,3.887821955847501 +114,94,53,26.33544853,76.8532006,6.190757459,118.6858263,banana,23.49247275808871,3,6.042544369567898,10.411307597505461,399.3878558253665,1.0421816862274453,6,14.08824937603403,53.533726878739984,122.57117304040804,3,4.4529646545517725,2,10.03879291701294,2.8998168638567923 +110,78,50,25.93730186,78.89864446,5.915568968,98.21747528,banana,29.30871124087917,3,10.236894497591763,14.84169747055332,381.8517062073429,2.7209767054791123,6,19.405844805993553,46.255193140033846,189.58415085726557,2,7.528830971671247,1,17.976945599728676,4.6141049698722085 +94,70,48,25.13686519,84.88394407,6.195152442,91.46442491,banana,20.718189891761554,3,5.4039778673392345,4.439165022842795,419.028804761657,9.99243875952363,3,11.777634328395207,6.000846969202611,60.27905269154682,1,6.628054492140628,2,6.261926757082536,2.1307500858274655 +80,71,47,27.50527651,80.79783998,6.156373499,105.0776992,banana,29.88301411162114,2,9.155012521456666,15.213543306289587,370.8265777936193,8.60510468170322,2,5.960392000092261,86.77981987178508,170.48394930956465,1,20.251109855632322,1,84.30355687720149,1.6142163582606246 +114,79,51,26.21009246,82.34429458,6.313197204,112.0700033,banana,18.23010693960116,3,8.001007690797934,2.2972149025593036,405.8521747583883,2.7849340760724783,5,6.201233702386217,25.72708576135403,161.4683243095217,1,44.45908410887545,3,66.68340147618859,4.784206755930409 +88,78,45,29.10403455,79.19588629,6.324270089,92.07835761,banana,18.730584097929267,1,9.515198722166835,14.707289994626322,408.587384129495,9.159651491370374,1,9.679122862166155,85.42152823409276,67.18167385925389,1,46.21744778931283,2,64.8955726943374,1.2313007616153961 +112,73,48,29.2440638,77.32017166,5.707488987,90.66727868,banana,10.236418935932338,1,11.06332737021467,14.93096207982888,367.80701326673454,8.188351286579607,4,11.348316403484628,99.1353612383801,138.78509223918883,3,39.41541846436922,1,40.61837706274816,4.016287638450098 +117,76,47,25.56202173,77.38229006,6.119216009,93.10247183,banana,20.533254436551665,2,9.366870371875624,14.79246763838754,382.0212936727345,7.048745204077591,6,5.940711317773674,78.25916555892715,104.97742665561066,1,19.50374793132465,1,32.21590855996318,3.619073085882926 +111,87,48,26.3985515,81.36028902,5.571401169,98.16752001,banana,13.1171819705369,2,11.66973852884568,9.358851369750337,359.7621466453938,4.073651432951074,1,14.675004970702995,33.32442009921718,58.29583581205883,1,26.377474033021205,1,64.45809371881758,2.276637738972317 +89,83,47,28.09577643,77.79586769,5.63127166,109.5408614,banana,22.404137971812943,3,11.162966421969886,14.042622902248658,424.3621921058967,1.928670127110461,4,19.948179060053484,93.89194979994564,155.75930047011695,3,8.741243458732907,1,46.83597984205164,3.4428619953049786 +93,91,47,27.84767901,83.31110751,6.101241579,117.2878912,banana,27.55667555080722,2,9.65186092426331,6.228119792540808,360.79649312961635,9.652687966776272,3,11.196988522390381,60.12708950158849,69.02751453205184,1,9.720119190201581,2,37.89242378369736,4.870448705635736 +92,81,52,27.39341554,81.4654833,6.438137279,94.31102057,banana,19.94864011567039,1,8.497703567129708,6.65699537058927,381.46199320459,1.7404658169429401,1,8.17947994116069,73.39638903916502,120.36272247815921,2,27.729916219222627,3,96.15110465454121,4.450381961402927 +105,74,45,25.14517635,81.38204104,6.098369122,119.218154,banana,14.208862521680636,2,10.437493087241696,0.17753555078378946,393.4598923649828,3.600119913862779,6,11.18422785676651,36.49148742374373,70.36439034657317,1,4.038049027266882,1,97.68876639385662,3.5016801292888635 +102,71,48,28.65456263,79.28693687,5.695267822,102.4633775,banana,26.47893498336275,3,11.157198458354944,13.065167305790448,394.87538767194013,1.3401273681365597,2,15.625517541363694,47.62165825902667,188.24374717444857,2,40.58674828785608,3,13.157158428654192,1.767977344317866 +94,91,51,29.16093424,76.67484233,5.618094446,109.575944,banana,16.34199929243283,2,9.513756849931628,8.036963188455942,352.4826186466844,8.67295390140297,2,16.041854748545262,8.131603863538173,157.16962318939147,2,44.59125466288734,1,16.215571792178185,4.255247128476416 +116,71,47,27.57278064,82.0638878,6.435785799,91.34276507,banana,19.183170637023395,2,11.852868351514662,1.5064254716894188,390.93297037384815,8.68193854197115,6,7.607388011577837,14.708915813658418,136.55047740249117,2,7.342904567684727,3,60.95080721214987,4.955820722219015 +117,79,49,25.40909896,82.36208097,6.176644228,112.9794804,banana,28.66033763504648,3,10.11195673530127,14.839807680278717,437.9486793716481,7.240395766392195,2,7.927144611983932,48.36269347040309,52.61145259552561,2,5.854667226981047,1,36.12329597223955,2.45265234721584 +119,72,55,25.99069521,83.33983116,6.220643671,112.0777152,banana,24.81120905960601,2,10.473805003358208,0.17118767070442242,374.5830735355215,7.011804520697462,4,9.829334079120807,79.25996443420644,111.32606950802665,2,9.71499517998351,3,83.81268817415865,1.1991542277074045 +99,73,53,26.29039046,81.06003778,5.871702211,118.6730366,banana,12.082360233995129,3,9.685712911894925,0.8883129337517204,410.9714503350251,6.992504467903647,5,7.808888292184088,90.97542950929282,146.9055825803878,1,34.10184531493009,1,19.406438509940806,3.42009562238864 +91,84,52,29.14827211,78.71024836,6.390741836,117.536781,banana,28.197370529551716,3,8.609898406463024,1.3321022219409606,353.03623323103847,5.542140943988395,1,19.049409467205116,89.94779957026088,108.0102982610089,1,1.9967054616352842,2,79.00648719562187,1.9192940643557752 +80,90,47,26.59743595,79.35898915,6.21084479,107.3944717,banana,11.506309430984572,1,10.026817744631089,16.736676140716305,394.4843365063117,9.818073923291145,1,7.990033712528942,44.371007636262995,189.198798563846,2,0.5622011977599706,3,92.42429639209494,1.2423241106596983 +101,70,48,25.36059237,75.03193255,6.012696655,116.5531455,banana,15.530924907457651,2,9.19425107938234,2.822569042629799,427.37233879319604,1.9495754101805653,5,10.188852586051226,27.641614193290287,133.66784955192446,2,19.044473974260757,3,72.775425717233,3.3041794434877234 +108,89,53,29.12036889,80.18080728,5.908770059,112.3982055,banana,23.119344861006976,1,8.968669023837858,19.4954167796932,399.42807084178395,9.2362819167225,1,12.901696061055818,10.103690434488621,82.37363432397902,1,24.834122760153264,3,67.89817123217507,3.536399676784218 +100,80,52,27.53911354,77.25629897,6.049801781,110.3262123,banana,25.524463437340614,2,9.505243056648847,5.505448399810198,379.26589962721647,9.93485357605481,1,6.11973546419548,42.7029233793639,126.96855250845971,1,44.20209350674671,3,85.23222682577698,1.928106238332992 +109,91,53,29.66727337,83.51014178,6.010095853,110.2511102,banana,15.794511260178512,1,7.274274653398306,17.80426077963508,368.2652385737153,5.703315712445146,5,10.472275735486088,23.414214163657853,164.87351229655803,2,27.622077241005545,3,33.01715348727906,2.5218952267946086 +82,78,46,25.05802193,84.97323747,5.738678895,110.4408803,banana,25.338599941106793,1,11.716617080477041,2.2904373355138308,442.9742594775492,7.9696037959651,6,17.697653333120083,66.45150425361285,125.26639085731209,2,4.65409925376663,3,38.85078133130989,1.1616141766776957 +106,70,55,25.86824781,78.52399914,5.74055541,116.3019555,banana,22.320775102816395,2,11.572974056296742,2.217029291420216,372.5952800163124,5.737878708329387,2,11.82165254491427,54.04695648205616,66.63436130821667,1,12.05824978234838,1,15.965303558043864,3.528893445857993 +90,86,52,25.85036988,81.95580471,5.793260262,119.0856171,banana,14.378057037948288,3,11.045736040525444,16.382971250404317,363.50603963494933,9.757099236524025,3,11.390763729836642,96.80722519006736,188.4896958564439,1,44.2026588415261,1,3.8610374475257614,4.194544600126358 +83,95,50,26.51682337,77.79913575,5.50947065,108.8547508,banana,28.194907437768567,3,7.240389374499693,1.3032657141797244,387.7821770451483,9.44856613376591,6,6.545454307196434,43.10119809759644,144.91924297014918,2,9.534724133943529,3,27.266663957955462,1.9720388412483474 +119,90,48,28.66725136,79.59242542,5.986442306,118.2583441,banana,29.75237262700032,2,9.012164867989878,2.8610418396072324,362.0648898410088,1.5530257142303436,4,13.073921718709265,71.37334549172665,83.13118127617372,2,23.74337882510828,1,12.587226817870501,1.9430193718097173 +107,72,45,28.14938935,81.54448882,5.790768046,91.40508414,banana,25.708471739896826,3,11.176903458383743,9.275402661681799,423.0544034841603,7.091390424607422,3,15.91580939596522,73.02743129535219,191.49752176821633,1,11.407584172130548,2,27.265259587985636,1.1648958556263516 +116,81,55,26.42313317,83.6995044,5.915546415,95.12322062,banana,11.295995627972912,1,7.012549120672988,15.689063888491654,354.81605892779373,7.999526207771023,4,6.012772304163915,92.99352217404262,180.70842699158234,3,48.19911711989498,3,52.85291915538481,3.9393246604491825 +101,75,50,26.59386409,81.40740301,6.242528278,109.9825551,banana,24.416039912164425,1,7.721136906644496,2.8422454232988126,441.5562016554199,2.1360334567069508,5,16.791209497467776,67.76734051558391,94.91751510641296,2,17.773724028664127,2,56.77409542686293,3.9610903428076347 +93,81,50,27.71822477,76.57853189,6.036079266,102.2099836,banana,25.781208025339247,3,7.65146830848793,10.577300247022052,399.67973196761625,2.218286163969639,6,12.006145190471608,36.56025071736326,54.920615001959334,1,22.664930959984776,3,58.60590473033106,2.670371141188816 +95,75,45,28.98333432,82.95958244,5.829898502,109.022564,banana,21.272419181042864,2,11.989832995090515,7.004003638388669,367.162042665388,4.144689014658733,2,5.616544199771487,5.942648096659675,110.606988854816,1,42.57072930114567,1,60.972056547494645,4.751879562298598 +107,71,55,29.42017919,83.96754496,6.088064451,117.229079,banana,20.61753669055696,2,6.803161315606813,6.168546217532997,371.63252826367625,6.747449450188395,4,11.276854747987482,63.57346960176522,183.9641309870998,2,0.581889380204198,2,6.9353102569503555,3.3234091023459347 +83,94,47,27.39872329,81.10523402,6.469370954,112.1355384,banana,28.52796280991322,1,8.918789970563157,13.075805259333881,424.55211942120786,3.1621610283400194,3,15.675761511243179,56.634505921721654,142.2998374544171,3,26.55786463052841,1,9.015567455702156,3.963342736401389 +102,73,54,26.4020227,84.41007614,5.720726906,111.0162259,banana,17.87408834951183,3,11.796320158543242,8.823167204814848,380.06526238636144,3.860645576018089,4,11.065346387414639,46.96743547423195,94.89607315602036,3,0.9989367787162362,1,1.8055876548962901,2.619658795498201 +86,79,45,27.81251452,82.69285419,5.80766417,99.20961514,banana,18.857476570663636,3,9.866232992109666,9.282577545118096,429.34007672388157,7.276744767592439,2,14.038757827961106,35.929992422073965,54.34566149308729,2,40.595627058758346,3,33.86236931876241,4.290405313809866 +117,86,53,25.19640218,83.55829874,5.703381728,115.8586081,banana,28.325902456647423,1,7.199617397932129,6.013541915655642,407.0695846633312,9.626488030478281,2,6.16603146531418,29.08195864131342,75.60097227870389,2,10.439229790470478,1,35.134174584043954,3.4410718154910986 +111,79,53,28.31193338,75.77363772,6.165001278,119.695765,banana,12.565318535077452,3,9.114565337533465,18.21709815672202,401.88530786264056,3.5428000871505034,5,10.980951553966701,36.841457540188905,190.8154419389171,2,5.77573688980903,1,59.34889982136383,2.8477146321339237 +95,74,50,25.90113128,80.47152737,6.002481605,110.10323,banana,12.140492059410413,1,6.307752498523949,18.48856976330031,375.25517488682283,1.6912936629694861,4,11.127168251244033,50.40849678858177,103.64972568576056,1,45.605074365805535,2,39.81081939622676,3.480746028419332 +91,75,55,27.48612983,76.11239849,6.212369363,109.2768851,banana,29.175327495168705,2,8.582610836664788,9.045567356060776,388.03068494349077,6.712049293063654,4,13.870994410820844,97.47333162437359,108.27932768196311,1,44.70250201361512,2,91.99749314681281,1.103839794752655 +93,83,46,29.38254012,83.50423735,5.765308943,109.2486647,banana,13.227263136868348,3,7.789179650193862,4.871629044430499,410.3954382702209,3.7594865979379044,2,9.457105336915925,29.124322468889318,108.77805833149617,2,39.50050937925871,3,53.407385523523274,3.7516214734589264 +92,85,51,29.22118628,81.08183635,5.740764682,108.8616474,banana,26.61634477390673,3,9.615613982055766,2.0949554733417153,427.7203059917793,6.702638485310747,5,18.138112469407908,72.4047794926328,66.20559784957513,1,14.007089730189875,2,11.29779820542579,2.0772940102187754 +104,80,54,27.09062164,81.33506906,5.879119455,110.1331182,banana,23.64088673750455,3,5.213924341872948,12.548312640716432,435.86171801584385,9.345113453773006,2,9.235689660273529,48.053784295785285,118.1578343495433,2,31.799609604221207,3,36.188101877108714,3.8255524381274726 +103,72,51,26.12643374,81.81365007,6.099478745,104.4812858,banana,17.411591381515123,1,8.370201790706803,1.4533845395745915,425.47707050784663,6.620878441116255,2,9.750358665345214,30.038942423895097,50.479500982936706,3,1.2198301621376029,2,98.30492670992926,1.1492373572882335 +92,75,45,29.01207743,77.95192527,5.674403359,90.43495443,banana,16.63685599608202,2,9.163123097571123,8.52912015825506,367.7380147536476,1.7845633388422986,5,12.437864722089468,22.386006224317345,170.79019417937394,2,13.728279190087445,3,17.944154994015605,1.8298510525115126 +93,85,49,27.96799119,79.28625709,5.694243847,119.4765557,banana,27.631991593462327,1,5.4551748869713395,6.973011253361423,402.14785400186264,5.553103255923066,3,11.223180484940467,53.18450433172036,104.1995601673656,2,8.68508673196821,3,99.28882643431969,2.7125461425612185 +120,87,52,28.0764455,76.05522115,5.905494703,118.9923573,banana,18.369332701314157,1,10.090547545606887,11.786457985210959,449.88834668983736,9.441879465338177,1,12.896648792314545,19.245261551592428,98.77029643768734,2,10.247071453295604,2,42.483014926553174,1.5227509843265343 +108,72,46,25.16278237,84.97849241,6.110844721,90.94554618,banana,13.083566603279369,3,9.75783462713622,17.395255043352574,387.26377555280664,2.276128470741809,5,10.087101236041395,61.95347221370556,96.73329433689935,3,3.548702259054781,2,47.83334750817044,2.135296558993779 +105,88,54,25.78749808,84.51194224,6.020445317,114.2005455,banana,20.526831018972622,2,5.934902047903253,14.282760918873432,438.13424937769014,8.563093804014317,4,8.500440310339712,89.49947766095691,77.03014587497077,3,39.41433595341513,2,64.30920035138445,4.939304045818896 +98,79,50,25.34119774,84.47321314,6.435917308,91.06493353,banana,12.53884387484913,2,7.665334151977271,14.103438574597018,392.1614221201162,9.996560281828213,4,7.028795148293019,83.86635456389267,82.17341704399402,3,12.041754877495087,2,2.8990064081105715,1.797201398619395 +111,88,55,29.44795403,78.34971537,5.505393833,96.45042585,banana,21.184210718597853,3,5.343629340775273,17.565681931325898,368.73405109612725,5.022820291476323,6,11.273220756993787,29.498011187845375,106.72827996056486,2,27.08617082826313,1,0.7375168927973297,2.563942594054962 +97,74,45,26.47522633,78.51833782,5.677719902,113.1161095,banana,22.82054916657342,1,7.968238869826388,17.6920836748308,400.09446783476466,8.542185369526404,3,11.090204942669999,2.1615029794269436,181.5013610913242,2,29.57056886470391,3,24.756620855330546,1.6062707722305336 +95,82,48,27.39489579,83.31172003,5.719014989,92.78133617,banana,21.757814358798004,1,9.888220059166665,13.855376984633732,389.3526303422334,4.29936277317319,5,8.198105230559541,31.16284718684752,193.74679060695505,2,0.8411888947684965,1,85.73100066563661,4.615583926374544 +89,91,55,25.08347445,80.261731,6.275572298,94.32961456,banana,19.839832895229613,1,10.95310827300801,15.301841934099446,380.8661048749861,1.413298621923211,1,9.040540649593346,61.514992963163614,177.5520904594253,3,2.75202209842233,3,94.9865095839756,4.086551780180214 +89,85,55,26.6719835,76.48541655,6.275384607,91.73358569,banana,23.961921889132327,2,9.489963537005455,6.471867507081958,434.6634446272299,7.1326634437187115,5,14.230284349836808,14.554253532748984,169.99995393891058,3,3.0680263490406534,3,1.1244899065431468,1.8627753862482055 +118,88,51,25.44926208,79.49221962,6.201911642,100.6619171,banana,21.259516464292563,3,8.258927778693504,9.307068231972561,367.74966611312004,2.541833340080773,2,9.671773994604049,34.09854996877014,125.61092857081643,1,46.45291777348057,3,93.58146307852427,3.5239763202604344 +101,92,45,28.22776705,80.6430384,5.758054257,98.00403016,banana,23.41065976745307,2,7.941116070376768,14.759443307494266,405.3345338723524,4.495260445117842,6,6.619226210823543,84.5213502137492,137.68668492743717,2,12.271642594236582,1,71.0343959275815,3.13309647162218 +99,92,47,28.1279509,77.48247073,6.323933647,103.5045395,banana,25.78484558924632,3,7.100583657424489,13.345319577737413,421.4405726447123,8.14045261132587,3,19.0129528667178,69.29188020915988,160.6337250087647,3,19.5677348591201,2,0.2649266444043952,1.279394712114605 +82,77,46,28.9470467,82.1888998,5.901100841,95.83016448,banana,15.141719267175484,2,8.403345806940486,14.538392437524521,387.63327926352514,6.605467721607468,6,12.972721837493198,46.54558529644929,192.47553191750362,2,31.90734794186067,3,83.60219831303561,3.510788251320101 +90,86,55,27.96236771,84.15403614,5.644486582,97.55986676,banana,11.229387089027805,2,11.767631229377928,3.2862505131953057,448.33745283342625,1.4111762562105594,2,9.407386730695462,22.477539321637185,96.5890597807709,3,3.6734182046660346,3,97.15802378676287,4.306572500486806 +95,88,52,28.00316034,78.90085998,6.235461772,94.68180316,banana,22.668378327418573,2,11.566289830371755,19.14650522802172,394.67034402278824,5.418596982803845,4,11.492222560284997,96.05688832395303,166.0479016258637,2,38.93190560681779,1,72.45010605397746,1.1969757132453158 +104,73,46,29.1400919,80.1190228,6.28236237,90.45142867,banana,27.146732440317972,2,11.82325042889975,15.270772831802939,417.2027712335599,9.435198362659824,6,9.537251193425599,50.28482956993214,164.66024351651748,3,24.14714764734704,3,1.1467196158396864,2.2622379798867165 +102,73,52,27.9122104,83.36307683,6.356090905,90.24211529,banana,12.797878443899851,1,5.489952429084051,14.912085680072622,354.8722961835253,2.1423696708813216,2,11.80440390094221,2.757849056082684,192.38838170532205,2,43.75918987676439,2,21.28481707244533,4.114900234474259 +100,74,52,25.43480512,81.53977797,5.837258235,96.47800391,banana,19.80368362939287,2,8.194500831556658,3.747601866796051,444.85611113805203,2.2617419179173197,2,12.67432505012567,16.51228678371367,192.67623820392848,1,47.46367142309605,1,49.83559953630441,3.76524632387363 +94,89,48,28.55980972,84.51602322,5.653437902,111.0843029,banana,28.957814237953468,3,11.7251915784244,18.13070113574214,415.1980318039025,3.237881082393708,3,13.805482905712408,57.80412125490687,91.79399187121398,3,42.71540477169628,2,87.84249410859692,3.061635017089876 +99,70,46,26.59580783,82.99556744,5.727469947,100.5123341,banana,21.354007052882906,3,6.883608512588595,7.189377098668681,383.98045703867695,1.8638775523783686,6,6.768483954129709,43.30837564855556,53.21213083999676,2,20.554686929645392,2,69.78811099907699,1.4002508249433543 +112,87,48,27.19711623,77.3970629,6.200111068,99.46950465,banana,29.78439711820344,1,10.138441159300394,8.973130658678128,366.77118782956245,7.81983630892144,4,17.53474765202055,70.28689618451757,197.87012983697647,1,12.36274066943716,3,55.985479590730726,4.342805791973679 +117,82,45,25.29391516,79.29122198,5.614471478,105.4220251,banana,15.584397075121004,2,7.7683814421636175,5.925945166568489,410.8027948954184,8.628985493296632,1,18.49521224581808,32.3741023922442,64.5454667859827,2,36.52924646195382,3,92.07414843236037,3.0468218287785436 +96,86,51,29.90888522,76.98740841,6.257369799,91.99964712,banana,28.77461744817684,1,7.694074293233861,16.18337528886357,364.2448230618667,5.740019822028858,5,15.337410382062473,96.21100982977501,149.89228123683662,3,0.16255248710489445,1,52.862909334112054,4.538575530013052 +113,85,45,27.94972463,76.63713353,6.037430836,109.0921631,banana,26.975793549868218,3,11.754585700765285,8.563171198028305,355.354567254719,8.583765283529544,4,19.97037131235514,32.859956069984065,132.5031800088976,2,15.61699181865593,3,94.6260442009146,2.9685500696425047 +105,93,46,25.01018457,78.76260938,5.760457558,108.3690513,banana,25.985397521550126,3,10.000456677671941,13.105259390723221,370.0804597824554,5.510576775151264,3,5.284084232215516,55.37381233840232,86.52833522466042,3,31.861284017307863,1,75.41771130836273,3.890809864919323 +85,89,51,29.21144871,84.70189923,6.158164422,108.5501443,banana,14.482681972169132,1,9.00643611748248,13.475142208259552,427.49678494344823,9.904854957732908,5,9.221954869984524,93.27100319822716,92.48101099932857,2,16.91994897182972,2,9.702331721106184,3.254032519027928 +108,94,47,27.35911627,84.54625006,6.387431383,90.81250457,banana,20.64717442786585,3,7.020558825761817,16.95137583935374,357.4229701866576,8.955251938936522,1,13.301019792094548,78.83700795458655,135.84359887193932,2,42.027669326339925,3,65.27968116971184,2.3332934790040865 +92,81,52,28.0106804,76.52808057,5.891413895,103.7040783,banana,15.283342019033839,1,5.354606925462043,9.573667049338459,427.99106969296804,2.2204136583461875,4,17.30771392430109,54.49950529943253,73.01433469753412,1,3.5225913617418723,2,96.17759935923752,3.3488505695340103 +110,71,54,28.67208915,82.20793613,5.725418961,94.37987496,banana,28.399852346490746,2,9.68956205082103,15.302181367936393,401.8110884217699,7.152591915249997,1,11.841643148359648,34.4137840601472,149.27264479590212,2,13.982788693711768,2,28.777049067158956,3.6424803904330694 +82,75,55,27.34585147,78.4873835,6.281069505,92.15524332,banana,18.555143687744447,3,8.396095163056769,6.650122181188114,405.3878215362177,8.840153815512029,5,6.077036955993403,7.4695975725765145,89.33713674118,2,26.42727502804495,3,86.24720879780006,3.259270844797937 +117,81,53,29.50704598,78.20585613,5.507641778,98.12565829,banana,11.573271067419459,1,6.316263759235625,6.745786041018691,373.67248661154633,4.108874470165744,1,15.94349852495316,80.83270465944659,88.00600335674346,2,10.144253466513081,3,54.991252710229574,3.6466821886643705 +2,40,27,29.73770045,47.54885174,5.954626604,90.09586854,mango,15.153237764318618,2,5.212209318188972,19.665625811403377,368.04096505557686,5.1512683065150116,2,9.004362734187211,61.582773890612295,155.81085615975425,1,12.525378412232453,2,0.767139280584328,1.9146314184876605 +39,24,31,33.55695561,53.72979826,4.757114897,98.67527561,mango,29.815689630254557,2,11.364753654106135,1.7650666453897035,449.45908858087137,6.812937754161568,1,9.888753997742356,17.285648704535006,184.5842361921965,1,35.240113808606274,1,89.11822211662881,4.268309289185184 +21,26,27,27.00315545,47.67525434,5.699586972,95.85118326,mango,27.38565575983308,2,11.513797253414538,3.0529033328737354,406.72560208883715,7.934945897236288,1,15.44637788402324,74.15333531683783,80.259544837464,3,2.2154621439191557,2,35.52674600925372,1.8133938466114374 +25,22,25,33.56150184,45.53556603,5.977413803,95.70525913,mango,18.972881644166975,1,5.854913008923347,10.095067141907538,403.5341508162376,7.610028300297993,3,15.04184898341601,26.18640033130206,131.24198060086317,1,16.436602099372234,1,41.92005730187189,2.8940414695796854 +0,21,32,35.89855625,54.25964196,6.430139436,92.19721736,mango,11.928400658221928,1,5.444153275031569,19.27385801687268,430.87234497122734,7.815785446443787,6,16.940205000176988,19.95420713331374,106.1888904057198,1,24.312857522159398,1,45.9756141593372,4.15812646700556 +20,19,35,34.17719782,50.62161586,6.113935087,98.00687989,mango,14.675872562535845,2,6.407590703985772,18.934049561828317,390.43361696096053,6.204922288756691,1,18.959595045115574,90.31038422405858,131.1759390675589,2,48.872594649911164,3,73.47984397528661,4.3456984654009805 +19,21,34,30.01592643,53.19212381,5.074272692,97.72843182,mango,25.940271838540546,2,10.752462916382324,9.653928815730957,367.31616540398335,7.92593638663964,1,12.175154027214555,63.34061175156555,112.38628828487566,3,44.90203171290382,3,36.25938241071667,2.3447709905550567 +18,17,31,31.74592134,45.16127859,5.667507706,93.75441586,mango,29.05599984484948,1,9.35757378942182,3.738089200227057,358.5920500588174,6.495439897637864,1,19.61080994129024,32.48700928869857,69.79957890183655,3,24.705041719116632,1,44.459408421379244,3.5802305091234685 +11,36,33,35.99009679,52.22780489,5.978634285,95.3713484,mango,12.970253989218888,2,8.326114116889578,12.696458898696978,390.42591406964897,1.104039945335964,3,16.330701248100816,63.51933564693726,182.3865652721956,3,7.721813274953488,1,4.232264029026423,2.222033397867616 +30,28,30,31.86641378,52.19331595,5.064613314,98.46768642,mango,13.17561760067285,3,6.684201672713111,14.797411545199262,379.01845063874157,7.747109266976485,5,9.52571080168973,14.757121558613173,79.34951018095876,3,6.545228761665384,2,78.9684350362596,1.9683796362735189 +18,19,27,27.75518664,52.34605806,4.772385986,94.11213345,mango,24.47669178874236,2,5.6268569664119115,2.004341335515374,435.8932715711568,2.8572932528071178,4,15.435591701627809,49.440327608148,105.97283401491157,2,19.276190455874993,1,25.726948977491926,3.813251231788439 +23,23,27,34.72413192,51.4271781,5.161148592,97.31258083,mango,10.113773482446792,2,7.845182417496389,5.447219990243588,400.03850110171106,9.99999015034425,2,9.118073697250987,34.51815271586389,122.33706548769418,2,4.540941515511832,1,21.238926462321682,4.825716715321727 +37,30,34,27.53907547,53.63549533,6.797779227,99.35408185,mango,14.677124413861492,2,10.938622747814467,8.907052748897419,403.24886131334665,6.529916273747172,2,17.397240556245126,86.82938135676135,59.08923793896368,1,9.590604048772455,1,6.160901887878756,2.1192554524854708 +11,27,30,27.69637763,48.5622488,6.39474303,89.85646496,mango,28.466059968889667,2,7.84347332562691,10.173530822301103,360.72707602617623,3.7988338349649418,1,6.0190532867932784,18.296057935871712,160.46350289723225,1,30.59030288214008,1,61.62433145011802,4.157665978159493 +12,19,31,27.25373364,52.66319725,5.566704378,91.87312479,mango,12.116268860828583,2,6.264368752680308,19.71563690487492,432.75990544308524,5.011290348657217,4,14.574196346125166,11.43298573892315,84.52425171463749,2,7.583673027371796,3,65.84559781668001,3.795940641406826 +3,28,33,30.33723921,48.88704844,5.755049971,94.42850522,mango,10.56729998261652,1,9.880710218674665,13.569236952013902,427.7502731111899,2.2029134070542766,1,13.724713379591968,31.958326477339305,156.11533701288172,1,8.888352949390587,3,40.23011805392112,4.148243441893429 +37,38,32,31.85744939,45.53106268,5.417340525,91.55845821,mango,16.020352572280615,2,7.225482495858943,11.54415593938815,377.5939042471411,3.6388247127360316,1,7.4339173492871495,58.9345448944986,146.0603777102092,3,45.80708166856856,2,92.90913444205383,4.277737682887045 +26,37,30,35.39986338,49.45962621,6.166173834,97.41054011,mango,13.515716639695825,3,11.836348727367918,7.045002557058622,421.4086810437386,3.0362445457527447,1,14.818564820370828,52.02031271400146,51.30104384258071,1,39.12135290133456,1,42.2975115792381,4.364951347651021 +14,18,30,29.80747243,52.13797867,5.191265116,95.74606104,mango,15.83044909575043,1,11.368463265035466,16.090160768062482,408.738558659662,9.744896268021048,1,18.244901642232392,80.78380897006187,100.71169551443022,2,32.538754136434804,1,71.79797018716702,4.442525820495195 +40,16,35,34.16438906,54.16482251,4.954739564,98.33351125,mango,15.520950459783,2,9.684546547948212,4.076746117295156,412.30044246522164,7.87537917555961,6,16.313046278511273,16.380641315099275,188.34717771095097,1,18.586115288438563,2,71.08536366851826,4.5877147763989585 +4,20,25,28.93270187,47.94053996,5.664587011,99.9834242,mango,25.451892160205688,3,11.983307126403108,5.04660514797274,378.21123896501865,2.4887360222467283,6,17.71556008046845,84.7589980566184,150.70526580365572,3,8.15994377886135,2,31.686544928669804,2.4095926986189697 +36,25,33,27.98392787,53.33018851,5.548584852,99.61465679,mango,22.6375026764087,3,5.675524728754592,9.335734865940466,441.5292978790143,6.276372468195617,1,10.046483937919389,29.916675730556353,150.5704491376427,1,3.685060020334535,3,41.750438938962965,2.701899224912262 +30,17,31,31.20478173,54.49960506,6.804437106,94.62954663,mango,18.59303977657086,1,9.908640214566315,18.199889651731894,353.1559946853126,5.333513437293568,3,6.615898559690987,81.87523500271139,115.42511319876529,2,20.44369038769103,1,83.69461211291572,4.32765545321964 +28,37,28,32.13409675,50.52559148,6.097869767,98.63333684,mango,24.53922115912867,2,8.598033003701104,15.273448915025902,430.2249163710166,8.72156033183487,1,11.78792384147933,58.13427645016299,107.75428531092112,1,34.28090472513986,1,93.2132688904447,3.030399108938353 +38,15,30,28.91862016,48.13974548,5.075504537,97.01331604,mango,22.109309180516867,3,8.67149886720926,17.355488002534827,363.20737842101283,2.643244250981022,1,5.394924300745588,60.79984583724315,196.43105045416303,1,25.843225156749273,1,8.575457284665934,2.0853571162404188 +12,37,30,31.09779147,47.41196659,4.546466109,90.28624348,mango,28.97651413578902,1,7.724918699066491,6.696853833508181,353.0281566812762,4.3030334220000075,5,7.008972353893549,37.954475616088686,189.71883058697694,1,40.54873490258497,2,27.509956367105936,3.1563018592487007 +38,19,31,34.73823882,49.08864345,5.855119268,90.65022183,mango,25.96472700919952,2,9.974807388350705,3.619421649099097,426.9053221336568,6.234116023415303,2,17.60026116214953,77.24229548343284,83.20248043139586,2,41.60946104160847,3,79.09408939355859,4.310243370995706 +8,33,29,29.98080499,49.48613279,6.442393461,91.82271568,mango,17.349656767443335,3,5.682681685666704,12.868864381052763,425.50991899004424,2.6434928257237456,6,7.745704552052411,63.51622484773989,138.71353935936656,2,9.909025658725707,2,92.3879545210987,3.716507871560343 +15,27,28,33.80398664,46.12866113,4.507523551,90.82549241,mango,15.634928991882045,2,5.857405506695924,0.3482219813111631,384.14010390479535,4.81013847255665,3,12.171331132368586,25.992772521455354,184.89915236043353,3,19.64006540060329,1,61.48317822225782,3.6066284349943993 +34,16,25,30.07202564,50.96040505,6.10729559,92.09609766,mango,20.715076606168907,3,11.357735837490596,12.868205321779758,411.23663209182325,7.187704113845516,4,16.0936163429313,15.065150342049439,133.2872655603867,3,15.268934476082935,1,24.769198947326053,4.071821805484809 +11,36,31,27.92063282,51.77965917,6.47544932,100.2585673,mango,12.462926012823116,2,6.658189512274884,17.56580147150928,358.47897699870185,7.029844640440521,6,8.366348050685712,85.70078117389967,73.28630513993075,1,25.616852287575348,1,98.87719740523619,2.767887042693355 +33,29,34,31.40948821,49.21729127,6.832979509,92.99739415,mango,15.720683747476397,1,5.002760418077519,6.734116270592827,413.2649981165205,4.849597883631073,4,16.205759308565398,15.011668850394422,71.60545368971304,2,6.334963616786659,2,42.67253287673033,3.552331227102117 +12,31,26,35.7877738,51.94190321,5.395275719,100.2160615,mango,20.57995716136375,1,8.434107945156772,9.019836126445318,413.5096980570487,9.036068298521116,1,17.05799703948668,48.13013130051317,130.00948679223578,3,31.622306581858687,2,42.1011065529623,2.651631894860917 +12,34,28,33.36140093,45.02236377,6.13526938,98.81596545,mango,13.087241229945965,2,8.73759285456476,9.533559194922939,359.35127428046866,8.242842977536945,4,10.131359195181009,85.44757115664035,59.55049462480298,3,44.58669256375109,2,57.22237055131024,2.6488656729355924 +5,16,31,35.96054636,48.69677802,4.555688532,98.00644238,mango,19.69151395508908,2,8.56087782448124,10.55864284089678,410.5396306284175,4.815182598917837,6,16.691024386379986,86.7180603140091,64.07907127470942,1,7.9384539450655875,1,22.282451838926875,2.65123293767258 +1,30,29,28.33333307,51.39586505,6.434197756,91.67241761,mango,13.56465894595029,3,8.648098801104226,19.15640832241305,416.4930891188513,6.778424443821644,3,16.58759929561223,71.64338521415937,92.8247601563515,2,20.490560449062876,3,76.09175261963932,1.3312439024009537 +16,35,31,32.27652024,50.19368841,5.316875978,95.99487068,mango,28.945067869860168,3,8.603667156833987,0.2065913675047537,410.579476517807,8.719099950312973,5,8.22380143738097,4.353965185209818,104.300861506187,2,33.32488885527923,3,0.6804897443117186,2.238705243812902 +35,18,26,31.99490489,50.84881347,5.279388967,97.38741498,mango,26.14085037429558,2,10.223447364698938,14.246003416614561,370.7795764543419,3.6024062936239156,5,16.866580304626183,17.90766495707595,185.7570161701242,2,1.4059166181180305,1,23.696850723523543,1.4507015182404164 +4,40,26,27.58258929,48.56916221,6.720041791,95.8445641,mango,25.392462588985584,3,7.350324999196648,3.8775227141460866,365.5566899373565,2.6744081232528614,1,16.61475644619076,10.424025153246175,125.38439689725244,3,40.73679271746584,1,86.17157484495104,2.755672365283657 +9,29,34,29.38471637,45.88744691,5.72742254,100.8124659,mango,13.32250068553737,3,10.103222279475569,3.76417346751299,376.32439864922185,7.763583681282601,5,18.617006975803307,24.950538210796026,167.65971112990937,3,47.372567878371285,3,70.11381187351641,3.8551816526236893 +2,38,33,32.38697531,53.2328243,4.691396195,90.21633216,mango,25.686458540505363,2,8.538126184601378,8.39717821832998,447.50999155316197,1.9442781448170918,4,6.381454922352157,30.558984450101946,55.49886082300576,2,1.9501967408222243,1,51.95037583821941,3.1687195174705014 +26,32,32,30.91471455,49.92963856,6.810186079,90.14047759,mango,18.13417431721645,2,10.786094478493714,12.165794691961953,391.15060316805,1.7447592480205663,4,12.203680850445497,66.37333138441791,123.88094527863902,1,18.863521126370326,2,17.774300117131304,3.315351680940747 +34,38,31,35.37775595,45.58110023,6.454045329,97.41586402,mango,12.250950933904623,3,8.179810521372492,10.042163274067754,416.97929280665664,1.8812584659740805,4,9.464530003698798,17.18517852972632,183.00886716216237,2,46.34503740025691,1,27.415865802861006,2.3857106510065593 +5,32,33,32.32362177,52.5896771,5.842763773,93.36718816,mango,29.934365822330737,3,8.870927057371205,9.867214172330709,443.27913746807815,6.588507225997976,2,14.645431828199303,6.54027127711303,143.85779771982436,2,31.576169053017523,1,97.63330265842927,4.1886943352146515 +31,29,26,28.22373428,47.40519056,5.024124684,97.76832322,mango,14.383905838249806,3,11.857356630025311,14.32243823889185,375.2131891953593,5.98443737789419,4,14.062227107567086,67.13949267198196,128.76021626961207,3,24.087683208405085,1,17.644583059757903,4.288568117811943 +34,34,35,27.27433181,47.16808054,6.422710539,95.257992,mango,20.95036774058351,2,9.5993931578653,19.36173328136402,387.58164653246746,4.299912494997331,6,9.140673308295046,95.25066434331677,177.47223893405862,2,26.468298338451618,1,84.10268479462717,2.019432164539923 +36,19,32,27.10710832,50.70880979,4.94295037,92.37238878,mango,17.787394502864416,3,11.55443526159478,12.398022895394085,384.3138914253102,5.608619681497929,2,19.937954866060988,90.66065568342695,141.34673528238787,2,40.88574286201217,3,48.440863762199946,4.5669052833921056 +7,17,26,34.89226666,48.75613373,6.414526606,91.63074547,mango,17.71277351180151,1,5.325111925681399,3.4765201330276096,375.25961144540406,6.222592932148673,4,10.00877819645267,70.56134789190057,103.90233721084041,2,6.836079498180775,1,46.58566333826983,2.2673632055422135 +38,15,27,33.7462686,48.50387598,6.777788126,92.26439205,mango,20.50207888167642,1,8.842771318745514,15.283105507044304,432.9567580075293,1.964330878345144,1,5.617528391220695,10.444626282556158,53.8175605751648,2,37.49104231272193,3,61.88928289240951,2.253294618242708 +5,19,25,27.3511056,54.43945147,6.441328044,96.27792547,mango,25.354738784996258,2,5.378872392919558,7.550335106731218,350.93478516205545,7.192034852598315,1,14.395449267801938,60.41607759501204,166.32532771304403,3,34.68685021849828,2,7.938344944155851,2.0655764275603565 +37,36,26,32.89300162,52.61323969,4.650536197,94.49161372,mango,13.856105173905371,3,9.00089293536961,7.1649319543635155,391.51081011861373,5.017660610549315,4,9.046370893924117,51.84114767210608,175.51582967415646,1,48.35737911211418,1,56.69439997342949,4.739084650469469 +21,31,32,35.38598705,51.42664176,5.254532213,90.29643888,mango,18.209124155162264,2,5.151235616995752,8.067064566503827,394.6440813528534,6.820278828977285,1,8.004778168305311,66.28808257406162,178.80655691289454,1,42.17762789718611,3,35.38950014110663,3.570451315458836 +37,36,27,27.5529736,47.90859131,5.910634533,90.40332704,mango,10.783959311288562,3,6.699745250130075,15.849165893429504,380.7558763974026,5.694371470707981,4,15.45580205149215,95.34139445636002,89.17570909603396,3,16.09283478874064,2,96.91403704874678,1.597657975646214 +23,23,30,32.82141065,47.45553843,4.755273631,90.89173106,mango,26.92493135810683,3,10.608561658907384,10.840999312272505,352.91785640617684,9.38755779624438,3,9.912014991984902,14.02201019292596,151.02006458177112,1,30.72540176084994,2,67.14698558757331,3.2050186837580985 +36,26,26,30.17294105,51.0845903,6.814630246,95.23444287,mango,22.368035030771573,3,7.485103077973636,1.8876236472418007,374.9701394223415,3.7599811337865745,6,16.076365350984737,42.01228869217829,148.86119798149065,1,11.667972740686361,3,50.47434443163694,4.059820122907609 +24,33,35,29.26382931,54.82257868,5.342866119,100.7586226,mango,25.629737763625258,2,10.363996173786898,12.357516995339143,356.44205651926274,4.358217045629873,5,17.575738133858227,56.10345896699454,140.54430102643914,2,18.101419809140957,3,49.990453212098096,3.007052048504865 +26,18,30,32.06097197,51.08494181,6.336234624,96.59816497,mango,12.396979879056623,3,11.572200260236826,2.017425182023278,440.4510293030868,4.614830027656963,6,10.840625068097474,79.26453051363697,130.3430383523095,2,37.66371595569794,2,22.52152398937344,2.068097232452359 +22,17,26,28.69818144,47.71875722,4.754435025,99.642454,mango,12.392641104323507,3,6.620135706584731,13.280683971314723,361.0312890274221,1.213871076964352,6,14.121603062378982,93.6580665983726,125.44311060182989,2,18.88569997404496,2,74.86243095133553,4.366930601578961 +11,34,32,29.14305008,49.40983294,6.831706773,97.55155537,mango,15.677192302870697,1,10.214782987424837,10.981876870916414,404.2140329337621,6.103912941643716,6,8.35833418435517,67.61513092498616,109.44631453871511,3,10.523572644714124,1,5.370390900236,1.202665947913653 +29,35,28,28.3471611,53.53903102,6.967417766,90.40260445,mango,28.796271812581608,1,7.4238511034422885,0.27264951853978037,407.6803607004108,3.2594768340407105,4,8.76695637635554,18.685893804904897,88.94216406000888,1,47.34881905795667,1,90.42296835182543,1.9875899454332568 +22,28,26,27.67256197,45.41692012,4.947683034,92.84991507,mango,24.24630233128739,3,6.725833302874016,17.579487517673225,439.98271417578707,9.89933243961977,4,5.149003838578972,44.05801407595852,188.26723330830296,3,8.467878606426215,3,28.119868880864416,1.386674874248285 +23,24,32,28.1218093,46.16888595,5.630619901,93.30247448,mango,10.691681153943303,2,6.690508617925482,13.288392080359444,426.48022847668926,1.7695317893625044,5,7.417074608224206,35.46534555152574,56.271673645187235,1,22.353106525136635,2,49.092626084718326,3.5250369715113274 +1,35,34,30.79375683,46.69536813,6.27339822,92.21318555,mango,28.64394564828418,1,6.33286251838077,2.3240261152923014,435.19734750364245,4.066302104353758,2,11.192610803061935,43.29270424757342,153.4998724982956,2,31.21216102332393,3,7.7517293976692825,2.1896080365222717 +2,24,34,28.89409382,54.80750249,6.472774648,94.76322976,mango,11.349458943073508,2,11.745399852463754,3.176058113591038,380.15245491459143,7.765241570267472,3,13.928006543012541,93.66291853960091,73.0004976028303,3,11.200451297971037,3,14.796929505859612,2.839841894632002 +39,37,25,33.33024826,45.61143594,6.953246506,98.28583013,mango,15.886645977396885,1,6.644275926980583,14.954989160768895,376.7001385911211,5.616012667614688,2,16.05195423045227,22.171801215110033,61.97061959420441,1,33.9141151528893,3,67.97266195829839,1.283070302462654 +15,36,27,27.78912455,53.96886679,5.643710216,91.01152997,mango,12.000506154949411,1,7.011213930015463,9.382618565801458,371.6628651391069,2.880681776101782,1,15.832406686044985,34.27695444529169,199.7696085130453,2,22.193884303513407,1,23.27049112764985,2.160106767729868 +3,18,31,31.65333432,48.20662669,6.392313973,91.09745581,mango,12.045818537223123,2,11.178545757297279,14.462440173864788,382.93352075275385,7.031637447651523,2,8.012454116402086,41.62839697955012,130.46750481159933,3,4.009759647512462,1,64.45514156303686,1.8340978457647612 +8,38,32,29.75150773,46.73723302,4.981816523,91.405983,mango,23.596282614507775,1,5.528111671444128,14.391395119761876,362.6939736398739,9.631558731083732,5,12.255022840224704,28.01460829166904,198.18363124538593,2,24.43941160711069,3,95.35116889648104,3.026772447503423 +33,31,34,31.32995611,50.22287593,5.421265283,89.78216168,mango,27.565137396918484,2,8.30278609110212,9.275386768695682,361.1337793298358,2.540245652344579,3,19.950884386582302,11.422324882026846,120.4462779683401,2,25.848419874035176,2,19.892768163969865,1.426228460647605 +14,29,32,35.63627319,48.97047762,6.942520105,97.51952041,mango,21.83194359588598,1,10.099539527536088,10.494491600347423,394.6145673267954,3.5427864233049835,6,16.259497866333383,2.149918135654738,185.66288279919476,1,37.72937009882267,3,79.55329503836295,4.360022181084265 +18,20,26,31.66524687,51.98594645,5.435840509,89.98024312,mango,13.101788392720406,3,11.737134587248727,4.163657696742861,423.66604028625596,4.9436028646781,1,16.115192997071944,33.520070548634266,122.84109623346083,3,18.35268937869956,3,35.65121566725233,1.872980972186455 +9,21,32,32.26935342,53.56092806,5.870116071,95.94035356,mango,29.34023694955561,1,6.030596401140092,0.12617165400783392,401.7787957179044,9.010799635299275,4,16.549000456414138,85.96235364259128,99.92315719772444,1,11.200639102784733,2,51.71972963944078,1.2121467566126936 +20,30,27,27.81005614,51.59445462,4.74910393,95.89898581,mango,28.427872693199404,2,11.325878339399612,12.361024482338008,387.6850994951384,8.819933673958825,3,16.586404782094807,90.29632802073307,50.89515262658392,3,8.023073098335681,3,43.70538282726187,4.52424981664452 +9,38,25,34.58561471,50.34035336,5.497946899,100.3060719,mango,13.225138142555844,3,10.644924615799095,14.992618175558096,420.6717457655312,3.48250227039773,6,19.848270714519472,93.41659716333359,188.55757541290686,3,41.85548348154198,3,96.97134141668006,3.398294995421895 +26,24,34,31.27180992,52.23810152,6.811291098,89.74409017,mango,27.072188394799525,2,11.078800673122377,4.253194453774749,389.3775983014044,9.218622788424819,6,10.11356139261532,29.71142871510296,174.4805144828922,3,25.962916665797987,3,21.762090085331355,4.9971303231714455 +31,36,29,33.93679864,52.72170281,6.460542749,97.4611918,mango,14.497889151035778,3,7.156714110942934,15.822584004023305,444.28196955779237,8.973983699727167,5,18.48651814501219,36.08879753424412,149.05049501552014,3,7.504453034610559,2,83.3107456116617,2.597192247416237 +14,18,35,31.09154239,47.02058367,4.791146778,91.46664318,mango,13.009933763252793,3,7.008055952107394,6.825324326750925,427.9846736749895,8.951496714930467,5,7.132093255868785,99.25979517070807,127.76690214778165,1,40.76998824238202,3,53.88177029416863,4.219090307535458 +40,16,35,31.89356292,49.02450149,6.4841522,89.59371481,mango,19.078530082322473,3,6.429873161269791,16.45672905508927,410.91048353675785,3.915643952530767,3,13.610872072169691,60.755801590424774,108.40822058204476,1,34.20698385590063,2,3.8175072581735248,2.7747337794052633 +28,27,34,32.45465292,50.69693751,6.526654345,95.04871605,mango,20.16287908097148,1,10.51943841792308,5.0083344367687666,412.97465579847085,1.9562090314633784,3,8.39208592476675,17.626101614922206,156.43312607480055,2,13.19785228797688,1,84.7094727164004,3.4120437769192806 +0,17,30,35.47478322,47.97230503,6.279133738,97.79072474,mango,20.966199351371756,3,8.73626150546118,2.2243681803518722,421.36710829186137,6.917189860104865,3,15.920464020677722,35.787020459290005,154.10427469865033,2,18.921362462531775,3,18.54321193645839,3.7774351776285124 +1,29,29,27.32961444,49.30347234,6.052026047,93.53197359,mango,16.82892056563115,3,7.8698546869647465,6.960873360697624,368.48532778182687,3.300031143922392,4,16.226513427836117,15.74260257996899,79.52362590478475,3,8.179879735331468,3,93.01664336616857,4.858995155757364 +2,36,31,30.90225239,49.95955487,5.73171945,91.77522598,mango,29.618378360382103,2,5.046765418593381,3.1481351291771476,442.9058294380934,4.7237841787535615,4,10.22311781342259,35.15909687899709,122.6433651080593,1,3.5565101873162863,3,81.5265956073904,3.824298999963472 +12,27,26,29.09382275,45.5661059,5.32307197,96.23520043,mango,22.681703927230465,3,8.290872703009377,12.331380330798073,412.9487642581278,4.957044797430302,1,15.546588173227724,94.0276981304365,158.60746395196685,2,15.987959037343257,2,94.95185982204275,2.1844486791119597 +7,28,35,30.02086169,46.78393776,4.66910839,96.63721027,mango,11.55177333359918,1,9.679904877439435,15.596502100812696,368.8228913451877,9.576864099342774,4,16.147271561864763,6.7471469456108935,154.80948645235458,3,21.27736766887064,3,87.2346352307003,2.229721270632325 +0,36,26,34.13072188,51.25786185,5.101206389,96.38808001,mango,16.15189052261151,1,7.25953424692906,9.920568708081433,382.8159231677604,2.7212068862482557,5,19.722095286644254,66.70104494797708,180.78760129111865,2,3.5504123429854872,1,99.34118066774981,1.5630711405574722 +26,35,31,33.44619894,53.05980465,5.339556562,98.05089394,mango,15.595314711780246,1,11.326764999357927,5.93735316584608,397.0707794675381,4.697437676902995,1,5.36289892832429,77.20367964618457,195.22949557130661,1,48.32960540692936,2,62.414838764644365,2.8009902477021953 +27,21,30,35.3915464,52.48823147,5.061081874,91.22881052,mango,28.029991510909177,3,8.72817466692484,12.597886429563104,358.12652889009104,5.88693124031704,5,8.039213788205704,1.3091974620817237,162.0651469017243,3,35.42478704565121,2,61.076782313926884,4.550731876599774 +22,38,31,31.53356352,53.06009323,5.821106036,98.57025046,mango,14.17633378507037,1,9.11360221362467,13.959549894264601,380.271498672713,9.94856375278071,3,5.710802946199851,99.11254849946218,161.2887164678362,3,2.4970433078432497,2,78.00022009949915,1.4494084948270483 +22,18,31,30.7645515,47.93791463,5.956027059,90.38503469,mango,11.108241239335278,1,11.02561183816479,13.471491272099865,352.5506673658353,2.3532226533346066,2,6.5141276884932715,0.2912858162559595,120.93984847031098,1,33.60746779727431,1,17.972231549152806,2.8966156322014616 +28,23,28,30.01821337,50.0983181,5.676032581,96.08745082,mango,22.87051321085243,3,9.81295467959671,15.062966831906472,434.31631355710465,1.773515703439831,2,13.00262331986498,24.70196361464898,150.25666850684064,1,31.779453859214424,2,99.24323670010587,3.463862867438773 +7,31,27,31.32863689,47.59319575,6.524114355,94.67344737,mango,13.933111449479496,1,9.509737446104545,13.50214048892388,356.6473433745257,9.390835482314177,3,10.845098926039574,6.423778544903858,91.720155830778,3,5.448930070731761,2,79.24347323551095,4.176852022414662 +29,34,26,33.88004781,54.39416048,6.273953676,89.29147581,mango,13.912195781612864,1,11.645756294396623,4.297113802966452,425.22336166750404,8.772105460548104,4,16.38686156151737,28.657552445378766,113.78180183756137,3,17.76189656722772,2,6.940524022604977,2.6724517434338284 +8,37,33,28.07802689,54.9640534,6.128167757,97.45373619,mango,17.420923946520322,1,6.568366898894003,16.933095045417886,396.3807808019393,9.278308559951308,6,8.090025076880652,17.947884029348305,112.7979138370323,3,33.83038047280012,3,82.926380779971,4.761396150796193 +39,16,27,35.53845018,52.94641947,4.934964765,91.54560427,mango,25.762900270322305,3,10.56658423712734,7.21459167800667,432.14988663543227,2.686887062448016,6,6.451612430943162,37.33870430589318,61.55656742589002,1,14.536984994086954,1,30.696160483330814,1.1744323791366198 +40,24,25,28.70595247,50.44030129,5.445008416,95.8946444,mango,28.05750322081822,3,11.60368749941923,10.474803363211189,398.33723779343904,9.425137015279759,5,6.571528996003135,26.55143172198844,134.34555973132368,2,18.915887941914427,1,36.298620105842275,4.086661569220803 +19,38,26,31.48451729,48.77926304,4.525722333,93.17221967,mango,29.774124577850348,2,9.107896447562602,5.707683204412735,380.7282732590555,5.183610483230737,4,19.323871537416824,54.82872454357999,122.81308612722137,2,40.00637718200626,2,36.10912076151161,4.97807202978695 +21,21,30,27.69819273,51.41593238,5.403908328,100.7720705,mango,14.823679511169106,3,11.131652589519263,9.7682373179501,433.3556860232486,2.2439757377097695,1,18.23555096377595,74.04818755858318,104.48565759466295,2,35.65197034412009,1,50.613751787905215,2.585278920178274 +22,18,33,30.41235793,52.48100602,6.621623545,93.92375879,mango,29.738825614820385,2,10.760095519982187,1.0866939828767963,419.14858881599434,7.373538347854294,3,15.899700996915952,15.315046707125257,156.25669793594778,3,35.30472775278633,3,77.8334354761513,2.9281337181842244 +31,20,30,32.17752026,54.01352682,6.207495815,91.88766069,mango,11.84767260897906,1,10.705365928232103,13.18635940850684,436.59214595725905,6.770849112113359,2,12.487580336474934,46.67522708632483,109.68732037488228,2,20.48841240135067,2,7.889667498026975,4.167741661378349 +18,26,31,32.6112614,47.74916499,5.418475257,91.10190759,mango,11.754106736230822,2,5.949850465387486,9.317438789853917,371.1037330090304,2.3348694569842157,4,13.192733909653404,13.732498522496606,77.93494848418956,1,45.85009662995419,2,48.000197060480524,1.1752984743342179 +24,130,195,29.99677232,81.54156612,6.112305667,67.12534492,grapes,20.347876568579043,1,11.118319676588353,18.804217234246263,430.5065751819473,1.1951903792254988,1,5.207192961491851,1.4172862509735706,188.95414187482595,2,46.00423391571693,3,3.5891858265399534,2.480357982448325 +13,144,204,30.7280404,82.42614055,6.092241627,68.38135469,grapes,16.496335589793823,2,5.200766039757856,14.564567042235275,429.00086641136176,2.748629066419663,1,18.551789207009158,85.66566178329018,196.7066562998277,1,20.612399034562927,1,14.319492404577428,1.1372937832334022 +22,123,205,32.44577836,83.88504863,5.896343436,68.73932528,grapes,22.168864617679898,3,6.44745130183923,15.52408840876572,432.8964486545,6.439147802351083,4,9.204864450674886,18.967161850471616,172.9760334545142,2,25.753076756276805,3,82.09382635083415,4.9348035870217615 +36,125,196,37.46566825,80.65968681,6.15526103,66.83872293,grapes,15.067755794891626,3,8.672953659538402,17.963515574186715,433.27527271906007,3.710994479420271,2,11.911083066033001,23.516699780553463,98.11425778695009,2,30.73623793106441,2,12.043291217252383,4.450684179741213 +24,131,196,22.03296178,83.74372787,5.732453638,65.34440794,grapes,24.22334231610793,3,6.965756858001104,4.3775452778904,396.0249927295587,2.614872528659109,2,5.664810725997242,2.0503168584691367,161.62913034429772,3,40.114240806877895,1,3.2636751410366593,3.4566573718141176 +2,123,198,39.64851881,82.21079946,6.253034534,70.39906054,grapes,16.705183656458175,1,8.609614686742923,14.829832934461095,380.9832706739987,6.947465462344696,3,11.034044571614098,14.87964432498703,177.8189288474846,1,10.844951792061568,3,76.18186919797185,3.9330895588276267 +35,140,197,16.77557314,82.75241875,6.106190557,66.76285469,grapes,14.62008457354159,2,10.226271704852973,16.78093173452856,413.06353563390576,2.4221680193400434,2,6.301045368228517,5.2496333827894315,103.36562046480961,2,30.74265171952603,2,87.33094314474414,2.501525471502492 +11,122,195,12.14190714,83.56812483,5.647202395,69.63122027,grapes,29.38131835722012,2,11.012704844717478,11.682948956718244,396.5410116170759,2.499690418509302,5,12.016137738590952,96.71358283582275,179.76101771844552,2,45.20095956412633,1,52.34981248380552,4.626408984973151 +6,123,203,12.7567962,81.62497448,6.130310493,66.77844567,grapes,27.627078170920313,1,8.733748359262439,18.73195083123363,396.62192315098247,2.5499244971222854,2,14.646934063915197,83.25699665255941,108.12586318195407,2,0.2134059110439679,2,52.55218301059171,4.177917785740755 +17,134,204,39.04071989,80.18393287,6.499604931,73.88467027,grapes,26.451612556723482,3,10.537050799574882,3.3394496368297433,439.6479376702555,2.272237680830338,6,6.789511070434468,33.94450949227623,134.91927036530026,2,33.74398721820629,2,13.580744827190848,3.369424440814223 +25,130,197,39.70772192,82.68593454,5.554831977,74.91506217,grapes,17.51148645603562,3,10.432567898016591,10.682190462941588,427.0928199364056,4.099136697793677,6,9.516456719930924,53.920307484104576,86.06217357088326,3,38.61620495997322,1,13.361415980280666,4.2136706433330815 +27,145,205,9.467960445,82.29335466,5.800242694,66.02765219,grapes,15.602127425028119,3,10.516704489781905,5.806513986085527,434.2757034578024,2.5643969116508076,3,7.171846852972446,77.10854473591296,195.84908559672579,3,0.5671276899259725,2,48.64512826154168,4.012138945227582 +9,122,201,29.58748357,80.91934392,5.570290539,68.06417307,grapes,15.125943929784174,2,11.68043481447691,10.401621923211646,351.8697653908431,3.2481087816261347,1,19.521609034369746,30.790299768514405,53.28686107745293,3,28.286154706873194,3,91.29478551750368,4.524707521019794 +16,139,203,17.82803682,80.96093443,6.27564088,65.84748763,grapes,25.623572519850256,1,6.512361567994478,0.8541918193835185,448.0668048342269,9.707039205011139,6,5.152325825186536,56.64718722087716,92.74499870877445,3,39.736595880271196,3,30.98365250897006,3.055783516130485 +32,141,204,8.825674745,82.89753705,5.536645599,67.235765,grapes,26.256420146185043,3,5.104906777333026,13.874822911267069,388.8692328642522,6.917806231919146,4,6.279970139168548,22.14687499970892,93.753587828945,2,46.868420934854086,3,52.21090210868722,1.3779595528012223 +22,138,195,27.83487131,83.51444973,6.208196881,73.02882766,grapes,14.947970460124811,3,11.274838151798434,11.861619528005267,366.90603557749364,7.382527541925609,5,13.411083618687115,33.31182540659876,119.91495334365248,2,46.64284780125082,2,96.17276879008661,4.591922307636116 +31,144,202,11.02105378,80.55557235,5.870600622,68.23963161,grapes,24.18920529781756,1,9.660157539001244,2.653339247290085,354.34884892952886,4.252726370982826,5,16.841870992662543,49.603077830365926,178.5027974917523,2,48.55032992861984,3,32.64937918265678,2.8024966896256607 +3,136,205,17.5862944,80.84806564,6.334771461,71.4065452,grapes,24.824483905062962,3,9.168872999954157,16.220509171661,448.12834192875994,2.5386487139602956,6,8.500404640651968,81.10735266935973,124.91581943356606,3,11.722363792144058,3,46.05430237647562,1.5302207968741057 +28,122,197,19.89363946,82.73366439,5.856575335,69.66256816,grapes,21.913312119485408,2,6.598409695714603,13.960380820535976,446.62675043442425,7.506281103953085,6,5.008420799267784,40.0785001460237,139.84672168339898,3,22.08136439551593,3,32.87276082058441,2.4748215350067513 +4,136,204,29.93707596,81.77713468,5.898944282,65.52279323,grapes,12.661393448287384,1,6.272751682423989,11.963985908137785,359.9507870743375,7.854175747491036,2,14.10140289237103,56.88557046207761,77.83189227854476,2,15.654898788290266,2,67.28258087046977,2.299443904511152 +39,145,201,36.73126647,80.58931938,5.775600435,72.24230804,grapes,19.020993788030772,1,5.421545702504631,0.9734116200489251,380.64949897653395,6.646926257444949,4,16.651619383503267,78.06295098668106,185.70645377513867,2,22.714501318257486,1,20.599291587648494,2.626357729763256 +38,132,197,20.42094753,81.54185044,5.931101816,66.93065667,grapes,22.007679841140426,2,7.640145692674437,19.810095075940573,438.62336842778325,9.323514852028191,1,8.57292676286141,21.443359639874014,155.27413571499994,3,18.95569323669271,2,20.012340025124576,4.525111774606778 +36,133,198,25.51939719,83.98351748,6.2286454,69.17281221,grapes,20.301319031545216,3,5.832292298220323,3.509860634463393,439.1272109977449,5.3931871625711105,5,13.89284217112483,23.14839141840055,63.44562444176315,3,1.758717705490398,1,65.55490814145453,1.5645308365154524 +25,121,201,30.50734778,82.71775569,5.594240603,70.08200379,grapes,11.714645667151553,3,9.123124856143942,4.624838205736738,443.52367130345783,9.828679122892133,2,16.988446917781808,77.04206167833692,134.99564651810348,1,44.1853390008047,2,32.40568155287621,2.710896733466158 +15,125,199,18.4269936,80.55625868,5.569230319,69.75734306,grapes,11.402795765612453,3,5.415000927988154,9.058340850052307,415.38574223372353,4.77077048165404,3,13.273344850108584,1.6838691453127108,189.53399071782414,1,27.297270639100272,2,55.12182974738206,2.7444985388383274 +24,140,205,12.087022,83.59398734,5.93202852,68.66813363,grapes,25.638660365926764,2,5.2578812407126305,11.917367652633072,408.2985743728025,9.451805466081062,1,9.127122437919297,40.58738553751561,71.22365474829576,1,40.08171949453727,3,51.233287865809075,4.557430900694884 +13,132,203,23.60115364,82.48336987,6.423216506,73.23901752,grapes,13.037254641214304,2,7.729509764612611,0.1145656097540737,408.5641414297775,4.74545611181106,6,18.399378098605844,51.444853231293116,110.93762484310342,2,39.36563993875235,2,62.698759877454165,4.817665636372601 +5,126,197,12.80000387,81.20876367,6.417500829,67.10439401,grapes,11.388349185657002,1,5.305920704380046,16.811210883275766,439.44387905158175,9.035840212309639,2,7.314279476569183,52.1248138321187,139.6791809291173,2,33.10709879124295,1,79.6415303797557,2.4115284903008396 +30,120,200,38.06099482,82.24729637,6.234904253,65.70148216,grapes,23.02240762427472,2,9.245744833616143,14.568916633004918,391.46138345864927,3.4283825755704314,6,9.574439121427538,63.49071187479104,156.67289734318328,1,43.84831710583838,2,34.720313873210195,3.5638260464258344 +23,142,197,39.06555518,82.03812973,6.000573725,69.30772897,grapes,18.246309706544192,2,8.578819745337046,0.40823999015429546,405.7531416321484,3.0880087384165913,2,9.216907322548213,49.40356417964426,86.13634506083083,1,9.338311709091379,1,91.21933800695501,2.0780742547913538 +26,135,203,33.78372897,81.16314317,5.685102769,74.53557341,grapes,17.225275574266917,3,6.605099881988845,13.320447855768547,442.00542866104206,6.705408933945247,2,8.80017799766222,17.14126267177666,129.1971075875992,3,31.37664190874431,2,58.33450994690874,4.020210665323116 +7,126,203,16.76201707,82.00335557,5.662140095,73.28712806,grapes,27.672479056191204,3,9.752134275893944,6.720288958979676,374.17172069815047,8.321419633984124,3,6.763185328863748,20.504022605895788,102.10683839764002,2,7.437883396966666,1,61.51929802015703,3.5297373394078213 +32,139,198,35.89307536,82.66850729,6.358186848,66.53946559,grapes,18.778872334351057,3,8.317899432902026,17.983821619121127,362.58061077352676,4.563895742321762,6,14.020573036443768,71.56820710479495,176.27241468035012,1,22.233061009059256,1,80.20093987264364,1.4753751027696613 +9,141,202,21.01245395,81.17931863,6.119495295,66.38448261,grapes,10.984919948676247,2,11.083892588240836,1.125775254890482,372.48744177347766,3.2882216470282795,6,13.706144121932423,43.67954866817977,53.91278689911475,3,31.750821620064272,1,78.04498392158962,3.957470130811709 +20,142,196,10.89875873,80.01639435,6.207600783,68.69420397,grapes,19.484099304103143,1,9.024742549931304,4.154619297336506,433.11640596546465,3.7319837735489836,2,16.074808875384193,96.62295868778337,135.7033850373049,2,1.9091230143778948,1,7.559196663725054,2.3186550757608044 +32,129,201,16.36251869,83.00471609,6.48754639,71.55665483,grapes,22.330633948436315,1,11.70067218917779,12.046701925160114,378.476363972584,1.091073418920009,6,15.376265974755675,86.8606693262562,116.16818244352449,2,46.15320406827636,2,42.93254505523705,3.9679546221730253 +3,134,199,20.28370163,81.32235739,5.81717753,71.06611222,grapes,23.788590159525025,1,11.45198497060995,1.4266814185660315,419.2313734357698,9.578323320405744,5,11.658965244237876,41.78807874899145,61.0359298873191,3,48.68084965178243,2,61.11486392126577,3.5208659698284177 +38,138,204,25.11108456,83.25447587,6.325480034,73.01026829,grapes,17.313184729706432,3,7.572945985229856,15.400613783094379,384.6826211859224,7.533527815604889,1,16.695660038546617,42.58665791642373,176.09719037371823,1,0.7349057764742373,1,16.30386230606242,3.964191081353774 +14,131,198,33.4641162,83.86742974,5.562790949,67.92204319,grapes,22.70373097856308,2,11.277909607976035,2.4007763796901793,377.1429785845643,9.446816909302862,1,12.13034679726718,49.4791239109173,189.46950408972734,3,0.6069531234466463,3,83.68687215004906,4.266625817700119 +20,122,204,11.7976469,80.86325389,6.487369687,65.06962486,grapes,15.056620653688066,3,9.771058773683471,6.496278990519164,404.968810079458,1.4483502508596757,2,11.938677056889574,21.933664870228053,171.91035872623956,3,33.70341307436038,1,30.40902279063885,3.770295642292007 +40,126,201,11.36300891,80.03100049,6.116982944,71.18289431,grapes,26.26395315703626,1,10.731730065116317,1.8646909789620914,368.8449687551723,8.610899420893688,1,5.455793986187772,24.048948421460658,182.87266604173357,3,19.081183316420002,1,40.14600762842054,4.3970182868502 +36,128,204,25.23542319,80.68700527,5.695792761,67.03840888,grapes,29.228987087502926,1,7.953328621750047,4.621229657619066,381.44692381605773,8.731969679665411,1,10.37043144179264,15.378505447760027,91.08008173258065,2,49.069225835521664,2,98.49465493055452,4.881125265537981 +11,132,197,15.99050693,81.23966573,5.734317007,74.40198861,grapes,26.830585221895113,3,11.727966320700226,0.09929434017231786,425.99981805100714,3.080484486764996,3,14.205326164246427,50.93582002553754,64.91055509058538,2,28.95764336455795,3,33.106916494267914,2.5022625085058503 +0,137,195,22.4359017,80.18612085,6.329499832,65.3973168,grapes,10.185962454520539,3,6.374898764779577,8.234247249946765,361.47191909719635,4.17996434585633,3,5.736138588625649,12.141341159915864,81.17487461980579,1,49.2775838078464,2,70.49484985269643,2.3781468933416363 +19,123,200,34.76086052,81.03544763,6.167013532,65.70430027,grapes,24.559204486515533,3,9.89029672035699,1.8977599192064143,413.4592038269283,1.645727398080517,5,13.489433419563987,28.264055495345175,183.75570065479465,3,25.51844230794774,2,13.368062567845218,3.319320942042521 +31,136,197,31.11047251,83.34010951,5.653776058,71.43001582,grapes,10.33026980115875,1,8.116303555856828,11.629396371478407,363.5635071504812,2.387687198984419,2,12.25142140852855,14.915508739120797,105.01724328003681,2,31.288994332857545,2,50.20628143766799,4.11328167218692 +4,134,200,28.57828803,80.95628959,5.840256272,73.34232097,grapes,28.71880532328536,2,9.931031915769513,2.131959700114805,350.4774122433321,5.428885364568997,3,12.267307865618344,68.49194070366936,195.25998606350882,1,49.52861751346978,1,51.177923385594845,1.2812860235102677 +39,139,201,41.18664903,81.01783402,5.539980812,68.68895899,grapes,12.65252837641646,1,11.692619282397615,3.9596626389020617,434.84787777127445,7.637829565488534,4,11.720919652266238,2.286993835886464,68.74730964413166,2,5.1317343542841956,2,54.632539929657,1.7357885358308747 +8,127,196,27.02766138,83.17093908,5.833302165,70.95666003,grapes,11.248818123109782,1,10.17051940229179,12.827381171417883,369.38433672624257,6.207797843565917,5,6.438303369448963,39.717974239698414,51.86176667044706,1,13.612639807133359,2,87.82006368671365,4.349723002199182 +39,138,203,21.19339319,82.33098331,6.399433771,74.62834921,grapes,24.271407784450503,2,5.080092933657373,9.543228115771548,365.73895896160366,9.10404923306351,4,5.726596281939259,17.790862069907163,82.42792699356917,1,31.65539161490829,3,3.0431481833970153,2.565814177465535 +32,120,204,10.38004759,83.44518113,6.138958698,67.3917379,grapes,11.099956508299684,3,7.9911382124626655,2.287884836566678,409.07160095456607,4.460667301565087,5,12.015068643615361,70.85331880982056,71.06269247215529,1,38.31314220670094,2,70.52943983971296,4.631603877553342 +12,142,203,31.3115978,82.56407013,5.972850838,65.01095312,grapes,26.56929895755497,3,10.980043622142704,12.495750389982078,398.15881312992946,6.493816722144114,4,19.578171160783512,97.21760002197806,55.09117389599558,2,9.823946162797098,2,30.491511073989397,1.4438023915730076 +8,133,195,20.46657776,80.97598029,6.456079585,71.29813872,grapes,28.33684740894858,1,7.62774362566026,4.0387081180481825,362.99164315482886,7.832591130601337,1,10.376261475908896,20.175634961200505,166.9901505026583,1,0.7892802288247291,2,82.53756676856794,3.6960584939447156 +8,139,199,29.36947679,81.53996362,6.336426667,66.13442813,grapes,24.105264289258383,2,10.653899759801124,13.02778097617447,393.5403994006502,5.455774175167231,1,17.500781133178577,37.773403804841756,166.265297640808,2,5.2127422792745906,3,60.718001062256974,4.503221067785754 +21,134,202,10.72302459,80.02130636,6.425419926,65.2982112,grapes,11.56502365464743,1,5.081437222586004,13.296203541737396,407.5691674130522,3.5424640914603938,6,11.62723947182315,17.85880150818482,193.351124606585,1,45.25056352245708,1,92.80570600008662,1.738080666061014 +40,140,195,14.97846952,80.49979873,6.294395676,71.63437433,grapes,23.21829705289648,1,8.29443769788704,7.32954846737444,388.0038027841902,7.060451134365646,4,10.929934293081658,23.072847383351693,151.39212603584306,2,33.323860896664655,2,51.32731796105507,2.7928653957554683 +39,127,202,15.3246651,81.67215994,6.477768039,71.60102999,grapes,22.6514815456319,3,11.195810784011428,5.1655282620879595,355.80987678243946,4.555670903851331,2,11.796360538237415,88.84990126848457,55.82584035452409,3,26.810838523526158,2,39.7445881297884,1.2938186809577523 +19,120,195,18.73932187,81.12109244,5.931538447,73.55807954,grapes,10.27446969830681,3,6.245597846771007,13.972374523228428,406.93069399013314,4.285511109385019,5,18.020010918914934,48.852104201198365,50.60435372748454,3,39.91705440640124,2,57.15418309356334,3.9277137118464323 +21,139,201,19.3642553,83.36094029,5.980598579,67.15094741,grapes,18.835335118011184,2,5.179628807913219,1.7613430841446243,397.14776289454727,3.846894053487636,5,19.6610653675873,98.93733455295266,147.47028618002685,3,30.26722019556963,3,67.78593240432608,1.5102837087411713 +17,136,195,41.20733624,81.61051026,6.389783283,65.90227462,grapes,24.93024936779359,2,8.363752566912844,8.758555086627437,400.03080966392116,9.515966979663878,5,16.368138782154375,88.60967432713794,138.26944034209043,3,19.06392630277657,1,96.79660512842855,4.974642460911946 +33,139,203,33.34214482,82.51034633,5.693287415,70.68098614,grapes,26.886004500313,1,6.712088787345264,0.20182962784198066,379.10446851795376,2.1311217136044744,5,12.244586002443643,27.107031342348662,124.45694859700934,3,32.44756555297195,2,66.22154842160171,3.736296204920579 +22,133,201,23.81995682,80.12211649,6.00299607,67.2739864,grapes,21.902404473279574,2,7.53242229993586,11.940202221103718,444.33164289383694,9.691940499614397,4,6.173710232033101,17.608914066608406,68.52576369985275,2,3.3247710602555216,3,72.25644961041607,3.2941308384950023 +32,130,196,40.66012294,81.24995984,6.372959542,74.03030056,grapes,16.96396246781107,3,6.787804831538917,12.46516202598829,391.0944074197805,5.992741677348981,3,11.077030190903617,28.09357312305769,187.90639629883864,1,44.20485697002182,2,91.99722468734572,2.8711388334906895 +37,135,205,11.82768186,80.2827185,5.510924849,74.10225057,grapes,16.780189640221295,3,8.666381051328106,0.41188088463707473,360.45634044873464,1.240644815077002,3,13.612812385287597,41.87171720324131,175.5998131314331,3,42.72026171451932,3,33.19890873317745,1.3927675643409123 +15,140,195,13.28504331,83.54193816,5.69945282,65.80006004,grapes,26.202623566777866,2,7.02492089451006,11.441332825410509,429.4283456208621,8.453876561532468,4,19.5874026788381,81.23989918055776,173.89997621003664,1,12.224867392471817,2,84.304563020987,1.1087376252745944 +39,132,196,35.83089092,83.32560104,5.778594403,73.67984885,grapes,28.57864596383116,1,7.734796919962665,5.891193279250933,361.54073734314903,5.8992461137105945,5,14.002928625779308,19.651000375933926,69.88824642409867,2,16.429004244922307,3,35.83890517436713,4.140179768147533 +40,121,199,26.18159716,81.03886263,6.315586313,66.05911698,grapes,18.163780013724548,2,7.921977942093024,5.612752355685182,416.45832486846814,9.322689539791348,5,12.843356663121389,72.83861793260836,163.1320381593003,2,3.6157983874724686,2,64.66001930270714,4.613628182929984 +40,132,202,24.57558351,80.70695797,5.971813006,69.706113,grapes,11.329050465502027,1,11.331680324553712,6.840271860012413,364.4129481860432,5.623231912882943,3,12.471518969288198,34.168194443420965,70.88071344821763,2,8.24992608246513,2,63.2677275393563,2.979517587232062 +29,142,203,29.67229086,83.71498986,5.891195653,66.48490371,grapes,20.76540487878467,2,5.045936530403499,8.286503152503077,391.2862239602749,9.591580621272378,6,11.706696582783232,64.45872913094712,155.47061887207352,3,47.91104239210136,1,7.996957150392459,4.831072607796554 +32,121,199,39.37102553,81.25353895,6.129812716,74.08101744,grapes,10.424958203965502,1,8.03649054811858,0.6047804158632575,400.182834061015,4.14333578131997,4,9.081836627964282,76.09384661279304,183.9097310415527,3,33.62969456103901,1,86.58539132781354,4.9622661494218665 +6,140,205,17.66558428,82.92903419,6.313085601,69.8671263,grapes,17.48389157563706,2,9.318153222448208,19.895476954971233,447.0562512025941,9.821364391687453,5,13.460000946223554,86.36946413396342,165.2600118877042,1,27.329844229914258,2,26.950453295944822,2.1618876272649734 +8,120,196,24.06679352,82.66396666,6.053662544,69.81855775,grapes,25.150931295049116,1,11.091368105418073,5.243991850902381,396.0690072292885,7.218775284520737,1,16.53161218719352,32.664062115052,124.78550780130074,3,35.454439554881056,2,53.31230406022406,3.1876566835827096 +34,133,202,15.31413469,80.09711412,5.804799142,74.82144653,grapes,12.70042217067424,2,7.6195920897077,15.579327217018976,415.3582206190508,1.647927074289674,4,11.766448768038675,57.60959618195541,62.12600925332787,1,23.879019001306727,2,29.55542338810243,4.080248962505253 +35,135,199,21.77466746,80.54942557,6.400719746,69.39630398,grapes,22.03301080540591,1,9.037526174777032,9.811619154845095,431.3343482040615,8.259090493343354,3,18.813378474671214,6.26872119566837,173.64843125729539,1,17.21935439478923,2,93.2405561435586,2.0898611572916845 +16,145,199,26.91624843,80.76838926,5.953966361,69.30927185,grapes,27.55411739335504,2,8.784610860931343,8.244979185675911,408.43138902255106,4.9936764112314656,4,6.320576889865194,98.38571614897114,50.38834294155715,2,37.26368861089372,1,27.60390218574853,4.772762438213158 +8,136,201,41.65602996,82.22118237,5.609255992,74.19664838,grapes,24.439522388269978,2,8.993371822317629,5.693625193255554,350.11290992847375,9.479764546641643,4,17.32232250193894,20.119656300413357,52.964845601130236,3,8.651581224393556,1,95.33081102689184,3.1837057272320513 +25,129,195,17.98667801,81.17712085,5.777271492,72.37127689,grapes,13.372461857545963,3,7.228892156071948,9.79788707305094,409.2209212871498,4.151222912077351,1,12.622412381288225,7.036277248575329,78.76704627423113,2,33.12339265627998,1,57.764446852955565,4.230184152402504 +16,130,201,29.12033769,82.79092939,5.682395429,68.8503047,grapes,29.48187169979946,3,9.236200125773456,10.847404002881937,443.30097904129707,6.762449449647546,1,16.39860447162043,58.63846535812134,137.89720380913064,3,7.641042673429855,1,78.83405081873346,4.092488990882343 +39,129,203,34.38922481,83.18392806,5.863996687,71.03001556,grapes,17.306128817610425,1,11.049779931289248,5.885028683478135,395.4139855587744,5.615047121775154,6,9.833618826213746,43.40807993652115,168.5822199141312,2,46.5926126086011,2,69.6844159768035,3.4939416343831726 +38,135,203,41.36106301,82.79782954,6.444373116,69.92107482,grapes,10.070927185903988,3,5.833881648624252,15.703769383785426,374.821216089624,2.1099155779397702,1,6.169632647772331,12.08803969571186,156.3039986054594,1,6.547539853987533,2,30.569372587717503,2.562292583436751 +33,120,205,35.12158265,82.26890793,5.550832178,69.71518491,grapes,24.796835911314528,2,5.2637964663816765,17.604783978881635,410.74045347777417,3.617901302616278,5,19.088685389896888,3.6638480186973688,191.2988880236906,3,30.666320033995987,3,69.24332695563264,1.5651268195707222 +35,125,204,19.6491772,80.15215777,6.107741788,73.69529586,grapes,14.012818343701646,1,10.77974296358557,13.11953204166409,429.7709641817895,4.256516055037412,1,10.63103895230889,1.8598415610821206,111.76343857772153,1,37.85574606900667,2,62.5210412576062,1.5607359373227516 +1,132,200,16.27852801,82.94270065,5.620745638,66.57462809,grapes,17.186455084550587,2,5.55954068390443,6.707939734563597,441.0045955654954,3.862613625906442,3,10.224465999460472,52.38547729055938,176.2615802492927,3,47.00922795224749,1,51.745418093696394,2.164823646160302 +39,140,203,21.11903604,80.63399198,6.349875906,69.27779761,grapes,19.64843562238713,2,5.596897115103051,10.999640955823729,419.8522368055458,7.659274996695711,1,17.879488669073105,41.025313255597574,61.958756044626675,1,45.84986719889096,1,57.74241995332762,2.6931522440398594 +28,145,202,19.2077707,82.9042841,6.484323189,66.83113717,grapes,11.879864319508734,2,6.259675705099257,6.29392912091302,431.9056557301724,3.926291112489052,2,7.351637762348728,49.63497688724969,94.74695707138625,3,24.70709638130189,3,36.435427767906106,4.044358560196688 +6,128,200,25.96308415,82.57813624,5.838748311,70.31782647,grapes,19.183086182483958,3,11.100008005571947,7.813251814291895,448.18996397936934,1.326478905904735,4,7.093745902612716,66.27066113443594,158.25459172539072,3,20.671769665987988,2,13.626203298986727,2.3356513425782808 +6,139,199,25.67385024,81.6212135,6.29099842,74.10919422,grapes,12.951263015127374,3,9.536327630198851,7.643248929356821,372.0350798310628,8.930473392105004,6,16.28513743787471,86.18134725793892,165.86231913387093,3,48.78535470874981,1,31.974293817378474,3.35951045640167 +29,122,196,41.94865736,81.15595212,5.638328481,73.06862952,grapes,24.30883058797581,3,7.415010139677875,3.8245200311142513,362.7021965388745,5.15464513342752,1,18.876190677800132,6.39297450833769,183.01863299393767,3,6.448507977338696,3,27.158414043274405,3.8722001414603433 +37,144,197,11.18994268,80.8084305,6.415555956,66.34234944,grapes,16.086148154120384,1,6.164542047138376,7.6537148666786265,414.04683183621717,3.484522073842711,4,6.775538323136537,71.09435719648845,165.63728297599863,3,10.403198830652277,3,41.86682700887363,1.099840373463767 +38,120,197,17.5438296,82.94703302,6.323722572,73.77063744,grapes,20.805396719280253,3,8.966821066317959,4.149904103132645,385.2810958091512,4.992222664186304,5,6.304349792346059,96.64266870038215,124.77367930959284,1,28.632466204273367,1,52.73731819830797,4.043878163201191 +38,141,198,13.05809741,80.28297993,5.757009965,70.75633584,grapes,20.22125963985225,2,7.240012660812663,4.136524286274234,382.40785009593696,5.5736279176939805,4,18.794372687325545,53.60714098048548,62.550071797767664,2,13.697315885434774,1,36.315315005422576,4.124759069558038 +14,121,203,9.724457611,83.74765639,6.158689406,74.46411148,grapes,23.497584478464198,1,10.793036903829783,19.833046897012842,405.18074459836185,2.9796519112185127,5,8.16905154879666,26.505881165945567,140.33962000081482,1,44.31777615804294,1,83.82672727648202,3.936387707998109 +6,125,204,27.92004934,82.93262435,5.733539807,69.92092839,grapes,14.387860606427491,2,11.208778285904451,15.674615951677612,388.28358771928123,4.190594180836482,4,13.595601397148558,95.42073627275711,163.4620666185328,1,29.1162936611071,3,67.56984387976344,3.2469504360013137 +32,138,197,9.535585543,80.73112694,5.908724337,69.44115171,grapes,22.546024199984057,3,7.019064616380854,19.9199826107443,354.70671601584985,8.6460300788549,5,6.314869927370623,69.10892334790981,129.61421446574238,3,33.20521376519993,1,17.717004761098686,2.309931003395532 +11,124,204,13.42988625,80.06633966,6.361141107,71.40043037,grapes,13.617083412408157,3,9.313099372779437,8.312904500697812,366.80669690745003,9.218050237827853,6,5.78881231493316,3.259690907889623,107.02740662403299,2,26.966940784868417,3,74.15920454286797,2.9874734314720066 +23,138,200,9.851242629,80.22631717,5.96537863,68.42802444,grapes,20.659253035003697,1,11.5931261600174,5.980267101423409,391.01785525778945,1.278220659232014,5,8.906972853132384,57.90837285963063,89.64248019859518,3,7.600589668165108,3,59.84541813807584,3.4226851937934066 +40,143,201,24.97256132,82.72828653,6.476757723,66.70016285,grapes,25.79774742042684,1,8.493290316580499,18.63055699687838,350.8865050150381,4.790904205690039,4,16.19371250199152,43.61904284821132,97.97807742822201,3,43.41065092752765,1,23.406272164870934,4.608156963527788 +6,142,202,27.23708304,82.94573346,6.224542938,70.42508897,grapes,29.968617484794752,3,10.555048085917589,12.362612093517553,382.68602826550466,2.5818124859462848,4,15.980734383080032,29.77239252138999,69.26566539091439,2,39.95702601380175,3,36.5853936743491,3.041394154068765 +37,124,195,18.70679077,83.4795292,6.209928251,66.5964488,grapes,10.585878969377427,1,7.0327914305877,9.991407035457643,448.83098416211885,2.509372031536241,4,14.250548651478022,78.63178056653503,117.51270786951746,3,8.575076617643147,2,20.510575841670498,3.1768969706708146 +35,134,204,9.949929082,82.55138983,5.841138354,66.00817551,grapes,13.620887594116882,1,8.60428261016723,19.87887962194215,356.2586011043857,1.9347815994509894,2,7.034460884221852,83.61378677730937,180.59551886170507,1,19.0046980683044,3,59.308094178353485,2.584635792261708 +119,25,51,26.47330219,80.92254421,6.283818329,53.65742581,watermelon,15.08057143272421,1,8.027283110067453,14.96982191349759,414.22599700584624,1.344140889128729,6,14.523878245032087,22.364049487884763,161.28518180011326,2,18.5315340878856,3,33.02319610973146,2.08080145911473 +119,19,55,25.18780042,83.44621709,6.818261383,46.87420883,watermelon,24.427121599577383,3,10.101718272355503,8.170348736431368,360.6510451474165,3.2532672950310437,6,13.908764762614,13.710835045428416,168.44399298878298,3,46.65051039730494,3,7.165788120495553,2.990449002019592 +105,30,50,25.29954705,81.77527562,6.37620108,57.04147057,watermelon,27.84067676259624,2,7.579268771166818,8.261587575971678,431.5347959508772,5.734523032741364,3,12.16716920366361,69.97663780354762,92.77118102875127,3,37.01421915647401,3,71.28015676851679,4.313107985607182 +114,8,50,24.74631269,88.30866319,6.581587932,57.95826144,watermelon,21.722580321044305,2,5.772596623130273,19.181056439686063,372.86409118748287,2.921236687365518,3,9.81437262168684,58.11745110331942,79.80731930159591,1,26.132573421882316,2,93.85424063745279,4.577691526922323 +93,22,52,26.58740671,81.32563243,6.932739726,41.87540028,watermelon,21.602344170023034,3,7.231309663750425,14.066736680175262,352.18779364242596,3.449338253540801,1,14.41379909393983,14.856152630265996,134.883311430444,1,1.3028932416761008,2,8.119585085852432,2.5212047514151625 +80,26,55,24.53442564,88.989272,6.140099215,49.11618732,watermelon,25.12719040421041,1,9.361535531115965,6.5253267734110665,398.51599018239193,1.9214001561947591,3,14.633184455423498,94.15471968944591,100.88329992197345,3,3.4643239345160337,2,52.15214520820989,2.6302694441372974 +85,27,45,26.0713757,88.7285657,6.467095849,57.79652846,watermelon,23.830453659792354,3,7.850181282368382,17.03606065616996,379.2687004119619,8.289147416341635,5,6.606951476626887,10.193711226253644,122.21610525792188,1,41.96179721013296,3,70.62777844750879,2.516279113240213 +85,22,53,25.96534238,89.77076659,6.849471704,59.46338556,watermelon,25.314460823671297,2,5.020614937208482,19.921116566851307,405.480224870631,1.9685751029663738,1,5.408733667279752,64.51343391119512,134.5862112896906,1,37.53404159645669,1,90.11547922911858,2.834910565512706 +82,22,45,26.22338015,85.34866045,6.512196212,54.60159289,watermelon,12.911916496105801,3,7.590671245732807,5.333382637164979,439.9962068503652,5.432401316970583,4,6.552262385631607,38.83775059989665,138.68233548882418,1,19.98851493752226,1,71.51384950893846,1.9139448763013949 +118,13,54,24.41311871,89.81574032,6.039584629,44.07843475,watermelon,29.529001844156213,3,7.3296592598999855,8.942970321241727,353.59423286169925,3.6708588720555113,1,12.27144021304244,41.09571996044994,154.20445015719667,2,23.17977192080432,3,6.7463574982636665,2.5287892400488894 +83,25,53,26.49195283,80.04678201,6.057697106,57.72799157,watermelon,18.944428139050196,3,9.774312500937185,14.754508319120168,428.5706645376688,9.684923032163953,5,10.225017442884901,5.248957745208537,104.29173227112187,2,2.4685429446858254,3,7.128698132757338,1.1587747091123592 +86,15,47,24.04355803,84.18406764,6.423898762,53.78929956,watermelon,26.621022154819595,1,8.46397942792327,2.7842410020183106,402.6465894153068,2.7648125900723537,6,8.978576670397572,51.53359901794784,146.74711575342664,2,30.9770688800553,2,12.65170991141672,3.2300132965088393 +101,10,47,25.5421695,83.31883376,6.936997681,57.57343233,watermelon,21.088463278597516,1,7.2436679849194725,9.836275473120022,388.97486235110614,1.1802005614026998,2,5.958716120730546,92.03340144377707,165.42229088622418,3,18.918563426276386,2,94.24733640915572,1.0307288884131989 +119,9,50,26.74550678,83.9195902,6.251286661,40.794305,watermelon,23.37443777472351,3,5.645978957694785,9.490062564344397,449.9843895315361,5.373932968490006,5,8.68954288117213,64.18630578773784,133.96539496961844,2,28.757362333676646,3,68.08270480000127,4.481753214768252 +104,17,46,25.7131428,80.22972777,6.190015912,43.08961827,watermelon,26.077866311517653,2,5.142872354139186,4.335479443790462,365.70973679730776,5.402968184419812,1,11.562772955033987,6.670529897760669,90.76985197987518,3,35.1229878236265,1,75.52895985858721,2.4022547261433167 +95,12,51,25.76484262,84.1726996,6.681606702,44.22066914,watermelon,21.471103993490335,2,5.207475486756047,10.752781667389634,430.5980311977573,5.573705502859079,6,10.154983810553922,89.03383333816734,70.37777676276994,1,25.450505891309806,2,40.46983396646628,3.2675031838461117 +102,14,52,26.79489868,89.64815231,6.51075991,57.74091817,watermelon,27.732078309628573,1,10.919997622339245,17.638958675979968,449.1074899046401,9.133904216544106,1,11.19473276779887,70.83869482143355,53.49338153291603,2,16.728700411814003,3,32.02400037667946,2.7659930888304234 +109,21,55,24.9004602,89.73524177,6.770278088,57.44942094,watermelon,24.415408129749835,3,5.701971325520555,3.76419869938166,427.01190768716117,9.712236543802783,5,17.14764564566257,17.049870293077852,108.49218094873126,2,34.16245591304561,3,9.07253393126699,2.055680592974894 +81,18,50,26.80750629,88.22874955,6.429788073,58.79889057,watermelon,24.026217182541146,3,8.706022169270602,7.244390807975396,423.08737097714845,2.1421991179394952,3,18.44552470032312,68.94815590491868,75.41706822969478,2,42.21221946783022,1,1.4787739190078697,2.051408429628882 +103,17,51,25.11189154,80.02621335,6.209888345,44.20656987,watermelon,16.80893605080174,3,7.639288518986463,5.475668322543932,391.64639733328755,9.667935901249521,4,17.12373830028952,47.17226506077539,79.3620932170511,1,33.148646162923455,2,46.06870132818089,3.8854460868794183 +105,14,50,26.2148837,87.6883982,6.419052193,59.65590798,watermelon,12.322833033084805,3,6.310621094426313,0.7466364312138007,408.4361893285713,1.9999768199040557,3,13.959919723447143,24.99863137888757,168.2725521814658,1,32.32397981360736,3,19.85793442532813,2.1665464442055984 +97,8,52,24.9103226,86.97190046,6.237861736,49.48575692,watermelon,22.4253864099393,1,6.950997185612286,13.733534472239695,391.2269331822146,4.335658249427974,3,5.204122428064978,63.808698603534175,60.86100923451223,1,32.332715711310684,3,60.75139543404752,2.860168528309965 +120,19,49,25.79448878,84.26830701,6.762471629,56.45229202,watermelon,23.61077070207454,3,7.996035175997341,14.987432022263704,387.497385907457,1.4383381802874893,5,19.394208196704923,67.75625932943122,180.18104003405432,1,4.580019100852178,3,41.413424237486005,3.8402128503947788 +95,16,55,25.26931156,87.55055105,6.612847999,40.12650421,watermelon,23.471283449453573,3,9.637369140486765,12.639977059334768,361.9440731272848,6.419037989361491,1,19.988474247705994,87.9054365835576,82.77037833553902,3,1.1960148013349603,1,55.593093607253486,2.710534773314128 +83,29,52,25.76402693,87.5931128,6.704688865,46.05122728,watermelon,17.722522156618602,1,7.62611267881841,15.082652897015977,419.3680018395526,4.166017930877185,4,5.1687481476634805,3.5145788671858247,185.04592558926538,3,15.1288871740911,3,86.27092572521985,1.613740632798009 +83,9,45,25.85483596,89.13163965,6.049609892,46.85176955,watermelon,16.788202945031202,3,9.798498651724604,2.2293186222111006,404.0991999914397,7.913993375279732,6,9.173622273050656,55.802304448060916,169.48296366236775,1,20.568616286704277,2,79.6159480812863,1.1712700919686259 +91,21,50,24.33528185,81.44030363,6.762030215,48.32113628,watermelon,17.437142014543618,1,6.1332952013513085,10.754195814955164,372.15779816203764,2.9407617210917616,3,9.645266632531841,31.006760561550305,60.13001053688698,1,24.41144064512491,1,25.76694512183425,3.4697449659086868 +116,5,54,25.37601283,80.99313508,6.65398725,57.23028471,watermelon,18.08431334081383,1,7.478626433876013,7.120855149425889,394.6132745782334,9.088694148318243,5,9.57679867302351,47.20326685878504,85.98139515218116,1,48.87996712188805,3,75.40355689788854,4.036225568416358 +112,28,54,24.86094646,85.05318563,6.738030547,55.29563514,watermelon,26.378982381060098,3,8.837854311390593,6.130790942492805,446.20107397970855,7.612079525024436,6,8.209344107060833,60.02285262140341,158.94908165448794,3,32.01692963569515,2,37.72303388268431,4.141245066202392 +88,29,51,24.71885473,88.94568335,6.095689937,48.45978627,watermelon,20.02658110656892,3,9.375478185503688,10.31176461654772,411.3653356542639,7.185595573512529,4,12.97604300804192,51.9748868593766,190.4045172655387,3,25.74300943332897,3,92.3988818325925,2.491773188485261 +118,15,45,24.21495706,84.20576992,6.538006356,48.01138482,watermelon,10.684028294412435,3,6.503033105305132,13.665384978017872,437.6056437205155,6.633508415048315,4,15.109093501908598,84.8225720100479,168.11809095514155,2,33.77242930806395,2,17.22963896482319,4.892097740497659 +92,21,48,25.81692236,82.043255,6.377427122,54.82963379,watermelon,19.777561369107946,3,6.3803952441017,16.717455024668855,432.2430466489977,3.494754945175141,4,8.603747389969428,5.923853939317436,74.06432056298912,1,36.43048610228846,2,31.133732070343477,4.819795995897783 +106,14,45,24.47018505,84.16390229,6.417011754,57.26773002,watermelon,19.587695356431166,2,11.654855160601707,10.24880869099337,415.74433066274327,6.805410542117871,1,15.233409063567308,11.139014315828788,75.54402366252853,2,25.919640881137735,3,54.80059981316967,3.681131488679155 +99,5,47,24.13078816,84.84494575,6.649086972,51.19470197,watermelon,25.428318334979572,1,5.864720192248127,4.731745813560176,427.7455354843901,3.3939878174708404,4,16.157157632536098,49.633489716995236,54.50146691508304,3,25.674493898409068,3,55.71600753587609,4.301176939369433 +98,8,51,26.1793464,86.52258079,6.25933595,49.43050977,watermelon,17.270074219558595,1,11.500941093358136,1.0608125948554603,403.21406079412543,5.049645774695224,3,16.93758030418249,7.729502671158606,130.61575212717233,3,39.341566916834076,1,1.710626706526741,4.473416232825268 +108,22,46,26.17668721,86.7295205,6.121168559,53.33484977,watermelon,27.553654543993705,1,5.430485821136482,18.4304325157788,378.38024489632215,4.470175467745232,3,5.80415584514136,50.954655050655674,70.4126351253287,2,4.438089265648642,3,39.05490499986185,2.3915488472516775 +119,7,55,26.03867719,84.6378378,6.031424482,44.3993381,watermelon,13.602948701916556,2,6.548405730132392,2.490517211820258,430.19643799013625,1.2383314070899143,6,18.08993932820846,42.3022071478197,131.3403914582902,3,18.16590755983043,3,84.55119574112895,1.1621800300106275 +117,27,48,26.53259325,82.39053979,6.835268184,54.30660782,watermelon,21.670056742486587,1,10.572155435224595,11.948188854348306,368.94892490957113,2.48576548243765,5,7.253150936184174,96.72595721642679,120.03831653295755,3,42.041712062899116,1,36.12767272603692,2.6172716606592603 +109,10,53,26.81938687,87.8274604,6.551750306,46.06193778,watermelon,21.819016082558637,3,6.486787135067582,5.189578579072713,418.3526924344442,4.15075438326129,4,6.034845277014539,6.90391056486842,129.85806102189594,2,1.5148232427392105,1,53.45224736158283,2.2056479392053614 +80,16,46,25.50405534,81.40297428,6.940236218,48.47833278,watermelon,19.82711147429742,3,6.474150788233125,15.37127343694738,417.4402110706576,1.0051495621635338,3,8.65068974196134,63.152497040236156,145.29000112141526,1,28.93126609277478,1,53.10149382619578,4.448753553084629 +100,18,52,26.20234499,80.38266489,6.87606733,56.47941847,watermelon,22.03001272495083,2,5.95191968486971,19.766122630415225,426.36759663902035,7.622463789576448,4,18.1589728019415,32.0896582550581,197.963726336477,1,28.69589149938217,1,81.04188873872057,1.3724827914472493 +91,7,53,25.13735887,89.28272716,6.457216535,43.52897517,watermelon,19.73592291960547,2,5.5748843117412274,17.448799536726796,361.8487569629669,1.5913425776662184,4,12.387624878198856,69.26865247717579,95.4069949603555,1,22.688458702061876,3,2.444295827576648,2.253836222281105 +86,6,53,25.92030221,83.47202566,6.921847888,42.10681516,watermelon,21.294355699453444,2,7.146288319698157,12.095184585370934,408.1378838657522,5.59472945569696,1,19.73861644960262,67.31367040793347,189.74731309561963,2,16.820273107879324,2,48.24028685070747,4.284445664724507 +107,5,52,26.6634609,89.98405233,6.881425746,57.40847165,watermelon,10.190471188400569,3,11.553719159703515,14.242953260840785,364.2333579662939,8.698565438470917,5,5.861486327553763,45.39981946361213,55.91068111215151,2,38.12713076702009,1,77.75469251395249,4.586737639892364 +103,16,49,24.06731461,81.64075303,6.915717008,51.75212401,watermelon,12.08634046970216,3,9.46828544850203,15.236036334518936,390.5829439425116,9.772643021617917,2,9.28066180407899,9.051909332210705,114.72785694205729,3,3.282094809061742,3,40.48316956220591,2.935638517295788 +101,20,48,24.6774157,82.75411437,6.206247494,57.05709413,watermelon,15.748256057965616,3,7.280528551190927,5.731066747164862,368.52827894814675,1.2007251791652886,5,15.28405738825433,57.26784609985194,90.97254205712233,1,2.093686147517598,2,44.23354577504169,4.763020017880784 +85,25,47,26.11440416,87.64081095,6.29542477,58.48160844,watermelon,23.720900193686727,3,6.525308812483548,16.426194003955853,405.43714661959547,7.319309478237056,3,11.208862013084783,58.58890101322156,185.41246774885494,1,42.93869185152409,3,14.72689880925695,2.2429487551550404 +84,7,51,26.81530456,87.65694462,6.399669044,55.74073582,watermelon,17.74959335643334,2,7.912803561907983,13.111270364581225,427.27850779914024,2.3380663010928076,4,12.126471591694337,69.17302025929001,67.1883483586093,2,21.58758804009032,1,55.72761443633092,3.7901315367148247 +102,28,54,25.15623099,80.27525115,6.862157042,55.49541453,watermelon,27.387744840904876,3,11.276675440303718,17.339316236520553,438.78689552818605,2.0848187480226836,2,15.68029500501044,72.42898304601184,154.60800348787296,3,34.12604122373445,2,37.200254639651256,4.885916153854552 +98,25,52,25.2801372,83.15393658,6.224066378,49.29456609,watermelon,20.072348046242126,3,9.737051827834474,16.448304752417265,406.1159997356679,3.8310898651628174,5,16.992765556353387,92.44517999016082,164.39093274730334,1,49.64350034301409,2,41.3368359142505,4.208976823296947 +97,25,50,26.22005978,80.90127035,6.093814669,49.08553937,watermelon,22.212875054130173,2,5.837542488732717,0.4146394415575516,430.10724127717276,5.555114912936913,2,16.160844188264264,42.960918764381795,80.5101650011845,2,40.170699093341995,2,57.73288490828834,1.1590580255399567 +90,16,45,24.92093261,80.61750795,6.291540278,50.55710813,watermelon,13.337424516681004,2,8.947332632249584,8.682777538989686,417.4226994891049,4.8046897641749,6,10.751337218333912,18.299802458198698,179.83212132332608,2,20.436254122476488,2,91.04808455988116,4.104947135405833 +95,12,46,26.21667586,81.01009354,6.32281728,54.65423596,watermelon,10.048511242971323,1,6.53822273874278,7.956761916813788,415.1446613771631,7.253802562223287,2,18.018385618937355,74.84494790251048,80.5088989621827,2,40.923278092226326,1,35.01971544401445,4.167206911219157 +82,23,49,26.81383586,87.21986949,6.873283991,51.70497792,watermelon,13.800710213243498,3,6.476821898514528,11.274558870972113,404.01856522110864,4.874897410088316,5,11.939953025453915,6.142642961606592,72.2049318713776,3,46.0158749531781,1,10.508562689161383,2.2289030560828893 +82,25,51,24.31334971,87.47409052,6.074209622,48.11248366,watermelon,12.29387392612072,2,5.712298604428518,11.536306544655075,423.2452181368836,5.710090453010161,5,5.738641538938908,0.06644432345032092,65.1315118703144,3,26.7606117073044,3,66.62767715411536,1.1201956677669633 +110,28,46,24.29105004,88.04541346,6.49889585,51.26046418,watermelon,26.31443477323952,2,8.00074455750525,14.256582480513467,434.8112383432898,4.163335207559089,6,16.85498145265477,14.118301723722471,156.54657477867374,3,25.65840572536876,1,88.96734455733026,4.790804024393259 +118,21,51,24.42998931,86.33904774,6.678805092,48.58241822,watermelon,12.834479681896692,2,11.313343614017715,17.9403890755909,423.43681297517674,6.5294532361024515,3,7.051313156336479,47.80262286402876,143.2130660647947,3,30.858656859575394,1,15.392180006377387,2.3759144136563686 +120,20,45,25.66576039,88.6984228,6.114128685,54.22722466,watermelon,27.30344959382624,2,11.073789201023295,15.520626780024738,381.0052523262655,6.142731061098618,1,13.042482716021372,96.1159317583696,97.37811807988484,3,38.835365210997644,3,77.13132561057198,1.1928217202070766 +91,7,52,25.07803672,83.46230461,6.405054243,56.39962921,watermelon,12.398640117408124,1,10.868039920191869,17.316863646839987,419.68002775931654,6.613028074714131,4,6.798963137732608,86.24981188262514,188.82342966762658,1,6.203623365148419,1,80.63467938126219,3.8325183197345942 +81,6,55,24.88910524,85.87059083,6.110142735,51.70699144,watermelon,21.294322434435742,2,8.541762329278692,3.345948683432396,447.99979124252167,9.561597632178763,5,19.566732282565603,20.575425742507413,78.0782379946382,3,25.020298471375135,1,76.4176335920374,2.0013702609116875 +101,13,54,25.42900869,82.91481799,6.828982708,56.34144589,watermelon,11.409634274015048,2,7.949673367535174,13.515732472298206,434.7398421246137,8.113062130141323,3,12.364499052661383,48.57451161045505,129.65755934236736,3,14.694442430631904,1,31.139881487668664,2.821541789728983 +101,17,55,24.37118217,87.1269128,6.451499764,44.63907691,watermelon,29.177952179205207,1,7.873385160358042,3.721520440778603,413.5288749015404,9.73736174094327,2,5.042549847623982,99.90956902704858,62.32583125548061,3,31.91402728172364,2,70.0089266016951,1.9240125087287057 +111,6,53,26.4930645,88.59143088,6.313512999,46.06382209,watermelon,18.86515605418325,2,11.434095037989884,16.041027350622354,357.5630440509389,5.606794550628744,2,12.938604470340767,9.556894506909575,156.39372900121202,3,48.106760424043784,3,52.87620342915918,1.4551256546789522 +107,10,49,25.83202912,89.00481725,6.755192025,45.24690619,watermelon,23.978503476769937,2,8.744513600914036,14.169677783622785,445.0462371426793,1.4063224928628262,3,15.801012853654711,92.25161079977725,168.959420004097,1,32.2988607252579,3,91.40107787698409,4.3797881987250165 +115,11,46,24.41592661,89.39655519,6.623167177,40.32161859,watermelon,20.602902571116914,2,7.408692340048937,16.22589474104741,444.7601765462798,9.453363503341997,3,14.33997001599311,24.93515968943929,85.04684201197321,3,29.167810804111433,2,61.18234545534843,2.9460337372661196 +84,25,52,24.37190239,81.2514818,6.12532356,44.20899581,watermelon,22.47252299621921,3,6.755166592863305,5.956562847637288,419.2256190345847,3.503217213705943,5,5.688996591851321,51.64439112731623,135.9048160568414,1,28.493075299237635,1,47.73851841020611,2.987962756334236 +120,7,47,24.24782473,83.03687902,6.653867608,54.7657624,watermelon,20.05074631056686,1,10.28000278529727,12.077638943522174,378.46355056540256,6.370378060303753,6,7.2290353258230295,79.06473715248815,166.57147874658767,3,7.477924741677466,2,14.159863605161371,3.8483408298452617 +91,12,46,24.64458469,85.49938185,6.343942518,48.31219031,watermelon,13.53062773626413,3,5.8303837928129045,0.7098699023932831,417.38301293565974,9.486207446286786,1,9.064528156230402,26.318748624131207,106.4400803141661,3,49.7999586356249,2,66.73347194425764,4.0594028089210346 +89,22,52,24.89681131,86.10782926,6.217300786,53.14626213,watermelon,22.643901148041618,1,9.612596751880464,6.388531666081776,361.7039794451841,4.977231571593711,5,8.895256210191278,91.20196030473168,160.55476192728185,2,41.12208769203116,2,90.9405498944429,1.0698493506472357 +113,19,46,25.41864024,81.12122989,6.286387658,49.52320689,watermelon,27.825121776386094,3,11.475216516480904,18.4941250256491,446.4255462848621,2.133570547520861,2,10.847954209252826,11.084107717682256,142.36409571467544,3,43.94957927534123,2,40.589980885731556,4.417890694735801 +97,22,50,26.26028739,86.14585891,6.7698938,58.97878791,watermelon,21.37637203655423,2,10.736385791433529,8.965556400384022,444.92165570702525,6.020797971168634,4,16.462154801354853,80.26119635477889,169.23831107647976,3,2.3188803402420524,3,92.06230002312633,4.079271858894021 +117,30,50,24.90123934,87.20772913,6.744966312,46.59207341,watermelon,24.934578042843963,1,9.34925222271637,2.647109255066933,443.41596364018307,1.4873165413688927,5,18.8363769204078,3.3935594910512457,189.12140155181967,3,44.85890711855053,2,99.51596314989966,2.399515218799641 +90,14,52,24.84740848,89.20454622,6.391858432,59.67927244,watermelon,15.7807815033009,2,5.1626474783856535,14.369199694695485,363.7134773251346,4.02439311489173,2,18.621772544236535,58.15916535276676,120.15469891590001,2,1.8940112230016626,3,37.78445683273334,4.1496272153323375 +104,23,47,26.98212846,86.70068316,6.770434148,42.91292205,watermelon,25.602121026510357,2,9.222825937177042,1.028621913334109,417.57955200241395,1.4244773376562077,2,12.580276463161656,37.667349218606304,108.82131199252791,3,41.478187448093436,3,32.79305858068899,3.1980171047629495 +81,16,45,26.90435747,86.25426228,6.727468157,59.75980023,watermelon,15.30228532605343,3,7.852294674719821,17.637267992371328,425.12714124507215,5.940236384627302,6,19.15748078672584,46.00049178137229,56.53822219553542,2,49.40044224355378,2,41.2537642009134,3.0771040052974485 +88,5,47,25.86475496,86.67468041,6.662244646,41.16554802,watermelon,21.43328587577617,2,9.971138818651099,15.92909247011615,371.45923579630806,6.623372603539079,5,16.131097342910724,73.58991039370508,183.99429737436932,3,16.48859903567395,2,96.52872560663911,2.3454508756211947 +92,7,45,26.70607759,81.14149505,6.944640222,51.51033554,watermelon,19.246636812408035,1,9.374930674358435,15.077588040997014,401.1532220207423,2.64256267663425,5,9.539892974351893,27.565351433852825,137.22543962820458,2,6.480086255261853,2,93.26723137404096,2.025332952299277 +81,18,50,26.44019475,80.91934337,6.507110986,47.81847573,watermelon,11.411661021388435,3,6.598225468953286,9.59515224824508,400.6358495213048,4.046969641764573,2,11.650246950045664,94.33511181063325,58.07817506594002,3,45.789022742332556,1,5.265470982022524,2.388237732260697 +111,5,55,26.283443,84.42478917,6.520663422,50.78669728,watermelon,21.777580522616837,3,7.873224439403135,10.79860632298281,360.03625597839107,9.441157267760392,2,6.94290032801983,22.006865046489,73.72876935924288,2,0.38328620014156933,2,99.18513137634633,1.1981476276512595 +108,23,51,26.84366082,83.85039964,6.106500787,40.228644,watermelon,17.695310570992287,3,10.483413883500084,4.9897152965814096,355.25891398283073,6.745932746829426,3,9.715161179167247,70.92186953901066,149.63760803872756,3,14.57346623824201,1,75.19432559758025,4.044716270531646 +113,30,50,26.03967219,83.9862443,6.277484043,43.87712348,watermelon,22.755856239388827,1,5.195165947753184,18.196918542564987,367.3605050429052,5.442825239184866,1,9.097620988369082,97.44530770184998,100.11407641958607,2,45.47041396537419,3,66.80295910961426,1.4008867051839653 +83,10,53,24.92994759,85.00802358,6.195142279,48.75859458,watermelon,12.155894355983186,1,8.847541762684756,5.890630201457389,402.01046102944656,9.153942291836294,6,11.297716720260599,64.9912465509,135.01115606008563,1,5.2609134815877585,3,72.84856220919735,2.4768340206514607 +101,11,51,25.50736962,84.24340241,6.792035575,44.2068997,watermelon,10.198437805362424,2,6.716068709719799,17.681376646519077,414.7569548836301,9.300187427365145,2,14.62947134592754,15.492241343391344,194.00269897218308,2,43.293732222284184,3,64.69503285509865,2.623236581518742 +114,21,55,25.4438391,87.9392312,6.472756256,57.51549686,watermelon,24.150433597295176,1,8.212500387824258,18.805287849018892,392.7810693229812,7.163423050624006,1,6.031491887413884,23.068484545509804,126.73007576950194,3,39.29414511095425,1,31.810278913821044,3.907076471156623 +99,6,45,26.12588914,86.5507939,6.000975617,40.71210074,watermelon,10.562518996422764,3,7.747470079091838,1.8698484637087587,374.0165695127267,3.255113075162396,3,9.185446323510396,25.633188799136143,181.04607156181368,2,19.353635358164293,3,24.471908514252217,1.3173399188494144 +92,20,55,25.10474753,87.5267616,6.587791262,59.26519444,watermelon,16.352388261341748,2,5.381503852817495,15.280478738703733,361.25455635317047,4.2376470155858765,2,12.857608790291494,72.03256358717269,173.04439018246595,3,16.853282571471357,3,86.45282845498659,4.677660546908395 +92,7,48,26.27520631,86.63249555,6.956508826,54.38748495,watermelon,21.60005269676551,3,11.719101101147634,9.23875101424981,398.89318101581324,8.652178274706053,3,14.516534772219089,41.65999981307434,183.49684900802328,3,45.207253532089396,1,44.8090195132886,4.916441309778198 +91,24,55,26.27061608,83.09194521,6.259086583,46.76837499,watermelon,15.66952298386388,1,8.748483826688483,12.310285436588373,357.8008720912217,1.6453643975108208,5,13.946415867705623,37.220353270560445,70.59540478674438,3,10.256480485468927,3,52.33491042501788,3.9579139130307723 +110,21,54,26.73690828,87.82430156,6.747537642,47.46447019,watermelon,16.64570674406493,1,11.633806630421953,9.230450414235772,433.9008984983309,6.500697335901975,1,8.33302766763729,62.45178570570536,154.1485706177147,3,46.672931635868494,2,32.18394673588837,2.8654457081307885 +112,25,51,25.04746944,85.5667282,6.932537231,56.72496677,watermelon,18.296730221674448,2,8.945755218612078,13.52918594316024,428.8993864547364,5.6069255346053115,5,9.046968852765332,12.760387974057785,85.63316804697791,1,3.0451823352083407,3,33.562316809925086,4.19541306894993 +89,25,54,24.69368934,85.56967628,6.353107393,48.99390828,watermelon,22.332135681504607,2,5.078649659286884,11.724172184659526,360.18439569556733,1.8876527078043375,3,14.092267306969013,73.2126689844116,63.11008006764848,1,17.752561594949224,3,90.20202131627771,2.453828107857343 +100,10,53,24.54356968,84.60808277,6.211748957,42.00660251,watermelon,17.99205611461348,1,5.904797107223221,12.022569125625864,424.9870499947723,9.85392327296059,2,18.613574193290763,90.99911368149675,185.3384339003948,3,0.9006575236781167,2,76.14111630980562,3.514958888269382 +83,22,54,25.89762315,81.96664832,6.277245254,54.49960057,watermelon,17.108719469787573,3,7.664980302032863,19.869153634390027,385.1834061389161,7.385627805040875,2,16.572473918345477,23.334166447670825,154.69495012621178,3,29.68970355682759,2,41.16622765456116,3.3616265642091867 +95,14,50,26.6333118,84.31756844,6.560443519,56.31866159,watermelon,10.824013233168412,3,10.019672661321096,14.593999027035053,438.7381022548419,9.670698253417966,5,6.88548851913935,72.25175226791244,81.99505163008746,1,41.78646129734237,1,13.093938432332974,2.861968903336076 +119,30,49,25.35794749,80.45846265,6.903020221,47.72078245,watermelon,19.06123846092757,3,11.20974087892672,9.848231523939736,362.8771381668897,4.876165257231532,6,13.953129266160827,12.86145789610974,149.46146378980507,3,44.86632092205215,1,68.5513079840399,3.9442181636918683 +97,12,47,25.28784623,89.63667876,6.765094964,58.28697664,watermelon,27.37628313135259,1,11.82199184635984,5.768700287537478,380.1775636774393,2.768453551321115,1,8.14314749470486,37.96804067298086,127.71929565888989,3,48.307097653382684,3,75.8208518605932,2.352583119175193 +110,7,45,26.63838589,84.69546874,6.189213927,48.32428609,watermelon,18.397784672917602,1,10.670729021099337,7.670149151051384,425.7781429456622,2.4144018897646973,3,11.810210300985455,30.467506113101816,142.976686436046,2,14.7338583591417,1,45.36693481832528,4.38054750103431 +96,18,50,25.3310446,84.30533791,6.904241707,41.53218699,watermelon,24.817854958358325,1,11.59360203614989,0.42541277744587047,356.01438905058615,6.085384098201592,1,18.9820246473166,92.7446791620957,159.82449310760563,3,18.184353126887963,1,68.96410011074785,4.265260766857184 +83,23,55,26.89750174,83.89241484,6.463271076,43.97193745,watermelon,14.960071918085777,2,11.792706665748858,0.8121473158322079,407.49612494894006,5.442120884537088,1,9.530170846247312,81.15434660510901,90.76772313484383,2,36.20557666983261,1,72.26775396102057,4.2172241217214 +120,24,47,26.98603693,89.4138489,6.260838965,58.54876687,watermelon,24.72318470716917,1,10.00073013800571,9.497846038832185,367.64572186816787,6.207398404685215,5,17.68987543177763,1.6983831463376564,155.3053682808843,3,13.281726441105151,2,58.058789570142544,2.937824559456906 +115,17,55,27.57826922,94.11878202,6.776533055,28.08253201,muskmelon,13.61459930168156,1,8.374898673496014,1.2989541223854006,380.16203378587323,9.032898780909001,5,14.63257214102336,81.29325655350875,64.81147079120959,3,5.658761975512938,1,82.49201546672697,2.411655012860871 +114,27,48,27.82054812,93.03555162,6.528404378,26.32405487,muskmelon,12.282475807709458,2,8.322449518587476,12.72416001397418,445.31989801899334,3.497036636879807,3,19.519511427842122,58.11604841695374,141.19355893864417,2,2.2938714730128487,1,44.547751278230216,3.49188367577355 +101,25,52,29.09910406,94.22237826,6.750145572,22.52497327,muskmelon,25.013074832082783,2,6.447218546966658,16.445610590927703,385.78882145817363,9.525805419895788,6,9.183320704021423,19.869522443870625,53.07939838750246,2,10.952129322817866,3,44.625483743000025,2.2172025020521264 +118,18,52,28.04943594,90.83130708,6.562832807,20.76223014,muskmelon,19.221817751887183,2,8.842752481861451,2.0547285941646876,395.84408373406484,2.337540560172008,4,11.354816482479045,75.20145831009214,143.78770997752196,2,49.83554587622525,2,12.740562489965745,2.0499224631600788 +95,26,45,29.91690582,94.55695552,6.117530021,28.16057247,muskmelon,26.321084325314814,1,6.069536072630834,18.712239343397346,409.0694064121561,8.268540843181574,1,13.68877246138556,22.414300143375975,185.4679255506686,1,26.151367628277182,1,65.9620129893643,1.7927761955915527 +81,25,49,29.86895762,93.25103208,6.076459669,26.26243014,muskmelon,11.93748914705946,2,11.490051608423247,0.5116778763148289,448.4869466332193,7.607617349699039,2,18.683738464018028,20.655526854295225,161.0074708460191,1,5.78590846580278,2,2.8399666099433563,4.254176010883148 +117,24,53,29.17220859,92.21405224,6.293486295,21.30290472,muskmelon,29.195912464852537,3,7.935584277796011,19.677790357316557,353.76720960682195,8.13542911453765,5,14.952482609999327,12.615207139254059,91.24112569778947,3,3.4849194874876757,3,33.524735926173534,2.1418570787146236 +114,30,51,29.24908541,90.06998135,6.069171847,25.93496537,muskmelon,16.861238686994373,2,8.41422431039912,8.817468225809836,406.5387701293301,8.822208409798417,6,17.366403221423944,78.41596566857551,170.4998435771385,2,22.822621590745733,2,4.437683271388604,2.749025522356106 +113,6,52,27.76317235,90.35567642,6.740983646,25.21609113,muskmelon,10.423127337590763,2,9.452678285421902,7.112187164652786,429.2625994355992,1.7435717491639868,2,17.09626896369859,89.99534546775618,132.41378417742283,3,26.962124619137978,3,79.57749911981355,1.1081705417334264 +108,26,52,28.82629037,94.26765349,6.201797639,26.23838511,muskmelon,17.188346404755414,3,6.341198491117228,0.5159105806061071,378.0450413782895,1.6831456188411673,2,8.311690455784124,62.06594461901515,149.5442802314285,3,4.004620039881656,2,53.904543538709504,4.742539627299169 +81,30,48,28.52379742,92.09688432,6.041027474,29.86681385,muskmelon,21.594300295136016,2,9.483979908430225,7.434052449692761,367.8387500949227,7.152925276244309,2,11.249393085946524,99.73985950449837,106.8661270173158,2,24.442875150239296,1,56.508289109103835,3.3432834600249453 +115,9,52,29.06785065,90.97685539,6.019372459,29.1194739,muskmelon,15.827244022290168,2,9.945290075837775,0.9828653424895029,377.76941211522603,1.6646682263047692,2,7.823865234301863,16.202263617753843,67.52705254523843,1,27.91541178675273,3,84.86494918105,3.9743034741796657 +83,7,45,29.08417927,90.73891887,6.704104127,25.33014238,muskmelon,26.640503241817996,3,6.30997468245723,5.2917251790724285,374.09964731224454,1.7757134666763719,1,14.554174891649968,69.53407660068646,158.75207963010973,2,14.86501140258108,1,84.63714671552437,3.710041440435618 +84,21,55,28.47090661,94.79453182,6.494251024,21.08484101,muskmelon,16.098333654928673,3,6.185410746960464,15.5234466679388,421.29752543899053,6.032880342873202,1,8.95170726602932,55.83744386425204,93.77574533653339,2,26.769780531618125,1,8.573927773290457,1.4629081733121665 +109,26,45,28.27973674,90.38971208,6.224535449,21.58992507,muskmelon,12.15128734277424,3,6.416110806846817,9.59734616169162,364.5718087858376,3.748354777866508,4,14.90449185917469,59.21216103656346,114.30601636912215,3,16.267849790021145,1,97.17373789618776,2.5238437312254742 +95,27,55,28.47212559,91.21322065,6.160414414,20.88620369,muskmelon,14.00298223280621,1,5.046102745823626,6.30203864310036,356.0818357392836,4.987546025694768,6,15.10068947163066,45.72398640406542,92.89077549599833,3,33.901855825751824,1,20.867060211728518,1.5127578880135735 +119,5,55,29.68846716,94.30111601,6.168757984,26.83924845,muskmelon,16.062880963574187,3,10.516192328761576,7.923933040671249,416.77274994527374,7.3726710345678415,4,6.326215721268702,33.17820170878281,77.87450358965383,3,26.917565055237162,2,83.0439742163446,2.1862823701170457 +110,14,51,27.02415146,91.66737633,6.085444691,21.26034986,muskmelon,18.077447710991343,2,7.528402266140802,19.415455196330587,392.035563280348,4.826780178001326,2,7.171945701854611,6.713849059724131,77.52136830296298,3,1.4971213125616556,2,50.071661703073275,3.0394326678718087 +82,18,48,29.09588297,94.16748386,6.159050816,26.70581328,muskmelon,23.751735741152366,2,10.660509236615365,19.13995661230026,422.6817066229204,6.2153359496898055,5,12.886109717593605,0.4580682279183179,98.06971732242107,1,7.1480588539304275,3,96.18502455176923,1.36524080197993 +87,14,48,29.69238699,92.58862544,6.606033244,29.1102594,muskmelon,14.094775208355001,2,11.224160360025303,19.07349833021431,391.06909386740375,7.327951956865402,6,19.94158723833417,70.57924991150077,140.08477892322514,1,27.040693203935007,2,35.41839904699421,3.579237597118789 +85,9,53,28.20619412,92.86798698,6.447662945,28.78654515,muskmelon,15.346038548397892,2,5.322739366425422,5.177393120888658,358.314710537237,5.254768635380155,4,13.808031972864287,8.999001777870209,101.24366482888195,1,47.25917635409957,1,62.86431314242111,3.4501090388627094 +100,6,53,29.05248036,93.92217834,6.105909623,23.66620626,muskmelon,12.614882619493192,3,5.590402251665099,7.912835812557811,425.5033174327106,1.108815526798625,5,10.649020581554305,17.88582314683599,63.84226764809659,1,11.221200492010563,1,13.92362409827923,2.1853742747110196 +107,12,46,29.57240298,93.61870344,6.559763394,27.56918621,muskmelon,23.60289859959217,1,6.876731762122566,2.8693593975713827,367.3474249433681,4.227668403247401,6,7.023448573709636,99.21471129924872,84.7752433099965,1,26.808598819308475,2,78.25239109989212,4.910663626847833 +91,13,47,29.10968327,92.43510994,6.14410903,27.95602304,muskmelon,21.561517703949477,1,7.084233589184421,13.084486043227407,445.0686707474853,2.6076940837190796,5,12.98294853336236,75.49864804445686,165.25335565029658,2,16.182823964679997,3,95.97108215960013,3.460467942529418 +102,25,50,28.20480805,92.91440379,6.099662369,20.36001144,muskmelon,19.03718406369557,3,5.614893516890749,10.355729281140817,444.6116215133145,1.4675603472349363,6,12.748623702973699,37.68427719413339,152.24460855187027,3,23.137404080264943,2,54.39639222398293,2.364459034387271 +117,25,53,29.11858526,92.12543021,6.413927319,24.52020164,muskmelon,28.919170091841963,3,7.2582638801874495,11.422098154950607,418.3249870697451,7.688826027694844,1,17.474665318770462,59.93535367880369,179.6803975469958,2,5.98846278748717,3,87.20965453316657,2.884378261900366 +85,21,52,29.62800691,90.10051615,6.075144116,23.69586761,muskmelon,27.999164189782423,1,6.610441686188105,14.014861081189572,443.42293208774487,2.71729765313728,2,19.31250367677011,31.55769292111913,145.03175125241575,1,7.742245433662803,3,18.15089549120531,2.5281143776679804 +104,25,55,29.81196601,90.36881284,6.123802502,22.68766503,muskmelon,10.246730022434704,1,8.39253098365192,17.754810616286125,430.11994957586364,5.3334569602926125,1,6.8291368782096,52.80865531440243,132.1831986863351,2,14.346869267237539,1,4.089198928264814,4.974024234405839 +102,24,54,27.72338349,90.93897939,6.698468621,22.81863447,muskmelon,22.329606839709932,3,6.051017292204926,16.758821273885612,388.67161201894874,3.896573103125083,6,8.081809490950969,88.38305322173792,193.80988853688527,3,0.31080191413654923,2,51.382203393030366,2.544036092751465 +116,25,50,29.26092798,92.92367701,6.088885814,28.70627683,muskmelon,24.937335777390842,1,9.090631606230161,3.763304668535725,352.2523219747634,6.885490978766973,3,15.26306209424044,49.31694333576336,80.38030899614631,1,30.374463758697228,3,44.240441913184405,3.3943413255332793 +100,17,48,29.72791119,94.29753295,6.367800632,26.52364146,muskmelon,27.02378663600873,1,11.267318007921094,3.7300035838883128,397.041253291706,5.312243033679395,3,15.014132878832253,58.25952637087379,90.77778002834698,2,15.566603704900366,3,18.38095775134737,4.868517088857807 +110,25,54,28.91105641,90.78413842,6.425930938,23.44398467,muskmelon,28.229352511634612,3,11.758310869332162,8.138876953800471,409.62440031253317,4.190237263395002,1,14.928614268450298,82.93738386445487,130.98840685460982,2,37.250876270205765,3,89.94346251953608,3.57415132507002 +104,25,51,28.96361426,93.88482153,6.469983276,23.56130173,muskmelon,13.921565654332607,3,10.830059424442556,15.016968283375052,445.57058427524225,3.3627676814647547,6,5.091293663169117,66.2639812785327,163.6580924686328,1,15.010904050812224,3,17.38446471385683,3.947212056703057 +107,11,54,28.59052369,91.33617236,6.094016338,29.44008034,muskmelon,29.204581729847614,2,10.268585058335919,1.6513288809944315,367.8733223969777,5.013319659517506,5,15.63498909357326,83.08000964985163,155.89871386775502,3,14.599739460457345,1,51.7412066014898,4.437243695680028 +98,26,52,27.33897716,90.69759008,6.150090899,28.69113835,muskmelon,17.276004022599764,2,9.381572324379011,12.501276558513158,357.04673209926955,6.418925587904132,3,6.8125564812920585,67.13482843140206,133.24279479028075,2,21.34579603434077,3,60.12253684150461,4.120247848707674 +88,17,52,29.90415889,90.75284363,6.646962425,25.37828397,muskmelon,24.039681761619498,1,5.860185696224211,18.567416125784966,410.2587412233957,7.002538825322334,6,7.679411837716394,75.80461692091531,176.5869346559838,3,19.646914287081152,2,35.40853056485037,2.8543609159269714 +87,25,46,27.42711692,90.02696201,6.379690748,21.7508774,muskmelon,25.787244489943113,2,6.710552695898029,8.476623589121491,434.8141506269436,9.393052040448444,4,18.789105754895573,76.50625388614232,181.33988947599076,2,7.18321087338884,2,66.79306603291107,1.2952186114941142 +120,8,46,29.55657523,90.70937262,6.732834334,28.36535596,muskmelon,13.999825750530306,3,11.488074658344186,7.615308528323919,388.5572881351647,9.136524921505185,6,13.285965606458994,26.957878222834918,154.5213818023066,2,24.82749954431182,2,51.06902342901191,1.398390258292086 +95,13,46,29.84070774,93.76312893,6.126019932,23.28207838,muskmelon,13.581078241216552,1,10.356894887745415,5.584744465888081,427.50322020320533,4.357710182303956,3,18.246982499564147,27.353748570185445,145.11760033124744,2,28.693320911572258,1,71.62385606318146,4.436371646894983 +108,22,47,28.53545677,91.72742702,6.161123579,25.1290048,muskmelon,28.982135482995226,2,9.161602080461755,11.159358022978616,438.81787687651047,7.234406108355488,4,10.814514667268414,80.21306913962447,71.60138048904422,1,48.01634816729275,2,94.93480657257099,2.337972107732756 +82,13,52,27.11535046,94.86907886,6.442810053,26.51924782,muskmelon,12.471833841614878,2,8.822223113474395,16.01375504998059,426.2179151056364,6.281998363559654,1,6.520176680415888,22.707522638224287,103.12848262273967,3,12.90665156736392,2,93.130495011934,2.7054272005527893 +120,23,55,27.84492803,91.60666594,6.732049075,26.47844429,muskmelon,12.067107354463678,3,9.024384973721482,11.024654532481206,437.7318363205695,7.091102750949109,4,12.065337156543853,91.18119461356467,85.974353186178,3,28.518503680168745,2,15.894539405080154,4.115392862148845 +110,22,47,29.03157242,91.82172592,6.243673725,24.93861254,muskmelon,11.521976739715187,2,10.919327056095264,15.599060126569535,353.52891972841724,2.8740937602438335,1,7.7815178439270145,11.915692086780016,136.87559990063562,3,37.844559067833295,3,57.8268863199753,4.159739619346444 +95,23,45,27.82424457,90.56698742,6.266208727,21.19014526,muskmelon,13.658592603368296,3,9.371350580902709,11.675449618541132,419.06948423077495,8.353611243907975,1,19.535852324570804,54.88757526727608,138.2029020239989,3,35.25117231362394,1,52.742064722170845,3.183261428873679 +106,10,49,27.72653142,92.00687531,6.350623739,20.21126747,muskmelon,28.633048466965107,1,9.720801085793632,18.871871697125084,394.97632085581427,4.884042465441955,2,16.503596909011797,15.657468934276231,170.4196141886935,3,41.4463390910407,3,23.91864921892629,1.4968312769782401 +99,12,52,28.69708334,94.30759855,6.002927293,22.21807088,muskmelon,24.191272961560372,3,7.951066458267496,8.524233400098124,359.3369194705196,3.282177951726664,3,5.117686057188358,9.620473115710826,147.22444635798695,2,49.61712706314814,3,30.973238303896412,1.3471739254093213 +106,20,51,29.73019662,90.97015715,6.342573112,20.49035619,muskmelon,14.32349780465393,3,9.560845686007283,13.712211088796646,384.2186602168302,2.287534332759183,3,12.523006958530624,73.71132587176228,144.08870430745085,2,13.778474980183535,1,51.12118482958267,1.2817447264828372 +83,11,53,29.54097171,92.91778307,6.163921248,21.9653077,muskmelon,21.139083686717008,1,8.032324723844882,7.838411804901182,354.6150676665037,5.037375790114838,4,16.897340606553108,81.30622641239317,148.96483184278736,2,33.212425146338745,1,6.8153566187162955,4.5409924687293515 +117,19,55,28.80311922,91.78336933,6.121745389,25.16359891,muskmelon,20.76764583191936,2,8.594881333466711,15.761576920950532,373.28594044295323,1.5563274318628506,3,15.413352677899569,98.05321972831243,164.10915029354345,1,3.6162076204539817,3,13.56106603822086,2.972980337594526 +98,26,49,27.29035669,90.53330091,6.130160473,23.49535234,muskmelon,24.407663527890154,3,8.931478823284344,13.753713275588497,368.3712697124544,6.151484011982912,2,11.28600201691502,87.22418003946129,196.67522663831514,1,17.233838732700608,2,5.552341307839059,1.2230362831951513 +113,20,48,27.46583649,94.87679041,6.440584681,27.27899847,muskmelon,23.39466615387142,2,6.714562149101411,1.039288092209265,444.287352746323,1.8827884498348286,2,7.2088806266331105,81.46311599069445,158.6897048483919,2,12.830121360645396,1,34.73328064904091,2.511059743383969 +101,17,47,29.49401389,94.72981338,6.185053234,26.30820876,muskmelon,10.99905404126643,2,8.220099559076845,17.297856383782495,402.30743149614705,8.624379900298386,6,17.692001113739266,54.011222670516766,79.62804323875835,3,11.769014405205013,1,3.060721347310824,2.8149540066189465 +98,7,45,27.79161808,92.51054946,6.157724816,26.85422624,muskmelon,14.361587083063084,2,8.499269232356653,15.570596165056932,429.12945530826096,4.458821384415738,5,14.867719060339406,60.631115625697305,134.39213187443812,2,40.707568963033495,2,5.2174822068246085,2.6336791416930567 +93,22,48,29.12533739,91.52291141,6.776987974,21.90440445,muskmelon,18.5999558383134,3,11.352790494571833,0.1628716110217976,364.621972126209,9.82331327459374,4,17.0410376145068,66.45094605443475,196.10186297008752,3,3.0447921777839726,3,32.08614917126735,4.762290063128177 +95,21,47,27.93114233,93.56161439,6.431970877,20.66127836,muskmelon,27.954329017137717,2,11.98271409808449,18.91437407243833,423.48121584907415,6.651037657528395,1,14.815793514581074,76.21961990982241,177.5494190482779,3,24.37506379108582,1,80.74378487076855,1.898925541532312 +109,12,48,29.45771748,92.12534736,6.708743843,20.76212031,muskmelon,13.580952195016113,1,8.321081846213994,6.109922535557342,387.63263402353937,2.8338382458735434,1,12.371695539365126,56.085433102483066,83.84224190851623,2,32.60935609150802,2,51.506352633827966,4.805399675831536 +118,12,47,27.96872279,92.17444796,6.010739645,28.94766949,muskmelon,21.20284577988588,1,6.323453695039426,2.749057815774407,390.4367775220989,9.26844929559307,4,10.310394689347717,51.76972308796641,110.33546152077085,3,1.7824553272847488,3,26.615444700662984,2.8684581698376843 +100,14,49,29.48882958,91.07574233,6.365956658,26.01909355,muskmelon,13.574201538317467,1,10.948137438484341,6.691616654732875,435.9387941940288,8.359886673614453,6,10.021269920415033,16.73861352164967,59.7206261876251,2,4.6670802423831645,3,15.573631604486614,4.4928137995731525 +89,9,47,29.47156259,90.77069618,6.668382766,28.75226067,muskmelon,29.669212656578516,3,7.487120703864998,3.389545797280762,418.96455880955546,7.989159229127943,5,17.517238009047684,72.30928032570804,142.0114351619101,2,46.52047286686194,2,65.69596292506465,1.5636855032037769 +95,16,46,27.0767265,90.14362622,6.74669542,24.4514648,muskmelon,22.997797768847203,3,9.28376084257263,5.520228492456434,395.88140280285995,6.217705616398856,3,8.998689027591164,78.03156516099388,186.15246948100108,3,42.076256308551955,3,84.09031519600887,4.778596130762054 +95,7,45,27.30008597,90.80015308,6.031665834,25.09484511,muskmelon,29.017296698549156,1,8.319959880895988,1.2552485190838403,377.77600503190547,9.217875178450036,3,17.15816893906969,54.75705951202513,133.2209310875953,3,25.121042425294288,1,79.46693076110472,2.836914173657686 +87,6,45,29.82729394,90.79007335,6.40077205,22.84203589,muskmelon,26.29904135698009,1,7.926870967930492,2.2483278065860257,426.865433247963,4.076402555504556,6,15.084376899794623,32.367437136581756,52.16698676252896,1,40.94431838677702,3,58.7230603141463,1.2839271602322073 +93,20,50,29.93061247,93.22980899,6.448792689,24.34814338,muskmelon,10.9771659955865,2,6.508541298638852,3.7312496442596976,396.9105688373528,9.125224668885872,3,13.456894545299233,75.55143863128988,72.15140896743843,1,48.9004849077092,3,91.49547212061883,4.680713395977086 +84,29,49,29.94349168,93.90741192,6.251420275,20.39020503,muskmelon,13.96817341455851,1,8.282612709579704,15.756823631393955,367.04841958802615,1.3683365561567173,1,5.63107686813183,60.7671719514601,106.2236939279766,3,22.316570670464213,1,63.57342354166987,4.193732221021415 +111,5,47,28.03306461,91.47355778,6.274452811,21.17924769,muskmelon,29.718802662604123,1,8.52445084631722,18.254370637641447,445.908047381875,9.18733085761202,1,6.61198545041369,78.61176035432237,87.57735236538299,3,0.9490771023170752,2,92.66679474810697,2.007543230326648 +111,5,52,29.8843055,94.0371147,6.135996372,21.0000988,muskmelon,28.76635936673021,3,5.733073394910208,18.525217856821126,444.1096206831903,4.960442397012651,1,6.272567014328746,88.06559262871983,72.30713628663838,3,31.25819816898937,2,65.37252575931586,1.442227284309804 +111,15,54,27.7058373,92.91185695,6.194090172,22.06207161,muskmelon,13.890549774079739,1,5.5855914738742145,7.173246546339069,409.12400945139854,1.9158918075280442,1,7.415229059568722,96.32969504571034,64.98481666077642,1,47.59886798756568,2,28.341054762204575,2.266348626374431 +89,11,47,29.78714005,94.65343534,6.327822962,27.8659442,muskmelon,15.304269463163255,2,8.122379230430038,16.26797555789777,378.93018104722694,3.5405703760967238,4,5.614339780200821,94.49799668881693,86.34403896857418,2,43.94809797010645,2,49.524195229900535,1.0252240572513815 +110,15,48,28.57819995,92.86597437,6.212567211,27.5987178,muskmelon,25.173891411542463,1,6.561525148614008,15.327127635685951,355.8301828469858,2.0728169927355813,3,17.486205606479004,86.35814868791569,145.09261470629886,2,33.26796201508428,3,59.114708625459,2.3073895235378385 +95,30,52,29.48069921,90.33698678,6.640470863,26.0365768,muskmelon,10.18326718307308,1,5.583184597681395,17.183991135339195,386.3280291932479,3.728589974641876,4,9.230047008175958,95.73033372942629,178.72965586230927,3,6.771111386270478,3,34.90753729071474,1.0721915392798267 +115,12,52,27.51492243,94.96218673,6.685553129,21.01796432,muskmelon,15.20174118005942,1,5.256058673700498,16.342121781354454,404.3035439399996,6.276861146759016,6,19.204765782556567,11.75591866793776,187.6483667148349,1,48.10154275920858,1,52.119102347845825,1.8439422002739532 +120,25,50,28.05457761,94.81637388,6.327210469,21.84869328,muskmelon,27.442356918819055,3,11.369397834282392,18.97131468056376,402.6935799865972,4.634358556027325,2,6.998975606212197,71.45364926043996,108.71324040127936,1,3.6799656545810633,2,8.461173114574228,1.744970294486909 +102,11,45,29.03167341,93.12603235,6.35544263,24.15591199,muskmelon,19.50739193707802,1,10.309420771716686,13.09500964399885,447.9899862656617,2.1407430518478248,4,13.387100104290198,50.5871732995423,63.17126024087995,2,20.209542773383998,1,42.29062386902365,2.086266183341845 +94,5,55,28.5854649,91.89216849,6.085682344,26.88372572,muskmelon,27.222030667553973,1,6.331846341198947,12.84419977532843,441.65408157463065,6.209082053990619,6,11.99446675948649,60.352928621981135,190.863552769897,2,32.83311093761836,2,56.997686397369954,3.238840467391426 +84,18,46,27.08808014,93.42402083,6.781050373,25.32159689,muskmelon,25.91864489014054,1,10.197450527635374,4.9248388386609925,355.3148292517624,9.94491866946698,2,9.544343868042665,41.69138823387376,124.1025900374143,2,7.045371424843971,1,87.93010779255673,4.286926956440934 +107,22,54,27.99611732,90.84660317,6.630301421,21.61893763,muskmelon,20.16162587696318,2,9.824138501120643,17.668594822696768,393.91530047595893,4.158232502709762,1,5.997505240214519,63.208216837907415,94.99132894181619,2,19.99361997514048,1,75.08315022942173,1.835859575555392 +80,18,52,27.87317436,91.14849627,6.484799661,24.05207925,muskmelon,14.784101974786136,1,9.344367129292078,6.857865606471747,448.13206618859834,5.991302252355313,4,9.510362051755845,78.95000007566153,192.0184285933717,1,22.961499005069985,3,71.80111149308664,3.2132341433548826 +86,18,45,28.96586565,90.71832938,6.566759102,22.25838137,muskmelon,16.471461498623782,3,9.87012106919969,1.8665217509595,439.8231443134894,7.419260549313163,4,17.83632067864938,29.696969396198515,125.15825561875089,2,20.492286443781065,3,82.40960859076758,2.288983770873945 +113,28,48,28.87726019,92.48839665,6.170520518,24.44267592,muskmelon,13.269824653078505,1,11.915231981675301,10.071935571636443,437.07073527360427,5.5387191846109545,1,15.822031700037668,30.38958236119702,167.24947652345196,1,36.779909768035054,3,87.70096231705094,1.290468254845465 +115,18,53,29.17052093,94.19790371,6.012480351,22.06994464,muskmelon,10.46651067931968,1,11.674130478799597,7.8166714465501075,388.4641805196774,2.3277365922955138,2,19.009974476649695,21.223305439487238,191.2534211555187,1,48.10492068014467,1,35.34728303248589,3.4645696453671584 +82,20,54,29.34033587,90.01506395,6.541150335,21.44532907,muskmelon,22.544099713848766,1,5.577480723941079,19.373922943416197,421.0243098018575,7.232966499739604,6,10.718204463385636,96.1240084690219,64.62797134381891,3,31.288148581598442,1,24.380889443986366,2.3295731191590137 +98,22,47,29.07265321,91.91533173,6.341400922,28.83568362,muskmelon,13.380221053457213,3,10.836791280365704,11.681962726073227,371.3701392777109,5.5141816255440625,4,10.242318046886407,1.2037224042416361,198.59147432343514,3,42.09323521116467,3,14.804570015376639,4.394742104367708 +117,25,54,28.68275966,92.50969311,6.150686364,29.11187663,muskmelon,15.361999194858747,1,8.159323380818503,8.644988194826361,373.30580541351753,3.876341580506856,1,7.263364751911627,5.851772047107251,144.8429930713454,2,1.9631792354564137,3,19.68073993332523,4.040309246362766 +83,15,49,28.92705913,91.39356832,6.438008153,23.20076686,muskmelon,22.562206093934837,3,9.267047843993497,9.65394144697822,449.14082889708226,7.212928234778993,1,8.484867674081414,43.602494183157766,150.26071411915325,1,7.819564734264006,2,22.826766919921216,3.9425439691991575 +120,16,51,27.99901833,91.64193051,6.547041903,23.28618248,muskmelon,27.211679894626176,3,10.77387706506159,1.514685825955171,373.02349713017384,9.861717075859454,1,12.791451058527269,91.34897367468679,122.47174379802878,2,15.870121676044802,2,60.88704193758638,2.466909482668857 +111,5,50,27.59350075,91.79742953,6.399891457,24.84266123,muskmelon,24.993083810404304,3,11.465098626995124,7.567065933176145,439.4052439943528,9.209966342708796,1,9.055768001059771,48.860429816688786,173.2205530986008,3,0.586265820331483,2,47.412264995991585,4.158826968788709 +85,21,47,29.87331077,90.60932469,6.186770318,24.69720481,muskmelon,13.250136856946924,1,9.874079291509801,19.01282992596933,438.11742401922845,4.148439922778046,5,13.818517574024249,79.91656902358892,150.50872882532622,1,14.682090086336741,3,95.57530489195149,2.333417812650409 +90,23,54,28.55852465,90.45773041,6.159020864,27.26588346,muskmelon,26.294648510147127,3,11.362163527960796,2.731755147402972,448.70757203380674,6.780455562809036,1,5.615677243366121,73.20933440295346,86.9081438883487,3,2.8601849961395756,1,8.045140096250236,3.0777230351392344 +99,29,55,29.19378695,91.46241065,6.660954816,26.48240255,muskmelon,25.928223604828034,1,6.05546406838709,8.464184028529797,369.644691057783,5.864583435058228,6,17.23646954354306,38.005135587835305,62.94746357426948,1,48.04922799440387,3,25.863807215697364,4.462907039705271 +102,11,47,27.98780984,92.78226196,6.504906979,27.14509034,muskmelon,28.481007218753096,3,11.109384744664876,1.7521389789158737,449.8746009032937,1.194118089971927,6,14.204617252663011,9.288800893597726,93.57449174116586,2,29.00178474800576,3,65.92827119300644,1.2183965818323923 +80,18,51,28.05380704,91.81758779,6.706053225,20.76582087,muskmelon,18.225796698593392,3,5.440460682571888,16.433957292236027,366.6973300893702,1.741576779824213,1,12.125184964129952,46.29687765884996,133.09081500702848,2,34.416830540609844,2,53.21777218931247,3.354245145392753 +87,21,52,27.3506296,94.2911951,6.067665498,27.21244021,muskmelon,20.60551995945477,2,10.729707978944305,1.877667450616649,379.7764763601235,2.716918368359814,1,6.054194949761598,25.43901945527732,69.77260870422785,2,10.951517540471368,3,3.8937039252061934,1.0292237056295974 +114,8,52,29.34081108,94.5513539,6.419083092,28.22908103,muskmelon,18.841556958957675,1,6.341708908050131,13.334204522159311,379.55644024343394,6.743247949553963,3,5.832684267954753,65.88031730833627,143.1594121272793,1,6.8608075597379745,3,60.00700642106555,2.4120968768326327 +99,6,46,28.61475136,94.22253035,6.39637861,28.98574189,muskmelon,22.45000076480462,2,9.958328659406366,4.93926264472025,401.6464423611608,4.041363637254262,3,6.957555874770313,5.1504759761785035,110.55182732646747,3,28.791309684197262,3,54.97921380732518,3.705755697053242 +89,25,50,27.04863538,91.34685096,6.375923383,25.08146686,muskmelon,26.635816089859564,1,11.624317737115181,15.745226943627781,407.48487790145634,2.179661978402418,5,12.065139572408313,88.37735060652888,151.3622231215832,1,15.50446205915198,1,45.74186443256796,1.3362663765937088 +96,13,55,29.5275305,94.57459443,6.700337732,21.13545688,muskmelon,16.849234899690444,1,8.837008119081315,3.323558859844724,394.9634588682946,3.4318479194479736,4,15.281466133607982,32.91850237563174,185.74288171329474,2,29.53262229675718,3,2.648883649212097,4.944745176741174 +82,26,47,28.50416396,93.46806467,6.565312653,24.20007242,muskmelon,15.657421669721185,1,11.676280822703465,2.377220350584097,366.5140054861114,8.399343299512463,4,5.072500815542467,25.59491076272401,185.81203902731846,1,14.429855160047268,2,46.01182656696574,2.9974512535123834 +106,21,52,28.89578588,94.78993038,6.286515359,23.0362503,muskmelon,15.482003868699305,2,8.61680805634689,14.975863789132,360.7642728889954,7.802801491902769,1,18.58044252339773,49.87758785872427,107.83159242222382,1,14.507992404251002,1,34.5882641635808,4.662981290752115 +90,15,52,27.04927452,91.3821731,6.448061578,23.65747461,muskmelon,11.741406230490481,1,5.438513947236451,7.9726233472066195,356.1379867706952,4.9375135433387936,1,6.977587920958992,56.3205653465064,67.368604034944,1,4.045545810300616,3,48.48056701292186,4.742968839895399 +106,16,54,28.96017885,91.69532178,6.585872508,24.7458198,muskmelon,29.921922574458492,2,6.6737503932801445,18.342716429354244,382.53915892148837,7.15158658072494,6,19.691032571524275,53.71393636270786,99.89148389347699,3,42.62519408241319,2,20.092086493340776,4.3801032685414025 +24,128,196,22.75088787,90.69489172,5.521466996,110.4317855,apple,26.113409078104365,3,5.621583404334913,16.669208892696112,379.08411221752937,6.984264351462168,3,17.074309034166177,9.285506758817775,150.12815322700828,1,27.90621993963222,3,15.502632603083022,3.0578455868655303 +7,144,197,23.8494014,94.34814995,6.133220586,114.0512495,apple,21.628617930752583,2,8.151742374359522,1.8607778492011162,356.8658498534117,3.948965914865975,2,10.093617151993929,77.04491597686594,81.99904938066902,3,26.231749119446256,1,35.40714712102775,3.6352739837956403 +14,128,205,22.60800988,94.58900601,6.226289556,116.0396587,apple,21.30397886962401,3,6.923475312199255,15.039728558613943,364.3434206140549,8.844218421473037,4,9.2554467632293,52.42063475122271,196.67365090159973,2,8.896378605792538,2,84.68365830350277,1.5337411419524845 +8,120,201,21.18667419,91.13435689,6.321152192,122.233323,apple,11.002961984706909,3,8.081127863370025,2.7750994014365915,385.9538933896109,4.294376069853428,2,5.867122527803165,65.86097298068677,189.28173817132395,1,16.616911103824783,1,43.26134173738737,2.119677648183922 +20,129,201,23.41044706,91.69913296,5.587905967,116.0777931,apple,24.791169815355325,2,6.308866921643073,11.533330444463573,399.65657603669626,7.811191525128098,3,15.831761013185616,95.53222858557847,59.18255794147022,1,32.814371371326914,3,66.96640907922003,3.5565165151724907 +32,137,204,22.86006627,93.12859895,5.824151693,117.7296726,apple,13.751786166706776,2,9.400339333220803,6.183786032127541,380.9512071678573,7.774405830286413,3,8.015535412326578,49.743899705352966,131.7978832877462,1,19.731777079218343,2,29.448398209477777,3.5621767146977907 +27,139,205,22.48403042,93.40819246,5.772179946,105.5473627,apple,23.364054449528396,2,9.298277618138187,8.986275117935442,368.6592834666599,9.307500944861644,1,19.783244556386244,72.90855817364499,115.60950887521345,1,23.72242428934091,2,17.584086207736604,1.6243502773440892 +0,123,205,22.02775403,92.96129462,5.790993052,121.1349176,apple,17.59047228537555,1,10.599974027086876,13.599664567985197,392.37290933222306,2.074244862327348,4,11.11943876013884,85.91952410032852,170.3587734643768,2,47.09159133412476,1,83.45238329967579,3.943476780110519 +22,144,196,21.91191314,91.68748063,6.499226821,117.0761277,apple,17.407843898955466,1,8.48963089262138,10.289223334047776,370.69892093145774,8.709818720814503,1,12.134003684834209,93.74236451314682,144.24179620189426,1,17.681274636396104,3,82.87747936643055,3.8310953160235703 +1,124,199,23.71059131,93.27392415,5.658473817,112.6676589,apple,15.669133663247177,3,6.3497644443697165,9.490699408053889,374.6092547448996,4.122411256881715,6,13.138850940167648,73.17344422196199,85.6429552683619,2,9.1717551887112,2,59.68616571844463,4.603586382328723 +30,122,197,21.37784654,92.72043743,5.573241391,106.1417017,apple,18.039768848277355,2,7.543042980960142,17.23228386771088,415.3838878651672,8.219436947480073,2,6.337293516657176,51.37414294322088,104.58563697151983,1,40.011551414497006,2,98.02866892845384,1.0524354078522844 +29,121,196,22.84852833,94.32130209,6.079497202,123.5977843,apple,10.466711501339237,2,5.993289027099072,13.543693922088252,408.0719374833119,9.008981965205814,5,7.8540399068499,6.409650395793609,98.31206247356693,1,6.688184188273805,1,44.461166056348944,2.075028678665239 +13,126,204,23.1094265,92.79630809,6.383180271,108.183792,apple,24.538642736472042,2,8.157212304809194,11.436261038534347,400.708683459206,7.67850428199606,6,6.47076945465918,50.729604158342134,194.95483184509987,1,18.023716966617748,3,87.53474747181676,1.561452521344695 +9,139,199,23.25230817,94.54128292,5.867420996,105.3558408,apple,28.95136222774316,2,10.376097395224686,16.65073055747329,363.3090243089582,3.348172736199058,2,9.404779932964058,98.97977938293066,192.4802887911603,1,7.330069580390819,3,13.114707387271075,2.6162537779229997 +0,133,200,23.67287749,90.4935574,5.708418722,104.2298028,apple,28.865416817367652,2,8.033255774009048,2.2599532695363322,359.26665674506046,5.1114408101829545,2,6.125251711869819,90.18923846333755,174.60254598807913,1,13.199484649819237,3,24.33247321336274,1.180123023663588 +30,143,199,23.76881552,90.59810302,5.7983508,102.2648546,apple,12.078995269273094,3,5.36518580284117,14.45362947753797,374.4447056460166,1.286929694763968,6,19.747971755547184,69.81154264110334,164.8600279221189,3,14.03033476838912,3,46.7770812954997,4.478685780045687 +36,140,198,23.34386401,91.47684705,6.28188384,104.4267991,apple,22.234432314251368,1,7.749101906311659,19.243673442870705,431.4571659736606,4.479488173631107,5,12.448982638638235,97.75357442077551,169.1939008270801,1,38.11390775644986,1,36.96948720572648,1.1282867068574314 +37,137,199,22.63946441,90.18451645,5.697945522,108.3405879,apple,23.71006867594174,1,11.465042349200917,10.979130600906643,445.609192512271,3.224874876603921,3,6.803404084147484,62.0258423917545,104.58511312069894,3,18.062919480150825,2,57.582198563808376,4.005726302221098 +33,121,203,22.45696744,94.76285385,5.605934087,114.8407725,apple,12.466760932567718,2,10.941249180418676,10.044948777695478,409.24979148726055,4.053199195555896,3,17.563952888797466,88.03847942756924,140.52397899763963,3,48.94995904564159,2,73.26956261766703,2.359688480087921 +7,144,195,22.96388477,93.58065995,5.85648105,104.6472986,apple,24.528183417344557,1,8.254107466109113,15.329094911174737,366.73020742764106,8.850615391268795,2,7.995979277306931,85.25535724785782,174.44108517911155,3,23.054530026612973,2,66.5798243158472,2.7638020290283896 +35,128,205,21.07273439,93.56585985,6.041053829,107.8737015,apple,19.771778480633905,2,10.171859964204776,3.784421171922141,381.12523621880877,6.442526438876141,1,10.684494248150756,32.34621816218971,160.5700206671807,2,5.1861040797590965,3,37.81441834547138,1.5703249892899636 +29,128,198,22.44075021,92.70785115,5.685062404,121.4977331,apple,25.696439102543714,3,9.710584101582256,14.90244927550152,376.8832007555286,2.0825150516926465,6,16.024818203114986,85.51139569980164,160.86213273186013,3,3.953666259292338,1,94.90936900198176,3.166367460057713 +2,143,196,22.71271308,90.45261746,5.669489065,109.8852597,apple,10.188538529844916,1,9.333151503509018,9.638197433384985,381.72369348023176,1.4416871933951954,1,17.816396140708523,52.624550258489975,141.78113696913422,3,4.85770616327591,3,11.87071216461797,1.6979442528415398 +34,140,198,21.70416965,93.44006288,5.751707342,115.1781396,apple,22.752303725576123,1,11.275314174361974,3.198472097589513,381.9851567108684,7.825347272878908,5,10.866655917474315,49.21283865469586,173.23137948751315,2,44.260613793279944,1,48.09517326245175,1.3038288633284543 +29,144,204,22.43324518,92.48667725,5.800448951,119.1025189,apple,29.191682533654507,2,5.384690510961596,17.40675482367116,355.5013888577361,5.538018500451801,2,12.807097014583638,81.0871414282641,125.71728859286546,2,28.993780423600235,3,83.92509538841634,3.131361278450358 +32,141,203,21.25941052,92.84416234,5.821347769,109.0658471,apple,29.040954593241757,3,9.977978519061551,15.773026798214868,352.06298874793447,3.1496332064762615,5,12.2538532269354,88.82895314704271,70.62341794508433,3,26.410105997028875,3,94.78762978466987,4.457509251588907 +13,144,197,22.9215706,94.89613443,6.28022267,105.6941544,apple,11.796683603416323,3,6.911885653476768,2.370131212676747,437.71354550765875,9.485953231011878,5,12.430111687584922,91.01678855499253,90.73060862841277,3,3.8146131813965587,1,73.57680005067198,1.609364431091873 +25,143,198,22.81212536,91.51861705,6.027314401,107.855225,apple,14.077575939367343,3,6.5681385624156485,7.836216597319532,391.06713758669315,1.6916636269243086,6,9.10851640616632,67.62454758726645,169.86674221726693,1,22.13056015887715,3,44.01389950896366,1.4740639459763911 +9,137,200,21.12152071,90.6878768,5.636687393,102.8017203,apple,28.391716965545513,2,10.955960365197551,6.003783567289429,422.0754293900455,1.4684372558799361,6,6.248347757070528,64.4069494125463,196.56875145137522,2,37.4829693480207,1,79.83734609510363,4.0528485037961755 +6,144,198,21.11478672,90.31528693,5.559363609,104.5086618,apple,24.857790589963248,1,10.543174492945898,6.646143277085765,397.5003899609742,4.692630550808789,6,19.931017152656267,65.23134862893664,59.82756219742466,3,11.520506194214148,3,86.06387188638928,3.91880479900406 +37,126,196,23.59997268,90.97597665,5.596449493,107.1728191,apple,11.327962406700324,1,9.208614193036311,10.81299382063997,368.19957629616886,4.733134384052606,6,18.143418726048488,72.73466829816546,104.21391783535574,1,47.36262733961716,2,91.30440211671367,1.9683823039332502 +2,120,203,23.12652652,94.71203306,5.893492999,108.6211833,apple,11.652755460649235,3,11.203647226623977,6.583361973028772,437.9165663897639,7.59498587380866,4,11.925854446244674,71.98012289738294,157.67568209107765,2,11.060487862539702,2,78.85873960152453,3.268800161401563 +11,143,197,22.98458907,93.3204487,5.875718516,122.1952483,apple,11.705669623554444,2,11.031240437529217,17.366692334628027,426.23840690028584,4.370984167639188,3,19.14121524911196,65.91444188538958,59.69178781079182,3,28.57707369238071,2,6.308523739098004,2.3444790467945196 +10,141,201,22.12659387,90.97818277,6.386021424,104.5412275,apple,10.700504071186604,2,9.046328778507363,0.07656970112627226,355.2755985363056,6.739199806520464,2,11.438704757773696,28.985405935969034,163.8462939504583,1,29.191018446770666,2,84.98440849087187,1.5516334476106683 +24,142,202,22.53779727,91.48135786,5.710819862,101.8474768,apple,22.457282140177888,1,9.640734746097454,4.425803199160702,390.6865748319925,5.832870126857847,3,7.218131493967207,80.65324973090013,60.91262251652109,3,27.37464051611036,3,77.26682352728022,1.8327475126676775 +23,138,195,22.49095104,91.70292746,5.795985716,124.3915101,apple,15.408636668176758,3,10.722260489285855,17.533308252273486,392.6336743308372,2.7296052968950395,4,10.033602874505364,2.999618645745028,137.30222940779225,1,42.99058856535095,3,93.61971461617593,1.219502895172047 +18,125,204,22.35548159,94.47811755,6.046673619,116.7366261,apple,11.015540427381996,2,5.704090026213438,4.044418911456984,366.3274996474634,6.3337175212136305,3,6.652535258458606,80.03471242297287,123.05004763265089,2,3.4716370037594757,2,27.781142687466897,4.405346700346339 +13,121,196,22.20700989,93.50574163,6.443382913,120.1593771,apple,15.044791862789888,2,10.032106494956846,8.193768363143981,372.7338514444381,1.4615820591801136,1,16.457200074247048,54.90029596973691,174.4996444834756,2,36.6997390498239,1,6.2864426117470895,1.2894620186814234 +26,122,202,22.44516988,94.73763514,5.617227184,107.1843273,apple,28.0150103591898,1,10.486298914430199,11.829731193316842,426.55501727791886,8.372990329548342,3,19.689896252498805,32.0252839374364,107.05543847780143,1,11.068468775310992,3,52.15940392242747,1.1388091524055044 +28,123,202,22.76643029,92.12438519,6.442289294,120.4359949,apple,25.459885233714264,1,5.1028052778845225,18.175830265085906,389.04230521551426,2.571346738464109,6,15.945665620588693,86.24368585240424,181.56692027820043,1,37.80908705627367,3,77.21531893801831,4.797733058781564 +26,121,201,22.19109412,90.02575116,6.162034371,112.3126628,apple,24.98409356773446,2,9.192250122893032,16.892686288498965,387.82096577008446,3.3336991521250408,1,9.830019221408818,3.064195412509374,175.96911718670944,3,25.81084044661231,1,5.209928131144537,1.4386600771850069 +21,137,196,23.6119202,91.70293849,5.812781806,123.5900822,apple,27.128445818141675,2,11.171347054570935,7.122758635695847,352.325646860025,5.017100156545027,2,5.487048847995643,30.360659877186215,93.02423841221173,1,32.6511251720023,3,16.368409118904992,2.4760199736385102 +21,135,198,23.86087054,94.92048112,5.765015126,105.0241329,apple,14.478620647799481,2,11.298922587832092,7.969388208844901,426.4196448669358,7.440189127622578,4,7.15719933485485,90.44414379990286,193.83550134129888,1,27.680805205374785,3,27.114373206199815,3.066444166533497 +5,144,205,21.42177231,92.62665309,6.184922574,102.8045658,apple,20.29288616518152,2,8.285456976683873,4.586446640254831,447.5150901688916,6.726378685561305,2,11.889755912875064,39.91820678658994,106.23472911657637,1,26.27613205805379,1,84.09566900044896,2.7512805215959335 +2,123,205,22.36629253,90.78572467,5.739652177,124.9831618,apple,13.281507973530902,2,8.233735730247563,17.74189602946825,392.2929149385685,8.674493303581857,4,16.105623048618682,80.81300435190198,161.94945171388022,1,35.20106735688824,3,10.29083239496119,3.3475035762758143 +15,133,199,23.99686172,91.61001707,5.824778636,117.6102915,apple,20.392678220973345,3,11.812086723102553,12.200447946733542,437.84645314715465,7.361593647872302,1,7.049358564462459,38.49839072026045,148.03075467634451,1,0.5378753543326675,1,22.98689645648334,2.5036815486773345 +31,130,198,21.80129837,92.73446667,5.554823557,120.0586671,apple,29.129038954063134,1,5.814658409334124,17.747947582227706,439.7016315341487,5.751031984818836,1,7.891667489934145,90.52946052962176,53.4847086882253,3,40.63252553282783,2,80.28588808577001,1.7485936927943873 +25,143,200,23.80436344,92.80441624,6.024248787,100.6192543,apple,13.284375220342358,2,7.429618504094105,17.497085879528587,380.089829887688,8.471825889652436,6,16.415919482580634,25.50946069556246,57.85897807744242,3,8.719592588891656,3,97.47677951376403,1.6986311353935406 +16,143,204,23.71475278,91.53331177,5.631333387,121.8961665,apple,27.298593872039596,2,9.430529081424169,1.6642757523000928,422.44272356994077,2.83791485985693,4,13.969935388821282,16.629013552660744,98.52467306064543,2,34.995128280006156,1,75.11843265111946,2.2605698471333273 +19,122,202,23.34467359,90.37981478,5.811975094,112.8954016,apple,24.96711936033529,2,9.69490834725315,6.276119813895946,351.97277376136776,3.274927669898056,6,7.83897988552844,1.0834987569113275,117.46636790658357,3,34.85002152659934,3,52.00607622697277,2.8667695914995064 +10,125,196,22.31253665,90.03577124,5.730557448,113.0688155,apple,25.14922916131482,2,8.387144258994542,5.138988394687381,350.90765647628143,3.5006239383357087,2,12.033314663957109,7.559914917806088,126.3123458443681,2,49.15832669840089,3,54.3370933429266,1.2515084472231575 +20,139,202,23.50201428,92.21083961,5.66999105,107.9868949,apple,22.18492472302985,2,11.03529617894829,15.228318412840657,390.0620401144933,1.1121813240684073,3,5.724691157944489,62.80940210758123,72.08055734151029,3,37.50881665364456,2,25.929456508516026,2.6266729902691903 +28,123,198,23.46260321,91.45665004,5.682751473,111.7763395,apple,29.45623917366988,1,9.695310869541387,11.509248675036662,441.3875801300943,7.072278370548964,3,6.082107050622019,14.806039959644334,149.6601923340302,3,43.250045787091324,3,66.93606570207635,3.6986944970366578 +28,136,200,23.06204373,92.39544055,6.245858905,114.7399101,apple,25.878187165783373,1,7.754896103396362,10.487875934055822,394.0058145775637,5.836857867808051,1,14.209520287451905,8.072132384541476,151.21530925627837,3,17.43741928445831,1,92.43318217851075,3.9569331980348776 +2,131,199,22.47420512,91.22759742,6.017370134,124.2179699,apple,22.81780915004636,2,5.198944351061827,19.707940162368477,425.2853736559992,4.543947035689646,5,8.763190064831134,65.20241190578517,115.48482677845351,2,36.893697164761605,2,31.697533026912794,3.500847308552141 +2,140,197,22.69780133,92.82223419,5.53456749,105.0508234,apple,16.89041734173178,1,11.225156416831354,7.848277738217613,423.6263955972688,1.0957187442151055,6,14.87571565127692,61.8059845084544,111.23811524036503,1,7.591295401685294,2,71.35195702789136,4.975473976243485 +27,138,201,23.66682067,93.90191078,5.952367662,105.4004751,apple,18.299666264549607,1,11.468567589740895,12.894859312607235,438.11911526705927,6.883416201360504,5,12.985794360547679,95.23808412683682,89.35547461258068,2,14.619538059820863,1,68.93801879136409,2.686683996381606 +30,127,204,22.50050273,92.45878335,6.126436584,100.9343903,apple,28.473644184913486,2,9.905853590291304,14.411007635855984,358.6809607842175,3.7545816397387712,3,16.049618867419262,72.24277529680538,144.64149658791513,1,45.03423713384171,2,42.98005613891799,4.595343869587406 +32,145,203,23.83053666,90.84422164,6.406818518,109.5966791,apple,27.442749078151824,2,7.689288279713577,14.459306988039632,379.979666390656,6.674222806735619,3,6.100555838344038,54.45934387571172,189.37306810857336,2,20.341553659834986,3,48.662752876405435,1.5749519224299373 +29,139,205,23.64142354,93.74461474,6.155939453,116.6912176,apple,23.801940601085146,2,11.058301839545202,2.372009739479808,402.9195238159388,3.5528309952909622,6,19.695418912770812,56.91217009830151,89.08092975689905,1,42.597214083789545,1,0.8974103421731328,1.575353374099203 +26,126,195,21.41363812,92.99124545,5.878568981,118.3979065,apple,26.748256387300586,1,10.232746252125846,10.589147657042366,383.92605249107953,4.252788210929097,2,12.40111717858846,27.860302377534975,180.33018142635655,3,0.7814819256247385,3,31.37778855974026,1.0136596946918592 +40,136,202,22.85267372,94.5764581,5.935336308,117.5314026,apple,28.124263002625373,3,6.732131540042072,11.360404301718772,359.2341474802378,6.063193872583486,1,10.595382968638575,63.31907081107235,118.97639051041106,2,17.43533924848243,2,94.33279833163964,3.955237364610931 +6,124,200,22.98208095,93.84505029,5.971332179,109.5852253,apple,13.764079363863924,2,11.978029634264718,17.66546104251525,351.3128431856589,5.413102640120094,4,12.531261424108838,23.75819508799266,135.8812099297652,2,45.5576904291672,2,87.5853534121824,1.333507086993547 +35,138,200,21.19909519,90.80819418,5.67130617,103.6838922,apple,29.813522103232984,3,11.58593411209555,7.669961521355225,431.8131247158145,4.307175336428994,2,10.614359212381746,90.22131246176983,160.51334488902572,1,49.35109025590067,3,21.841123045104627,1.2592036814418845 +17,136,196,23.87192332,90.49939035,5.882155988,103.0548094,apple,29.997859652841264,3,11.250418266666054,18.489901702013633,409.7400810314742,6.997207550868217,3,19.40868417786845,40.32031112507306,64.11279989095868,2,41.3201405600203,2,89.49180692821872,4.809378924907595 +33,134,205,21.0365275,94.33919546,6.08551916,114.7412734,apple,25.260337171727148,3,8.822029979770353,6.322844163609096,385.98525222436604,8.418055439114934,4,13.800036650344984,5.626261207147221,195.02625406452367,1,45.82368162092179,3,1.9191871657191828,2.6735692730172165 +16,143,197,22.61711614,93.51978375,5.90402645,116.9256766,apple,19.021829020021308,2,5.960007143100009,8.550097444146385,406.8653140125368,3.203787816272686,4,12.96134996632858,47.4587736510368,162.18119764575658,1,41.67821177761068,2,13.41945558237223,4.755821290922206 +27,120,200,21.45278675,90.74531921,6.110218826,116.7036582,apple,14.434298232988604,3,10.185544937294978,1.4260515223715342,401.3091769624517,4.973672377758637,6,19.68736167095781,64.35522640634339,95.32043388213488,3,22.717898060710624,3,22.11724424469771,3.4726499999055687 +29,145,205,22.81227579,92.12992101,6.212302608,109.3383552,apple,25.836797345128105,3,7.311129495677685,1.6620239247414914,447.48240534382023,7.287522466873409,6,17.883587633694255,89.64286722954043,86.89475594378672,3,24.29705238384437,1,99.13242000851919,4.112970017914007 +3,141,197,21.98141856,91.12719303,6.142803397,115.4789148,apple,27.11813855530109,1,9.619702488480161,18.645523911152672,412.5758969740788,8.142762659599157,1,6.202621084039565,14.772800360645832,168.4121573583074,3,42.25093959020138,3,9.404887421815268,1.152071718984761 +15,123,204,22.52709326,92.54780429,6.365972688,115.3830068,apple,18.906002562998392,2,11.657186547471449,15.912336963668059,398.8980954455944,6.458582998782504,1,18.789972239712483,48.538228806963126,98.17994985320148,1,8.447824591652909,1,92.38368920499522,2.262732701697352 +5,136,195,22.35628673,91.92360477,6.264202804,107.7697413,apple,20.416138917943663,1,9.769855539005494,12.73541000594503,378.7577992835709,7.934085643947292,4,11.851252902102999,60.13613001589574,110.10457348712976,1,48.317640004432455,1,35.83278739097322,4.988039444453781 +10,136,204,21.19852186,92.15595143,6.276198595,105.8554351,apple,21.17401651116081,3,11.52034177751396,2.7709838137474763,352.65394337500305,8.035049627234114,6,6.657300601902539,6.1511736425680645,80.12740659571429,1,20.071950833158812,1,26.095039476701775,3.161331073712504 +7,141,195,23.8812458,93.45067555,5.514253142,104.9116663,apple,29.610669156605724,3,5.635475174501436,13.549388315241156,400.24912328368384,8.252349438205663,6,19.45496934264777,5.462051716870175,81.90678243999565,1,42.715315197679786,1,77.01631924384664,1.3085385402298986 +2,129,201,22.78234161,94.36803516,5.682343744,122.1449949,apple,18.113655543784688,3,6.865101788809612,1.0446668077159238,378.9333206442151,4.467042679252787,3,12.949364810746566,64.69117238501696,80.76446303613021,1,2.19651743292249,1,16.344431172361663,4.855056584467276 +29,138,197,22.19055385,92.43764169,5.830892252,121.6622761,apple,28.874737938819358,2,9.148677995984755,1.7911439786865557,439.3400190110597,7.254939696871271,1,12.035868474853583,15.185083235621622,137.05991608616355,1,32.78633563583087,2,0.46630993075584826,2.8720790780814434 +30,137,200,22.91430043,90.70475565,5.603413172,118.6044645,apple,15.869740923889342,3,7.4082874944495325,6.694860692047609,372.70957915237386,7.056425513641589,4,7.244277205428311,2.528157289536792,181.41354229855995,2,21.151099336515017,1,33.6597417545724,2.511405634088793 +29,132,204,23.08950736,90.22507299,6.0967531,108.2166601,apple,19.442630222088752,1,8.694694923671923,11.187965789269743,412.2500757534764,5.902645193054866,5,8.137432622099025,4.055445896800681,161.21925512563695,3,36.30760632275577,2,43.50416102039478,3.1832874497957 +14,139,197,21.72484506,92.83975602,6.056529526,121.6961761,apple,15.737639960939829,1,11.35276243849204,3.999796799961486,402.96708553336987,1.6591565916603892,1,11.588258399913094,99.60050638184555,171.3266760179174,2,35.16683809665268,2,24.477274712679154,2.6364459728415595 +18,125,203,22.44307715,91.59234006,6.160267496,102.5565807,apple,26.033503023353923,3,5.535032968970341,3.0752172523507526,392.2084427211176,4.049705621882335,2,6.097864783254899,47.664368560016804,185.20976571402218,3,37.80246307201909,1,13.409580698445689,1.7679611069396648 +33,143,204,21.1316077,91.95769858,5.814434775,122.5391946,apple,17.5931236747446,3,8.646476508198953,19.552043183117235,350.7285162547156,4.953177281453366,3,8.886680990963521,42.44087872388286,172.30570030808298,1,9.127786572187746,2,79.2025929060199,3.1758830795374138 +40,144,196,22.71750705,92.25479855,5.987262638,107.0289866,apple,24.430866480639864,1,10.734956288926846,1.4010561159543111,420.0482596515518,1.391766014391875,5,8.129636744396034,61.96483422312682,97.5131478051324,1,46.905515616894114,3,31.361396841471688,3.605665284977037 +9,143,197,23.75033085,92.88160462,5.570020684,117.6602827,apple,24.742094013552006,2,5.939698785620681,2.7983920638254545,369.21620077778795,5.991411682395984,2,15.444422825856863,92.75133572376133,116.19275842177083,1,28.581083023371708,3,42.03467507759805,3.278274611977272 +38,135,203,23.76121837,93.661643,5.965551311,100.825956,apple,24.262925420825326,1,8.648658942336464,8.462022556988565,415.69340318817615,3.458188801186802,5,11.347882665136888,8.519389590527416,147.27590151496395,3,3.4314829320139184,3,85.4307390698953,1.8915939986546446 +28,130,196,22.13450646,94.67695747,6.062356467,112.9203223,apple,29.69277638902345,3,6.861763476260014,6.114823970881044,434.44450973730005,1.7307007586365635,4,7.116611370981166,3.790408349118002,164.35702005773132,1,23.364534407816063,1,73.99830620821457,4.83405901495941 +35,142,203,21.17089176,90.23730166,5.895319002,123.6495149,apple,14.060726060721125,3,10.027869635696282,19.778642777029393,366.41008229688754,1.4471587504560395,4,12.187817689331201,25.59572346156054,57.50181445050095,1,48.57396125250497,1,62.2063515233653,1.5499472505052103 +12,129,205,22.36238282,91.15761594,6.119432215,118.6832725,apple,20.850269433774642,2,7.591362959540126,16.251538113141372,388.29191438401324,1.2048419196419973,3,16.851358346719245,80.60434936136231,91.27591632380502,2,1.2435731378973636,2,83.02144931433727,4.095623823584193 +1,135,203,22.77856513,92.70124029,5.624203283,113.7759219,apple,27.16869072995614,3,11.76314700212399,2.313462885118185,405.34817832554353,5.391244189707512,2,7.296410853360252,89.33240831353456,59.05723322645373,1,5.456456768746298,3,85.73170000447064,3.5772649394576326 +0,145,205,21.22503442,90.09877774,5.52078314,113.9760462,apple,22.32911133423478,3,5.55416258140868,4.41225412821886,448.3046516695685,4.911259472148124,5,9.078924693560321,40.410333684367984,136.84939977105222,1,40.8209249627524,3,78.5256001329812,2.1083923699823064 +31,121,201,23.15791104,90.34396882,5.731535258,110.712841,apple,14.993177552546504,1,7.52662385848792,9.379228764543555,352.94057503769005,2.2339431431866714,1,15.509559029965102,10.989172234264032,188.62834720899727,1,28.420708151164913,2,93.32479361009588,2.914949385299296 +35,131,203,22.42776057,93.91722423,5.893490899,102.7230739,apple,28.459380508253687,1,6.9056937744512314,5.147378840243251,399.9789519599269,2.2884113145845477,3,17.10328432722146,60.30887678392985,179.51523737879972,1,21.801301809294788,2,64.08311201883114,2.552570720391014 +29,140,195,23.64082979,90.95257927,5.560521058,116.7431319,apple,20.65470123376477,2,7.622637870245186,4.371516769321511,396.671259298517,1.9831264184972164,2,6.047528012366456,64.18483411400857,182.84727928285562,1,36.03309028695458,3,65.0058276479195,1.6951419584723424 +33,138,198,22.29423493,90.69033986,6.222390798,122.7418744,apple,18.513644209442305,1,7.633594637451805,3.4632817339101107,411.53586210620085,5.895681912458212,4,9.108077310496505,25.17687950060754,198.34024289581902,2,28.026076037162124,2,2.470816541005083,3.4975678808524044 +14,140,197,23.35225078,90.90054697,6.071255131,113.0381382,apple,18.59403333362835,3,9.448349325321047,12.794807990308044,389.3698139217082,3.7797553774641575,4,7.79159713285268,56.26388641777825,112.81273123519844,2,7.493823430737651,3,47.38491329145296,1.0149025632928677 +35,145,195,22.03911546,94.58075845,6.231950009,110.9804014,apple,27.06012525449916,2,10.305065825138264,1.9025005955645202,417.24258391509676,6.5045155878160825,3,14.752543623061493,33.72786602276808,166.40316489001674,3,20.629067688345614,1,70.45334558967762,3.36506485995545 +40,120,197,23.80593812,92.48879468,5.889480679,119.6335548,apple,14.299331698195829,3,8.044799744625099,18.76804693578339,353.2111378568333,5.695483977278562,6,8.449351067734415,95.83670165151942,119.64704260314237,2,49.352523407384105,1,7.621344726971746,2.4433366635023814 +25,132,198,22.31944084,90.85174383,5.732757516,100.1173443,apple,26.716653238873768,2,5.24069234004205,9.031118284734834,385.46193326095397,1.2207525154702994,3,6.567544569673749,66.65883918537111,95.91734238511444,3,1.3093317938740412,1,69.3564285622321,4.04444320550466 +31,137,196,22.14464104,93.82567435,6.400321212,120.6310784,apple,10.617323465665303,1,11.973004902774923,4.8751400201390505,436.19679470873456,8.209662804245049,6,10.913120518138854,60.90004993194691,61.13385788468252,3,32.598381934034535,2,50.7534858423501,1.3075454952884908 +36,144,196,23.65167552,94.50528753,6.496934492,115.3611268,apple,28.392120563130813,1,6.500656669541299,17.295104208061765,401.5209386625795,7.674012168842182,3,14.268967594569531,51.54497974078106,65.02089887532615,1,35.73065332322296,2,40.38518645970428,4.468498004051664 +10,140,197,22.16939473,90.27185592,6.229498836,124.4683112,apple,21.829635938189416,3,8.39664937244213,7.417253930796663,428.5798827658048,1.7614305814339923,2,15.773777685534716,94.44620680105751,169.17738776838848,2,31.164841794047298,2,92.55506734732954,3.8105431564131615 +22,30,12,15.78144173,92.51077745,6.354006744,119.035002,orange,17.717882107211548,3,11.35176105881973,5.465350999061971,443.1801115795268,1.3062412796793552,5,12.694493441461018,40.53007548944112,190.42776986798444,3,27.924397393901362,3,88.38392792310394,3.6340345658969535 +37,6,13,26.03097313,91.50819306,7.511755068,101.2847738,orange,10.448250249599369,3,11.210980353267024,2.04822353553602,414.7778954980439,3.4112575517307677,6,9.413081959516985,72.02399111878836,106.37861436089086,3,48.858209194899956,1,96.18362691483789,1.1521989570149924 +27,13,6,13.36050601,91.35608208,7.335158382,111.2266885,orange,28.669939256209126,3,6.1554113505591985,12.7362352898108,380.02989288702264,4.863111583054195,5,15.891874429660552,43.396446477751894,193.40341519764596,2,34.12978830261202,2,8.145829169708897,3.7905207775402476 +7,16,9,18.87957654,92.04304496,7.813916603,114.6659511,orange,13.866481527972097,2,10.802391219180112,14.028277080832376,372.56224301660006,7.684275746046994,5,10.525476626469791,38.99639196828688,170.58868315984253,2,24.20564883987074,3,58.09406660342895,2.584127200433268 +20,7,9,29.47741671,91.57802915,7.129136941,111.1727497,orange,19.956068032538454,3,9.640746221825342,12.622742907185433,417.77374720539717,8.159204588362776,2,15.087016658338912,40.26401349747897,81.96454449515383,2,3.0809550941842065,2,37.83366467273584,2.1539023856176835 +26,27,10,28.06903173,92.91487288,6.079998496,114.1339416,orange,26.9054708327141,3,11.857353603072575,1.6870445930652633,397.7173662554347,4.70365434126594,4,15.073197324958786,15.737003462095512,165.86602552643964,1,47.80287266540321,3,56.0014901012154,1.899256715514385 +5,23,15,25.66901098,92.04670813,7.408939392,112.5424199,orange,16.12102292698541,3,7.863965833011269,17.58660398167198,428.68594501042594,8.13033509426549,2,12.53880140779631,16.861421874450976,79.02620312711905,3,49.194489798213475,2,75.98014421129938,1.305136260409494 +0,18,14,29.77149434,92.00719952,7.207991261,114.4161786,orange,20.888830393151355,1,6.468970771820089,6.504407161997943,383.59458422331556,1.1407479394795246,5,7.012203739363134,38.26635295493901,150.2924298169918,1,15.335107904959338,2,87.39256467407293,4.738504638640478 +39,24,14,30.55472573,90.90343769,7.189259647,106.0711985,orange,17.156634719000138,3,5.958550887107908,1.0153344419068122,446.6748470742034,3.1462509315044223,5,7.989409454369653,80.4778900254728,186.84642598195143,1,44.33517050957706,2,88.68146899410435,3.182082000410789 +13,23,6,23.96147583,90.26408017,7.365338111,102.6958703,orange,19.909008468490686,2,7.6771030033571055,19.3327286368809,375.2356840846833,6.34539612439254,6,13.913874336018024,15.163486841693196,92.46507763832074,2,4.969032622644293,2,57.4460199775499,1.3385371378037596 +21,17,15,23.98289638,91.5473145,7.455991072,118.4901697,orange,15.989417171902998,2,11.444717866598808,1.159192741961026,396.22752404732137,5.216013699038715,5,18.146992568694927,32.71417629489021,110.31192747268996,3,14.870276405923534,2,35.94595984343179,4.8318051870071494 +33,12,8,25.26052689,90.31153735,6.822282114,117.3695296,orange,26.230585132311415,3,8.353557032325424,4.094466655393177,438.0803112537542,3.089381987273313,6,11.131519005932315,42.191909963862784,179.40272482446582,2,14.416936615252235,3,94.81366896209894,1.703206970282657 +6,9,12,31.08368929,90.14362642,7.028746406,109.6894658,orange,22.937653086720488,1,9.538533959149103,16.242510645637473,424.4455310643519,8.436483220336374,3,5.727102920388845,73.31581149423934,103.29541656452723,1,24.66085360338556,1,55.35242096235542,2.573438094361388 +19,7,10,14.78003032,91.22062116,6.118430299,100.1961762,orange,13.180987216876519,2,7.858049598547611,0.8759995313270608,355.09923417026636,4.784511628078753,2,11.169359964333271,32.91812312885416,184.38900754740877,2,12.805655974103974,1,77.0674661726959,4.869240801173041 +24,18,6,26.56608303,94.45239715,6.285312759,116.3796525,orange,17.802350182908363,1,6.193539461714605,3.2343829605319607,359.88391325899124,1.6360442871283665,5,19.32990423505377,14.499709314630781,191.44476595253826,3,38.43485536101973,2,40.053313952972566,1.6840580100023588 +9,11,8,24.85903405,94.39000473,6.559236744,111.7803734,orange,18.574942276133196,1,11.972127839672524,10.945623862086935,356.1224091652143,4.809273141377284,5,15.597234387541775,59.348261198964344,109.95121303031766,3,21.60537014282362,3,25.513000230878934,3.3271738083017146 +31,8,7,34.51465139,93.63812684,7.163245982,103.5684926,orange,29.390817717049742,2,8.236420020585403,3.295278848252976,438.34466939317036,3.944116906792146,6,10.4599805925133,80.06747238249568,185.7874166292933,3,49.61895012056338,2,85.74127139429972,2.7508205987625964 +22,17,5,24.12188673,90.72351622,6.945562889,102.835632,orange,11.32035076596589,1,8.972845595590751,4.521170124207668,382.7640028696985,1.0860582463271964,1,18.593496363179952,90.83390475114247,57.47004577105094,3,24.540838679451447,1,7.504140711886286,1.5723283095617826 +13,5,8,23.85340379,90.10522549,7.474710503,103.923226,orange,14.455353203603693,2,8.96264038095052,3.800672426702738,427.60415568181725,3.793205339749476,2,13.137225789444502,23.942921092273718,82.0149498853448,2,1.8196697349286306,2,54.893943910897235,4.185674079400814 +16,8,9,24.60297538,91.28408653,7.601189843,111.2948115,orange,19.163848127334347,1,11.643485409402835,5.401151520397893,420.26878526953624,3.101680256497172,1,11.901938488829956,3.7983580495319136,108.78074946007291,2,10.862276731269976,1,95.23273218198315,3.613685635739064 +4,13,6,15.63211033,94.25966183,7.561143224,101.4705704,orange,11.838777321520137,2,10.179915336485859,7.63469399759479,397.4920344540514,2.9031834146767843,1,6.25578196613688,0.9386876215173312,126.99140157649605,2,19.1582012267955,2,78.0139735419078,2.5030789160078455 +0,25,14,19.33516809,91.97978938,6.361671475,116.450422,orange,14.183409491732808,2,7.888856825737378,9.164391486622293,423.6691179428264,6.4969772483720005,2,19.639259047776875,6.038912513880145,93.67651031815072,1,27.623178450584774,3,74.39868542126251,2.0665624177815225 +8,7,10,28.2620488,91.98317355,6.929216014,105.2132259,orange,22.904246694002484,2,9.554330258679352,7.882935109044984,431.45046126851014,4.959474059786627,5,16.92628960783366,95.2663638833872,102.4069556815423,2,12.14047903617545,2,49.035460930036336,4.0446704272220115 +4,23,5,22.67594476,93.36348717,7.477935216,110.3332655,orange,19.8048853159575,3,8.236676203856277,0.39934018736729193,363.5396936970841,3.1638232514478757,6,16.587874909047756,3.41743717537889,124.78645033513807,2,38.18690362118949,2,28.558636387543867,2.8214617659847456 +33,14,8,21.03200078,92.9641969,7.684420446,110.6823944,orange,23.899279293392844,2,7.911919253711135,13.042352552578993,433.687910605299,2.4510699917155736,1,15.77584807574976,56.60630079586457,163.74878454383958,3,21.224834168974215,1,4.123057646690221,2.2292311285015076 +30,7,15,33.23453301,91.06053924,7.825531916,115.7659902,orange,21.33762498122857,1,5.629478435234409,16.23092226312683,421.89680962152795,6.431777015677185,6,17.799332411860593,52.03743417568446,177.4639238170444,2,19.568101778880976,3,88.86018715331524,1.859056522156811 +21,29,12,22.30318989,92.15987039,6.438668989,117.3688104,orange,24.068975135530803,2,10.017654314100827,7.134735806660338,428.0222144861716,1.2581789315642469,5,13.384804307176468,14.593309229835128,106.14279701735036,3,19.586460157658347,1,85.29529746054924,4.493409669219764 +11,14,5,11.50322938,94.8933184,6.946354724,115.5683776,orange,29.703517930278306,2,7.2423125107732,7.984765774568075,374.51703009237013,9.013009544722385,1,15.187582911450233,45.77258189825273,75.19724981776201,3,31.553162533083484,3,99.19692963917733,1.929278350603337 +9,8,15,14.34320488,94.35734702,7.994465371,110.2223123,orange,21.181482515177116,2,7.437795594195423,19.803440504500024,367.252320595142,4.487539325509476,1,8.640195657799072,80.11399442801658,100.9614403749539,3,17.39868293379802,3,24.25051166013995,1.8380848361249518 +5,18,14,33.1056981,93.48447453,7.434118807,119.1709113,orange,15.53648439502464,1,10.736894523574657,4.418354103839739,366.3763492368787,1.806217077941277,3,6.200889266973281,40.161169051925974,147.10704852975869,3,35.37955804118021,3,72.01179843603326,3.833708111204844 +29,25,14,30.49183837,90.4582865,7.781988584,113.3302105,orange,15.540157561344648,1,11.38975876585445,2.6953499542897297,363.7281182779157,5.2822752983622,4,15.758517471143774,83.86533690705689,176.2610977771576,3,2.9100869578331245,3,94.02367370079484,4.8340036312968975 +33,12,15,30.25578031,92.03272799,6.052318465,116.7173125,orange,25.18414908131166,2,8.841332956391252,7.096222161608314,393.2208465382737,6.286496728226956,3,16.92965587233621,71.83185882714704,50.575000290818906,2,34.75748936939234,1,52.71860897017806,3.598348174635466 +8,16,6,12.22816189,90.26457428,7.106650373,108.4161706,orange,25.073403819008497,1,6.666063585709995,17.882781174570784,405.57275153310286,3.261230064907643,1,18.560141171085682,33.1872188296365,187.10537729212092,2,17.565320274772322,1,98.40430772431009,3.851433418383737 +15,14,8,10.01081312,90.22399223,6.22094286,119.3941064,orange,19.209226074995136,1,10.913494691906799,9.258281069068037,445.85170016482664,5.780389747461326,3,11.294654999619393,0.7968872666747617,183.63879303410212,3,25.7767381446179,1,42.82834829120782,3.348017298965436 +16,7,8,22.79196751,90.60901895,6.420457311,116.5084074,orange,24.595538730717397,1,9.580872817965432,5.330284110429218,363.9307984592116,4.527172622611916,2,18.36532659824169,37.416680135213035,136.83634986946586,3,15.21701936811713,3,10.862265201735378,1.1207582795350195 +0,12,7,20.18432263,90.65458473,6.969249676,116.8130969,orange,26.909881697175123,3,10.559544927778472,14.572726228000326,398.78482592988223,4.4651282492677975,5,16.02713438900652,22.687324298809276,96.95276030578059,1,23.46979064359252,2,74.24751874738308,2.033096744375815 +5,25,6,30.72119881,94.01331956,6.011302181,106.8118019,orange,22.79244210343155,3,9.748895290710367,6.315816079954568,424.7288607368821,7.337118380898586,6,6.296548795435126,95.35509045270324,125.19015966383434,3,26.805479371396366,1,47.33684442247203,1.7692104742850447 +6,8,11,24.35590861,92.39651663,6.600948788,119.6946577,orange,28.698326575942925,2,11.480737028284894,18.67330046216329,446.42943364563695,6.319000642195205,2,16.126241838258064,29.27905759198883,89.34279936720634,1,1.6043705227876293,3,44.27944786611202,2.843978950318198 +10,5,5,21.21306973,91.35349216,7.817846496,112.9834361,orange,29.417382729870795,2,8.858378115096784,11.423919834585725,418.10824529855466,8.876126817210217,5,14.951419950917282,66.84309430569877,147.03936237480713,1,13.968036875310307,1,12.857864249253137,2.257118014353904 +1,17,6,10.78689755,91.38411917,6.8198271,117.5293447,orange,19.724745791936545,1,11.478897800573964,2.270295509396665,409.71141988184047,8.60163353359939,4,16.283006596891767,65.46069173007292,92.09545066731167,3,19.51570861844315,2,31.9520323507658,3.633153560981519 +1,30,10,11.89925671,91.34663797,7.291405641,103.5771468,orange,15.35845327102689,3,8.708804364311758,6.432159323526712,353.91442476933975,6.474478096213587,5,14.06346144856095,3.3969950129027815,114.4325132652738,3,0.14164494139790595,2,64.81361309988972,2.9747706920685144 +0,23,15,22.56664172,93.37488907,7.598729065,109.8585753,orange,18.269990490067194,2,6.7380793492675375,1.5749297677061658,414.5950625533006,6.485464692601147,3,6.92326640206021,13.680753741687157,70.9905654056479,3,29.18793875424081,3,9.723331738962848,2.65779162395643 +24,27,9,18.86883219,93.24688124,6.157135092,119.3936976,orange,11.21451611986409,2,7.702836020860988,1.3147801655058355,368.0124981990626,1.2276225973497468,3,6.695035052254912,30.187275860883023,185.3772992005613,3,46.93837180747231,2,33.86781455784733,1.5215500755213114 +36,11,13,17.34083741,93.04897191,7.1917274,112.7194284,orange,15.44449160051871,3,11.795494183700711,13.783475710201564,387.8222179503325,2.5771201356813056,6,15.72625617903873,96.01580620642608,75.79029387760536,3,3.4513361573214327,3,72.4814574786914,4.9623749661681344 +40,21,8,34.90665289,92.87820148,7.418761774,102.1906333,orange,25.40678677362972,3,7.210015507968928,17.79457251627108,408.66637701380034,9.036034735995337,1,14.625995181574915,83.32897883579858,89.24663345599373,3,48.80631028614603,3,26.989926709604372,3.0539152073632194 +40,22,6,24.53610067,91.90997228,6.488221135,115.9787989,orange,17.067900874513,3,6.261862244192833,14.800355293734208,440.6841595601946,1.6525507333498413,3,9.613227559064844,16.64939691870626,143.83393670900392,1,17.66579816691674,3,45.84171089277081,1.3445933753595685 +32,18,13,13.8377282,91.74780462,6.044167236,107.9873218,orange,16.876425328309097,2,9.36382328513665,17.40035305882657,419.7294128123874,1.6394300641299677,1,7.580221373353777,34.451966058248594,157.6281074590163,3,38.807426802156506,1,68.4152749300404,2.462070566678687 +9,10,10,22.3551049,93.52211892,6.010391864,101.5164589,orange,28.399772046687875,2,10.653238823796048,8.687350837169262,416.14392850243024,9.04387955399735,5,16.651399796083403,14.428318793715821,138.27973225942475,3,25.178810839699317,2,60.32481976290992,2.373822352629356 +13,16,8,34.74004942,93.12316972,6.949838549,100.1967854,orange,28.580723922361642,1,11.706888410405455,5.717466966588169,415.10146996862863,9.1137955145022,6,5.875869792731601,42.450731077778215,91.57575487446013,1,36.18637295440541,2,15.609798534695884,3.413619340137101 +15,9,11,11.54785707,94.14861001,7.907956251,108.8289171,orange,17.11827360971662,1,11.971045780920512,6.847881058209369,404.43869739904096,1.129856704252913,6,10.281415302889293,78.48717442795412,107.88225859661671,2,16.503529694818848,2,70.68077128376137,3.8429218152689404 +29,11,5,23.13338811,91.94670335,7.639788459,104.4224145,orange,20.87396807319682,2,7.616996239200082,5.688598550380735,410.28654275614974,3.3509635448773247,4,11.910097894762586,11.472389469661493,74.10143217829099,1,40.77750511182843,2,43.298687026046956,1.6053512121616156 +1,15,9,29.98364695,94.55239717,7.53350946,115.3560318,orange,15.532948055771902,1,11.113951510497884,10.154415155035947,435.80862839346673,3.31150124492547,4,6.0555133816011875,7.847222919683839,89.48404263562622,1,26.565047735840032,1,81.83857283391397,4.4557628118522 +18,5,11,20.87947369,90.93756231,6.251586885,102.4550786,orange,16.398181219278232,1,7.511546293823313,11.722983967516472,406.5161104918442,5.0900634795321755,5,11.233066575375597,87.89581589546484,72.82220008131874,3,48.029160404047815,2,35.67409507494806,2.8638056898191477 +14,22,9,17.24944623,91.13772765,6.543191814,112.5090516,orange,18.83368125391216,2,11.768008548015032,1.4879144892807172,442.64260833650246,3.6747245879814203,3,9.2644040706104,35.08684140225905,191.58883746638037,3,43.89248362199549,1,29.249259860061528,1.8062121109731994 +33,15,7,15.83388699,91.68293851,7.651225301,109.7571416,orange,27.152322635424106,1,7.892048333285752,3.4335021092044093,369.14091436018316,7.584044295156777,6,12.354686535187465,56.64746342450406,114.59086392109123,3,19.362254054353034,3,82.12315051263278,1.2358251523368189 +4,6,7,23.01014302,91.11764246,6.708889665,112.6738296,orange,18.55664335060832,3,10.99382106534437,19.871115531315773,435.1333793125931,9.321260818615375,4,17.98275130314844,56.64360918856618,65.99854769206468,3,14.465073224966313,3,88.46440806064892,2.3242347834158372 +17,16,14,16.39624284,92.18151927,6.625538653,102.944161,orange,24.289082081608385,2,6.69379390586635,7.626637607719586,445.43504502966135,5.101994471824836,1,15.49771957577355,60.967159303999885,185.81229799325797,2,39.725170782024406,3,68.97798630695648,3.0674816297608913 +12,20,10,24.45132792,93.10527686,6.528354932,109.4711098,orange,12.90586964418318,3,5.654151819909976,3.711491053259328,395.6033776988041,5.086627646753048,3,12.190252871687047,92.57190893243364,52.065936254978766,1,43.46120027198124,1,7.6896563615334586,4.098692267021198 +34,29,8,31.87859192,91.15248149,6.450640306,105.3437825,orange,20.95082841874664,1,6.356475642247812,0.4749767809801497,440.05436710799574,9.035216619470686,4,17.560435418026735,89.41115034395668,88.55773094043481,3,15.21077146764933,1,57.683129018582434,1.8882487385422637 +39,28,10,31.34920143,91.48247612,7.181907673,109.1549823,orange,11.417388819274958,1,8.169594935563422,16.030366863059395,383.48786843989865,5.062599712264464,1,6.931793252519581,92.06873852251721,62.14217555060688,2,4.233709674900959,3,99.6711833584999,2.441067173312076 +31,25,12,18.05142392,90.03969587,7.016482298,111.7793889,orange,11.170926213548938,1,8.954606384703032,9.449275432238402,434.99215487370105,8.983904296848353,6,9.175239439364876,33.94903913879806,166.23807531343883,3,46.907949366284534,2,1.8519188576182177,2.4332455749321715 +12,6,8,30.84835031,92.86773675,6.388617138,107.4142681,orange,12.740281803182636,2,9.340071976673787,7.302321716095683,438.1066017903882,6.992863044569792,2,17.542047890482092,51.36085243080865,194.57941698092668,2,17.423743826131428,1,76.82174145174606,4.213638731828076 +12,29,13,22.45616931,91.52781832,7.57125447,118.0069295,orange,23.109576281045193,3,8.500433767004509,15.696301737879262,375.77880003506203,9.011863606320459,3,14.209690490435735,48.94438553454279,117.53397303357163,1,6.980330803421742,1,16.173813941752723,1.3749037296360882 +26,11,11,13.70319166,90.95589386,7.609348255,106.2944879,orange,20.54809814425508,2,6.444050705537716,4.149650513655145,367.19701820321575,7.463924406158924,6,7.142349060158192,50.36402731589222,132.56550711881744,1,34.72324376152116,3,96.08959201847739,1.8285835461149418 +19,24,15,20.48954522,93.72485075,7.137136973,111.8391951,orange,16.501703945706403,3,8.659614756162684,17.63063548516436,386.49150599849855,9.228068662016668,1,8.687252019497576,76.67992739258796,145.50793722437163,3,28.562472633108264,3,92.8078915111719,3.0121732861924264 +39,21,9,13.20844373,94.02769434,6.354022554,106.2696156,orange,22.392835420014194,2,11.751601563416376,11.595938890234539,418.87789018994624,6.262045734707961,4,18.978082353191496,96.35201995676985,53.51958225515938,2,22.600933826134284,2,43.545487375284885,1.5888352355132445 +16,29,13,32.31944397,93.67804556,6.196907944,117.6236473,orange,25.085537537321954,2,6.49299939240038,19.609114911456494,410.6477140827169,1.2509433219663904,4,8.494867752981106,87.36195926298227,150.30286046657392,3,35.50360862680123,1,16.353592760953028,4.3950130812828005 +36,29,13,20.68185224,90.91510525,7.829507245,109.7513927,orange,22.445022425005092,2,10.0860109211849,15.351356743375618,387.25174140270263,5.781025588047305,5,9.739675976754972,25.997072512920404,154.41193582304,2,14.05083979751574,3,56.7143482205333,4.604491383086153 +37,23,12,31.52675982,90.50621806,6.395258356,113.1169398,orange,25.43151559963759,1,8.984180250929612,16.16180591833384,354.88273356929244,8.40843928213271,2,14.125397025429086,89.85704123536237,65.0409171983259,3,48.55123189669431,3,54.301643121103304,2.506593669963315 +39,9,15,25.35467646,91.81183218,7.992041984,116.7555937,orange,13.451100527527979,1,8.801889520430612,7.834730825810732,441.5990458292334,8.170814999508394,1,19.908104210845423,53.42581604273447,89.49092365451492,1,20.32132420591245,1,55.91429975695262,2.581088618326298 +31,5,14,17.66545409,91.69865887,6.583411671,110.6857506,orange,24.63273222117275,2,8.932815593544875,4.719546057011696,444.73265782731175,5.952679051463292,4,6.597333083396782,40.99961880221786,113.38844856342548,1,18.547290393583932,1,6.838801095381985,1.3581024920315081 +18,12,8,12.59093977,91.81668769,6.206053072,119.3916718,orange,27.154031934792112,1,6.762180113710674,10.24435421702012,424.1857623339886,1.050747468592599,6,7.427601834888229,72.92305231027387,55.32030300915854,1,21.011290484585523,2,83.1087785412069,4.005910025367338 +20,20,10,11.86631922,93.68394562,6.976997772,106.060149,orange,12.655452806967292,2,6.662520579307481,7.527889802251533,430.40056605529213,6.448828397710349,5,14.719515613495986,35.622518707622916,88.93148007885189,2,0.251000860857703,1,89.6654480408335,1.570648787569879 +5,8,5,11.03367937,92.22706805,6.562594972,112.7715925,orange,14.717822426835927,1,11.186505684314644,11.225745040940433,358.5474689895075,6.5712565836006895,4,8.707565685061398,49.13614302519179,152.74213974097623,2,21.414098445610634,3,75.70951781701228,2.7996056927889295 +20,8,12,25.2990432,94.96419851,7.260416405,117.9733424,orange,16.20937179133302,1,8.371574695234713,15.86056052695022,401.9603378965288,1.1448052161569588,1,6.539236550523825,93.0686963601568,162.02079593347247,3,36.567692920756755,1,95.98977742772796,3.074705668958859 +25,21,11,32.23797837,90.15406807,6.460044778,104.7052254,orange,22.92781897921299,3,5.000710080682302,13.124342810727505,410.12016986716264,5.869718048404425,2,14.59148690402554,98.32453762959287,112.80201618444775,3,29.619525783010126,2,75.57690417949613,4.836127959217324 +14,19,14,17.68408797,94.35815354,6.699164936,108.0638166,orange,21.559579416776657,3,6.0096615538817675,12.727896256492508,354.6239250964563,8.997240622298271,1,9.71295181256832,43.934765931993525,58.999059259472595,2,42.223744860809326,3,17.90409370930902,3.4236055170858277 +37,18,12,10.2708877,90.19147747,7.401121811,106.6955204,orange,25.887347343767445,1,10.355202962513575,12.39395894059313,377.67148291309263,5.481284597057175,4,10.675260798826514,19.34727528480462,188.5975130541039,2,15.166063743280917,1,77.33427498190974,2.0243416320660406 +26,15,6,17.22034507,94.78797376,6.912033409,108.0054343,orange,11.861813428727714,2,10.729333831577774,0.8399640912733086,360.1742264957766,7.666731264334597,6,5.712658741012413,71.63868014748995,87.60129437319478,2,1.4795347064884223,2,2.528538799708624,3.594436144785529 +13,22,5,19.667056,90.50096668,7.764040111,100.1737964,orange,10.355936537780083,1,11.750802012423375,13.274567256926307,397.22509852364834,3.6822550471145483,2,15.296518295046582,2.2147900278036814,136.52812950472273,2,22.710003136056127,2,52.40966895683095,2.6155018300008064 +32,25,9,10.35609594,93.75652041,7.796034006,101.1456947,orange,23.61436403669359,1,10.899322802178885,15.541522450409177,355.0610845574044,4.7032077466614854,6,6.469321282341008,52.76661604139462,124.67794503336515,1,30.929859934318944,1,99.5404036802573,1.7693760976960973 +19,7,9,27.255435,91.71369387,6.969883483,101.139435,orange,16.42877792270614,2,10.764748606168567,13.975093807954803,375.4028471018693,5.335701217564553,4,7.1258708549672445,73.84948850374343,102.53898626170165,2,49.5873913949624,1,11.409450972799162,4.639499899533814 +28,7,9,34.5917846,92.13229786,6.730757538,115.5650287,orange,24.740204926385942,2,10.047253504017728,17.88530992724566,376.59148566381083,6.5549251413833804,4,16.802034184765375,30.887002304879484,101.21400155139924,2,19.07171393255254,2,14.492699234510342,2.0984153728082933 +24,30,11,32.39523995,94.51768464,6.601395755,113.25373,orange,21.525716715643995,3,7.996244143718383,4.203030333504345,424.0717114345999,9.064114726747224,1,18.927473997799098,34.144506700398225,120.89310430237936,2,29.28802688928845,2,22.887870506133602,2.0614103878407315 +7,17,10,10.16431299,91.22320999,6.465913274,106.362551,orange,20.631926709490557,1,5.58174605264778,1.3232647114493012,428.7289405006187,8.360252482739533,6,14.307530808593446,49.24770120075143,93.65842811310648,2,38.66123242490566,3,2.8531764492088274,3.070001259540684 +18,23,8,21.49118657,93.43949693,6.41354791,101.4819888,orange,25.316237537597253,3,11.179797230921329,19.133791324607024,446.61504908231177,6.433390896154507,6,8.654212700613044,13.416580343785734,117.6780472521611,3,2.6096072744295764,1,45.81780590631021,1.2630426338034582 +7,20,12,16.53460397,94.76759975,6.475275337,110.0447896,orange,28.583340186150693,2,9.864223862655276,1.734824222199054,355.3336258630668,4.61309432041703,2,12.636172350267554,85.08895259939267,169.56510545966745,3,35.08375395186483,2,1.04188272944995,4.0354547537709555 +20,23,11,31.8520694,90.12220323,6.407715561,109.9455062,orange,20.02249345533638,3,6.874258310635085,18.666201351896497,409.484642538185,3.116208477313757,4,15.211573187022479,57.326149065205314,182.26869813426958,3,14.551559803824404,3,37.82239711955726,4.9653417509013185 +18,14,11,28.04799508,90.00621688,6.550814117,117.1311498,orange,22.869532887274897,2,8.587313261994838,7.922319446652121,387.84634290242104,7.779835713220713,2,7.1102644709296605,64.61569832958212,92.14397378109769,1,39.65138581681208,2,26.654595488460288,2.947492918417998 +34,11,10,31.75048899,94.59551226,7.36220835,115.1989301,orange,14.254746874307285,3,8.960905953506693,7.566988593463185,383.4597979845663,8.740819600198664,5,13.624906828981723,74.60810283473947,168.49897510630896,1,12.175771911781268,2,43.17363104938712,3.2580766764831663 +20,29,10,29.07412717,93.27189064,7.36549204,100.7896871,orange,25.539072244651912,3,8.326355753647563,1.3105386082768877,434.4716514175756,9.012403832897608,4,7.8438639958009,87.45876366000755,166.03620633787108,2,20.17134735669679,1,19.777216228956817,1.272364247112745 +37,24,13,19.14381903,90.71037456,7.8546243,108.0230792,orange,23.599825691650366,3,7.334661089219814,16.28699907996174,400.2016697565316,8.487696549685722,6,10.084859142084298,43.55435354770469,79.44622616438309,2,25.571541901878405,3,54.93452961416415,4.372006929395216 +12,8,10,16.14820285,91.4448027,7.995848977,107.4287664,orange,16.894401696679736,1,11.45499524273139,3.041298621958042,440.682271389205,6.379016067384335,3,18.37894166031314,51.71251281440138,191.83888015193796,1,44.60141807255605,2,16.78358364946464,2.966327329656014 +34,10,14,34.05296914,92.05811721,6.725600855,116.8020848,orange,17.506104900338258,3,9.114820452651495,8.45336227560044,358.2793320388245,8.035496694272082,3,17.141056005643904,46.767643466717104,170.98431012417248,2,22.35500151470561,3,80.9470779703431,1.2580828402296014 +6,13,9,34.51423957,90.56151463,7.786725333,118.3271968,orange,19.163308249971102,2,6.819156019852653,8.839790083656265,362.10524316418224,7.5323717553088425,4,5.623301400882795,80.19024787724877,171.42217387702792,3,38.13603118619196,1,16.198476173144993,1.2401881162127695 +27,30,5,32.71748548,90.54608254,7.656978112,113.328978,orange,11.768006354308664,1,6.9699264699318935,11.316674401678116,359.41147411181356,9.716501139302677,4,16.31744074513373,9.89526588465981,166.71645261209306,2,37.610386505085195,3,75.31940044338373,1.6113025466327913 +13,8,12,25.16296632,92.54736032,7.105904818,114.3117197,orange,27.860009940953976,1,9.97886116575005,17.831446868567294,374.3814059451658,6.873719327038133,6,9.360785218696265,62.68066235280879,180.46991613636726,3,9.696227166782394,3,33.693318780117146,1.6513751622031072 +6,7,7,27.68167318,94.47316879,7.199106204,113.9995146,orange,25.60735510337522,2,6.967093237374402,12.129292441528431,357.65056473216436,1.4228640212963475,1,15.42018748878223,3.9808351658158103,152.36010018960314,1,16.022261879889665,3,72.56256925315027,1.9593741859894371 +40,17,15,21.35093384,90.9492967,7.871063004,107.0862095,orange,21.39328988700335,1,8.711074061499891,4.518362357829813,410.6515834598084,1.220154370379202,4,9.362445434811189,43.869084369758326,175.70639527144527,2,30.447488064278883,1,22.997895059095384,2.147489768866198 +31,26,9,11.69894639,93.25638873,7.566165721,103.2005992,orange,15.937523119314164,3,5.678524831557894,16.37610434128878,387.1922370686192,5.417500347773751,4,11.642705479373998,71.43570882742895,193.8331498056675,1,9.229531219906828,1,12.432670447790084,1.6675712023085776 +61,68,50,35.21462816,91.49725058,6.793245417,243.0745066,papaya,26.906733431816313,1,8.094210582447051,2.0660174985644875,446.9325387470367,3.920678375657516,3,5.086447882270382,86.10868470482323,66.74536138917273,2,14.936392741512616,3,57.95921654296209,3.204117994026548 +58,46,45,42.39413392,90.79028064,6.576261427,88.46607497,papaya,24.630705355725333,1,9.838624617032437,18.190395150816975,363.06215302282817,3.3132409707557793,4,6.490374642651632,68.01188642108986,50.59066760979931,1,32.90311473854091,3,11.799474243377794,1.9022084360826694 +45,47,55,38.4191628,91.14220381,6.751452932,119.2653877,papaya,17.689887685589287,3,11.175264150700041,6.569357454569538,407.09745971095145,4.798338484512312,1,10.176805108419803,0.8654756258006935,125.27353873095547,2,43.05637000038992,1,63.34066977781042,3.466943518981163 +39,65,53,35.33294932,92.11508608,6.560743093,235.6133585,papaya,16.71903240081658,2,11.151482968541915,10.815078076407067,351.5093383830188,6.072254754974239,4,6.598211906090004,57.01665059662613,152.79247734549136,1,20.573181973892574,1,61.27501270590579,4.524903668201279 +31,68,45,42.92325255,90.07600528,6.938313356,196.2408242,papaya,15.801002180552725,2,6.026434694085877,14.078401611270515,400.4953542179545,8.991706634831914,3,17.860215516125063,48.90689955107425,69.10336413976074,1,11.878644674996808,1,87.64536289421878,1.7044095180990748 +70,68,45,33.83508569,92.85470152,6.991626158,203.4044028,papaya,28.512721027027055,2,9.542551013372059,9.224507775402422,434.0869495846282,5.254636730080238,2,17.59844998412885,32.77240037238561,154.32569950263985,3,21.545900102369757,2,58.583438221532006,2.187542885444324 +68,62,50,33.20258348,92.76437927,6.977700268,197.5282582,papaya,23.133311760793937,3,5.206002585320413,2.7220625942126286,351.4400030229795,7.86238864701007,6,12.406974006437828,69.50674197633258,108.49517382088933,1,11.735814165293856,3,61.0481026527256,2.4017162933623766 +34,65,47,23.48546973,93.71043692,6.833768535,191.7760562,papaya,24.362217189717786,3,5.037598914860304,0.9506230913500091,357.86444260247777,4.444785569908624,3,5.407246224312809,95.99603278086792,59.532249196801075,1,32.62218355104567,1,50.81909003287165,1.7630783986805638 +38,68,54,29.33710543,90.81781439,6.739170045,202.0572747,papaya,20.578068710416716,1,6.833847467393654,18.145297492281568,440.69803506302543,7.410568087358063,2,10.072509415060125,23.114126992495066,107.33412752411095,2,14.367363248491577,1,76.35230042592845,3.3772346047797104 +69,64,47,40.21199348,94.50766912,6.993473247,186.6762324,papaya,15.998537217446074,1,7.3166649415101475,17.273196082461514,405.3691060633182,2.964596839625187,2,17.283527910863036,63.22546938807508,78.34577613332343,1,37.69922958191017,1,15.3120703457867,3.0943473529472554 +58,51,47,42.13473976,91.70445386,6.757470637,197.402901,papaya,29.96510053555073,1,8.091486437429555,19.334668986797283,427.6542033390301,1.7356708014733542,3,5.464154838674412,54.95685805073961,124.1169805911808,1,24.029489466466575,1,5.161945204883378,4.895614819383168 +59,47,53,32.86316618,91.4618874,6.850663232,47.271547,papaya,26.025871410582305,2,7.415531516033113,12.283523789446525,410.8754041553115,5.6381706484143095,1,9.505507741923022,38.40749139477475,186.5566680066508,3,21.202105698521724,3,5.92244288790903,4.513311803384234 +44,64,54,29.80744318,91.38048469,6.74274935,232.7046126,papaya,11.758750963342976,2,8.360162510605644,19.00724781436812,439.15025429327636,7.291198665003789,5,7.193765811567969,99.47548862979225,75.79408069846781,3,39.434818193305574,2,56.9683301313197,4.455614137052466 +56,57,48,31.56213762,93.0484859,6.506120752,63.62250788,papaya,13.076911809501063,1,9.659630983448285,17.759614934585414,431.29710584392524,2.238405727159805,5,12.98619881435225,65.89579442962766,181.59828241549238,3,9.99030023792492,3,40.84458192147834,3.735124605425648 +69,60,54,36.32268069,93.06134398,6.98992719,141.1736926,papaya,27.59924349991253,1,9.9096545383974,15.987677774702107,371.9735878548882,2.041180424074371,5,5.119143674990672,54.67695373423778,81.17609615791619,3,10.88439472386979,1,48.49318090893677,1.2544298567877963 +56,58,49,37.13165026,94.60761797,6.69215564,172.4788062,papaya,21.47270928827209,3,5.2166001960276125,14.25633099653329,439.94582557217785,9.936981461252437,1,8.781959167896172,85.18943562238923,120.40306872208537,3,5.459624815826652,1,17.40881027102068,2.805362862489642 +49,55,53,38.4418717,93.63739039,6.544029776,77.71566883,papaya,22.71343868769598,1,8.647468397564543,18.77641232930091,389.99455344596447,3.060253670188509,6,6.527762889353231,42.46925257979663,142.17322355458816,1,36.30348835818834,1,93.12351227250657,2.2826201878292416 +38,51,52,32.66160599,90.78931681,6.927803911,78.85085502,papaya,25.44367907664966,2,9.134143278405844,1.1084683310541577,412.60519392092544,7.5510876242909655,5,16.645852250142823,79.17011358573075,102.1504071970941,2,25.90220694201879,2,7.833210983014283,3.035429616070879 +54,65,47,27.92765919,91.55594211,6.721835879,149.9107557,papaya,14.28098187089007,3,5.354143026222712,14.494575976548925,394.73910909525534,8.388763233400805,6,7.934476763413258,46.806362427064066,154.58206965789654,1,6.693182383138313,1,50.6261137422763,2.0287466663480376 +57,57,51,39.01793345,91.48815629,6.99223441,105.8841531,papaya,21.400903958536936,1,7.844285145399443,15.35247668810769,421.87255149973345,2.2279379242233213,4,18.170863322480024,24.70869650119084,184.73272952225392,1,35.70284525558453,2,59.05815825970737,2.198677305316353 +39,52,53,32.51247398,94.65904123,6.704204398,51.07048113,papaya,27.516807825357716,2,6.992055418883276,1.890310008505014,378.9777047711418,3.516676472381503,2,10.349963216486518,6.960984551302696,147.14808723458407,2,33.12847621860298,2,46.68542083107605,4.913667981122673 +58,67,45,38.72382798,91.72514851,6.702424548,62.62377075,papaya,12.320207864219695,1,11.773380409258277,2.316834685609477,382.4298910249767,8.33050988302454,6,11.08262795129849,0.5227106537737614,197.05595676081188,2,29.236995857587484,3,94.5746511094091,1.489841654607448 +61,64,52,43.30204933,92.83405443,6.641098708,110.562229,papaya,15.672019150218633,2,10.030074710130775,13.44020869618549,448.4191630291825,9.840912677984292,2,12.660968660407129,47.59342342240968,114.86358735953718,1,5.142477751892111,3,2.3462482287189723,2.0596477209071975 +34,62,55,27.58548913,90.72526502,6.585346229,238.5008779,papaya,26.472415085220625,3,8.590445796367279,1.5558427117537854,360.673574659591,2.8688470412931126,4,6.3372051053078255,60.72385480187431,94.40254529005638,2,30.396497199810828,3,28.499070714580622,3.717652960445682 +31,48,45,40.78881819,92.90951393,6.563134737,132.7923586,papaya,28.25670425292909,1,11.458037887774132,3.658048811152408,439.12809790291857,8.591694555751893,3,5.081397657201462,60.63446560673727,170.09907330161417,3,41.676474464564805,3,7.080324140033422,2.5622868585316074 +47,46,52,23.19451074,91.40301608,6.502289473,206.3999208,papaya,20.606943190508154,1,8.726609010417487,19.32264605654963,379.57753443749294,2.9426436364263338,1,18.400506682900755,14.793830305625299,56.41980671575871,2,11.26188457057985,3,72.99377525855077,1.2159752419855931 +32,68,52,32.68067385,92.61715632,6.800321319,248.8592986,papaya,10.219504284814157,3,6.528084248073613,2.7023307566143107,378.99029642474386,1.8489564520902593,6,6.708324762602832,19.676514114086675,60.56229027196673,2,27.800775879554564,1,66.17521865360345,3.205485314166244 +36,59,46,34.28879307,93.61082872,6.721130543,127.2509777,papaya,23.422196413132237,1,10.576337451363687,16.702058209596892,370.85797489597354,5.84110010299529,2,13.260582832511524,91.85179332529364,101.46591411909165,3,25.249274374987195,3,32.65532705631999,3.761597949368809 +61,51,51,39.30050027,94.16193416,6.574677594,120.9512466,papaya,20.59763893573932,3,11.189501728091354,11.623244947074433,360.30001444180607,5.628031564274114,3,16.011833019515777,70.17442105783033,53.72585695826199,1,10.249037880259749,3,8.997141415084531,2.7822301947388 +70,54,46,39.73149053,91.12220596,6.919342407,122.7628653,papaya,17.24999906685082,2,10.637673626098021,8.431380922679798,380.7922374145383,7.215520273330089,2,14.015378362192097,40.39908931779029,113.51330147935133,2,34.38434019612725,2,52.53853843441071,2.882459460578002 +44,56,49,39.23342464,91.25589286,6.519779583,64.4478499,papaya,27.566634062193124,1,7.352670480122367,15.114052095897318,391.4204131931389,2.6888545478662484,5,15.643539581496574,11.375796551074458,85.82814174036538,3,48.62571461609988,2,29.316157901922356,2.7249647544869933 +34,68,51,27.34734861,94.17756725,6.687088098,40.35153141,papaya,27.51495608001963,1,5.773551576938972,6.7181941504499605,404.93839970571764,2.830494856107055,1,6.486628046071109,35.00164519555007,191.3005440783266,1,7.181554809152896,3,18.5181708109026,1.0346472637902475 +50,59,47,40.76998685,92.09278584,6.747975732,209.8678411,papaya,26.183722397131493,2,6.665055808453818,12.992449126652554,421.44695223860947,2.6742102193299173,3,7.832651727915643,75.42079335688204,129.9997512190551,2,3.7551038389567526,3,4.759044689874747,3.4096438290776216 +39,70,52,26.26559543,90.79668055,6.65149129,59.49373381,papaya,20.285976168436644,1,5.609673138826256,5.010912993994965,393.70959058923677,4.65996897605013,1,15.703495896172328,73.95022853040551,82.84986088221976,1,2.1014026676201114,1,62.829604346663125,3.210794024330161 +34,61,49,28.12971499,93.3210737,6.502675132,117.8201907,papaya,20.130745125682004,2,6.564028474815308,16.534473966776076,375.65245835940925,5.087438590623886,1,8.905278575896794,73.68068397665377,163.98884187683456,3,40.36432662227764,3,15.017200746442194,2.788066871961795 +44,60,55,34.2804607,90.55561637,6.825371185,98.54047745,papaya,12.809403007775856,2,7.8275319751854795,1.080898815719138,401.06867047390074,4.418145675284098,2,12.60845435673258,83.12929147372763,114.98219635444806,3,8.80320267414722,3,75.26073973287257,3.469455773290845 +31,62,52,33.7960155,93.00754254,6.99104104,182.026807,papaya,22.964571499729807,2,11.930850096151314,19.48137663130575,366.30591606235316,7.580859339817239,4,7.202646078170261,17.19012917426086,134.10739186977622,2,22.024747121528026,3,15.364204558712114,1.8172802056863455 +65,62,51,31.53243779,90.87394933,6.511624841,207.0735119,papaya,18.957775711030546,1,11.5911098887517,6.67949597320423,406.14212451613696,7.208279182835694,3,11.319976021967491,74.44860891473986,57.82081369386991,3,1.4138136359547526,2,70.05643133376056,2.9059129255087033 +44,57,53,42.30495821,90.51431779,6.93172108,74.876786,papaya,17.770429913323206,3,7.276931187299709,7.50676031375201,383.9926581856439,3.876440498844548,3,12.790605998482658,6.04008008220659,134.47108304361345,2,28.61957387713023,1,70.22310069561225,2.134185349751638 +50,47,48,24.63676897,90.61964344,6.712772333,218.2299187,papaya,25.068816466184494,3,8.662425238583827,14.183036513709075,401.9428811151547,8.672049259031706,6,14.876171486223594,14.688525995303737,86.20303706454222,1,19.537614002021453,2,76.82706844145144,3.2544395971297897 +43,50,48,28.28222883,91.37059792,6.63016515,179.2720807,papaya,24.540671714294106,1,8.389691867117223,18.83898870736472,415.0911056518783,1.1404350027543855,2,5.598452534399535,90.69093242038544,170.1647152006396,3,5.49256345807253,1,91.78224171437517,3.8076959913999326 +60,46,53,24.48620746,92.98254537,6.761953186,183.49095,papaya,10.813331473912516,2,5.806257740065444,16.335482367717262,359.6413904532819,7.525397644638131,5,19.313643469309497,41.637161690102474,158.06994422078694,2,38.53714625685462,2,58.25969184738252,2.487073272913899 +70,68,55,42.84609252,94.63548176,6.691202286,78.8099639,papaya,14.210942109074615,2,9.241738751752361,2.7449025659840487,369.6457490373534,2.755744736713435,5,15.127758283757213,89.03373395819946,181.66015259294798,1,25.605297254845205,2,80.84732996195902,4.0040752376835105 +59,62,52,43.67549305,93.10887229,6.608667684,103.8235658,papaya,28.041730230148186,2,7.165073062005311,4.177753373762592,358.4591609771533,2.540372573126564,4,12.65293440926693,22.14552295132607,93.28564001541702,1,36.743496476225395,3,78.27637874114806,1.5709683922364603 +60,58,51,42.07213781,92.92203105,6.840802254,165.7412972,papaya,13.498886949366664,3,9.941021228876401,19.4321955183357,375.81662396986087,9.402403445699731,4,19.892158274465224,0.3217760729364971,143.50055339798018,1,24.241040301227617,3,63.4699931313985,1.6773411482611893 +42,60,47,33.46873719,92.12746225,6.834808348,136.8277041,papaya,29.622119391729193,1,11.547187533828355,19.742814426025763,392.32152437216854,7.1922562698004135,2,8.978021378558742,63.630787477322095,63.53919782739117,3,44.57212810031251,2,76.12493818616466,2.911644709444103 +35,66,47,31.7018373,91.66232213,6.953439161,48.83810592,papaya,20.921706614577328,2,9.434376180303488,16.61131301925184,423.81430538338856,2.3565205093486394,5,11.94038729292574,83.19017242246758,161.26803363553313,2,8.629335603992056,3,96.23414057549489,2.4291314522607563 +34,65,48,41.41968393,90.03863107,6.665024508,199.3096432,papaya,18.770621098934726,1,10.856886340709183,15.736742020632375,357.8232048174297,5.302015968926105,2,14.063853511368078,32.28991247337195,98.18447734746894,3,39.79718856062269,2,2.6882951078088357,1.145611131528693 +36,54,46,42.54744013,94.94482086,6.662875839,214.4103848,papaya,18.03068164167901,2,11.514039151714515,18.22558548134182,445.3386353659589,3.232496516349841,3,11.74028434493579,44.77956453129058,177.31715702677437,1,41.69235353970873,3,7.025568880222687,3.353358995520278 +39,64,52,28.91842453,94.63676767,6.678695788,63.68794608,papaya,21.730712007454187,2,7.761420487723624,19.080889220396656,411.33613034850794,7.778507643858717,4,9.938197664559569,4.95235848878367,112.80966771998814,3,0.71324937125537,2,20.54522016954997,2.420171713873082 +37,52,47,43.08022702,93.90305729,6.54277684,211.8529059,papaya,19.46664456625801,1,11.199261682818033,8.806075323720691,441.05031329217735,1.2017990819681237,6,14.609822870805317,73.3965229109283,65.17159146098425,1,40.943491933765294,2,96.16895525838733,1.6717061744282304 +33,47,46,29.20300896,93.96834049,6.839443833,209.4083305,papaya,27.678201472007082,2,8.521516084785803,17.702169238439,402.7840041902042,7.89188269914041,3,9.043884867843012,57.977666047433566,94.00318740089085,1,29.77358446231071,2,60.166297257067136,3.6237414558172927 +34,48,48,41.04224355,91.37258067,6.805277038,181.527598,papaya,15.734266193167041,3,9.735432027750162,9.432378051021868,429.0868837795624,5.084611206021507,3,14.780374554885272,17.939552308629757,178.05323719589924,3,8.911509102256849,3,53.06378012871572,2.930380970282047 +49,54,50,25.62446619,93.18240298,6.762522087,97.26336657,papaya,14.374812486344187,2,11.748985339044086,15.743080935005551,369.896977042358,2.9788455862889034,5,19.79169107668557,60.81298209716215,182.99513743485267,3,43.574048331860084,2,23.106662437992842,2.1099600376825043 +40,65,49,35.32876402,91.06138506,6.678449318,163.9069365,papaya,22.554211948709074,2,9.83705306002928,16.564874194367537,368.86347557526057,3.6652809715675794,3,8.797278891971754,19.131150782360617,100.1668949802169,2,31.263032268085112,1,36.39230928188796,1.4694572323428394 +68,52,49,24.42561272,92.27749066,6.577192175,63.35298768,papaya,18.330854935563828,3,8.677039258369108,2.441921071159605,431.75835903619753,5.218635730026867,4,16.340099262584914,74.9703357884373,68.94242075091458,2,22.194022039680622,2,97.6919553392209,3.3318685494246507 +50,46,52,31.18298415,90.21646909,6.734005648,54.01872359,papaya,21.933087138686584,3,11.902642349589078,19.829188096606252,406.19031144533125,9.61608337455316,4,6.956132614540701,77.5274938892253,196.91445049736095,3,30.885049689178008,2,82.47113419314682,1.708254809825104 +65,63,50,31.88342554,91.3256535,6.524459342,79.27201575,papaya,21.227273586286934,1,10.850690165679472,9.535915588450738,383.7154492040982,7.417555386326712,4,16.06754447979937,42.766874691805015,94.1873355775962,2,19.516590325948258,2,65.79516897785618,1.1782839230453748 +40,49,47,42.93368602,91.1756748,6.501521192,246.3613268,papaya,16.915447591455198,3,8.372122167960576,15.785627987640787,374.6563668485717,6.25128424837692,4,12.740502778581202,79.19836974154278,182.87264546141486,3,27.23682986930511,1,91.72068723163248,4.9260926731668615 +42,53,48,23.11407669,94.31994776,6.758479569,231.5153161,papaya,17.999723092851553,3,5.892382831145507,9.475687317503992,403.20816847270726,8.565270869918454,3,17.045972180827384,52.453433320175336,80.62705005038588,1,20.244674119406664,2,3.0887146513079555,4.022441244895647 +49,55,51,24.87212063,93.90560147,6.676578778,135.1694525,papaya,20.619743664679493,1,6.710702041729311,1.979500417577753,367.67034634948277,1.5916360204889952,1,8.880582160582783,88.37770947761771,106.88710097637318,2,2.2978205029076495,1,12.614755926887678,4.436880179594342 +59,62,49,43.36051537,93.35191636,6.941496806,114.778071,papaya,28.774145801388677,1,8.87551048853432,1.013899193577854,359.25108074627644,8.032944307166947,1,18.824119163729915,89.61166749060816,81.9316744947925,1,27.126745352429666,3,37.01121006327831,1.1663237104469935 +63,58,47,26.83054058,90.75379971,6.864143752,144.6656444,papaya,11.309831371911493,2,5.0714170037735,1.201724779870974,448.68884472991476,6.569731440223268,5,5.501491884920891,0.9669516224375396,198.4233497105028,2,24.463013693482626,2,46.65083117194778,3.3359476777672854 +70,65,52,30.42012134,93.12659793,6.583528529,75.95295,papaya,29.31039012136707,2,7.270578820475427,12.792222261103007,350.4886293609476,7.640509255123416,5,13.010250638679173,78.83125140122658,54.67683678575163,3,30.68328187998927,2,50.43879297866323,1.9471604522268282 +63,50,52,28.64555584,93.22642604,6.751747609,115.8163936,papaya,15.463638590216256,2,6.183840318458334,14.980473561370394,417.35711415412857,9.054292199110408,4,18.480995830886016,18.95046005018535,100.41706609048377,3,15.747526080499835,3,2.795558879660809,1.0300894491404358 +40,64,47,32.50037548,93.47888842,6.893509446,71.73759526,papaya,15.812551079924324,2,6.6803211772998115,5.770664896923343,383.794161597533,9.111232868530431,4,19.02234439833697,11.85807321354746,101.15899477501355,3,25.0311189788939,3,61.41433368890779,2.6486918209819312 +63,58,50,43.03714283,94.6428898,6.720744449,41.5856585,papaya,24.05184914662287,3,9.098483094141415,14.821606287440654,440.28735842551663,4.432708265144523,4,14.913553995010304,37.461647522841645,65.24990332515739,1,35.017212418599684,2,53.96992972376326,3.3503873732527203 +45,58,49,30.10773379,90.34546355,6.827812549,75.24521981,papaya,29.96830156148778,1,11.03634322695537,13.324071728205023,449.57696849086807,6.008998996760948,6,16.020718052630762,95.91286100596706,91.15100840908072,1,39.338528050310586,3,0.14390755213732342,4.513938518643689 +66,69,47,23.69212243,93.61055571,6.912299695,87.53393983,papaya,18.989186141616887,3,10.481355952176196,4.5855337152387925,441.5363105686713,5.275658090605019,4,14.934250291623254,87.67793720687128,160.90684564463436,1,10.16398553599242,3,58.91784880309253,3.85447289874224 +54,67,52,35.67667332,93.30641944,6.586107335,141.3381168,papaya,26.59149417677811,2,7.635273212664643,19.152759416267994,352.68705674271735,4.77058540314957,6,15.642807724177548,74.22359549670105,185.26459460837034,3,9.387056290304919,3,78.95524569271083,1.2692963297637418 +69,67,52,27.71948962,94.43877142,6.827305908,82.83061083,papaya,20.81634125783072,2,5.132527921778863,5.321725101418013,388.2653687962985,5.5279896765606455,4,7.42738077743748,71.79933375408353,159.54434391971674,2,5.162644854398296,1,56.172914408248296,1.2537204641209572 +67,68,49,35.26824831,92.38282957,6.821774589,149.8488208,papaya,12.880182784384493,2,6.745046811389404,17.315164557944577,392.4968055649721,2.7608346613183103,1,10.96605503299493,52.038419946178536,60.29661497795084,2,43.008030609853925,2,58.21671804391604,1.9457053436250797 +45,57,47,23.16855863,90.78821158,6.656458831,161.6892093,papaya,16.67152868568296,1,8.700724638793965,17.569523172197258,435.0955856703055,7.816589136829344,6,5.2878802757282735,75.63688981705394,121.30225101269946,1,16.469339678281226,1,31.902202029316207,2.294668765366497 +56,50,52,33.08706051,92.25197542,6.770384816,88.1300769,papaya,20.750323641039678,3,10.036085975127882,12.680488209745754,431.42833870235,2.521669263174357,2,11.448820808894716,60.81698725418547,70.42554517173872,2,46.86041868326887,1,94.12762043342873,4.6137593476509 +70,50,53,37.4620912,90.44967809,6.933809743,172.3458448,papaya,19.413015581216605,3,9.303116707579065,8.999118680779654,436.41307559091797,3.9090680923993326,3,14.721670644719623,89.53190634045289,191.42914361796926,3,41.521310293166174,2,80.28606550409954,3.524611999682914 +44,47,45,38.73218907,94.73613484,6.579441304,218.142147,papaya,18.391528472838345,1,11.247329936870983,15.206732284527673,426.29370594045065,7.830391402220913,6,14.718416297645899,57.357700176347606,179.056655107626,1,4.455124811854915,3,73.79141278579317,2.6150014090920637 +50,60,47,32.57720726,92.74889453,6.92791761,93.7942847,papaya,12.678302090759459,1,9.991371069784996,12.013647389717136,402.5532718370433,3.497648945840512,4,17.884493900476688,57.357683598093224,63.08649555151926,2,34.32298041048651,2,34.225897872771284,2.33376536952347 +52,51,53,38.38231475,93.10378595,6.985804083,210.2735346,papaya,15.356110722789815,2,9.974753841432836,19.02483649098899,441.01475750056363,3.6174639346133195,2,11.303823623947743,70.92182332883787,107.52075426853717,2,41.20751085007263,2,38.19774710376428,3.172125528410933 +35,68,45,42.93605359,90.09448142,6.612429546,234.8466111,papaya,19.521057794343292,3,6.714192007809455,3.337713376609246,350.29545308835804,7.265497884825528,4,19.9086338734396,59.813927900868435,192.04750810771645,2,47.80726745251792,3,31.27534008793973,1.7185682599891527 +68,69,52,25.65492304,92.74501561,6.813383387,52.95477913,papaya,16.461293179091783,2,8.700239373898267,5.725583654965611,414.10923926490676,4.9531026445120885,5,6.536903794378271,40.33546374508583,161.07910666314754,2,1.0279710430287359,2,16.217751868107165,4.860232893132721 +32,55,52,37.58899717,91.99740365,6.9677596,159.6577388,papaya,18.503828246385154,1,7.344281639266306,7.796568347685302,429.5751203680354,6.83264075429295,6,5.732479676605217,25.468451954780345,115.7926273533095,2,34.99229299593874,3,46.922434142972534,1.332565278368023 +32,55,51,29.60718808,93.15642801,6.57398033,62.68710535,papaya,12.691603799052496,2,9.53448807748208,6.994659512948386,446.6578846141623,2.124489139822238,6,15.985269952696695,4.854128851179185,134.14889439761612,1,29.301132801214635,3,48.220711649934834,4.01847793069694 +48,62,47,25.34756111,93.02871078,6.803094965,174.4012337,papaya,22.17965115410852,1,11.265646666045278,18.654104117601072,396.6818245994328,4.231152880520806,3,6.775329697182644,93.70777460410218,175.09407966380383,2,9.1722701878795,2,13.063397614844108,4.912017992681892 +39,69,53,25.9300384,93.02357765,6.964955435,241.8202079,papaya,11.945961317375023,1,11.131772235434482,18.672345373882603,431.7912529958204,7.6397713428666245,5,8.043340862079674,48.788305812554796,178.76407708699185,3,46.55891756333332,3,47.32347822146975,2.554775988429912 +49,61,45,32.76795887,94.57377401,6.764213299,240.4795923,papaya,12.892683752551335,1,9.10811222510089,15.999654391558293,415.51132544348036,6.3562475253121375,1,14.309197843194378,7.485672410851274,135.1950054273402,2,45.79562077076363,3,95.07379611170764,1.2640625727425063 +48,57,54,29.02328049,90.20396783,6.617703178,126.8069869,papaya,16.968561324686164,3,11.572935894240398,3.038221665352012,433.1599342778494,5.301945095584878,4,13.554681942403231,85.3941027991614,100.96652717377616,3,31.056762483785572,2,4.238608443518288,4.171586465264122 +69,66,49,40.00439101,90.17015833,6.52711001,92.11877372,papaya,16.741834928011453,2,10.695138267239702,15.726241162858926,392.83263482042787,8.856081603957097,2,13.064711715937605,74.45537944418284,141.23295765435176,1,22.334565701480507,3,16.992405445615134,1.6612679593527049 +53,55,55,33.32315744,91.25271223,6.709668804,234.496633,papaya,24.095735927477847,1,6.613956303206415,8.198115035106019,410.4029842986661,4.493367579564108,1,15.912671018677674,9.13870491309382,95.49500531647567,3,20.466353553402044,2,30.023342556457656,4.296252487702731 +38,61,52,31.22790131,94.94021378,6.620729882,46.44279118,papaya,11.562340048755221,1,9.441318144058988,14.155338900084573,435.51242286257803,9.996494756977654,4,7.107477661386849,89.19969192682682,134.12398555651336,1,36.93200118429885,2,74.02092254638963,1.4988838087059593 +57,64,55,26.68386496,92.9585411,6.583760499,62.50689682,papaya,22.223867837770705,1,11.991041166034698,14.589779481624507,445.0928333539549,1.886001730409155,3,11.226564327193003,20.142149977528067,174.27636651034845,1,19.409105910701197,1,35.00561501213141,3.208657837756228 +51,57,55,24.70528368,90.14732171,6.676407337,108.4103158,papaya,12.229344875900477,3,8.68766313523803,6.402836391212665,440.23874476367916,3.442893940839815,3,16.20675510641274,38.94334989197564,72.54698356028473,1,4.135160141236738,3,12.011771389272507,2.2463374171046158 +56,65,45,38.2016825,93.97379963,6.751298936,218.0908814,papaya,14.351445771487693,3,9.375731170843437,1.1025022672056517,370.9920181602563,5.656166490407234,4,10.464934334631067,65.4660565481061,151.52352085772986,3,33.02091619695332,2,52.29908125021113,3.1497096367342334 +54,66,52,36.56769731,93.79503425,6.867554147,104.4218596,papaya,24.49355731481664,3,6.450266954796312,10.022000901292135,375.09442208396945,2.266358430532646,1,11.436974356712856,23.095767642106622,93.8098323732352,2,34.96469382307829,1,19.201751864245608,1.8500976423155522 +58,55,47,26.05375792,93.69111672,6.742490027,240.6863901,papaya,28.531181252908315,3,10.90337389622462,3.911184248935491,362.9395830741769,7.329607316752662,2,7.039409912344215,23.70713749066993,128.9369769910385,1,49.84185964277522,2,63.131271468400485,2.783781641349805 +68,70,54,31.29986342,92.76039164,6.986228647,54.77830202,papaya,12.158081008876225,1,7.188123807435622,7.541698335706233,445.296645879074,5.219172619797352,4,14.760684097783846,9.717404684582032,54.97336759069617,2,41.26265107376678,1,53.70979089693826,2.361947237546152 +42,59,55,40.10207731,94.35110201,6.979102243,149.1199989,papaya,25.366454882396546,3,7.14360928854546,13.70629312142627,384.47262725564104,6.760240468982643,4,6.465600765200482,4.919546003054465,90.3097770916595,3,11.277726887542382,2,71.2109121341481,3.0001213235409967 +43,64,47,38.58954491,91.58076549,6.825664782,102.2708231,papaya,27.92906065152072,2,10.729712021368183,4.0439282906086715,428.95427221803067,9.713981867311643,6,7.711800955757495,78.24936784611874,127.77157188414058,2,22.688049789306387,3,43.832128593021956,3.159455868493706 +35,67,49,41.31330062,91.1508798,6.617066674,239.7427554,papaya,12.306331603805344,3,7.963601147559772,2.872931461188706,383.27592212103394,1.787321456254477,2,14.899383011863458,27.781503163030674,144.60947761319864,3,8.042056296817135,2,35.98467126815653,3.409441250119234 +56,59,55,37.03551903,91.79430166,6.551892638,188.5181422,papaya,22.134314334633135,3,6.284040826899938,2.7083621496831456,429.8422302507863,5.960430291189909,4,6.99047861180536,18.90789671752726,54.618607941952675,3,49.523914504217444,1,76.58257422951633,1.4186635029589838 +39,64,53,23.0124018,91.07355541,6.598860305,208.3357976,papaya,28.859922890212687,1,9.247626207408473,16.40353425714151,394.44898774360695,5.6302071582870825,1,18.825408719856078,62.06961459548421,175.53791354036383,3,32.71855246345728,1,60.53873964236536,1.566720672407286 +18,30,29,26.7627493,92.86056895,6.420018717,224.5903664,coconut,25.848772595105103,2,5.547351615501868,14.272734194250026,390.5855324394667,8.28326825402684,1,13.458606746572837,52.6936920119495,56.22154661233122,3,28.782786127617587,2,86.90835446619704,3.228162944026294 +37,23,28,25.61294367,94.3138837,5.740054567,224.3206759,coconut,24.071076576558717,2,9.884225554204342,16.111706454072166,414.6855482190528,3.5456378341544306,4,11.395353533807324,17.847818357543055,105.15702117516886,1,33.54418661323647,2,16.87378899273292,1.1860927395825334 +13,28,33,28.130115,95.64807631,5.686972967,151.0761899,coconut,14.661277834572319,3,11.823272625945926,19.09420673941376,361.80249137561276,8.031681659150008,4,13.565912819319019,69.65648569264833,67.56219982028082,3,47.788664966928664,1,57.92638043160563,3.295214526946238 +2,21,35,25.02887163,91.53720922,6.293662363,179.8248944,coconut,26.786372404317703,3,5.787683954209762,2.2288285242667394,438.5321089204349,2.3913406423202,5,9.441842097337059,73.28436594821346,97.50124190798894,1,40.34586597240338,2,76.54050054237997,4.539231288477778 +10,18,35,27.79797651,99.64573002,6.381975465,181.6942283,coconut,13.724500461809814,1,8.85214723602008,0.9886687856390708,447.5694448079091,8.50884732939286,3,17.98412741661102,52.67998376856517,177.32410271255884,1,26.95689641904852,2,13.971143414225661,2.0448814301619764 +7,11,32,29.25902906,95.11294697,5.542169139,184.7624496,coconut,27.986669616025814,1,10.616487657847614,7.685490931443903,418.0574324719819,8.916261573075296,4,19.174559591940742,75.36782654686527,84.75182050119653,3,44.95260600869276,1,41.67681974197368,3.2028463502379396 +39,5,31,27.10134661,93.69979946,5.551963184,150.9502632,coconut,16.73073733205308,3,11.717986457524564,11.297318191302148,360.53902392299557,9.397210307914765,2,16.40720175617409,21.129917133810437,65.63500131260565,2,35.97203078390796,3,16.279393942759334,1.3168933038965633 +34,6,27,25.84726298,90.92669463,5.860740481,147.8888994,coconut,23.35090475621889,3,5.599985356972839,14.365949659816845,360.70201174019525,5.594179131462967,3,14.671505331385358,34.238089004748616,149.76693048590732,1,32.376606063618354,3,1.1463639995337283,1.2360242136237929 +31,30,29,26.58580443,90.98617591,5.558807063,178.8116076,coconut,19.017648864009292,1,6.536459743956851,13.169834840878138,439.8660486511585,9.374167451791369,1,10.233263959631138,55.635454591236275,140.73452527343795,3,38.1834060759755,1,7.234705049700763,3.8704917749047145 +25,7,35,28.38503882,99.18843684,5.55771171,189.6711349,coconut,17.547595379350277,2,8.707117330592318,9.764707954262544,436.6246490506351,6.287436117170946,3,8.199473571458563,78.22148629227657,121.23391848759056,1,22.175216898480905,2,18.00314014625949,3.2613881573364427 +16,18,26,28.43647052,91.81320717,5.568365926,145.5414413,coconut,26.40740953331289,3,11.297603687864655,15.56613650254012,353.12362330738256,9.389334451498408,1,13.821696720786909,44.87018346856762,175.23213079211382,3,38.76295223048526,3,30.1754582629694,2.775337736329966 +26,10,33,28.27298134,96.93649473,6.07071786,198.8234862,coconut,18.117574438098707,1,8.963345323362665,12.647827069740348,378.7289576179632,4.592996797171095,4,17.057474074908278,20.275325742721286,72.99056012372688,1,35.9305218594811,3,32.005523354215434,4.670269946289473 +27,8,32,27.00648436,96.46168931,5.627860549,144.3331315,coconut,28.56188591654544,3,7.706410327644193,1.145099716517386,408.61938414241837,5.592807401309599,4,16.808380108198783,1.440997992392612,94.42803996985612,3,8.381102818845227,1,35.36367605492692,1.215543310172753 +37,18,30,27.63551259,99.34854917,6.38488418,157.9171537,coconut,18.384330070476658,3,9.674221139746836,12.88808186844137,445.2675017756765,5.743174968767658,4,9.488548111149395,12.204933052511002,183.29324661949198,1,9.884740192966241,2,90.15347084010465,3.2155212181542865 +19,15,34,26.29644905,99.65809151,5.685889066,215.9195049,coconut,27.59950501573593,3,10.912798116359667,9.75766345478357,384.13291280822597,8.66441275661731,2,15.292969156754282,90.89262724815732,149.26296739232674,1,41.81402304518285,3,98.414343864732,2.0374935570731485 +0,19,33,27.1326009,95.23797989,6.234458417,204.7206567,coconut,20.57670167577803,2,6.115956850773732,5.34548629994889,368.3515065086322,8.873144890005161,6,17.563959473449845,92.65544384376709,93.64842568508509,1,4.4635301060318096,1,71.0715314989671,3.6669127324325643 +31,20,26,25.56567803,97.61361544,6.443168642,199.7936345,coconut,25.891091923669727,1,6.854979293600513,2.4419349254374434,376.7681591658557,8.28943947037218,6,7.298792305001493,83.3875693178738,151.77160143792923,2,18.550187281459053,3,61.523442486545434,4.481283984224433 +9,17,32,25.94951662,93.40548703,5.842317989,172.0540491,coconut,17.55277740851953,2,11.837722915945754,13.159299659295407,355.1512753867021,2.1042390120694714,2,15.29700229464038,97.46579183575956,135.80677614696026,1,11.598309753565761,1,66.1925945428744,2.7370862697583846 +22,11,29,28.03380598,95.01630593,5.955742971,218.0055713,coconut,11.05272560848151,1,5.531733445891389,6.84086461811543,375.04871580093203,4.880434146886413,4,17.5448948018278,4.456910723721952,192.6223789965811,2,10.014136216086595,1,25.294236576929073,3.4394920997583496 +31,6,26,29.12859129,91.30924833,5.741367375,157.2388553,coconut,27.681269146344203,1,5.374747741644067,14.716859870449351,393.7850278283644,6.06728532238868,4,11.361140170733787,55.20479783507155,66.86053887350167,3,44.135838513048455,2,29.49503294394119,2.4923597381904417 +34,6,30,27.0828252,97.00155491,5.948342571,171.7575545,coconut,22.469894116469312,3,10.85840698912399,10.84789296562101,420.049052282345,2.987938381696102,2,13.019380642194628,67.82602810678344,139.14192755622366,3,30.115191022036996,3,31.215059318287242,2.555057247232391 +24,6,32,28.11321494,90.01734526,6.387067562,172.4813641,coconut,14.004584887409244,3,11.313738989522694,16.5794148210242,406.1676368069324,2.3300450196021534,6,18.433642030902078,61.758970986025616,184.9570551423476,1,11.377722982991934,3,86.8781127123308,1.0975100570808571 +1,8,26,27.5136304,94.18955816,5.562911913,156.6732553,coconut,22.495507240741382,2,6.055778348566757,7.02089151205014,400.5690145514541,1.457593507557051,5,5.190790374351771,9.24078295850056,60.4888514255235,1,29.650833334357042,3,91.55400506408226,2.435053724201106 +31,13,33,27.63834933,95.48763389,5.85971872,205.5463111,coconut,12.574152069211795,2,10.7480932106707,7.610724490453656,422.4297040073561,8.401641427601552,5,5.5423792205635785,21.559675242408115,80.29805841517742,3,25.129455553620794,3,49.655719050317735,2.3393151728749717 +10,9,28,29.01256899,94.01014388,6.282955073,150.0500312,coconut,24.142284407863755,1,6.485303235512436,3.811598748855274,352.6204626403467,5.289825012425604,4,5.004340767425479,34.25699110653097,168.81338534918729,2,5.569805288773994,2,82.35062333040922,1.3231094101864604 +36,27,26,26.58413917,95.78923137,6.25449571,171.6262299,coconut,11.24208903278504,2,7.04475007710191,1.754439076844787,376.7105078801732,2.527133229632409,6,12.839695071642161,97.39876898294546,83.283197225888,3,3.6339073874645464,2,78.22225812300844,4.565973375676789 +38,24,33,28.28905147,97.00396405,5.973853124,142.9403233,coconut,20.03952444600173,2,9.627592649295245,18.371837190309463,394.718892949172,4.507961040377258,3,19.518715488207324,35.90318545895853,86.28313765206225,1,33.95428035367268,1,18.398970529890136,2.7765813065274765 +11,6,25,28.69164799,96.65248672,6.081568052,178.9635457,coconut,19.062385610598604,2,10.754512309632851,19.970441163239528,366.33881360203884,3.487197732746554,3,9.331495819922349,25.657587157150463,75.259615568046,3,1.7014440614145587,2,84.59806825974326,1.1690589366443565 +16,14,30,29.70931288,96.30484325,6.37466756,209.8453993,coconut,11.058341903418379,1,8.871298508138285,7.694941951886594,374.9140671027305,4.198836310710563,2,16.627645914686546,36.912524230001544,87.43087634094319,2,46.13059373455805,3,66.07595319509304,2.045980501353539 +33,14,35,27.14865285,96.66355213,6.027707171,149.2433497,coconut,18.293565781396197,3,6.088326787733639,17.104685333469043,378.5933231407973,9.29983812110335,4,19.178908008836473,69.5388579162638,144.17810757194673,3,46.9321147490321,1,51.74039408610194,4.876742919570183 +16,6,29,29.28725038,91.95614918,5.868285082,132.1491176,coconut,25.228928159218903,2,10.03037958625439,0.4117896734478732,401.08699444694514,1.5094955318273016,3,7.6641434114598175,36.012221776047305,180.31079213191427,1,2.7290278552525358,2,57.558659287397994,4.476653221395323 +32,11,31,25.06871967,93.31410447,6.205931638,134.8419069,coconut,12.625081303372337,3,5.306418161939492,5.40440011228319,431.1380754243637,2.165378716453869,2,14.972692050997374,50.90643886325006,74.42049577513521,1,28.65868200990836,2,53.85855942012785,4.771829181939327 +38,14,30,26.92449525,91.20106019,5.570745386,194.9022136,coconut,28.29449724086291,3,9.738087070163392,4.907581688566049,379.92057502773247,1.0465451476663004,1,19.70828805148331,44.133141556303165,133.9575737472112,3,33.18471121979653,2,1.8888241114359272,4.0871545878315745 +8,6,33,28.27804288,93.64761266,6.095261013,171.9457959,coconut,22.082172890648796,1,9.291660158272716,18.081538349892533,399.87124864571956,9.835765583622349,4,10.520070495044301,75.72776407443669,176.4055643498201,3,16.377262963583185,2,29.845589972386964,3.1950230148120493 +23,6,33,29.18032562,92.73041222,6.025789594,204.9603677,coconut,14.876704045611946,2,9.300566906802162,10.09274801839493,357.96770969945476,4.784934121371961,3,18.803329634116043,69.964443474296,137.99130805882788,1,38.61365947991386,1,98.25145452716546,3.1448994330223963 +29,25,35,28.3575072,91.64509299,5.542873799,160.7306991,coconut,27.998149316613365,3,6.085519692208339,8.847914118680965,442.7296468425696,7.642429161247153,3,9.660214847614892,89.3841878511034,128.13692601333287,2,20.93395029188794,1,68.0697123686819,2.1021966502761478 +24,14,33,29.38072512,93.27565685,6.366219551,218.5241851,coconut,24.56715739284488,2,9.144626929274759,7.402525453783504,354.3879530164388,4.803794856713791,1,13.77835849835835,33.54431568469738,144.7176546223448,2,39.37225494483591,3,9.292087124966342,4.320542789736342 +32,12,30,25.39241091,98.08951196,5.579845008,218.080385,coconut,18.09387348442908,3,8.809491754778822,17.509260606355213,414.99527386166443,4.0536861944107665,3,9.982963618915335,35.57855010389389,94.15617383873035,3,2.7723671327484323,3,43.71205135510061,1.5834887489557548 +30,25,31,26.31270635,98.62048026,5.804965067,208.1181381,coconut,19.72179833331019,2,9.846962210939342,19.066191226276864,436.8449979193406,4.996545250108882,2,13.74331165195205,70.09632455090939,101.56258993777911,1,21.618255043146977,2,5.544445858018965,3.9545736664460933 +14,21,35,29.52501367,91.91185319,6.121005506,194.3100272,coconut,16.634203658287515,2,10.946292106549755,14.482028809584445,355.4610442229236,3.239739204779615,6,6.886535898420032,18.38082581481454,78.15967896209568,1,3.835744661342233,1,80.61491941919309,4.185019617162384 +27,22,29,28.83214859,92.17170353,6.000248647,145.4172387,coconut,17.416790208175474,2,10.983112036109588,19.48846078856254,398.08737130329934,2.1188440022465618,6,13.062652170542012,10.090699125368507,78.10462445279582,3,5.484265314638731,3,83.67987787492609,2.7039101194804567 +40,5,29,28.48444906,97.76865458,5.820978791,160.389421,coconut,25.65831556186053,2,9.340298445388672,5.352605401219861,350.0019704275458,2.697532528386203,4,11.736979605440796,16.588145587948343,174.81614704427233,3,13.275442141524325,1,26.14688170791749,2.342886968272908 +17,11,32,28.74013335,93.39676499,5.620733794,156.7650823,coconut,13.2375226078302,3,11.040453474568938,16.072299392316207,353.88128664791844,8.976531730207482,1,15.12227071685752,97.29541590307547,137.0380544014738,1,43.69025374890722,1,39.56814483673989,2.9872146398744386 +30,30,35,25.00872392,95.59224018,6.001936419,165.8092179,coconut,11.009555763380677,1,7.12754520742984,14.119915343746595,403.9513641256721,7.809220331832718,6,14.221272592623645,48.769375508606835,167.7721023607175,3,11.2935824687049,1,40.26866798805755,3.914909181777554 +28,10,30,29.8690834,91.14723422,6.305740522,192.7678575,coconut,24.25224762503133,1,7.258072697703888,16.305813647135828,422.0706975655987,7.772412350121893,4,7.746319755008896,34.24390576794464,165.13135951267918,3,44.07953255829499,3,21.249620150241565,3.6587338091096275 +39,7,29,27.54273211,94.59086121,6.362544111,150.2012138,coconut,16.945426191100967,2,7.273847794633417,10.713095702888676,427.62245686474733,8.900399880948605,6,6.293750618670728,14.573784428232328,186.93995831870922,3,16.344519890731448,3,83.55349924558985,3.2317714630525525 +32,20,35,26.52166434,98.38227669,5.588655387,144.6261698,coconut,20.43053659143354,1,7.278447655319965,12.6515101991832,371.2845395402207,4.220940201478813,1,9.546412017973461,14.614540249207252,180.31568719636812,2,10.697940754976015,2,74.73484543420795,2.1413744741204264 +7,15,32,25.03512351,95.89739958,6.182232762,174.796583,coconut,19.77070664765405,2,9.601293077974397,12.069532294179954,356.8766082316087,9.028791249236892,5,17.539376486383397,45.6128857691609,136.49242735419497,1,39.677527382192736,1,2.379873000605004,2.6536749804560773 +29,17,29,29.20394909,95.66997327,5.959493188,211.2506267,coconut,17.79403942451213,3,6.892845591645347,7.873028092137615,401.420922363291,6.707536762907776,3,15.150744603270617,40.03812129918569,185.57782693209828,1,48.23867219694311,1,65.45209666101243,2.445839470804731 +34,15,34,27.05826457,91.10510371,5.677282678,224.7006953,coconut,24.179420794925647,1,11.826794772073345,0.47961136749896216,410.229758806242,8.779319087217802,1,13.434857249597874,36.2641984441752,196.9320972619908,2,21.76446713324915,1,60.48668488552663,1.6637220105463348 +14,23,25,26.18552389,96.96637916,5.612122797,135.4186222,coconut,22.675344541677383,3,11.047891097451831,19.57423003137672,396.5693714846035,6.65731665202184,4,12.856475096204854,41.541409907562844,131.5541856611116,1,46.58608581661767,2,77.64465454693773,3.5482305122366657 +18,19,29,27.59376845,92.48519606,6.206077742,162.8432736,coconut,16.09776051154894,1,6.533611710247795,14.676555553721322,361.0268665641798,4.974977366355132,5,12.306141133068598,9.578282746338962,189.98897760140355,2,48.12687102437533,1,50.14492417769626,2.5517433014992545 +7,21,35,25.76011662,94.65830608,5.764812076,131.2451414,coconut,26.44792305494764,2,9.396610679011397,2.397055468664986,411.25269500982733,3.7724037573310696,4,11.733337208926862,62.90332698068691,131.5650423396615,1,34.70236354806225,1,70.17955654326195,4.599074388838497 +24,27,34,28.87862994,95.11320315,6.203376525,145.0583117,coconut,23.206261667690804,2,7.848375167905652,1.7169268952159267,362.84787910823246,3.6093745672041173,3,9.577223449035564,53.046189167257786,50.31997772500782,1,9.47207576865824,3,46.79608631867072,1.572903397315621 +39,29,29,26.50908611,94.48414544,6.143662699,199.8778403,coconut,19.058790654673643,3,5.617939168016613,15.585200485433187,401.5783193970102,9.942528818910803,6,5.346366304189206,16.13425110697505,162.28107493535322,3,0.28860366820475103,2,48.67482780259612,4.573651637061884 +29,8,28,26.87037587,91.72546257,6.100429497,214.4128874,coconut,15.851283719089107,3,10.939865092557294,1.7730783948841,384.3346705719765,1.572693434480249,1,14.202752278366733,99.0965657950097,163.86326353788792,3,2.2055244119176476,1,37.13838432636795,2.6640535561408454 +10,24,27,27.57283516,94.90485697,5.708409601,145.9298935,coconut,16.6099940590011,3,10.642414376528967,3.4511561361623344,426.04198407209594,9.81505971055905,1,17.347107961732988,68.5329893241213,181.90371227730054,3,7.342749318181285,2,73.43218342075525,2.735781201681713 +0,29,32,28.05912437,98.3670985,5.868255858,171.6516396,coconut,14.99349287714806,3,7.9533027633440465,14.94517012815803,370.28353956045237,5.0025093676678445,6,12.109278652234508,78.79843977510215,139.65365017368123,3,37.74274479322497,3,43.18773072097601,3.8502804642853725 +32,11,31,29.51611558,92.56492864,6.461225827,131.2116167,coconut,17.412679457142016,2,10.874366965850182,8.070844303516447,356.67022479917455,5.5857979892634555,6,16.518367047056174,79.77446236308893,126.83110482603189,1,41.018469557293336,3,84.28495797061926,1.825274004322329 +37,10,32,28.96318258,95.16333673,6.165084855,222.803013,coconut,12.174878731885316,1,11.383576155488297,2.0662734953242112,382.0061739243414,3.818037927769606,2,14.50526314726747,15.549190720796735,98.59417987999171,2,18.084048618197855,1,66.76971928814804,4.5299873386056975 +20,29,27,25.09897688,92.36099489,6.047044342,157.7592626,coconut,19.288614241246933,1,11.016505233110502,6.001648294834128,369.01414334298767,6.218029999229503,4,11.80363531657043,37.275119335705796,64.36667170653654,1,19.53098383245056,2,52.052286929373594,2.141115269364994 +31,29,35,27.1872282,92.19906776,6.137102505,141.3220576,coconut,19.58991367356124,1,11.812742290914208,0.7333317708192455,355.20729929836784,8.985826085051984,4,12.80074515522442,5.352578303793509,74.81203324299918,1,29.065973393824017,3,62.30899516297257,2.3570763302645443 +17,30,27,29.03065024,90.79093862,5.894027065,205.5720367,coconut,20.27668828856517,1,10.681520821111908,17.538593670010737,449.4365181807699,1.3258452533308507,1,9.890520369815066,18.084830383971873,170.52421669656854,3,23.033501999914247,2,48.71949793959893,4.793763729418501 +1,12,30,27.754298,95.94643831,5.56222383,131.0900076,coconut,28.400944581202737,3,8.072236813128825,1.4322560741361845,376.57112597588565,3.849708705957164,2,6.928370597781435,58.799048258830155,142.4222220056953,3,9.76322669578053,1,39.4079691721129,2.3161492475171714 +6,13,29,27.31155708,99.96906006,5.832608028,201.8258633,coconut,23.787005205803695,3,10.617535843134,0.20688376301716715,445.3073647583381,6.025939064501026,4,16.550053769129196,51.89154674577606,179.6158835391582,2,11.721211876680432,1,16.60003042164322,2.8299870957704063 +15,28,32,28.84270971,99.64328526,6.218571874,224.4016682,coconut,14.736332609330848,2,10.301020177317902,12.392705627999405,425.7532461575619,2.6090356619493478,4,19.814575017660896,77.49295886357427,75.87943190560725,1,41.266510054221364,2,89.75614773196276,3.776193149188283 +27,24,29,26.61423461,96.97300803,6.142010637,191.006688,coconut,16.200104463510527,3,5.1704923379083985,16.23809842876627,395.27031002128,9.70238467132262,6,19.013666216949005,24.369925330050602,165.94990266410656,1,22.866389652085388,2,61.280593138273495,1.4479227459250534 +3,23,30,29.70143197,95.65754365,6.078807239,215.1968037,coconut,25.401156173317116,1,7.158422122566827,3.555028582556561,371.14856728558976,6.279669873102464,4,10.61819445538476,94.73685499808505,182.39127625283862,2,41.322494437908055,3,50.19204495771796,2.4511784393674114 +8,26,26,25.54759871,91.64194826,5.702484758,212.867626,coconut,17.951842175818115,1,8.395177722367302,8.484334708244099,356.8031428850272,5.022077841085935,4,14.74427461771406,9.381201328707933,123.32145763633378,2,18.949184620993215,3,7.605898200000006,2.371780715630507 +20,28,26,26.37978453,91.49882979,5.547594847,167.0470997,coconut,21.683632285849384,3,11.774566744168599,5.572140519616675,377.8808975360482,6.767524913487785,3,18.65780128357209,20.122019108692314,143.1656958270931,3,26.7786271103117,1,47.12323485743698,4.852254127441504 +26,18,27,27.45907759,92.90736493,5.836075368,142.1430003,coconut,12.972852034216771,1,6.732214959570486,4.826529559088231,401.0203581127265,7.5892258338961724,1,16.2315139237791,21.25612381553299,116.83778477149886,1,20.624727118819834,3,9.221100678294935,3.922775754156191 +1,6,35,27.02269204,95.71935435,6.231662767,147.1682459,coconut,18.437517983027025,3,8.749067327161278,5.4937074139469955,434.52247583987713,3.481288673535208,3,12.888425236135037,77.78708272672694,50.6998237195811,3,3.9008014942622795,1,41.503011409128845,1.5287183852884159 +27,30,31,28.98545306,90.73966792,5.718120393,148.8398374,coconut,10.536952344835463,2,9.682660248294887,1.834695594858966,443.0465008431289,7.916605211107285,2,12.389341953244813,31.372624421314665,137.13483831777512,3,47.21529359121132,3,96.51552654666304,1.219241500773847 +23,7,34,26.1055118,91.52421214,5.852038202,134.1279669,coconut,22.054673318944317,1,9.683031892389705,15.199917237695672,403.5923655569988,1.5605090304147797,6,7.888187257135622,4.952906113269318,116.92754646988347,2,0.6585268262909172,2,11.455543326311535,4.605279792956713 +0,26,31,25.0707247,95.02156793,5.547933273,192.9036306,coconut,11.895978475898856,2,10.098279992897664,2.478786450935242,392.54476019040794,8.949847051161699,3,5.332131250918137,35.26848896332236,70.45662778244707,1,49.58573645519264,2,65.68537637510846,2.1303088354530875 +38,6,25,25.54963273,96.92786777,6.156259104,191.2996157,coconut,21.81278116743666,1,7.431273146946202,14.411416824973784,440.02490113815884,8.703555589340887,1,18.16500726557228,10.201279321839763,198.7775467148289,1,26.306940796964263,2,52.04284193630134,4.943596368297641 +25,12,26,28.56973521,95.67906668,6.436314406,134.8370348,coconut,25.23095719185271,2,11.126094163564801,14.041233984592374,385.76193138435906,2.7946141341626625,3,18.991112242429367,35.20003695511315,151.80501921476332,1,8.890726437896474,3,18.12960776736756,4.425768674446557 +40,5,32,26.07010807,96.7036223,5.981169595,143.533473,coconut,18.217609908990507,2,8.759752820381056,5.116676553230468,408.93826905324335,3.1141932475840655,6,8.881732048816382,98.4994217497938,76.3411469234891,3,33.873943905299136,1,55.590478915846994,2.7323716598146843 +0,19,31,25.51791333,94.38420565,6.271952833,178.7297725,coconut,21.341096377656136,3,10.853760783085717,18.78665932140445,418.5962862464961,5.471524692998466,3,17.40463820811879,48.139322813751676,177.53011086159125,2,13.110857578813578,3,19.437771349461176,3.8521870827098925 +26,9,32,25.9490364,94.73860514,6.470465614,144.1571109,coconut,24.905226935465855,1,8.673997495443304,12.69539209168434,399.05875581047184,6.682388313251266,2,8.381227557053851,7.073426705972185,94.03569099237106,3,1.4904323779681983,3,79.10905133474354,2.9248939513378662 +35,30,34,28.2974764,95.41122824,6.141502001,182.4482352,coconut,26.156173665281365,2,11.986368999772056,0.9905689892464831,364.0036020562467,3.9320113764915483,4,11.040976991022971,71.76904979618085,129.736753962676,1,29.809647584195133,2,29.268193047679425,2.0203446532303966 +19,30,30,29.56549169,91.40896307,5.826381164,224.8315729,coconut,15.895201313092835,1,9.894367062593885,0.9061640540828586,419.8671922494208,2.1768988819001738,3,9.53158569565441,59.88238023447008,64.1520826617105,3,32.16457332286512,1,84.56037123735092,3.943953628367089 +31,13,33,29.69952329,95.21224392,6.342463714,148.3003692,coconut,23.74527134815115,3,8.191548540718255,0.9572056956251118,368.50467966078656,7.336677734280704,4,17.76482368730233,98.19559919446121,199.30090227814614,1,0.3468537761907975,1,91.2901819866141,3.40487869388468 +17,29,26,26.14162144,93.28415295,6.071897347,195.4115025,coconut,23.838467121601667,3,10.186602971811064,5.63610560571435,432.7489898118616,1.7370671632065424,1,11.641764381703132,43.907379027373814,86.80142964390808,2,3.4430112297040685,3,73.36582784981785,3.778093163228831 +2,30,30,26.00175125,94.79998418,6.331051715,209.540094,coconut,28.69534537930209,2,9.939970884906234,0.3768794452922686,366.2566965778691,8.59041779106591,5,16.131165328719682,46.28842713838526,186.4145156277011,2,15.436361578757563,3,49.956619223883706,1.1777379554290581 +30,13,25,27.15116142,91.48889469,6.413184638,164.9182225,coconut,12.474496428881569,1,8.921713861193423,12.484714118201797,449.82555571371574,8.502650509760787,5,7.119077455587313,14.502048674541356,195.2346878090338,1,5.173570137464223,2,1.9471521187476304,2.0054771664901434 +8,15,33,28.97318719,98.09861043,5.50158009,213.9011021,coconut,28.180092196465523,2,7.907547340160317,18.484791596464806,423.2506609317392,9.264236972939036,4,18.037775701017843,99.6684209563422,145.056030870783,1,9.839023564020783,2,45.09682338320101,2.696681874970835 +18,12,35,26.13958446,96.38580769,6.338720873,131.3387935,coconut,29.57022162944702,3,8.093444382471684,19.242312233300062,397.63695435589506,8.0764411364167,5,19.70732659938346,55.071847256059684,177.2173099167901,3,5.571976682300289,1,93.42226807802851,3.5274405228635906 +8,28,30,25.51618488,94.33465411,6.015672239,135.1272491,coconut,28.060707035450058,1,6.485153277466552,7.637386809804346,372.21549390655105,1.8655918312830844,1,7.879957386032463,47.541704670734575,192.10339009153964,3,38.31194945694569,2,87.25348144964026,2.8818110889414146 +40,22,29,27.55821802,99.98187601,5.735364307,174.6256481,coconut,12.210252656801796,2,8.859667956493599,16.745313746476388,371.0067261229897,4.855768308965072,5,15.250148894102129,87.09815402262855,103.38397336666306,3,20.446461403052147,3,89.74674867276813,4.026058803994525 +27,10,33,27.81132822,97.48410555,6.465906333,154.0621221,coconut,14.741278343993498,3,9.328639984893313,1.1935742562762996,446.7417610640879,7.491005407145096,6,11.820608315775585,47.98496554990229,188.716862852116,1,47.607017618485855,2,42.277028234385114,1.6881469038288905 +21,20,31,25.60033702,99.7240104,5.855457599,165.8248732,coconut,28.12604180725749,2,5.40072177442714,5.185362660895885,434.7076079525561,9.637050283518512,1,15.265340706844102,59.1138176230815,122.58442238147552,1,43.203217466568354,2,43.01389246689581,4.564384847015996 +3,9,35,26.91641934,99.84671638,6.318552973,225.6323656,coconut,16.93443790337816,2,10.888459274360965,9.809974863905165,368.7938768970136,2.2267925402219273,5,17.694079774630737,80.45711705179862,86.26157186087448,3,18.827792340580235,2,29.765104493625582,3.9225139607847455 +22,16,27,29.1797902,90.27214288,6.006784979,188.9252083,coconut,21.96322763179176,2,8.547045891536722,13.787657583125714,407.7857176357071,7.653834022489334,1,18.403023885331322,51.50388237755143,170.5637601461586,3,15.543429870501813,1,55.98452929497086,2.8395605114251925 +27,8,30,26.44600063,98.29937782,6.008386283,221.2258168,coconut,15.487296064798892,3,8.642128093237076,5.397216577191378,420.0996482606779,6.1592475717438635,4,5.926601651596215,86.35810662034218,137.7724042089612,2,42.472746862084,1,42.93455008251487,2.749871790355241 +22,8,33,28.43572863,95.8840407,5.665785202,203.9283708,coconut,29.45772492848779,2,5.597789328109737,5.501089929048042,379.3502566085638,8.492168647316173,1,9.294338660707552,74.8422898426792,67.75173160506834,1,0.650495067016188,3,95.74661550951494,2.8964688658012023 +28,27,32,28.94099669,93.00109012,5.764615485,191.7723087,coconut,12.198011201576115,3,10.954285012613457,15.075379612952366,407.4361134570364,8.89300630040144,4,18.716423271569244,85.25994683735094,54.41892583282219,2,36.896148536344306,1,51.32046753360403,2.790193767039074 +23,21,26,26.45488737,93.45042636,5.901495544,149.2220255,coconut,11.92328660471102,2,9.396827899714566,5.935529450709827,389.13035058846236,7.512536459483438,3,14.547714547081373,63.02410024863647,70.42307201385259,2,19.69002302050215,2,69.64535216730927,4.961514634682835 +37,5,34,25.79490531,93.84150618,5.779032666,152.4238712,coconut,15.359092106395897,2,5.13312265498206,1.292314308145881,443.0797828400724,9.86737834267835,5,14.149697315739127,25.11948531806113,164.07256879546418,3,6.0837073672858955,1,13.464062374517349,3.491540076360256 +19,26,29,26.93141945,98.80313612,5.67154928,166.5712879,coconut,28.987565067884525,1,10.619209385938056,15.579491168432183,401.73191600635,7.534615691431686,5,16.455495153147222,11.126372266143447,102.93148149286115,3,11.067760834809388,2,77.30143834522663,3.4583236016918066 +133,47,24,24.40228894,79.19732001,7.231324765,90.8022356,cotton,28.151764283507713,3,11.955776009621214,11.08083798541207,392.08986740457584,4.695821304347842,1,12.099801993788903,85.40715087311865,109.26891104685524,3,38.461349633764634,3,68.02076696864347,1.439020925003649 +136,36,20,23.09595631,84.86275707,6.925412377,71.29581071,cotton,25.180208907319944,2,7.699290418161665,7.197522686034141,404.1230358724431,7.778894115907043,4,12.070334008191303,29.330144777337196,107.03346401279396,2,15.307644584172685,2,6.137302526801392,2.2860047366533163 +104,47,18,23.9656349,76.97696717,7.633437412,90.75616738,cotton,27.742538159100572,1,8.745028257890997,15.603674976603203,350.3601471144205,3.8729622757471502,4,17.567928764578088,71.64484159111477,104.12017826725753,1,0.7720179223026591,1,79.10298233065387,3.6702524881754948 +133,47,23,24.88738107,75.62137159,6.827354668,89.76050416,cotton,14.713067683521118,1,9.95690591265646,17.404727154425906,418.0982208429959,5.29070969311196,6,15.97491022386533,27.954159782431653,89.52382420142655,3,7.7608456130557,2,35.402222227436866,4.436006882542839 +126,38,23,25.36243778,83.63276077,6.176716425,88.43618918,cotton,27.36776018446006,3,6.416953204079634,2.7281910512790675,437.423465947126,3.8703783047229616,2,6.543027340063996,33.46864086903457,174.02835566705483,2,33.400583749590815,1,36.55407893042033,3.672545177107925 +126,50,19,24.69457084,81.7358876,6.628722836,78.58494391,cotton,26.552156408079167,3,10.129147866317142,12.799494901222651,419.6961158553345,4.258425854221295,4,10.697538972401745,89.80737201169843,128.37322086780338,2,29.254426470281057,3,12.278700047022785,3.2003145681395506 +113,41,20,25.0017188,80.53965818,7.256877571,96.32600992,cotton,16.55843681624998,2,8.579862214019014,16.405636456180986,395.5142655898112,7.794219005255698,5,5.819710634487638,31.635987946150735,178.67179927151457,1,10.835636642198265,3,36.0268631433922,4.556578168541916 +121,45,22,22.45942937,81.30681027,6.443785385,64.23026638,cotton,17.731656666551252,3,10.263876923889867,18.57957030481279,391.58740589986127,1.0925324953994249,2,9.89666636967814,77.70747338432817,149.45025430962647,3,35.27045568596509,1,57.877375581428545,3.35342276063425 +121,47,16,23.60564038,79.29573149,7.723240151,72.49800885,cotton,12.053316118446787,3,5.230294451819854,5.732803086369493,409.31818927800487,5.372018034515619,3,18.88689174102189,62.437928824163706,147.20971780241265,3,18.751874532650277,1,24.036190512940493,2.072035605645054 +129,60,22,24.58453146,79.12404171,5.947448589,71.94608134,cotton,19.960784182786412,2,7.963666543720995,12.463024786384032,424.5597416379186,4.472672717247164,2,15.108542334918805,24.15995215583202,79.1857549214844,3,49.152541957163045,1,12.420410648160408,1.9718145162304355 +107,45,25,23.0865933,83.55546146,7.227745516,71.84080724,cotton,19.26752852271388,1,11.778235179133645,2.5848840268927176,372.56999879759115,1.2663080251503462,1,15.262781461660067,13.39615899461074,83.87280823515061,1,31.848073284652163,3,73.8536845695366,1.9750627825581861 +122,59,18,23.5000992,83.63488952,6.219469084,79.81328183,cotton,15.499224597901106,2,11.522603942528242,18.183149392868383,378.6989028975375,8.321196329223717,5,9.709491411229388,57.79310255760484,199.20101231526823,1,31.476969101152218,2,97.13714617392647,1.2999811783033226 +140,38,15,24.1472953,75.88298598,6.021439523,69.91563467,cotton,20.14844764267223,1,11.263396494389593,3.5469471265631336,391.7079448119956,3.9190757120710678,6,16.75268018528669,41.6501605994882,72.6739770822027,3,12.925644049047852,1,35.69201501899556,4.9827527241577485 +102,49,21,24.69315538,84.84422454,6.253343655,89.799462,cotton,20.412640997085227,2,9.471022633004079,9.5961367158153,380.664141353043,2.8063090301049947,2,12.603308395687005,31.3139622375319,188.73347457189928,3,47.44298883836125,3,10.982116254669139,3.5079747253130944 +111,40,25,24.484692,84.44932014,6.187455799,90.94342484,cotton,19.623758417713837,3,8.131240889369506,18.132260798013988,370.5996019770353,8.386367017874296,3,13.267600689994126,21.36041333983646,59.234874881477324,1,18.03746085964169,2,87.77633147185472,1.9177994224069774 +131,35,18,24.49112609,82.24415809,7.057693366,64.02949379,cotton,21.64441452211161,1,5.130420458515761,12.539926183933602,351.7499825728234,5.1341826227804175,1,8.454061083806492,13.079243500530813,76.11877588535036,2,14.802124600130162,1,72.00251533792262,3.8194004215387025 +135,43,16,23.47986888,81.73049149,6.720449769,86.76287924,cotton,29.540618719300586,3,9.73780575553948,19.675927976347122,393.00883823743516,5.520647811516404,1,12.179633540231311,11.983669715567757,95.16569532665645,2,19.571437705459577,2,37.509265126747295,4.8399594103813826 +100,46,18,24.18586246,76.04203958,6.431689506,69.08056728,cotton,13.46017893156329,3,6.769630559847569,11.926986090826182,374.08214074808956,7.468823608217285,5,19.379321381251906,76.41057578574383,56.82282248985309,3,48.34980529262561,3,0.07327715517134736,4.032387994778632 +123,39,24,25.00755095,78.17952126,7.453106264,86.06411872,cotton,25.451043959019973,2,5.450609756798874,14.561291122170223,373.3117907934975,9.682291487971431,2,14.641495741262371,60.91771178464786,159.6303245962132,2,18.175763458679373,1,73.99698152051417,2.5378162992915434 +117,56,15,25.99237426,77.0543546,7.368258226,89.1188212,cotton,10.904882585409903,1,6.6115696164741315,12.188095014964828,405.7825221380586,8.764136830392598,3,12.279267875031955,86.53946101505369,161.77995862720428,2,10.903805385839672,2,7.615969274712652,2.2728908168651176 +121,36,24,23.66457347,81.69105088,7.352401887,99.36898373,cotton,14.073355684063886,1,7.231411225977618,9.055657280819773,364.53270821073056,8.059020382757954,4,6.112562444052075,4.057829067310714,159.23748821557956,1,39.849325227288546,3,38.046937555546535,3.77394163190111 +101,58,18,25.66891439,81.38103349,6.652143699,78.59595817,cotton,27.81550260634695,1,10.656090212883413,19.83642343205428,413.8062052511188,5.61108637756993,2,10.783717374547942,31.32553918753187,68.87109012508323,3,16.056923214249537,1,7.45490066245007,2.191200978658704 +107,42,24,22.04612876,84.62978302,6.144631795,86.00758678,cotton,16.766984283148666,1,7.011374838983805,11.614276926453437,376.48286322921257,7.374192756996922,2,5.006916159749311,40.963689237668156,192.08180975102624,2,13.38511950392835,3,66.26871372790183,4.650178179856588 +100,41,22,22.4204752,84.55794703,7.318802162,93.46595573,cotton,28.371856639154164,3,9.210787081190144,18.542511827064803,431.7404550138395,3.4765659547602974,2,11.614458084707161,37.23698618973456,81.23566601135084,2,20.022704668355264,1,31.807785395596365,1.4878121571954348 +125,39,21,25.03149561,82.21276599,7.954629324,95.0191318,cotton,25.389506442999338,3,8.699757899631175,9.291155984956518,390.13345671807076,6.490102725461301,1,13.75949150703064,77.9132640255481,181.2150130763364,2,38.17305861459076,1,2.4402431880092834,1.5997781580550448 +105,60,23,23.53371386,77.21705554,6.207652157,87.54004943,cotton,16.03414939021262,1,8.890457606870449,13.083673827561086,374.36831096135444,2.7801573010201777,1,18.601445330672867,53.06598697099145,141.98522239273007,2,44.41560328454103,2,34.93332038418638,4.886453001060524 +102,46,19,22.77076388,82.5993307,6.631005298,81.49543437,cotton,18.788352745099555,2,6.524711838644853,18.132296514481087,356.8775740122797,7.125961150394913,6,12.244670222516978,81.5500929277901,175.20228947499794,1,3.0518096558960983,2,52.86390654497723,2.7725691023139576 +131,49,22,25.49848236,79.9751579,7.306918817,67.05961949,cotton,18.305105619287808,2,10.364054606247024,5.8043005565905155,365.41887242615695,5.378809010519477,2,9.516371205622862,49.27118072929883,140.7413931209254,1,47.02620023338281,1,9.555144618643608,1.6778813259510046 +139,35,15,25.248679,83.4630147,5.898293044,86.55517751,cotton,14.897121614712535,2,9.42837343370543,10.954925170606888,367.2667563248214,7.11717116720989,1,14.916183771511976,74.26872923606575,143.28337589266903,2,27.212137008629693,3,22.695007718847528,2.744706079544496 +108,36,19,22.78249615,77.51235009,7.238566893,64.61444234,cotton,25.840350215857782,2,7.449663623939765,14.090090335296608,441.78934846838615,6.786972148930851,1,6.455849106837389,82.1279826091589,138.13067316391346,1,38.899494610335424,1,37.83360826682076,1.3076102600485435 +118,45,23,23.37044424,77.43198948,7.977651226,71.67870701,cotton,27.146712359936515,1,7.048998021004702,9.663174629190685,386.2054331027368,6.300996280001526,4,6.90310627082483,52.73674984778831,198.2633189264652,1,48.293466061707235,1,36.62356957228623,1.1517124601557778 +107,51,22,24.86560781,78.22080815,5.983075895,79.56866268,cotton,20.29456309019759,3,7.672901302405044,19.642948763198717,403.02520036157483,5.50922576933047,6,14.965917012762839,24.919602686303865,94.53131341632513,2,31.291900239037496,1,14.897813856217045,2.794146792100462 +125,60,17,24.14386157,84.51591287,6.785723961,80.36146974,cotton,13.104822277411008,3,5.2380290717357765,9.829434228942649,363.34137845092175,8.248410306883107,5,12.478509235084678,15.885072218634155,56.382029648308986,2,46.36784593915917,3,58.52650653742594,1.2160704932196689 +113,37,20,25.03300222,79.04368718,7.393441155,97.10087029,cotton,20.72493921446361,3,11.796321039464981,7.459591263195263,379.08242566247196,2.7360745656249366,2,18.577987110679466,41.24176654635223,141.12587680711164,3,16.310988822139468,2,92.68418186695952,2.9686097950799843 +131,52,16,23.65724079,84.47601498,6.486068274,88.54479121,cotton,29.052629196736945,1,7.638445276990009,6.237417031451214,428.99129422084843,2.2485304549899965,4,6.77374974902325,30.719937420607156,132.26659088462736,3,22.565256226686238,2,80.84568734237124,1.1570695113994094 +115,48,16,25.54359718,84.09229796,7.175934962,88.94245493,cotton,13.534667485478327,1,7.174165988779304,19.043315291676638,424.1510879875234,8.774174910090501,3,11.448186446270526,70.12886318669163,104.44203206712541,3,32.933887648066026,3,69.79383095899672,1.5994700009596778 +113,38,25,22.00085141,79.47270984,7.388265888,90.42224164,cotton,23.388197721763337,2,6.675978604610906,6.984465302075797,358.7862440180498,5.089879132858498,2,11.482222240258755,23.534018860224716,83.36220378606667,3,5.834697406921857,2,44.93298668426583,2.057124794288806 +111,41,18,23.64328417,78.1258666,6.10539819,80.96157332,cotton,25.703843421335844,2,5.042195988491201,8.601495748613381,385.5707664476288,1.940663617738732,3,19.083346493044857,58.01378616361883,189.56443997068646,3,34.006209353796045,1,80.93485958017843,3.0161057056701157 +111,53,19,23.96436009,78.02763149,6.419536555,84.63148859,cotton,24.612350370749787,1,9.933686503383129,16.279915298855023,403.6620998948868,2.823956348100763,4,12.355143445089553,78.02783991460164,132.23336948200455,1,5.913048462057108,2,82.34808245660746,2.680242661859615 +122,48,16,24.65425757,75.6350708,6.307585854,61.82980133,cotton,23.945131298494015,1,8.928584142699053,7.06179738062966,364.2675572824239,8.649918777709786,1,5.428441060440491,95.86357335482812,110.12290359038155,2,32.8635621902962,3,28.0116829815946,2.537135738827808 +108,46,17,24.3017998,84.87668973,6.93221485,65.0247867,cotton,10.893473198635954,2,10.083731137972979,3.9358448765710663,399.7727236433949,3.705209348583125,3,14.423943901095331,4.951688716808478,73.30732639108447,3,36.264292729096645,1,54.60347379983076,1.7247284697617409 +132,41,22,24.29144926,81.02453404,7.810865753,90.41694635,cotton,18.16567313749778,3,6.946458734128365,18.4516548792447,396.5445042647143,9.2404749631236,5,13.853139119483233,3.6724494764209914,158.875942161854,3,30.114403359677944,1,52.913698619602734,1.7894509813278012 +103,42,17,24.29470232,84.61527627,6.527541661,81.05902285,cotton,10.950341192921014,3,10.151309655748838,3.420297467084774,388.46093759492766,5.582075950204377,3,11.251599775792222,74.9204389900727,111.66790528806303,2,15.611220868086361,1,16.756663653133952,1.2012077616535595 +133,50,25,25.72180042,81.19666206,7.569454601,99.93100821,cotton,12.356822407038637,2,5.814740524688752,19.255800603106948,446.25182855488845,2.7009914185505597,5,5.746376412601678,24.373200035119847,198.85529488313992,3,24.515087851405276,2,77.7423197071933,4.812279891661529 +127,37,18,24.87663664,76.30050373,7.041065585,91.9223468,cotton,21.428113716760716,1,5.457210684558276,12.985560919701125,394.9594380003614,6.647956323883029,5,16.26583970526756,74.74421807387068,150.15861990749454,2,6.771608780020999,1,13.487324004847846,4.106624320742881 +110,39,25,22.60612115,77.34264002,7.208795456,75.13617229,cotton,19.20594693836033,2,8.270083920039438,8.367184645797977,434.32774055279054,6.351260892520663,3,14.13206420276677,45.951737120773096,69.17362155791035,3,32.35894482182171,2,86.85752244723486,4.497163284906004 +131,38,19,23.86814008,75.68339729,6.814341946,90.4547185,cotton,14.077493384495256,1,11.752059124277896,19.215326402799054,438.60222157748404,9.968953928627243,6,8.62626571728173,12.653194512390066,190.94487581847693,2,17.712528214043527,2,58.67322566300185,1.4733820949536773 +108,38,24,23.41022496,76.43836957,7.442217061,78.82199603,cotton,22.912703877555476,3,11.18279265524372,5.481892506943198,382.51962647738264,9.302335437558048,3,17.041560862472167,59.26940336328378,53.43846084784839,2,40.68077991495638,2,36.174270948917,3.5718657298515266 +122,40,17,24.96440768,81.31677618,6.854558957,80.03995829,cotton,21.080100035680427,2,9.364610876016208,2.9271108646512367,433.3213432233627,7.498809617663081,6,6.150484113796052,25.580870190991146,147.55935061973446,1,35.11018868528435,1,8.433993175711517,2.941031093157038 +111,50,15,25.16820129,80.30351815,7.884550475,84.62419032,cotton,16.446264715540345,1,8.919073520175008,3.676154353828447,414.6803777868065,8.425970755263307,6,15.494936546545905,25.390876606654565,167.77064746707694,1,43.916187044545765,1,1.5214516912189446,3.8255592818403694 +140,40,17,22.72767171,77.07598065,6.006085786,77.55176318,cotton,28.651471114102804,1,11.114211342146731,2.9824252548794705,382.2187609468167,2.3354601726143476,1,15.190903184692964,25.091289064833656,165.17017328031994,1,19.8775290985029,1,60.93492251710073,1.9107718294553728 +100,40,20,22.45145981,76.25674874,7.432043735,86.84998693,cotton,12.808849928179967,1,11.813289649769779,11.216207103356519,403.9755539596891,6.405026613263242,5,11.888721316835781,89.56458775494617,143.70680006155115,1,6.202160617644953,2,29.11056699660324,1.097307815309963 +123,50,16,23.04920461,75.53835214,6.498052108,70.65644296,cotton,13.858547155976744,2,9.354336506825092,9.953738376692794,384.13793125542537,6.856893643644829,1,10.916862686670434,12.986825240417089,147.23531360005626,3,22.021138762734427,3,21.806255938188702,2.284079521392807 +107,36,21,25.29250148,75.66653335,6.205263534,62.64174227,cotton,13.750481184875479,2,8.57706875090549,15.363657399398829,352.17829430470425,2.709760218482492,2,8.121680877770583,14.446538657901641,134.6839331670297,3,44.579170554334326,3,11.598617592232552,4.567257950852788 +118,50,19,22.95604064,82.33733678,6.360812227,66.48339303,cotton,28.133405808609727,3,7.351589844112478,19.48736833138378,431.94945515124914,5.358479877765539,5,5.815847806346584,65.83464416471226,166.09544676544482,2,17.956391641312518,1,80.6860874778635,2.9349276296983646 +103,51,20,22.80213132,84.14668447,7.046607434,91.6389565,cotton,23.597865975941378,2,8.2678961751759,2.871317121556445,360.12789074603444,1.8288906005972985,1,6.6842985906695755,31.242267525532974,165.65608019107017,2,3.853887383258292,2,10.576808559063577,2.9959695625912515 +133,57,19,23.54234715,75.98203329,7.947011366,84.12536744,cotton,18.496974931380997,1,5.456600314634341,13.259173626449712,388.53193332373996,7.760505860034132,5,10.590836490841497,32.69961064470955,72.33558555901921,2,32.21281191543937,3,39.84200085946706,3.33493171572539 +129,47,20,24.41212325,80.80343786,6.281913858,98.60457373,cotton,25.658079513064248,3,9.042823844892308,10.103214175633745,371.8106890652331,4.213637008856952,6,14.646191956104303,58.85647833499469,150.88962236027547,2,30.754664622235055,3,83.58091978846177,1.2689889557438132 +116,52,19,22.94276687,75.37170612,6.114525877,67.08022574,cotton,11.299444545646747,3,10.08396838839369,14.59494589995392,360.9387416283293,4.74716782755805,5,13.215552101093696,65.10915994588645,104.51908699800339,3,3.672825917807143,2,2.3663271972851563,4.884827216836367 +114,40,23,25.53676123,81.13668716,6.753978061,95.4262599,cotton,19.537341490138353,3,8.211086305922594,9.614688279037207,354.1407482579642,3.7323953553574363,5,18.669297059296518,29.684740170866796,187.63211963423333,1,0.8228097238958176,2,15.602045354034244,4.055317019746899 +131,60,17,25.32023717,81.79475917,7.425041316,83.46532547,cotton,11.368837456120149,2,8.816594487390685,0.31562132596205883,392.41565692017673,9.826030445070014,3,19.75281430561502,13.418535334500238,72.97582053038352,3,29.76609937804332,2,64.15550840614524,3.247005147557258 +107,43,18,22.426733,81.53480799,6.745104394,65.54475812,cotton,23.39587454955139,2,8.921316309333184,7.795046195045994,379.01556197955676,5.066455865382414,5,8.446782800396454,50.7437285969786,133.75254055073822,2,43.968521944739635,2,29.701184310001693,2.8713812053772427 +123,44,21,25.78544484,75.00539324,7.641116569,91.39578861,cotton,16.989144158084784,1,11.11529225937914,0.7295328252160793,398.3178779259425,4.886379170702017,2,19.29746523639279,93.83030322951053,115.45965542586548,2,23.415335205717163,2,46.63679227783927,4.879503781827148 +112,49,25,25.68959532,77.90621048,6.470135478,66.19426787,cotton,16.548476182735968,1,8.102046048722038,0.9594015654352583,353.7443802359333,8.940130057945872,3,11.445036356813228,75.57576663299153,196.30363652833282,1,27.063149132499927,1,64.40016425030977,2.9412223847581944 +119,44,15,22.14593688,82.8597549,7.091992365,60.65381719,cotton,17.927525764518553,3,8.96623863596472,19.029153577895265,397.3128062150633,1.3967414520049983,3,19.5995219209858,23.69599570115527,114.74288319379255,1,48.87402315936698,1,48.34961649137095,2.9835382348903567 +130,59,19,25.07278712,82.50257909,6.520403794,93.51042684,cotton,18.392844686935806,3,11.617691965140015,4.592301521774709,411.61247988920036,2.791544240883138,6,18.515613108805518,79.95477775039265,111.55123078154233,3,31.862310121157893,2,41.460292977783794,4.157859012748555 +127,53,24,22.21506982,76.17851932,6.127939628,70.40557612,cotton,28.28785501593033,1,10.651890766766135,12.279560410488003,406.3559725922266,3.3122460650360144,1,19.105252684172534,42.569033077842654,118.81855400711677,2,16.759417087699163,2,14.74816755921422,2.874441364259564 +134,52,18,23.9643129,76.59175937,7.994679507,76.13090645,cotton,20.189868918154012,3,11.216177439580607,5.8969720759305,422.7666727143589,4.734976408775425,6,16.95816687397687,28.91747696800716,124.49528681289583,2,2.8477566172317506,2,66.3107406693747,1.349766288939899 +109,36,18,25.40059227,76.53237965,7.524707577,62.5138867,cotton,24.22831197302924,3,8.613266387278342,6.519033364573681,365.19121464231114,7.511357096390567,1,17.041413536263462,95.87193779503632,95.96432240050252,1,10.975537971306732,2,22.28862878301343,2.995600355731928 +100,48,17,23.7805123,83.03878838,7.827877818,66.26555904,cotton,28.177092280000103,2,7.245895374782523,16.83879083610578,411.75945998901545,9.85472497317278,2,8.10484183197612,99.216638493547,98.66878477944559,2,14.355624187448962,2,34.8257502963094,3.2106785765750288 +132,52,19,24.16402322,76.7433897,6.436691764,61.94626051,cotton,19.89646830056421,3,10.804023470209728,19.338018797763564,381.54457505677385,2.5432862758432613,2,7.814507975108542,65.41354078230823,103.66412530165333,3,38.19362146774877,1,32.22375032542995,2.091110754154142 +102,37,25,25.31468463,77.91757121,5.907930899,72.82902109,cotton,18.178618713912194,1,5.178290862765839,18.13407894270338,356.0045768973722,3.37206655013728,1,15.557433503554815,31.550125705435928,115.07131093177586,1,0.6031900057873574,2,83.38894045853674,4.259559344710851 +111,39,22,22.60361557,80.3509046,6.135025006,88.57395505,cotton,25.065182462428773,1,10.08944135593795,17.571937730412866,377.5556467310298,3.8319406981073922,6,14.466007584582087,54.36771667925756,64.67934168250915,3,36.61745228841443,1,42.52966207350389,2.392077312836356 +117,51,15,22.9535715,78.71555832,6.044556594,99.75336197,cotton,25.766288275380234,1,5.592013854303504,8.390318789236437,431.131650347325,9.179226528254352,6,8.572740906285082,81.98425082376126,147.87500524727858,3,35.885202998956736,3,23.291139081656485,3.306983159599747 +136,36,24,22.74446976,80.41198458,7.59781958,90.07326633,cotton,13.467257051310288,1,6.062233006437458,15.518830701659601,400.3356827960958,9.795358129480359,6,6.641312474685122,72.7151523464044,182.09581558472433,3,47.59362168428595,1,75.95540772356551,4.143892130743773 +134,56,18,23.80834611,83.91902605,6.691268104,70.97358303,cotton,22.873426498240647,3,9.065676466358717,11.828669523887532,404.52874884660633,9.339148192728784,2,12.787337726186745,87.26160734748224,199.32393909616934,3,30.504132778164273,3,83.96540776900801,4.439673833448767 +112,54,15,25.46228792,81.56641891,6.175492306,76.88582484,cotton,22.76091181290154,2,7.386374826438994,6.1537849960961495,398.14706956348573,5.700917639637359,4,19.754520311569376,44.22490937532035,82.29613921373789,2,13.960754528985643,3,15.731727941785678,3.835107660339237 +105,56,15,25.96779712,81.97904282,7.272316209,74.14169043,cotton,13.022598702062304,3,11.809362025557908,0.18316000385301567,396.0909221177417,5.014479723361223,3,14.884890757034709,30.0516860144041,117.21200367055792,2,22.316850434674702,1,44.92553979198437,4.17737487115499 +140,45,15,25.5308271,80.04662756,5.801047545,99.39557151,cotton,24.064683454107026,2,11.508539472849545,13.998674565573324,428.9813666051365,3.320507894185029,5,7.804737871401688,73.10490604309965,74.79008678109258,1,4.248663134646103,1,74.89848150051472,4.168670734974292 +126,46,25,24.43847399,81.69801729,6.757457943,60.79645852,cotton,27.904626369169414,2,11.754195476803133,3.597513708237716,443.2702606334837,7.42526349465857,6,15.551570915686904,79.44196312121973,152.93451220153014,2,3.104145924264934,3,83.21858964188698,3.2033850543922817 +106,49,24,23.03887865,76.47039772,6.983395573,90.64770699,cotton,11.074538017793023,2,6.883082739087616,6.627340403171624,427.9341285220829,7.761244933785096,5,14.067149851961437,85.93575540239962,135.43701561765482,1,39.27648104951812,3,29.602826171450978,2.2817908366668846 +121,53,19,23.51308653,76.72621429,7.976889498,80.11272117,cotton,28.914272623918485,1,7.931491404456913,1.0831561350996188,414.5398333736644,7.9765460438031965,4,12.862438027581144,93.42653644843071,59.97389337799559,3,40.843416105668396,1,67.05678364391477,4.855826023576963 +108,60,17,22.75805656,76.75768356,6.558902588,97.76600619,cotton,27.198144361077667,2,9.352468944495424,3.474253862975638,408.66950695971383,1.4167656776265622,4,7.39685945679343,67.16843519724114,128.67267650647986,2,17.095188460778875,3,22.64489584716195,1.7986080328796237 +116,56,17,24.71252544,77.7293114,7.979090365,85.24963302,cotton,27.255177027938572,1,8.095487396564256,17.28670417570428,375.69709711568294,6.8701360489868195,6,8.532349934760967,10.784445777547468,82.92349760862379,2,34.33353590354385,2,5.645844384372323,1.2849472978780403 +100,52,19,23.45969093,82.44777468,7.903528673,93.50153555,cotton,18.146939370630957,1,6.259547853655869,18.710499163743012,358.3806884002048,7.903640036293845,6,19.125194907006332,11.593773261884088,101.24255467488207,2,29.8112756733523,2,47.26517324644933,1.1178466486469363 +129,43,16,25.5503704,77.85055621,6.73210948,78.58488484,cotton,18.251191668156316,2,9.846418472622364,19.35145071950972,403.5119885768368,6.895821423792727,6,16.136255805220966,16.930808537915464,127.10205805068446,2,19.210768382586902,1,85.21689981482926,1.7077554941749495 +118,44,23,22.08458267,82.82904143,6.691690476,67.06459777,cotton,11.976562880987379,3,9.932916505559007,10.411331323890467,428.91273377158865,4.46972997760623,4,11.417062100772057,82.30579120911639,188.86501570851527,2,3.976749933629992,2,53.784179222459926,1.7567328842269223 +117,43,25,24.68854799,78.51206972,7.839849298,69.31153566,cotton,11.85753074361109,3,5.815439838678499,1.2804362251307477,392.33623628177384,5.994470645424461,4,14.799086401847935,14.015417092366732,152.5063049534814,3,39.73068609409254,3,26.535610312955992,2.4968274666990133 +126,37,21,25.84997269,84.16855231,6.61448588,77.03421249,cotton,27.24570257396686,2,9.062056008036237,4.4776403519396695,354.45783306873136,3.384221477244054,4,16.56920958267621,70.02841333536716,158.8562078594927,1,10.392941728960064,3,12.216221126342464,1.0164297519510859 +120,48,16,22.46054478,75.40989245,7.456971816,71.85436078,cotton,19.114633279682554,2,7.231303593125018,3.7957078157848523,441.9517549248638,7.0427733643497445,5,16.89087111460446,64.98478526789985,59.796599479496955,1,40.4709250429555,1,8.286654204706457,4.562280408280323 +102,45,16,23.65629976,77.52425987,7.2942193,74.8984994,cotton,20.017110778545348,2,7.687168112600203,15.213949270597508,444.9521990314827,6.910347464769371,2,9.024254760135705,67.8433725368516,78.21373884062353,3,44.91389285465697,2,43.918183329495875,3.346505226593783 +131,56,20,22.00817088,81.83896111,7.762647875,92.23645249,cotton,19.292906949041857,3,7.583181167762778,6.333184040442661,386.34644424819277,6.944161684202751,5,14.65131344066336,52.86706157446772,75.57770860579086,1,30.272533052328182,1,80.53832359598032,4.743608000731834 +114,40,17,24.32630461,80.13456404,6.363406102,69.45072055,cotton,22.065704318970912,2,9.269441149821507,3.2645398750779053,432.2661112817719,9.94540873529359,4,14.564653710694856,44.74239243001102,71.19616412063493,2,15.786405729711628,2,9.172952536950053,4.455575020315186 +101,37,18,22.92360984,82.68738535,7.63737841,92.91915074,cotton,20.7554159609588,2,8.617874563563923,6.723813702969026,409.31363506845713,1.123275620241906,4,17.910165198809295,77.39191934681148,144.46740640892386,2,2.3616627853399184,1,43.80830583715942,4.972600054285735 +106,46,20,23.43821725,78.63388824,6.200671976,81.15072105,cotton,22.22145513378657,2,6.671864132446136,14.605737127677894,387.55295768550195,6.992197099951964,4,13.430313343292774,17.55642366934844,136.9636866266148,3,7.237031439759711,3,81.89093856093395,3.245223116980456 +113,38,20,22.10718988,78.58320116,6.364729934,74.94136567,cotton,26.092338856094248,3,10.20069756507834,17.760919561843668,404.2636624943853,8.314314338045156,3,15.622168006844422,25.08264943674936,100.1320043635288,1,23.703225222301107,3,77.2982896463321,1.202531566650809 +102,53,21,23.03814028,76.11021529,6.913678684,91.49697481,cotton,14.05782656290738,1,10.204616717910426,12.182892805650988,363.6025116457563,1.0508770548252115,5,7.35575005184657,90.99922868809242,197.7090028013876,3,12.846762897858726,3,21.129192235481376,4.240513280784109 +110,39,18,24.54795322,75.39752705,7.766259769,63.88079866,cotton,23.505031095110127,2,9.143337537960415,10.578909970420707,354.94630269592705,8.441332715993141,3,15.120236624219512,45.63814997948141,183.1554601710277,1,3.1213978931366517,3,90.86297907582734,4.421955734933681 +107,58,15,23.73868041,75.77503808,7.55606399,76.63669195,cotton,17.976434107611972,1,10.027443505191567,16.66741740873986,408.1755344769629,1.639757824131599,6,19.25478146862218,98.53229078954443,132.68552470180828,1,45.02552451204915,1,99.62994129270884,4.005809101831249 +120,60,15,22.31871914,83.86129998,7.288377241,65.35747011,cotton,13.813650400922198,1,8.34975507621451,14.326331553602799,391.089095045729,5.776348680904549,6,14.923220129406543,45.27869732010279,95.54231955177747,2,28.81577359646763,3,42.635369692011615,3.236825024347703 +89,47,38,25.52468965,72.24850829,6.002524871,151.8869972,jute,17.767683965423615,2,9.24746776742683,14.680317390318269,420.1000900008126,8.75845417057526,3,7.997518618377588,52.91560582796904,90.71533074830157,2,43.10518429596646,3,69.66298346028168,2.2241059528731455 +60,37,39,26.59104992,82.94164078,6.033485257,161.2469997,jute,20.422314869535914,1,6.0764845949667485,11.756233084592074,369.78696760665053,2.10592277076431,1,9.891199366937476,83.45287359178775,100.40715349485444,1,16.8094340224828,1,7.423019530632702,3.353406467216856 +63,41,45,25.29781791,86.8870535,7.121933579,196.6249511,jute,12.431389871478997,3,6.733361719861563,17.751678332968215,441.7362374391358,4.907929732960426,1,8.901491954158882,19.157895378069657,161.0080343731229,2,23.391924006508287,3,63.807194314743334,2.798712482095221 +86,40,39,25.72100868,88.16513579,6.207459637,175.6086697,jute,16.512402224199015,3,11.269242967838313,8.987912284006311,385.99808561626315,8.527743639802813,2,6.42993893600279,71.06261228056438,135.45289720660742,1,16.862349244139306,3,32.05330198079628,4.272710238879335 +96,41,40,23.58419277,72.00460848,6.090060478,190.4242157,jute,14.229025841834247,2,5.561572493298895,19.238925768731562,359.8558802432774,1.4453703757831506,6,14.260611280555425,62.376352253333934,85.88590405276469,3,16.031511525305074,2,50.416130306797044,3.94206601951254 +100,35,36,25.31042337,72.01364411,6.346715209,190.5577618,jute,17.990423375580363,2,10.262731519378825,8.99188949654484,400.33010827374966,4.83100499038849,2,9.462593963298579,60.35737145365153,88.94495060282044,2,20.72517039256299,1,49.93076567390665,3.345452893925041 +63,37,43,23.41798979,85.08640476,6.661957897,185.7446728,jute,26.249124348998215,3,5.626831309488358,15.877737264026162,386.76453970930186,8.401675895564635,3,8.339141467465225,56.88373015104903,123.14717910109164,2,3.622663818946792,1,35.404079381607104,2.976793938377692 +70,43,40,24.35564134,88.80391021,6.176860192,169.1168028,jute,27.017947967752875,3,9.496189645224483,16.3579459690587,371.886326792061,3.439075166005021,4,9.46824599786829,63.6220624923512,73.49137448584582,3,24.832284470914423,2,11.579721296613766,2.372034634020892 +67,55,44,26.284017,75.14640198,7.251847296,182.2685447,jute,27.402756808604323,2,7.4821032292117025,10.35719060995433,426.1112481116733,8.89331473980176,2,7.9737513964034346,97.44507306191579,112.25170312985539,3,2.5611892000023895,1,51.67677987168441,2.3585933566574573 +74,40,40,25.13842773,83.12053888,6.386259978,169.3388465,jute,21.105961905060312,2,10.796293780301173,18.53200464709446,410.8531797346537,8.017029518865794,4,17.474163541068187,71.53490768087742,79.66631936179628,2,20.95133930639092,1,75.12174838978406,2.1227589461284238 +89,53,44,24.88692811,71.91711523,7.319735475,150.2498675,jute,19.88370803016982,3,5.1596917751025835,19.003014029944328,444.985436205019,4.438269860791205,3,15.5024548909328,99.81644429020956,134.1933724816157,3,30.21716770552493,2,64.25674130317589,1.755518263380937 +74,46,45,25.75734909,88.36668522,6.025028997,189.4263485,jute,20.078951481109264,3,11.222086580953107,9.733990856436389,360.3151911757925,9.810546937416392,2,13.260990528686474,85.10087981678474,194.10699264547532,1,5.54547659472534,1,48.75868557633613,1.6168913263026266 +89,41,38,23.12844351,74.68322732,6.344751947,199.8362913,jute,14.583181562911136,3,7.49781843907736,2.9391253985665533,366.1989278079281,5.559056159769827,5,11.43216934773972,18.825127922492555,94.96520622103009,1,21.762485598341584,1,19.971489310920663,2.6713633638228154 +60,55,40,24.9949957,88.95692783,7.02777956,151.4935635,jute,12.098465709033965,3,9.08177787769231,5.483546753598121,374.526673255268,6.400409456622779,1,19.922403817825092,59.48086971098988,71.62654108913281,3,47.67804519527812,2,85.22970852232609,2.700308140184361 +67,43,38,25.21622704,70.88259632,7.299304715,195.8645552,jute,20.862010923388805,1,9.253882618460917,10.027409738697388,430.4077230987176,1.0310227940628014,6,9.556684341786667,91.94988842604161,58.24457670898289,3,1.66379851977983,1,24.781288716057194,2.981652711923935 +70,38,35,24.39736241,79.26861738,7.014063944,164.2697011,jute,25.5540921371553,2,10.944399559588474,0.6055709586598002,407.7958455610235,5.123366713150365,5,13.698363683190635,90.7053073177228,170.1537412234884,1,6.478122584416507,2,46.529169611643894,3.2691769932608463 +74,49,38,23.31410442,71.4509053,7.488014404,164.4970373,jute,19.39425294377949,2,5.2273008772391805,8.526774416813183,432.29269742835584,6.7332293521811994,1,14.531521511232175,12.007742453468161,122.50392426926217,3,49.65884380372649,1,88.16679647488482,2.333453165849807 +90,40,39,25.72668885,81.86171563,6.626503893,191.9649389,jute,29.94912784213244,3,5.396148610190196,13.511455649060967,377.2772137178462,4.034119518122074,2,5.500704440738595,16.871414166767686,106.32891145592926,1,7.417749045577294,1,1.4315976075256698,4.049714365698599 +82,35,44,26.96656378,78.21047693,6.239011,169.8391177,jute,14.31311338573272,3,6.241785974632963,9.918380333427434,367.1083431343126,5.0389558427860575,6,13.214941716593131,45.12270660480904,155.06950392314099,1,44.376322057743714,1,71.01908660131659,2.8205620881680487 +73,45,37,23.70467146,74.63745355,6.742688094,181.2783964,jute,21.56306756467927,3,7.830123596933335,10.844084673782135,394.66327190725974,8.57495743789919,2,11.38753515468073,65.69245658490343,146.4564477356177,1,22.221172411911827,3,38.2197253352357,3.882760168179311 +85,53,38,24.90075709,73.84186449,6.588017308,153.8990984,jute,17.575165149551303,3,9.992272922742142,15.232742156913446,353.24811903748906,5.500261723922966,3,5.734388528682154,71.02663901734522,113.18721463676529,1,22.150585269805518,2,86.55994085398837,2.3994895422471476 +81,56,36,23.39605743,72.60512854,7.097586415,174.7876411,jute,14.638855477740059,3,9.077472656154495,16.31891887906267,435.9599507861081,2.035834728763751,5,6.908954174898226,45.89259574210707,163.76032302773902,1,41.7889397801761,2,90.84126774421891,4.153582799935988 +84,55,38,26.8748389,79.78725152,6.956682743,173.1017097,jute,28.5806924590807,1,11.258799588250945,14.916138262997256,358.13206798864894,6.028935973872074,5,13.924241397843442,38.72725459523664,110.76121976142352,1,14.735106781470291,1,85.18235193298005,1.0717928009851154 +80,45,42,23.1426498,74.99739774,7.380396262,151.9035477,jute,10.265691516507482,3,5.116130450561281,9.579143319509086,413.44148364884154,5.823237578048962,3,12.379285897985827,92.17311108764581,126.8711016436148,2,32.975009926555735,3,87.53566075896472,3.7279016031387324 +76,54,45,24.29496635,77.62976013,6.176618831,184.9800516,jute,17.012374321392628,3,6.704144652357225,12.827229434025904,423.55375149509314,6.970662109515863,3,13.299365283075856,67.79059128250118,154.3674169663304,1,36.72884309782761,2,63.45354402782729,4.799408723107563 +76,56,39,24.39459498,89.89106506,6.551130445,197.1220049,jute,12.251031402864811,1,11.430076189429538,7.165210134980775,417.6484722028566,8.95327905545188,2,19.35190321156272,34.16757228100553,188.98464258484907,1,20.117278719689153,3,62.17363241511248,2.5774274351424085 +81,40,45,25.7629429,80.76238215,6.427726565,174.5071843,jute,22.172512053007235,2,9.12947889086137,11.499532401087109,411.9692935578049,3.5276756830934155,4,10.200625398563737,56.776725971070775,163.43800910790304,2,13.63577058723649,1,42.65007853304426,1.63418930964745 +76,44,45,25.4879684,84.48235878,6.740947635,168.7848886,jute,26.664930509052184,1,5.263525883776876,17.44817398431784,416.72536946201933,1.8445663680715214,4,11.12617812642248,22.367452141122612,116.41522335611764,3,26.63894046543474,2,41.91785585791475,2.154479839742195 +69,47,40,25.37122686,76.2403666,6.130136384,183.8270791,jute,29.77048860504933,3,10.123506146431708,10.432171219734457,421.4597292172297,6.914028064959028,3,12.043594271092097,3.06784500729661,118.58062381506628,3,14.075684987558951,1,31.794670978265682,2.538568257266978 +82,40,45,26.21312799,81.70476368,6.667633355,180.1237765,jute,29.67466452862166,2,8.64698442766475,11.018114873347084,406.37165600380274,6.783038538433298,3,15.440119474236585,55.75103266542835,62.46309885826663,2,21.42986710466336,1,44.29469574490456,3.065387058868739 +69,57,35,24.30748599,78.54340987,6.186814392,186.2337571,jute,15.922194233655581,1,11.239182777496858,12.380550603201518,367.32723342253973,6.489729198381788,3,15.965260706472467,5.398529688285136,189.26313026753405,3,42.05640490351614,3,88.12767084566,1.1351287563757833 +81,36,38,23.76554749,87.98329901,6.334837865,150.3166152,jute,15.89334840525219,1,9.472242736053078,17.68356485511799,367.18793334703327,9.450640968771255,2,14.625473787333044,35.827188388035644,145.03510317652302,2,4.635155102792682,1,83.79120850951658,2.8873059670486554 +67,60,38,24.79853023,78.53037059,7.16214284,162.2847429,jute,26.59739862161026,2,8.572153555461853,16.50972829337684,364.21018617871647,2.606474878980598,4,5.625841727264945,93.88260361612299,130.48816989268084,1,33.240860469289096,3,0.2508888234327711,4.386982662477605 +72,51,40,23.20683504,74.09956958,7.422318499,199.4766779,jute,11.137798914431894,2,10.378997906023601,9.581843529616501,402.46865250999616,4.107923311533053,6,7.290051877798707,16.237775739019455,77.82306231727507,3,6.044217553171244,3,12.582194424427195,3.236258100302575 +65,39,45,23.66805429,70.89000744,6.768001309,184.4633281,jute,19.603952652223036,3,5.473169555710194,6.4180953950326325,350.7298872165086,7.09338449705408,6,10.495326010422605,47.86033763688574,173.81983700175644,1,6.398978234102392,3,64.6070806289935,1.8280742397629242 +78,50,43,25.12417673,85.72530641,6.348441469,159.5718087,jute,17.350890535659858,1,5.064500727080514,19.891359237156397,438.7106174320103,2.603252179611136,4,7.741566483943583,25.50140248336029,129.77503007345933,3,27.718515918084314,3,66.06295865572535,1.0868548056620013 +77,52,41,23.89069041,83.46409075,6.097294061,167.7230632,jute,17.84149031862414,2,5.127257454972142,1.149361261665307,444.4014318840174,3.431119672408978,4,14.55462999730052,30.591092095286665,181.6964047695043,3,38.50766078731926,2,26.863909962896738,2.830012078902274 +89,52,42,23.09433785,81.45139295,6.14132902,196.6587013,jute,29.625506980443276,2,6.142220009582688,11.269879598959584,363.0069045499851,8.76577843751461,3,18.793348259533833,10.937526990128177,104.25653441626622,1,3.0383432774663097,1,90.32671959557645,2.3188701659292983 +62,49,37,24.21744605,82.85284045,7.479248124,166.1365886,jute,11.875862812343085,3,9.194175525919775,12.078937651326488,377.6270610977994,8.852485971004233,3,14.906872718034846,17.29066392277635,82.71159650091977,3,49.751257587928535,1,38.574184427302185,2.8957400988965825 +90,48,45,24.06475727,71.31342851,6.509174789,153.6390212,jute,14.490836811297964,3,6.1295810327624825,19.4627349732693,380.8524847776745,5.106708340896481,3,17.494095423291867,14.971882003786853,144.02693009425735,1,47.457602220865816,1,41.70962599917929,2.9672987313075656 +66,47,36,24.85441411,74.4407048,6.57256106,175.572958,jute,11.994661224514807,3,10.740460823444455,14.986502906266972,440.5133918928584,3.545555849801357,6,16.74076713861924,34.89744684346886,53.769386633263714,2,27.54442735425232,1,50.497446230445675,3.9614108372897725 +80,52,39,26.41915161,76.85691248,7.165696848,197.2101782,jute,12.025555160607496,3,11.502773055482097,18.3362417244968,429.6499234756257,9.12541851147823,2,19.04888314690641,9.732517356502035,191.9222335308865,1,20.86266450103737,3,3.1224825676689494,3.591886705824868 +89,52,45,24.89326318,77.01222585,7.207457208,196.469984,jute,28.032778544167666,3,11.735085538919598,13.22612812409573,363.45765749432763,6.695144712226781,3,12.260730378013658,48.36410392206982,55.32314645831764,1,41.346725954144475,1,14.102486265261827,3.4464401949818826 +77,51,44,23.25583402,82.7015932,7.124333547,166.2160846,jute,25.99834271950722,1,6.698883076662428,18.430729354895863,355.5107802090195,5.1835225732782595,4,19.485552587518715,85.35205824147219,79.37790618089647,1,34.80442678288334,3,52.05343791940511,4.026407531558388 +94,37,41,24.7634518,87.06071115,6.463538707,179.1630865,jute,11.134390991338917,1,8.19651499448606,9.56457376247278,404.8906890138018,4.116215620399335,3,12.9392204236633,25.011433941214868,160.82450580403122,2,8.585772558600718,3,95.15689267511213,4.736787186596204 +75,41,35,24.97042599,78.62697699,6.856833064,166.6415254,jute,14.322326690337254,3,10.968102409782272,10.966450241503587,383.10942648838915,1.601384826761013,6,14.473097137569189,87.14838007901878,159.0612978646435,2,45.20089373642154,1,92.18671159612418,2.9687401809365452 +60,55,36,26.12797248,80.49172597,7.132389299,150.6326874,jute,16.88963551289274,2,6.591885012250675,6.960989134289177,391.04340031421026,5.530480550220615,6,6.979575013746615,93.05107884349137,64.45643709539047,2,15.808148130673672,1,77.02308799492903,2.2310537530519565 +62,56,35,25.97825807,81.65769588,6.235357638,163.3488091,jute,26.306562440467495,1,7.48385675944597,13.639386482609092,431.9402962828727,9.616125223275146,5,19.510352840424616,38.33685412106932,139.84111070565928,1,32.81934165726439,1,65.82511491607632,4.114013671651049 +84,40,42,26.2830571,73.35763537,6.704273839,186.6898282,jute,29.71032072499192,2,8.922195595344332,10.893424175552033,428.51296752378516,3.427352404866317,2,6.593802686203436,46.835185865894815,70.41464777902965,1,17.195741346948136,1,8.877128923948586,3.7225664825743654 +100,56,40,26.38905406,83.31240346,7.433313409,176.1516409,jute,11.006101701812238,2,9.179618395674481,2.296052054917568,427.3508020333488,4.504569519073457,3,8.305587074015538,56.60715839324294,83.12536483820415,3,23.717992316796288,3,2.1151469236361353,1.8199942634533621 +75,56,44,25.2746335,73.7459581,6.109478059,168.0432282,jute,13.26369870476028,1,5.409148034004714,5.019200610647474,380.68142226451135,8.966918357994103,1,7.610540361206949,86.01012654087322,166.65320338716063,1,35.06452841225753,2,57.52084758629658,1.063358046293696 +78,46,42,23.09499564,78.45959697,7.095413294,155.3851533,jute,11.137747225723729,1,10.021245422120973,16.790591433294576,437.4852342847917,8.404334245212892,4,18.323473563219412,52.88872290446511,194.24591722676288,1,3.5382986185505336,2,6.867685490414754,4.288786780063352 +82,48,36,25.79351957,81.76904006,6.352076783,193.2418382,jute,12.836443377920073,1,8.838726255734736,2.979816130587931,385.8714198266298,4.11318736520715,5,10.703988199612043,40.877345510381005,123.20713263261099,3,31.01097440313042,3,71.89863036444987,2.986431213197381 +100,58,41,23.17403323,87.88255345,6.658769991,160.6217342,jute,18.736511205272386,1,7.502184619010908,3.9697229123105338,438.78502873012576,3.5332825284730442,4,10.096929694398503,58.914217131157706,124.88223124446776,2,5.319566387141656,3,8.040522337600542,1.9997382421904684 +88,50,40,25.63215038,79.95150917,7.051822472,182.2582277,jute,17.777651691177912,3,5.018772898195607,8.135712439433295,400.2651142413955,7.123659848901249,6,9.276307352952415,35.58475953580368,160.15599456340544,1,39.68329243988673,1,22.47515661757048,2.0834285073429104 +67,41,40,25.848795,87.81661683,7.333143205,152.6194403,jute,26.81682366852201,1,6.143599104231096,9.985184312548885,383.9369475736786,7.692059939499588,5,5.429196509169849,87.9271720959559,84.74815710430966,1,27.699122227521233,2,53.55230143307077,2.0629336913391367 +72,42,43,26.56767277,80.90424543,6.352771037,181.2915605,jute,17.892974836777494,3,9.933301211672209,9.862542721168593,371.1748826617812,8.008564755023752,6,5.838973087826488,91.73647020818817,135.55711637823214,1,41.48127831159649,3,38.567315745934735,4.086867740684302 +89,40,43,26.24532085,72.97198375,7.124050134,189.9711184,jute,23.955364277581367,1,5.245321767685116,4.6604449066349645,389.42258312483654,8.334702710487006,3,18.95130140886981,44.22949462670083,189.47641097269684,3,31.70702114898425,3,39.38362485316691,3.4925682024049065 +89,57,43,26.91515043,73.19897535,6.998787171,177.2233048,jute,12.913834094108532,3,9.200138808027319,11.43323513442606,384.84116082778723,3.3193200378571968,3,7.3267015601255805,76.80558635676003,145.40000161285036,2,5.752068871491778,1,50.28211383974072,1.0738626302699452 +61,41,44,24.36972377,82.11319791,6.537914958,159.9210934,jute,21.172513216459222,3,6.212717363147618,18.65374607013702,365.4885544526411,1.2207058551813796,6,15.265092570184205,38.75887172963689,95.98675306839488,3,28.025086489447443,2,10.280069310577266,3.7130655695985144 +79,45,43,25.71901283,79.15532398,7.171054239,187.1735424,jute,24.77096279268085,3,10.670456914084781,10.616964136757023,381.4463733247702,5.785821528686622,4,14.921742036339147,56.633221401366306,194.74072848189388,2,43.255674278717144,3,51.218068313817454,4.952564990496803 +84,40,43,25.01157559,88.3313023,7.228268228,169.4168014,jute,12.080064187509754,3,9.056126892858865,11.848274025860402,428.2111580281057,7.938710883539316,4,14.818179279707113,37.62304480665454,114.7675902185968,1,21.268610740007603,2,91.48876486051678,1.7687145425439246 +98,43,35,25.40785911,76.44048625,7.319952206,188.6372826,jute,10.667365708154236,2,7.770554294053623,0.5772072600864209,380.31700505097444,9.973918015413116,2,7.86732542323147,91.51431149859158,152.24530754196064,3,44.68888808879602,2,23.758428463079373,3.334200790718736 +75,36,44,23.28081,74.27607475,6.613341343,153.7447398,jute,28.927231809538224,2,7.431161152925293,9.701124825449373,368.2032681605779,4.366096362170447,5,18.955063317930524,10.007672253102706,170.75711101332783,2,23.027155908900305,2,75.8766558126532,2.668625038217788 +89,58,35,23.98651719,82.09053379,6.096838784,167.0576456,jute,29.050821528983043,3,11.634342487107205,1.4827051164736638,449.2404637687343,2.5495264723337474,2,15.626385189785164,89.78860094482148,138.30857038925944,2,33.39776232237774,3,57.440073851722396,4.437861915979597 +91,41,37,24.48556447,83.20630007,6.132570523,192.2316221,jute,13.212868150613586,1,11.625648002961775,11.779330983841996,421.681766610136,3.5460593367695408,2,6.079580245001063,46.52277116668753,109.46797305121976,1,0.017756487870346227,3,73.80931189845381,3.8113204210435168 +77,48,36,25.86705009,84.09985284,7.36008498,154.8390847,jute,17.858398944634363,1,6.5574348766597605,8.278964635491622,370.0576266141891,7.8650026138232425,5,17.801158026982968,49.029673836300304,182.14286974123425,1,8.699633045728527,3,28.540357076043588,4.888516890744399 +66,58,35,23.5643831,79.46283115,7.321619041,185.25947,jute,22.499873585836895,3,5.86017438648281,2.394227952007797,362.4246970466864,2.154198966694744,4,6.750341509679826,14.739703314315534,125.25619331237893,2,10.554650995058923,2,75.16468097664662,3.490640218111445 +62,59,41,24.2248758,74.89465426,7.175170657,192.4931257,jute,11.922048094408044,2,7.621747158783365,11.884933241875718,431.35107778622665,1.120510222551913,6,5.386530687181983,94.48427266266204,62.499750155210734,2,3.94187966560135,3,62.40100760454808,4.060180132901364 +82,35,35,25.49386782,86.97061481,7.299076163,176.5268267,jute,17.605865260017964,3,11.476868391255879,6.258734179135876,429.54701458969635,6.8130367402479415,1,7.262267913270218,56.8688398022247,96.17313824721589,1,37.02472559730669,2,60.38916828800481,2.38496586412348 +61,41,35,24.97178693,79.47557931,6.842966479,195.7571622,jute,11.984999511463448,1,9.932451794794478,14.753736567493318,405.16814471312983,1.6883070937097573,3,19.803971639010996,82.47447413212741,60.08990062055904,2,39.09496142337822,2,55.03750739272601,3.0040569532628436 +99,57,38,24.80624984,82.09281674,6.356295568,156.3616174,jute,19.28135004488415,1,6.32506460728552,18.410888039022858,381.6521268522965,2.8734546121485165,2,9.193849968779896,62.27677997458439,100.14754802451725,3,41.73320937570238,1,55.5349248760158,2.8405194444648627 +70,42,43,23.16814977,76.66724969,6.508342839,157.1215052,jute,12.4965744125287,2,7.005498522421753,6.050143864254065,386.65571535436766,7.445109092493055,2,10.780705803147663,69.5452500519428,112.63467953972662,1,32.35289347480727,1,15.092263460459144,1.5712091824280439 +90,59,35,24.25133493,89.86454053,7.098227926,175.1742112,jute,19.444760785788063,1,9.356738405047029,18.67595292718299,354.12957623423733,6.332189619128233,4,12.623615346166527,76.37917034180974,185.73748184444383,1,25.325221000588417,1,40.39873564046235,4.969862795835384 +73,43,42,26.58361011,78.00774772,6.310699968,154.8238864,jute,15.963386244910323,3,6.860736818152619,19.734880531005665,381.4761389743183,2.953499534536744,1,5.053442809642982,55.06142002484171,126.83764777540219,3,16.562645375218505,2,82.15709750124108,4.645126134000405 +67,46,44,26.82489244,78.20392774,7.093328631,153.9199807,jute,23.950618925887262,1,10.803973301795464,4.840692202455726,357.14696548221514,3.304826875573523,3,6.300291902802643,75.23881751194001,109.79351370442777,1,35.10126378302781,2,75.56065016887388,4.9740810527644586 +84,37,42,25.49674786,81.13449097,6.691074249,169.9288234,jute,13.140437181572864,1,8.454868693367024,13.36831056353401,376.3522231292921,1.7343297453139424,1,5.905922959828323,94.46746962139066,63.117866933613044,2,18.84345601473514,2,72.78694824865862,2.7531855148627478 +72,41,36,24.09874353,80.57226761,6.187746776,176.8604109,jute,29.320803383265737,1,10.841926539136791,18.631198966721502,364.2674914846384,1.658430548187897,6,6.5218069665328064,69.34609122363452,142.44955946225565,3,42.12640789582993,1,19.055958194611865,3.987503989226313 +71,56,37,23.18866654,86.20899734,6.491506245,176.103677,jute,26.199649350380096,3,9.964893812816793,3.7473276705016922,362.52965050613307,6.849496257352614,2,5.7933133349089,4.6110747812501796,199.75266291345133,3,33.06477543051305,3,53.56666920631632,2.0953954384820723 +64,53,38,26.24347471,78.51063754,6.855362875,183.4065252,jute,10.118846759257703,2,5.118375024424661,6.603132039148272,435.28271909803834,3.885159959649005,6,7.1346539969886145,4.489255783363144,172.22092375134736,1,44.07522386861299,3,4.0943242751614655,3.109778381318859 +65,54,39,23.75091572,71.14782585,7.124571593,160.0889553,jute,11.050504794004382,3,10.588695965157463,13.924831278157335,448.56757217546794,8.924681315281795,3,17.605915960081088,35.65522207290639,97.48689569336932,1,9.485963946883375,2,48.363226213866916,1.806879656139118 +60,58,37,26.13871511,79.1188943,6.067302109,171.4892533,jute,26.41942634413939,3,11.361445878468135,15.662910187543817,435.75884997288887,9.404974771471563,1,10.270208953810023,93.59315801966027,199.34446417920003,1,12.108435535070022,1,64.09621659221952,2.932772605210914 +86,39,43,26.14576648,71.23690851,6.432051512,193.1007598,jute,12.48595862613647,2,7.565290591076894,5.88187837312309,436.8874430350763,9.208335246905435,1,9.470339439382071,98.88813288802014,159.0952197795702,1,36.304976747221644,2,51.421557915924545,2.233196702121453 +90,50,44,26.91643698,73.48655995,6.253408852,171.4716375,jute,23.761118388351555,1,9.419083757405009,15.49630228183555,419.779603842178,6.088037386347792,5,12.836103729011995,4.715132596730265,154.70129490087294,3,35.14543854738259,2,62.55155278676211,4.168697656470995 +91,38,36,26.5232969,77.17331847,7.287318723,157.8548562,jute,23.02564795658849,1,5.9338743984225255,16.19440523796098,402.45516834916634,2.264156767016056,2,6.133304571294452,65.54843999688579,65.09420423563071,3,49.73172075544813,2,22.759620525228286,3.102235025945145 +87,48,38,23.81579631,80.94023552,7.161865733,190.312216,jute,18.557868625019392,3,11.65546072136413,13.538593785585496,422.0592725568684,3.9405173164145446,5,12.579110202036654,95.46660912035786,138.0230653907572,3,31.897183482156056,2,46.074354531794334,4.704657227625956 +72,41,36,26.50838667,86.84264005,6.065898283,152.9801697,jute,28.006202417960747,2,10.183317757799433,3.6315757059271525,430.03204578413875,1.9170308287310052,4,16.970912729573435,48.80995443313253,56.805639525914344,2,26.210458387566455,2,33.56073120901877,1.1318943837651338 +71,54,35,26.63952463,70.95705996,7.311077075,199.3355744,jute,11.561910846849637,1,9.944284454255342,16.646938108837432,404.69962519162704,4.401180132393311,1,9.360578419676846,34.58255180685946,198.86073234488808,1,20.50857791576981,2,80.13442318753802,3.315584393336151 +82,46,41,23.3250131,79.79609448,6.581693772,187.3096148,jute,19.674002504009138,3,7.194031324863888,18.727523567556258,351.5400665170053,6.9118608410910305,6,13.585421861314131,11.24183505642453,191.4007487508728,2,36.637408573008585,3,91.09228252614432,3.7445758829956732 +71,52,43,26.47549543,73.96164569,6.732826127,180.2513601,jute,12.517358028129866,2,7.237639560452281,18.34265595840626,405.8698101113194,2.2032986970045148,5,15.895780389122958,40.59706941397187,127.21378031444581,3,24.70711934608553,3,53.21674080467842,3.031458054777814 +80,43,43,23.78756036,74.36794079,6.014572075,172.6442654,jute,21.5002212703505,1,6.557478499022682,17.98384125542337,363.97253400695536,1.062429854016699,6,9.947151785602163,65.1174354413185,51.66199226377792,2,34.27646107086543,2,80.61574613972056,2.8571517456674624 +77,55,43,25.49941707,75.99987588,6.663559451,193.7141828,jute,11.088520793789092,3,8.40502494360062,12.050390828615868,439.73591164795357,3.509343624454489,2,11.663635871842502,75.51772535807635,99.06898365313128,1,0.4646916044235394,2,61.42541169732802,4.786888355173611 +95,57,41,23.24925555,73.65346838,6.434610995,184.7674863,jute,11.44437823312961,2,6.041058440305131,9.68846764543952,418.9531220231509,4.061761878039832,5,17.918977008851776,18.134493918106532,118.66986461047102,1,44.012513505014375,3,58.41955363899935,4.63983534575153 +63,47,35,26.98582182,89.05587886,7.432768147,193.8778713,jute,23.93873377525639,2,5.047153245226385,19.686258442681755,431.5642628573068,4.5968979569268225,4,12.53029419552388,35.60159477374293,105.3239386518253,3,37.89723190983225,2,68.33173969981779,3.653334447654966 +93,43,38,23.61475336,86.14290267,6.987332927,150.2355238,jute,11.720043870140849,3,8.515349483132098,10.245721875957983,445.71728785225775,9.036817289710042,4,17.350657159236547,35.75693194773114,158.38056632585693,3,38.758431547688744,2,6.981094761541485,3.373874541095973 +87,44,43,23.87484465,86.79261344,6.718725189,177.5147313,jute,12.30894752723027,3,8.480217336443202,12.605264170896982,394.31301792814014,8.319157844477353,1,10.86886197640732,54.796305664556286,138.81985669420368,2,0.23219932669062415,3,18.253669156618958,4.657908004216723 +88,52,39,23.92887902,88.07112278,6.880204617,154.6608736,jute,16.93858398518732,2,9.734811612028617,12.495273162751632,368.36703225905217,9.091342681635734,5,19.325948359834356,9.04826478160884,150.99400840434404,2,1.3122056164789897,1,32.55192296295766,1.4627658027764285 +90,39,37,24.81441246,81.68688879,6.86106911,190.7886386,jute,15.927120810494973,1,11.674255493857405,17.418627767011085,410.0294704795285,2.8152329263365234,1,17.428483959022095,98.5067769897878,193.13772937258116,2,30.056925725189267,3,11.27326978860691,4.571747674944234 +90,39,43,24.44743944,82.286484,6.7693455,190.9684885,jute,21.21379205595914,3,7.054745566339386,16.293026287998263,387.43149764451476,5.4670209929278695,4,11.297967586811211,85.35233658076258,61.46814443865807,3,12.72833734534507,3,48.771518717789775,3.635942303262281 +84,38,43,26.57421679,73.81994896,7.26158085,159.3223075,jute,10.634014274492007,2,11.07108106947015,15.329634833418943,389.9805891906126,2.1501301781889675,1,9.954171085128598,51.01640011726777,51.34953291673352,2,49.59779840017758,2,94.93679909826729,4.677841273603107 +91,21,26,26.33377983,57.36469955,7.261313694,191.6549412,coffee,16.916588897200498,3,8.322250326195732,12.489344587899271,383.7683486342638,9.284903975541589,5,14.08058697043257,38.817196672062025,194.40826186030029,2,34.60445014220267,3,80.5965801827112,4.054186324487022 +107,21,26,26.45288458,55.32222678,7.235070264,144.6861336,coffee,20.00090771188006,2,5.997592758159894,16.67279126837885,376.59011332198287,1.3365686620541266,5,15.997287220851982,0.9548984428930374,62.746796299975976,2,34.5724475577184,2,7.240345838789219,2.2841433635883246 +83,38,35,25.70822684,52.88667115,7.18915558,136.7325092,coffee,11.578841258002317,2,5.596198992370247,1.7063358475191448,387.8157092686401,7.545382620232643,3,5.26962877095298,17.872940262843805,169.4328990770692,3,38.12049172683803,3,56.071971669895134,1.1941787147555347 +108,24,31,24.12832546,56.18107663,6.431899748,147.2757818,coffee,13.433545248794221,1,11.341732874161586,4.32065858210178,414.71916295621105,8.102974756019195,5,11.652720409596064,23.148377767822904,177.33795580329695,3,14.838059795350533,1,55.74397757937463,4.23632507139286 +116,28,34,23.44372334,60.39523266,6.42321105,122.2103248,coffee,23.830296279767076,2,6.952318185060211,3.2035436389486804,441.573071314323,9.637916965207218,1,9.433745782156521,71.37818411303755,172.23787995872692,1,28.82273680988995,1,36.522919740673274,4.234085453496045 +116,23,25,23.4123707,52.26994674,6.869720196,139.3670753,coffee,26.868761849494668,3,6.741904344963057,8.41635863687356,373.9811437698543,3.7946831762811923,6,13.390083100988004,0.74848518583156,118.79743314609638,3,34.373895240317914,3,67.43339040151072,2.3733735137751237 +109,31,27,23.05951896,50.40609436,6.973839707,164.4971875,coffee,17.16695726308303,3,8.791710334527412,6.4835020954602385,380.53872995131246,7.858590562339093,5,9.234018321560303,6.748173922094958,194.01625314522278,2,17.60502386664487,1,79.86352668208652,2.381105886389448 +89,25,34,23.07895447,63.65861483,7.184801627,129.8765443,coffee,19.445991850298363,2,8.920955547529887,15.016815881742122,440.7218160890714,2.9919734928461716,1,17.036117239023092,64.20942524246426,71.74147023571035,3,27.889843918751673,2,84.92609587908677,1.2660512890893645 +118,18,32,27.6496114,51.11044023,6.351823783,122.8392822,coffee,12.866965731468312,3,8.200849142363136,9.108346507900686,425.1492620193771,1.0899159635120048,5,9.1748432673774,42.272878359003805,109.65935973017294,2,21.352809502308332,3,8.53882363897036,1.664673504741212 +111,32,34,25.46743689,69.35161206,6.392048018,171.3764462,coffee,26.42388036618837,1,5.582737093186684,3.8416527831679215,402.81011459049824,3.3894509451347945,5,17.562629008370585,47.865090077993386,186.78007391067695,1,24.530938832305253,2,35.27464384812342,4.469232980006041 +84,36,28,26.7350622,55.55164819,6.119892347,140.6305213,coffee,25.934870608420166,2,6.87062451863867,8.20102788141164,421.7018818546001,4.761386660039561,1,7.534289230795107,39.533832009071105,132.539808630373,1,20.297399455595976,1,30.64104503980819,4.566577052121682 +85,33,25,26.20811417,52.50987966,6.910823945,189.0944824,coffee,26.916367433487636,2,7.72310561741066,10.364404240106808,435.41885890370287,9.504703403549605,4,10.884613360191281,64.55332518907078,57.67770222215255,2,45.504405791529585,2,96.26984290979632,4.678008463443028 +99,15,27,27.0424167,57.27927475,6.501157208,165.6872119,coffee,16.94238300403439,3,6.685100446153818,9.51182933917507,377.29932607094895,2.160944282391657,1,18.141281482948308,81.65063846921899,92.9304495126446,1,25.03183409893038,3,38.74461816236238,3.8197818453118257 +81,30,31,24.65090184,51.93952357,7.027585559,135.1386537,coffee,22.03348979894905,2,9.471675369785345,16.913309274121527,355.83539287539054,5.773249150206677,5,18.8785842849939,75.65218934306105,176.59092295464893,1,20.53880265922825,2,44.18727050767567,1.2043462712267807 +95,39,29,27.35152643,55.99375012,7.13411409,148.9812525,coffee,25.97531776991413,1,8.759641046749937,8.1157406827446,425.70262113387287,6.719291746197753,1,5.113985611369592,52.074861433269206,109.21864820697499,1,47.42083381640764,2,69.71479809617928,3.5841472420105807 +81,34,30,25.17787724,62.26244581,6.647765997,135.0119649,coffee,24.825085623972114,1,7.810870397388827,18.496616238907183,382.8688718983793,6.285865703210486,4,17.42275832646464,84.0329804506935,68.3801094728131,2,7.143310879960091,3,97.70431233551578,2.503320576370518 +80,15,28,23.11438731,68.00096043,6.703270635,161.8944624,coffee,25.920081711119483,2,7.766460938619801,8.65143206869541,391.09000656417174,3.925577399039941,1,19.01023658524253,32.09426098797306,102.84335315147642,2,2.6945611484281984,2,6.546134405495641,3.904308218488823 +104,20,26,27.22783677,52.95261751,7.493191968,175.7260273,coffee,14.878731823205325,3,9.457576993628766,2.465405187756997,350.7416042469828,3.627023972297582,1,7.769744237160524,94.85465885767583,175.31427140341918,2,2.1599421218509773,1,34.12740969424276,2.5969156838480405 +109,29,28,23.26316991,60.5160021,6.724688503,194.1755471,coffee,18.125823639529287,3,6.5941879720830725,11.217091366295413,378.0270202057277,2.161077032329504,1,5.158685708348761,98.35448069181356,72.0669149096092,2,46.06738954651159,1,84.33116388808432,3.7796075864123932 +100,32,26,25.234661,57.53161469,6.043485685,124.2261737,coffee,23.488739391031093,2,6.565148259904972,1.8310462402710415,354.5374430412891,2.7719447906836194,5,6.473784700860738,3.3092000435237745,153.87646097082438,3,21.1109007668489,3,60.421301478639236,1.9955817380281506 +100,24,28,25.59535262,57.72920846,7.101661011,195.7733251,coffee,23.64276517827145,3,9.48364054176714,6.254329596332527,402.90296188444654,6.201465171267159,3,19.117097987275823,3.8974908777130723,187.73782800471034,3,10.672672296407699,2,64.669241714141,2.4839975692669443 +83,21,28,25.5674832,60.49244602,7.466900683,190.2257843,coffee,20.171119633832074,3,9.500470579224508,7.873987200482951,364.6431887093297,5.538343628677435,2,7.934372693516788,68.00752974815153,126.42537348564473,1,3.5939648060086924,1,4.2878278650621215,1.4397503379883756 +120,23,28,25.67324193,51.29043632,6.877799264,196.2736367,coffee,21.98237866520371,1,8.403351292704773,11.133358666191604,384.4297077162644,5.911817507142141,4,15.379318695467202,95.03373072027792,171.98740161433886,2,40.042576134251775,2,64.22533392921035,4.53711817253506 +104,26,30,24.40726724,62.65692638,6.410992833,148.6977358,coffee,20.85250953487601,2,8.588136130677029,9.739311339126038,396.304411058434,3.8415993435422795,4,10.446419209675437,23.99420262724924,175.60337085708113,2,43.07619350394103,2,29.896496935944928,2.0292342925038973 +108,33,31,23.69287069,66.76090123,7.393825704,144.6576424,coffee,17.95969252858681,1,10.910253684547104,5.7962118545833174,402.5011555829496,2.096677800913018,5,7.9702972204956435,14.593575400066783,69.12106949243804,1,27.0285614457992,2,49.9305541237568,4.106368559610237 +91,25,26,24.53460016,66.99765375,7.482414225,180.5059257,coffee,29.205015410113376,3,7.537815906152752,6.901906185551501,391.10751734789454,8.402343278817686,5,16.780821414763565,16.439099715152427,75.27380919682,3,48.03162081905489,2,46.47107682406776,1.9434617022857994 +86,26,27,27.13140403,52.89368299,6.081172981,192.4280381,coffee,17.618101316413828,3,5.161529270817132,6.871980420539455,371.4524946176257,8.977993989151095,5,7.8285927179946935,21.803855897666725,131.16201999919878,1,30.177367396895388,3,4.903798637906542,1.3227768834028955 +98,18,27,27.56088634,68.49299897,6.516312148,167.4358075,coffee,17.474198570513803,1,7.551936808345527,19.8217752220514,370.75486062461,2.7576524573708188,2,6.617098682772124,75.44616907713775,120.79611718561684,1,33.69700694597779,3,81.70894573723741,2.7730142551607755 +111,27,31,23.59302313,55.27564977,6.043330951,191.3980675,coffee,27.915403777453818,1,11.579607701972815,10.880401582552341,394.31605037234056,5.00134330151204,6,8.35909361772702,12.223184170410061,158.20434068766141,3,3.446747772795178,2,28.642625753517382,2.3926696995845544 +84,39,35,23.17714381,52.13864034,6.959404135,117.3113562,coffee,18.698452007810552,2,10.484388433306886,11.112022092007308,439.4925335174895,6.330618213080838,5,7.910196360014545,38.275536689267234,152.25029262775763,1,30.94654401839317,3,14.697580847456582,4.630534390586973 +98,27,27,24.71384065,51.29142534,7.238109556,197.6439711,coffee,17.69191629213744,2,9.617037122852143,15.00147553346293,396.57144766926046,4.675713369494713,4,13.1879616385799,35.070426874924074,135.37321556442475,1,2.7817769705454576,3,63.36944309925347,4.555295950610086 +118,21,34,24.38534644,64.72543073,7.234258375,119.6324109,coffee,26.01864083720235,3,9.812013773967553,14.028149818755848,418.5921240609715,5.345179101455301,1,15.495212584730785,44.23022550567304,75.28772241518749,3,2.1564420678990395,3,18.144492798392854,3.5162359246792 +103,27,31,27.15998538,51.59100753,6.691541233,126.1752206,coffee,19.81781935120666,2,7.540677613010809,8.288063452270364,353.2357380876273,5.356498015130511,1,16.17283424555942,58.48123322103663,165.54972520658617,3,20.492573230553624,1,88.70234539466348,3.327802044989469 +82,24,33,26.53543168,67.09608099,6.809593554,120.6494434,coffee,28.955131876380158,1,6.211927667006483,15.0936907713128,372.027630992602,4.777856305739771,1,13.43596537490813,31.963150966422948,92.53851990842693,3,39.84691544011017,1,9.364126748529555,4.08150987580696 +86,31,35,27.01207284,60.76645256,6.485761419,191.4508931,coffee,10.88876336302598,2,6.803256378557872,16.326659634443054,433.077292196548,6.637538916676462,6,19.34132349839401,82.68538018412187,127.2907759934466,2,8.728063733841596,1,7.90397700911476,1.9358896831346333 +88,35,35,27.55906475,58.45742907,6.784460602,117.9389993,coffee,23.33584732483188,1,11.370563794545754,6.908108887612245,390.0262964148799,4.262605739254374,6,5.304437832189704,44.01276031533013,82.73182407679495,1,46.11475082967226,3,32.82728208766051,2.9921160009964383 +84,27,29,23.32293161,53.00366334,7.167092586,168.2644287,coffee,12.461034644235166,2,5.163131303483213,6.579202941292737,377.1336250830626,6.6997934448361836,4,18.956149817477424,43.88923426777917,89.16928413341029,3,42.17098287519199,3,50.89498989752007,4.5322357677786576 +120,40,33,24.23850608,54.30329632,6.73410539,115.1564012,coffee,18.562032154156064,3,8.584143607039973,13.293567236355631,421.41988764991333,6.081613993988652,6,5.704819243892765,5.924323150352418,114.84027538489265,1,38.82775300909674,1,91.75900255129447,2.6881813776916252 +106,40,30,23.42611644,64.10651528,6.779984384,122.6847408,coffee,19.804419449448886,1,5.3774357291552075,15.250893398049383,363.62088282884275,9.6120587774359,5,9.432235978845185,25.493397950602947,93.09903631181432,3,9.846050336849899,2,27.834756928642246,4.986842732804034 +113,21,33,26.02241444,55.83288958,7.277422738,176.9020924,coffee,18.465641718085436,1,7.609507154217084,10.481325821629678,404.84198817136405,5.217395888061986,2,15.2863285902774,8.034086025467534,65.68642913016697,3,24.946083650105177,1,42.50795108554677,2.9301700188220696 +117,34,25,24.83846178,56.7685316,7.21270048,124.4135035,coffee,13.942680096299735,2,7.42341351260527,0.15885111858717993,384.1442158977517,2.020414046539497,2,11.442483997875195,30.64912676234236,166.57501416749966,3,11.637725372254904,3,51.709073126071715,3.8114812538330933 +80,30,25,26.24092174,65.64381357,7.487266991,148.3771202,coffee,17.74077642660384,3,10.06869948232751,5.179562507387647,376.2624448280993,2.4385856988018513,2,8.238429011489572,16.818533029190462,182.68185488913196,3,31.490926881752102,3,8.85472183645084,1.571257851929254 +88,21,27,24.43011925,66.02411187,7.231166546,181.6368274,coffee,26.454242212497533,1,8.068528842901864,11.58470558286256,419.99047822491036,8.129205417413557,2,7.470654004508477,54.98857936836136,66.89124515598157,3,19.263454889800546,2,67.34876070877073,3.218585099934662 +113,33,34,26.00373964,62.1445102,6.559817161,153.477776,coffee,13.805697759104326,3,7.008864400801875,0.027266730200721234,444.24348180256067,7.539755744879675,5,8.38950345486913,84.17840066530927,124.50062155154735,3,10.786827216800743,2,65.7283546353167,4.889080257735876 +87,23,28,26.22367404,62.26594559,6.979590627,193.7461968,coffee,10.987355311073593,3,5.995550680786487,9.568174257293776,354.5034516926162,2.6803619601910316,3,17.46386329976527,42.386471200213435,115.09741006321192,2,36.34155309779391,2,14.942843914778969,1.7612233067739829 +113,15,29,27.09617155,63.55324262,6.779230041,190.2440566,coffee,26.8667605830191,1,10.33057406192339,14.829222126888844,423.3618374428057,6.6541564946165135,2,12.108328252368707,91.98233865959496,60.642981087775425,1,1.5118172905314031,2,96.06943708853981,3.858408927614959 +98,29,30,25.64004392,61.03273481,6.217974349,199.4735636,coffee,14.964746086605501,2,7.202522250308457,8.289854153350635,422.852472559832,1.1760837129932753,3,16.518957370572725,32.482353803212995,126.57700672097238,3,1.8233206878226826,1,97.63366600901301,2.232463278343964 +97,29,27,27.74576987,54.36976075,7.205078785,139.8619431,coffee,18.27194337646806,3,5.532054683888051,19.58005647104767,376.9964505985208,1.8452535409159283,1,17.15917599386367,61.727031588119566,138.38444969912157,2,19.28654263776132,1,85.64415164406498,2.185036640543267 +85,35,32,26.24928198,54.28617819,6.854011265,133.1120232,coffee,15.18124692778565,1,6.923654041807552,10.931841788583245,352.28795791204976,8.304706474922893,5,5.548701761496407,71.45275596371704,135.722410908214,2,6.830558302286066,3,26.532216979132272,2.932988909429761 +82,29,35,26.67377159,52.24226285,6.246872394,156.1543898,coffee,20.360542273772705,3,7.971147115660766,12.146829041140057,432.801439502271,6.7678656104988635,6,15.91158754885152,46.38185167197619,79.34047464731918,2,27.84434288905225,1,79.09971437221193,3.815820836253507 +103,33,25,27.10210397,55.7497332,6.911066044,139.5013171,coffee,15.577928881206464,3,11.050800992437654,6.396214529677968,374.8130436211387,9.630145044867614,4,12.729657811177278,61.64521246539113,93.76314280269176,1,42.78385548913406,1,25.125033705292378,4.838865636424938 +112,17,28,27.62975458,61.26002598,6.777417989,196.6492664,coffee,23.62934942267834,2,9.519423243741885,3.1076415442978256,424.1598888473711,5.710620665058844,1,11.070684148173559,86.6528603885178,192.2284224026493,2,39.41843864440398,1,9.27232416564604,2.269833309131303 +99,19,33,27.5364547,55.51673151,6.273741983,130.6377143,coffee,24.574820971757987,1,10.255972786240116,1.1263787771041511,359.09536608185357,8.77023963686544,3,11.079961320804662,32.63584569365378,189.86700450442996,3,19.01309225560744,1,85.12113162671386,3.483038619705111 +120,20,34,23.56960509,50.56339727,6.906124587,130.3797119,coffee,21.863339690008956,3,11.675816121834451,8.049470973808745,357.96448814964316,7.936910590489785,5,16.013856640294275,16.566582868615455,89.48282616023883,3,0.516971074202166,3,47.252726581012524,2.116204208439644 +114,27,28,24.99451759,57.93250202,7.162802357,192.8736822,coffee,19.028658412710797,2,10.141647777709782,10.308429043068037,411.30687952841754,3.192323345541473,6,15.203162722600416,22.842667364725155,123.46346585724932,1,10.079795388537004,3,21.744516317387973,3.2390969596436667 +100,40,35,27.56441788,54.41094079,6.955787351,177.816092,coffee,22.442720008302917,1,5.481214317637382,15.147706520820865,387.7656063373172,6.283476894869647,3,8.221832193195354,66.20934108850935,174.45942019867135,3,28.496012044302745,1,66.13962203449972,3.748584840236874 +108,35,25,23.98143338,61.10935084,6.971963169,161.5279095,coffee,14.446748288731563,2,10.900613027177164,8.408809474239122,422.9443590813135,7.350693924000186,1,14.645313457675018,47.819014530624116,108.46449825704295,3,36.829016314957876,1,48.345058584849895,3.2750798765187428 +115,31,30,24.22984659,67.37768353,6.840927967,122.4073418,coffee,26.40756000379458,3,10.183756187835456,0.947895995719461,431.6035329030406,3.152548284596577,1,5.220482308825394,41.87255586939409,194.62078796851173,1,28.940488767992896,2,20.110531563232968,3.786630187482751 +87,28,30,25.60153969,68.66257977,6.536676653,168.8383605,coffee,13.881553404963675,2,5.686624894851892,15.176478570438343,379.33956966204863,4.50524847018094,4,15.615052141826004,23.850571517281704,148.19280414683715,3,16.812477345191922,1,98.92645001277512,1.1590750571469184 +82,24,26,24.31274458,53.57285558,6.089443603,184.4103931,coffee,13.627410365181795,2,10.786744207932113,10.803630859178071,409.3152900536683,3.8632105019350043,6,8.82084048911401,16.51742823287401,111.12982220320889,1,33.551075053742146,3,66.46545093026657,1.1596409371574303 +94,26,27,26.36629861,52.25738495,7.456460375,177.3176161,coffee,14.924920535153532,3,5.051497384979205,16.50105882482992,406.9730796778742,3.552977353389375,3,6.534388460632296,73.42347271310275,196.27523976474484,1,49.174936066687586,3,10.712846420267674,3.783307479052532 +87,28,35,26.5602777,57.1621814,6.759211911,152.0616227,coffee,28.338278758974212,3,8.669736494107692,1.5170890918765667,399.39164092341747,5.43708127345178,2,18.8791784811104,44.334138932159675,127.13765674405565,3,1.1175626742326805,2,95.08139101201051,3.7943786141155194 +118,40,35,26.35034208,58.50650238,7.460174812,121.5586297,coffee,10.531476639175859,3,10.984976791896047,11.572138207663711,385.5478722920658,8.949744370304002,5,8.275422279810961,34.34895330560608,85.29477172866314,2,45.81951611886165,1,87.60060947665231,1.5299657808794591 +87,38,29,25.20406808,57.88370456,6.652642579,156.1457255,coffee,27.713555970828143,3,10.138153276039969,8.032639147485622,426.6243207428062,5.971270844370167,2,10.90026131382653,30.46837392008581,96.41907705083196,3,27.341598902691523,2,72.38911275613678,4.177386852401883 +92,40,30,23.35723208,55.18792166,6.026287448,171.6976946,coffee,26.861193728314632,2,6.797145617548467,14.201781817205735,422.0344641940794,9.779439333088275,2,8.966929634446341,83.31292847891467,110.28795171033818,1,23.514133483259442,3,80.32368486196576,2.090534646022882 +97,22,26,23.60567546,59.68849145,6.074190142,185.1568059,coffee,12.621666672039906,3,6.91387497730444,17.355367751534445,369.746464940774,4.8327148639504705,5,19.566175484160148,38.85909183357593,180.4040271237194,3,42.53396483974716,3,84.74198522321528,2.697325550662706 +99,40,32,24.18471151,69.94807345,7.045543056,163.2708732,coffee,28.39294463671515,2,10.980414962783447,0.4017499553811543,437.01222826349294,8.633899475658115,2,6.197976595452292,97.92414732321859,131.22994914109483,1,36.208966225710334,3,94.90199339518003,1.9729951717892789 +89,28,33,26.44414097,53.83876189,6.993236001,175.3723314,coffee,19.50173649981193,3,7.928251969651355,5.063784366042103,412.6056260090952,2.8723466516038494,1,18.405801540761587,68.67657623259036,152.11563219325274,2,22.30156525341923,2,99.95727435075142,2.2998482900662403 +112,39,29,26.12492233,63.37479229,6.726528895,147.8035305,coffee,13.125280180931995,1,10.81370162055756,1.9360362776695106,400.92712760364265,8.648942117140823,6,13.482653811136325,80.0685522103035,178.03667223125913,1,37.75695863112446,3,26.82312106302622,1.6797349062832052 +111,28,26,27.77363343,64.47858698,6.937352845,192.7121236,coffee,10.701686070933352,1,7.780727629804778,15.228052697719969,354.518088205616,2.766148119518961,4,11.985369986207587,32.284062561735524,64.62505399434005,2,13.439963235932211,1,4.5597034587093415,3.6675172597370365 +114,20,26,25.55656667,62.67087838,7.27905689,193.5866233,coffee,20.081173044273452,1,9.527135134219911,10.687313692458869,402.98790890682227,4.077329983806473,5,17.720447196694607,1.445438927997944,135.68817026686753,2,31.069536938050657,3,6.33018616582156,1.2508612696172636 +117,26,30,27.92374437,67.96910852,7.079850922,115.2325531,coffee,19.74268426244155,2,5.316290311731468,13.042700790209757,355.00655880103204,5.049767015491457,5,13.309928893935993,36.050161038040315,59.70947809294499,3,5.4205104699727835,3,82.63755085080179,3.147423747976089 +111,29,31,26.05968403,52.31098539,6.136286518,161.3432535,coffee,11.829330126916311,1,5.303627120250579,3.9967224329424877,437.5212181900934,9.007977687485857,6,17.974165761339318,70.12681863034099,66.67036516664521,1,9.66209681504836,1,73.8273655097617,3.503331307324854 +119,30,28,26.35770906,64.57578034,6.505203696,163.6269496,coffee,29.312188107723514,3,11.319390083366885,9.53472545785585,364.8397279643288,6.022753528950439,6,5.519763549834372,3.7978939551608026,174.3271360684962,2,31.009862239298357,1,9.489044280624093,3.5030056677317405 +116,40,33,24.91370487,54.15319242,7.042089492,129.5481144,coffee,10.224701079795066,2,9.521125131530539,18.53444195934275,417.5384051292857,6.348511003032453,2,16.59347953094382,15.390573068871227,159.70702350617273,1,39.59455757717947,1,60.87966558059489,4.868037459853507 +95,37,35,27.31317116,68.4233391,6.348337519,192.4288139,coffee,18.19586032923393,1,8.708390763414553,13.786265719306156,388.7159821051115,9.429577497926854,6,17.87575322421299,70.77257001930647,66.28186119150821,1,43.880534205950944,3,99.0519237400327,2.9792091927190705 +86,40,33,26.1387869,52.26311691,7.432322234,136.3027766,coffee,26.430133215983638,2,5.94943080378396,19.457113661310363,393.9986146068263,1.9681381465168055,2,12.397630367211349,31.245745823086313,179.59627225967,3,32.78539224267655,2,81.81949398373696,2.5801279762560094 +117,37,32,23.1069385,67.06230539,6.787658922,162.5769606,coffee,23.57746130613952,1,5.643907629623579,10.900040851367125,394.80540150166723,4.901662339897946,1,17.746623463406042,15.20054937730736,111.91387152472907,3,2.007879864575396,2,78.73955619008586,4.592643496455722 +105,18,35,23.52648086,68.44030686,6.743417121,171.8839938,coffee,18.51719879757228,1,8.671470953871694,17.17783873014142,400.6513360157355,5.906657077143388,5,11.334299363286746,22.74388018482325,109.21127413478104,2,14.836567732239692,3,84.04635579237056,4.696973632871321 +109,23,25,25.11711046,68.48030408,7.00733163,194.8773479,coffee,18.216295729891232,3,8.98387529345675,8.745359870122705,434.27194699821666,3.243587863004935,1,13.803449832943658,38.45188690913947,179.57380433576606,3,44.99435109295262,2,94.16407939728629,3.5894390868279378 +80,18,31,24.02952505,58.84880599,7.303033217,134.6803969,coffee,18.438362596834466,2,7.521657810214986,5.134494634741458,422.2391345728312,4.9641246131319985,6,15.878609245039211,31.095419628381915,91.95561593563122,2,20.524902772071542,1,78.23544150287918,3.3490378361786117 +101,31,26,26.70897548,69.71184111,6.861235184,158.8608887,coffee,28.749579004480516,2,9.852503337545429,13.557580290453558,379.52279688142926,7.479056987422591,1,17.458456440050384,17.830304125834274,77.12408576411302,1,26.87026221198051,1,19.837487964758203,1.2407546076958842 +103,33,33,26.71717393,50.50148528,7.131435858,126.8073984,coffee,27.90183973869437,1,11.66215282517514,2.324002312151414,438.36866057502823,6.266938412264151,3,19.346227666480704,81.89223232175623,105.9968443526586,1,6.007991296173759,1,39.23306104212055,3.472756440751938 +93,26,27,24.59245684,56.46829641,7.288211994,137.7044047,coffee,27.089995715785612,2,9.42978416041818,10.275427926514247,393.01596681145656,4.374038495571538,5,15.607354266430418,56.8657618981927,198.91613969105796,1,39.15095646997872,1,49.969046240847234,1.3563719536903451 +104,35,28,27.51006055,50.66687215,6.983732393,143.9955548,coffee,10.07925126717412,1,11.3301193793534,4.954276345902895,351.1478149280893,9.505791216886305,5,17.0311121644123,5.972522237408418,164.8741166092643,3,36.252923984774284,2,15.241243599169717,4.0998090816280275 +116,36,25,27.57847581,58.52534263,6.172090205,156.6810374,coffee,23.320519483369498,1,8.95465588980817,2.4321991616505967,421.1340156439659,5.954199019964379,1,10.76005474288019,25.288482409847802,77.71366892549729,1,13.320951967207744,1,70.72992777701124,3.4648755910985582 +107,38,29,26.65069302,57.56695719,6.35118177,145.105065,coffee,29.826788511722356,3,5.318890246281764,10.135087530393662,375.99906818123117,3.116471461937506,4,15.190427078147515,3.9680439611720852,64.4249385943942,2,13.292637681930126,2,49.68992057136191,3.1815580762133138 +101,33,33,26.97251562,62.0183627,6.908671379,142.8610793,coffee,13.803873596397116,3,8.167869320141943,9.245461706769673,395.21538339259575,5.433539466539919,2,14.27072427384216,2.886482133037094,169.0513042392963,2,15.742623579605135,3,92.33155479967935,2.04930024802155 +107,31,31,23.17124551,52.97841162,6.766184468,153.1201644,coffee,18.296256557764067,3,6.428406067678784,19.67639070238986,387.01617304183736,9.131122698029502,6,13.613430487856641,56.26405680409713,167.9479081298221,1,49.18989936970398,2,30.920527378909245,2.804276166517868 +99,16,30,23.52652084,65.44340921,6.392791654,186.1728203,coffee,12.642971277836256,1,9.915135131088793,2.3828545976045,410.448728365925,4.703797877840363,2,13.047310684076043,47.299550417957605,56.844488326645646,1,21.54864289531475,3,40.225686814412796,2.673302215982513 +103,40,30,27.30901814,55.196224,6.348316257,141.4831644,coffee,13.19095469253482,1,11.480067670234824,19.10683130377579,416.92345520012736,4.263579466352212,5,19.214758722391522,5.553972078659197,98.21511003205694,1,0.3850781073277909,1,54.08781998123497,1.8613324331258077 +118,31,34,27.54823036,62.88179198,6.123796057,181.4170812,coffee,13.508814256108518,1,11.133205665256487,19.494306071184837,408.1436896431313,7.9003183504683765,2,11.986324682910077,18.598826821563087,146.35756501438993,2,22.823900390909053,3,17.753445252430023,4.132128235196412 +106,21,35,25.627355,57.04151119,7.428523634,188.5506536,coffee,28.477930555278643,1,5.424222642810601,0.6607793529765438,407.41728729455775,4.563434130489964,2,17.445867297359342,41.2482653682816,82.204708513072,1,34.92379920967915,3,75.56941295940646,4.56478144525538 +116,38,34,23.29250318,50.04557009,6.020947179,183.468585,coffee,28.4397675573041,1,8.12970400936235,4.974811975921371,374.2861496672049,5.416485126000258,5,18.190925676595686,24.31642879356446,78.32780393541171,2,1.7378188762561364,2,87.93801307518771,1.9886621355781835 +97,35,26,24.91461008,53.74144743,6.334610249,166.2549307,coffee,22.85402795020538,3,5.486677104030552,11.21095734965416,429.49592157786026,1.9750399242878052,4,6.827054608640566,47.227095787149906,125.67752272746043,3,33.2609226712504,2,38.221399353845165,1.1837923585608587 +107,34,32,26.77463708,66.4132686,6.78006386,177.7745075,coffee,10.697756538547269,1,10.330875167890733,19.19236115355042,439.0790691558914,4.720354769209413,5,18.597260283179743,87.43119850497924,185.8333807032337,3,31.415618482726305,1,77.7196389992403,4.111618985243631 +99,15,27,27.41711238,56.63636248,6.086922359,127.92461,coffee,12.203829682038808,3,6.0705583715897875,10.603401086640208,405.2595398699845,4.141147768764266,6,15.417978614467254,36.95835436323838,198.54102074271773,2,18.79750965801862,3,22.336838635661127,4.190796328348858 +118,33,30,24.13179691,67.22512329,6.362607851,173.3228386,coffee,28.989175529548135,3,11.097182116394928,13.842015985356813,360.48260538132587,1.5996140522008162,5,12.95667508217523,79.67865796464093,86.72438065470436,2,38.805888450916044,3,41.782729211210835,2.4470104278279448 +117,32,34,26.2724184,52.12739421,6.758792552,127.1752928,coffee,13.642304736922634,2,8.097337065880122,16.537830888139748,415.5143138839929,8.934076932532399,6,16.86813051240768,31.00715649291702,72.19142098690531,2,8.395497784784894,3,49.619791249176885,4.1193880040029285 +104,18,30,23.60301571,60.39647474,6.779832611,140.9370415,coffee,23.911727578408552,3,8.639741540157758,14.481756579084477,413.1237161382283,6.401993710119095,3,14.652123227033409,3.5741911097655232,175.10424061806972,3,26.784996183697135,2,47.27126705890273,2.758819113898633 diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/detect_iforest_pca.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/detect_iforest_pca.py new file mode 100644 index 000000000..ff3bd84d5 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/detect_iforest_pca.py @@ -0,0 +1,131 @@ +# detect_iforest_pca.py +from pathlib import Path +import numpy as np +import pandas as pd + +# WSL/Headless-safe plotting +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from sklearn.compose import ColumnTransformer +from sklearn.preprocessing import RobustScaler, OneHotEncoder +from sklearn.impute import SimpleImputer +from sklearn.pipeline import Pipeline +from sklearn.ensemble import IsolationForest +from sklearn.decomposition import PCA + +# -------------------- Config -------------------- +CSV_PATH = Path("data/Crop_recommendationV2.csv") +OUT_DIR = Path("out"); OUT_DIR.mkdir(exist_ok=True) + +CONTAMINATION = 0.15 # the proportion of anomalies in the data +N_ESTIMATORS = 300 +RANDOM_STATE = 42 + + +EXCLUDE_COLS = {"label","crop","target","index"} + +# -------------------- Load & normalize -------------------- +df = pd.read_csv(CSV_PATH) + +if "id" not in df.columns: + df.insert(0, "id", df.index.astype(str)) + +df.columns = (df.columns + .str.strip().str.lower() + .str.replace(" ", "_") + .str.replace("%", "pct") + .str.replace(r"[^0-9a-zA-Z_]", "", regex=True)) + +candidate_cols = [c for c in df.columns if c not in EXCLUDE_COLS] +num_cols = [c for c in candidate_cols if pd.api.types.is_numeric_dtype(df[c])] +cat_cols = [c for c in candidate_cols + if pd.api.types.is_object_dtype(df[c]) or pd.api.types.is_categorical_dtype(df[c])] + +numeric_pipeline = Pipeline([ + ("impute", SimpleImputer(strategy="median")), + ("scale", RobustScaler()) +]) +categorical_pipeline = Pipeline([ + ("impute", SimpleImputer(strategy="most_frequent")), + ("onehot", OneHotEncoder(handle_unknown="ignore", sparse_output=False)) +]) +pre = ColumnTransformer( + transformers=[ + ("num", numeric_pipeline, num_cols), + ("cat", categorical_pipeline, cat_cols) + ], + remainder="drop" +) + +X = pre.fit_transform(df) + +# -------------------- Isolation Forest -------------------- +iso = IsolationForest( + n_estimators=N_ESTIMATORS, + contamination=CONTAMINATION, + random_state=RANDOM_STATE +) +pred = iso.fit_predict(X) # 1=normal, -1=anomaly +score = -iso.decision_function(X) + +df["anomaly_iforest"] = (pred == -1).astype(int) +df["iforest_score"] = score + +# -------------------- PCA (2D) -------------------- +pca2 = PCA(n_components=2, random_state=RANDOM_STATE) +Z = pca2.fit_transform(X) +df["pca_x"] = Z[:,0]; df["pca_y"] = Z[:,1] + +plt.figure(figsize=(7,6)) +mask = df["anomaly_iforest"].astype(bool) +plt.scatter(df.loc[~mask,"pca_x"], df.loc[~mask,"pca_y"], s=12, alpha=0.25, label="normal") +plt.scatter(df.loc[mask,"pca_x"], df.loc[mask, "pca_y"], s=20, alpha=0.9, marker="x", label="anomaly") +plt.title("PCA (2D) projection with IsolationForest anomalies") +plt.xlabel("PC1"); plt.ylabel("PC2"); plt.legend(); plt.tight_layout() +plt.savefig(OUT_DIR/"pca_iforest_anomalies.png"); plt.close() + +# -------------------- PCA Reconstruction Error -------------------- +pca_full = PCA(n_components=0.90, svd_solver="full", random_state=RANDOM_STATE) +Zf = pca_full.fit_transform(X) +X_recon = pca_full.inverse_transform(Zf) +recon_err = np.mean((X - X_recon)**2, axis=1) + +df["pca_recon_error"] = recon_err +PCA_ERR_Q = 0.85 # top 5% will be anomalies +thr = np.quantile(recon_err, PCA_ERR_Q) +df["anomaly_pca_recon"] = (recon_err >= thr).astype(int) + +# -------------------- Union / Intersection -------------------- +df["anomaly_union"] = df[["anomaly_iforest","anomaly_pca_recon"]].max(axis=1) +df["anomaly_intersection"] = (df[["anomaly_iforest","anomaly_pca_recon"]].sum(axis=1) == 2).astype(int) + +# -------------------- Save -------------------- +out_csv = OUT_DIR/"dataset_with_iforest_pca.csv" +df.to_csv(out_csv, index=False) +print("[INFO] Saved CSV ->", out_csv.resolve()) +print("[INFO] Summary:") +for col in ["anomaly_iforest","anomaly_pca_recon","anomaly_union","anomaly_intersection"]: + print(f" {col}: {int(df[col].sum())} ({df[col].mean()*100:.1f}%)") +print("[INFO] Plots saved in:", OUT_DIR.resolve()) +# === Persist trained artifacts for streaming (Flink) === +import os +import joblib +import pathlib + +MODEL_VERSION = os.getenv("MODEL_VERSION_IFPCA", "ifpca-1") +pathlib.Path("models").mkdir(exist_ok=True) + +artifact_ifpca = { + "model_version": MODEL_VERSION, + "pre": pre, + "iforest": iso, + "pca": pca_full, + "pca_thr": float(thr), + "feature_cols": list(candidate_cols), + "num_cols": list(num_cols), +} + +out_path_ifpca = "models/iforest_pca_artifacts.joblib" +joblib.dump(artifact_ifpca, out_path_ifpca) +print(f"✅ saved: {out_path_ifpca} (version={MODEL_VERSION})") \ No newline at end of file diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/detect_residuals_and_hybrid.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/detect_residuals_and_hybrid.py new file mode 100644 index 000000000..f34982325 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/detect_residuals_and_hybrid.py @@ -0,0 +1,161 @@ +# detect_residuals_and_hybrid.py +# Residual-per-Feature (OOF) + Hybrid union/intersection + 2-of-3 majority +from pathlib import Path +import os +import numpy as np +import pandas as pd + +# Headless plotting (WSL/CI-safe) +import matplotlib +matplotlib.use("Agg") + +import matplotlib.pyplot as plt +from sklearn.model_selection import KFold +from sklearn.linear_model import HuberRegressor +from sklearn.impute import SimpleImputer +from sklearn.preprocessing import RobustScaler +from sklearn.pipeline import Pipeline + +# -------------------- Config -------------------- +IN_CSV = Path("out/dataset_with_iforest_pca.csv") # from detect_iforest_pca.py +OUT_DIR = Path("out"); OUT_DIR.mkdir(exist_ok=True) +RANDOM_STATE = 42 +N_SPLITS = 3 +RES_Q = 0.85 # top 5% will be anomalies (threshold quantile) + +# -------------------- Load -------------------- +df = pd.read_csv(IN_CSV) + + +drop_derived = { + "anomaly_iforest","iforest_score","pca_x","pca_y", + "pca_recon_error","anomaly_pca_recon","anomaly_union","anomaly_intersection","anomaly_2of3" +} + + +num_cols = [c for c in df.columns + if pd.api.types.is_numeric_dtype(df[c]) and c not in drop_derived] + +df_num = df[num_cols].copy() + +# -------------------- Targets -------------------- + +preferred_targets = [c for c in ["soil_moisture","rainfall","temperature","humidity"] if c in num_cols] +TARGETS = preferred_targets if len(preferred_targets) >= 2 else num_cols[: min(6, max(1, len(num_cols)))] + +print("[INFO] Residual targets:", TARGETS) + +# -------------------- Residual-per-Feature OOF -------------------- +kf = KFold(n_splits=N_SPLITS, shuffle=True, random_state=RANDOM_STATE) +residual_mat = pd.DataFrame(0.0, index=df.index, columns=[]) + +for target in TARGETS: + feats = [c for c in num_cols if c != target] + X_full = df_num[feats].values.astype("float32") + y_full = df_num[target].values.astype("float32") + + errs = np.zeros(len(df), dtype="float32") + + for tr, va in kf.split(X_full): + X_tr, X_va = X_full[tr], X_full[va] + y_tr, y_va = y_full[tr], y_full[va] + + imp = SimpleImputer(strategy="median").fit(X_tr) + X_tr = imp.transform(X_tr) + X_va = imp.transform(X_va) + + scaler = RobustScaler().fit(X_tr) + X_tr = scaler.transform(X_tr) + X_va = scaler.transform(X_va) + + model = HuberRegressor(max_iter=500) + model.fit(X_tr, y_tr) + pred = model.predict(X_va) + errs[va] = np.abs(pred - y_va) + + residual_mat[target] = errs + +# -------------------- Residual-general OOF -------------------- +general_score = residual_mat.max(axis=1) +df["residual_general_score"] = general_score + +thr = float(np.quantile(general_score, RES_Q)) +df["anomaly_residual_general"] = (general_score >= thr).astype(int) +print(f"[INFO] Residual-general anomalies: {int(df['anomaly_residual_general'].sum())} " + f"({df['anomaly_residual_general'].mean()*100:.1f}%), thr={thr:.6f}") + +# -------------------- Hybrid: Union / Intersection / 2-of-3 -------------------- +methods = [] +if "anomaly_iforest" in df.columns: methods.append("anomaly_iforest") +if "anomaly_pca_recon" in df.columns: methods.append("anomaly_pca_recon") +methods.append("anomaly_residual_general") + +df["anomaly_union"] = df[methods].max(axis=1) +df["anomaly_intersection"] = (df[methods].sum(axis=1) == len(methods)).astype(int) +votes = df[methods].sum(axis=1) +df["anomaly_2of3"] = (votes >= 2).astype(int) +print(f"[INFO] Hybrid 2-of-3: {int(df['anomaly_2of3'].sum())} ({df['anomaly_2of3'].mean()*100:.1f}%)") + +print("[INFO] Hybrid summary:") +for m in methods + ["anomaly_union","anomaly_intersection"]: + print(f" {m}: {int(df[m].sum())} ({df[m].mean()*100:.1f}%)") + +# -------------------- TOP-10 -------------------- +try: + top10_idx = df["residual_general_score"].nlargest(min(10, len(df))).index + cols_to_show = ["residual_general_score","anomaly_residual_general"] + \ + [c for c in ["soil_moisture","rainfall","temperature","humidity"] if c in df.columns] + top10 = df.loc[top10_idx, cols_to_show].copy() +except Exception: + top10 = pd.DataFrame(columns=["residual_general_score","anomaly_residual_general"]) +top10.to_csv(OUT_DIR/"top10_residual_rows.csv", index=False) + +# -------------------- Plot: PCA 2D colored by Hybrid union -------------------- +if {"pca_x","pca_y"}.issubset(df.columns): + plt.figure(figsize=(7,6)) + mask = df["anomaly_union"].astype(bool) + plt.scatter(df.loc[~mask,"pca_x"], df.loc[~mask,"pca_y"], s=12, alpha=0.25, label="normal") + plt.scatter(df.loc[mask, "pca_x"], df.loc[mask, "pca_y"], s=20, alpha=0.9, marker="x", label="anomaly (union)") + plt.title("PCA (2D) colored by HYBRID (union)") + plt.xlabel("PC1"); plt.ylabel("PC2"); plt.legend(); plt.tight_layout() + plt.savefig(OUT_DIR/"pca_hybrid_union.png"); plt.close() + +# -------------------- Save batch outputs -------------------- +out_csv = OUT_DIR/"dataset_hybrid_iforest_pca_residual.csv" +df.to_csv(out_csv, index=False) +print("[INFO] Saved CSV ->", out_csv.resolve()) +print("[INFO] Saved plots/results in:", OUT_DIR.resolve()) + +# -------------------- Build residual models for serving (Flink) -------------------- + +residual_models_by_target = {} +numeric_feature_names = list(num_cols) +for target in TARGETS: + feats = [c for c in num_cols if c != target] + X_full = df_num[feats].values.astype("float32") + y_full = df_num[target].values.astype("float32") + pipe = Pipeline([ + ("imp", SimpleImputer(strategy="median")), + ("sc", RobustScaler()), + ("hub", HuberRegressor(max_iter=500)) + ]) + pipe.fit(X_full, y_full) + residual_models_by_target[target] = [pipe] + +residual_error_threshold = float(thr) + +# -------------------- Persist artifacts for streaming (Flink) -------------------- +import joblib +MODEL_VERSION = os.getenv("MODEL_VERSION_RESID", "resid-1") +Path("models").mkdir(exist_ok=True) + +artifact_resid = { + "model_version": MODEL_VERSION, + "resid_models": residual_models_by_target, + "resid_thr": residual_error_threshold, + "num_cols": numeric_feature_names, +} + +out_path_resid = "models/residuals_artifacts.joblib" +joblib.dump(artifact_resid, out_path_resid) +print(f"✅ saved: {out_path_resid} (version={MODEL_VERSION})") \ No newline at end of file diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/docker-compose.yml b/AgCloud/services/Cross-Sensor System-Level Anomalies/docker-compose.yml new file mode 100644 index 000000000..b2ca00545 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3.9" + +services: + jobmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-jobmanager + command: jobmanager + ports: + - "8085:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=sensors + - OUT_TOPIC=sensors_anomalies_modal + volumes: + - ./conf/flink-conf.yaml:/opt/flink/conf/flink-conf.yaml + - ./flink_job.py:/opt/app/flink_job.py + - ./models:/opt/models + networks: + - flink-net + - agcloud_ag_cloud + + taskmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-taskmanager + command: taskmanager + depends_on: + - jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=sensors + - OUT_TOPIC=sensors_anomalies_modal + volumes: + - ./conf/flink-conf.yaml:/opt/flink/conf/flink-conf.yaml + - ./flink_job.py:/opt/app/flink_job.py + - ./models:/opt/models + networks: + - flink-net + - agcloud_ag_cloud + +networks: + flink-net: + driver: bridge + agcloud_ag_cloud: + external: true diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/dockerfile b/AgCloud/services/Cross-Sensor System-Level Anomalies/dockerfile new file mode 100644 index 000000000..9626ca2b5 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/dockerfile @@ -0,0 +1,20 @@ +# Dockerfile +FROM python:3.11-slim + + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + MPLBACKEND=Agg + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + + +COPY . /app + +RUN mkdir -p /app/out /app/data + + +CMD ["bash","-lc","python -u detect_iforest_pca.py && python -u detect_residuals_and_hybrid.py"] diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/entrypoint.sh b/AgCloud/services/Cross-Sensor System-Level Anomalies/entrypoint.sh new file mode 100644 index 000000000..d42c7f5e3 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +if [ "$1" = "jobmanager" ]; then + echo ">>> Waiting for Kafka to be ready..." + + # נבדוק אם Kafka מגיב לפורט 9092 + while ! timeout 2 bash -c "echo > /dev/tcp/kafka/9092" 2>/dev/null; do + echo "Kafka not ready yet. Waiting 5 seconds..." + sleep 5 + done + + echo ">>> Kafka is ready. Starting Flink JobManager..." + /opt/flink/bin/jobmanager.sh start-foreground & + sleep 10 + echo ">>> Submitting Flink job..." + flink run -py /opt/app/flink_job.py + tail -f /dev/null + +else + echo ">>> Starting Flink TaskManager..." + exec /opt/flink/bin/taskmanager.sh start-foreground +fi diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/flink_job.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/flink_job.py new file mode 100644 index 000000000..0c3e6b6c3 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/flink_job.py @@ -0,0 +1,147 @@ +import os +import json +import joblib +import pandas as pd +import numpy as np +from pyflink.common import Types +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import KafkaSource, KafkaSink, KafkaRecordSerializationSchema +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy +from pyflink.datastream.functions import MapFunction, RuntimeContext + + +# --- ENV VARS --- +KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") +IN_TOPIC = os.getenv("IN_TOPIC", "sensors") +OUT_TOPIC = os.getenv("OUT_TOPIC", "sensors_anomalies_modal") + +ART_IFOREST_PCA = os.getenv("ART_IFOREST_PCA", "/opt/models/iforest_pca_artifacts.joblib") +ART_RESIDUALS = os.getenv("ART_RESIDUALS", "/opt/models/residuals_artifacts.joblib") + + +# --- MAIN MAPPER CLASS --- +class AnomalyMap(MapFunction): + def open(self, ctx: RuntimeContext): + print(">>> Loading models...") + self.ifp = joblib.load(ART_IFOREST_PCA) + self.res = joblib.load(ART_RESIDUALS) + + self.pre = self.ifp["pre"] + self.ifr = self.ifp["iforest"] + self.pca = self.ifp["pca"] + self.pthr = self.ifp["pca_thr"] + self.fcols = self.ifp["feature_cols"] + self.num = self.ifp.get("num_cols", self.res.get("num_cols", [])) + self.rmdl = self.res["resid_models"] + self.rthr = self.res["resid_thr"] + + print(f">>> Models loaded successfully with {len(self.fcols)} features.") + + def map(self, raw: str): + print("\n=== RAW MESSAGE START ===") + print(raw) + print("=== RAW MESSAGE END ===\n") + + try: + evt = json.loads(raw) + if isinstance(evt, str): + evt = json.loads(evt) + except Exception as e: + print(f"[warning] Failed parsing message: {e}, raw={raw!r}") + return None + + if not isinstance(evt, dict): + print(f"[warning] Unexpected event type: {type(evt)} => {evt}") + return None + + print("PARSED EVENT KEYS:", list(evt.keys())) + + try: + row = {c: evt.get(c, np.nan) for c in self.fcols} + df = pd.DataFrame([row]) + df = df.replace(["unknown", ""], np.nan) + + for c in self.num: + if c in df.columns: + df[c] = pd.to_numeric(df[c], errors="coerce") + else: + df[c] = np.nan + + for c in self.num: + if c in df.columns: + median = df[c].median() + df[c] = df[c].fillna(median) + + print("DEBUG >>>", df.dtypes.to_dict()) + print("HEAD >>>", df.head().to_dict(orient="records")) + + X = self.pre.transform(df) + iflag = int(self.ifr.predict(X)[0] == -1) + + # ------------------------------------------------ + # FIXED SENSOR ID HANDLING + # ------------------------------------------------ + sensor_id = evt.get("id") + + # Fallback: if only "sid": "sensor-12" exists + if sensor_id is None: + sid = evt.get("sid") + if isinstance(sid, str) and sid.startswith("sensor-"): + try: + sensor_id = int(sid.replace("sensor-", "")) + except: + sensor_id = None + + if sensor_id is None: + print("[warning] Missing valid sensor_id, event skipped") + return None + + result = { + "sensor_id": int(sensor_id), + "ts": evt.get("ts", evt.get("timestamp")), + "anomaly": iflag + } + + print("OUTPUT:", result) + return json.dumps(result) + + except Exception as e: + print(f"[error] Failed processing message: {e}, evt={evt}") + return None + + +# --- MAIN EXECUTION --- +def main(): + print("Brokers:", KAFKA_BROKERS) + print("In:", IN_TOPIC, "Out:", OUT_TOPIC) + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(2) + + source = KafkaSource.builder() \ + .set_bootstrap_servers(KAFKA_BROKERS) \ + .set_topics(IN_TOPIC) \ + .set_group_id("flink-anomaly") \ + .set_value_only_deserializer(SimpleStringSchema()) \ + .build() + + sink = KafkaSink.builder() \ + .set_bootstrap_servers(KAFKA_BROKERS) \ + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(OUT_TOPIC) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ).build() + + ds = env.from_source(source, WatermarkStrategy.no_watermarks(), "kafka-in") + ds.map(AnomalyMap(), output_type=Types.STRING()) \ + .filter(lambda x: x is not None) \ + .sink_to(sink) + + env.execute("sensor-anomaly-stream") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/requirements.txt b/AgCloud/services/Cross-Sensor System-Level Anomalies/requirements.txt new file mode 100644 index 000000000..96c97529c --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/requirements.txt @@ -0,0 +1,6 @@ +numpy==1.26.4 +scipy>=1.11 +pandas>=2.2 +scikit-learn>=1.4 +matplotlib>=3.8 +joblib>=1.4 diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/conftest.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/conftest.py new file mode 100644 index 000000000..2ac86fa13 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/conftest.py @@ -0,0 +1,71 @@ +# tests/conftest.py +import os, shutil, random +import numpy as np +import pandas as pd +import pytest +from pathlib import Path + +RNG = np.random.default_rng(42) + +def make_base_df(n=1000, inject_anoms=False, inject_rate=0.05): + + df = pd.DataFrame({ + "id": [str(i) for i in range(n)], + "n": RNG.integers(0, 150, size=n), + "p": RNG.integers(0, 150, size=n), + "k": RNG.integers(0, 150, size=n), + "temperature": RNG.normal(25, 5, size=n), + "humidity": RNG.normal(70, 10, size=n).clip(0, 100), + "ph": RNG.normal(6.5, 0.8, size=n).clip(3.5, 9.5), + "rainfall": RNG.normal(200, 60, size=n).clip(0, None), + "label": RNG.choice(["rice","wheat","maize","grapes","orange","chickpea","papaya"], size=n), + "soil_moisture": RNG.normal(20, 6, size=n).clip(0, 100), + "soil_type": RNG.integers(1, 3+1, size=n), + "sunlight_exposure": RNG.uniform(5, 12, size=n), + "wind_speed": RNG.uniform(0.2, 20, size=n), + "co2_concentration": RNG.normal(400, 40, size=n), + "organic_matter": RNG.normal(6, 2, size=n).clip(0, None), + "irrigation_frequency": RNG.integers(1, 6+1, size=n), + "crop_density": RNG.uniform(5, 100, size=n), + "pest_pressure": RNG.uniform(1, 100, size=n), + "fertilizer_usage": RNG.uniform(1, 200, size=n), + "growth_stage": RNG.integers(1, 3+1, size=n), + "urban_area_proximity": RNG.uniform(0, 50, size=n), + "water_source_type": RNG.integers(1, 3+1, size=n), + "frost_risk": RNG.integers(0, 3+1, size=n), + "water_usage_efficiency": RNG.uniform(1, 100, size=n), + }) + + injected_ids = set() + if inject_anoms: + m = max(1, int(inject_rate * n)) + idx = RNG.choice(n, size=m, replace=False) + injected_ids = set(df.loc[idx, "id"].astype(str)) + + df.loc[idx, "temperature"] += RNG.normal(15, 3, size=m) + df.loc[idx, "humidity"] += RNG.normal(25, 5, size=m) + df.loc[idx, "rainfall"] -= RNG.normal(120, 30, size=m) + df.loc[idx, "soil_moisture"] += RNG.normal(20, 5, size=m) + return df, injected_ids + +@pytest.fixture +def synthetic_dataset(tmp_path): + n = 1000 + df, injected_ids = make_base_df(n=n, inject_anoms=True, inject_rate=0.06) + + workdir = tmp_path + (workdir/"data").mkdir(parents=True, exist_ok=True) + csv_path = workdir/"data"/"Crop_recommendationV2.csv" + df.to_csv(csv_path, index=False) + + + return workdir, csv_path, n, injected_ids + +@pytest.fixture +def no_anomaly_dataset_tmpdir(tmp_path): + n = 600 + df, injected_ids = make_base_df(n=n, inject_anoms=False) + workdir = tmp_path + (workdir/"data").mkdir(parents=True, exist_ok=True) + df.to_csv(workdir/"data"/"Crop_recommendationV2.csv", index=False) + return workdir, n, injected_ids diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_detect_iforest_pca.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_detect_iforest_pca.py new file mode 100644 index 000000000..869c5da68 --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_detect_iforest_pca.py @@ -0,0 +1,47 @@ +# tests/test_detect_iforest_pca.py +import subprocess +import sys +import pandas as pd +from pathlib import Path + +def run_script(script, cwd): + res = subprocess.run([sys.executable, "-u", script], + cwd=cwd, capture_output=True, text=True, timeout=180) + assert res.returncode == 0, f"Script failed:\nSTDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}" + return res.stdout + +def test_iforest_pca_pipeline(synthetic_dataset): + workdir, csv_path, n, injected_ids = synthetic_dataset + + root = Path.cwd() + assert (root / "detect_iforest_pca.py").exists() + assert (root / "detect_residuals_and_hybrid.py").exists() + + run_script(str(root / "detect_iforest_pca.py"), cwd=workdir) + + out_dir = workdir / "out" + assert out_dir.exists(), "out/ not created" + + out_csv = out_dir / "dataset_with_iforest_pca.csv" + pca_plot = out_dir / "pca_iforest_anomalies.png" + assert out_csv.exists(), "dataset_with_iforest_pca.csv not found" + assert pca_plot.exists(), "pca_iforest_anomalies.png not found" + + df_out = pd.read_csv(out_csv) + required_cols = { + "anomaly_iforest", "iforest_score", + "pca_x", "pca_y", + "pca_recon_error", "anomaly_pca_recon", + "anomaly_union", "anomaly_intersection" + } + assert required_cols.issubset(df_out.columns), f"Missing columns: {required_cols - set(df_out.columns)}" + assert len(df_out) == n, "Row count changed unexpectedly" + + + if_count = int(df_out["anomaly_iforest"].sum()) + expected = 0.10 * n + assert abs(if_count - expected) <= 0.05 * n, f"IF anomalies off expected ~10%: got {if_count}/{n}" + + + pca_count = int(df_out["anomaly_pca_recon"].sum()) + assert 1 <= pca_count <= 0.2 * n, f"PCA recon anomalies looks off: {pca_count}" diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_detect_residuals_and_hybrid.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_detect_residuals_and_hybrid.py new file mode 100644 index 000000000..a699f4efc --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_detect_residuals_and_hybrid.py @@ -0,0 +1,42 @@ +# tests/test_detect_residuals_and_hybrid.py +import subprocess +import sys +import pandas as pd +from pathlib import Path + +def run_script(script, cwd): + res = subprocess.run([sys.executable, "-u", script], + cwd=cwd, capture_output=True, text=True, timeout=180) + assert res.returncode == 0, f"Script failed:\nSTDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}" + return res.stdout + +def test_residuals_and_hybrid(synthetic_dataset): + workdir, csv_path, n, injected_ids = synthetic_dataset + root = Path.cwd() + + run_script(str(root / "detect_iforest_pca.py"), cwd=workdir) + run_script(str(root / "detect_residuals_and_hybrid.py"), cwd=workdir) + + out_dir = workdir / "out" + final_csv = out_dir / "dataset_hybrid_iforest_pca_residual.csv" + top10_csv = out_dir / "top10_residual_rows.csv" + hybrid_plot = out_dir / "pca_hybrid_union.png" + + assert final_csv.exists(), "Final hybrid CSV not found" + assert top10_csv.exists(), "top10_residual_rows.csv not found" + assert hybrid_plot.exists(), "pca_hybrid_union.png not found" + + df_final = pd.read_csv(final_csv) + + for col in ["anomaly_residual_general", "residual_general_score", "anomaly_union", "anomaly_intersection", "anomaly_2of3"]: + assert col in df_final.columns, f"Missing column '{col}'" + + + union_count = int(df_final["anomaly_union"].sum()) + assert 1 <= union_count <= 0.5 * n, f"Union anomalies seems off: {union_count}/{n}" + + + assert "id" in df_final.columns, "id column expected" + df_anom = df_final[df_final["anomaly_union"] == 1] + caught = sum(1 for _id in df_anom["id"].astype(str).values if _id in injected_ids) + assert caught >= max(1, int(0.33 * len(injected_ids))), f"Union caught too few injected anomalies: {caught}/{len(injected_ids)}" diff --git a/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_low_anomaly_rate.py b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_low_anomaly_rate.py new file mode 100644 index 000000000..08e5ec6ec --- /dev/null +++ b/AgCloud/services/Cross-Sensor System-Level Anomalies/tests/test_low_anomaly_rate.py @@ -0,0 +1,28 @@ +# tests/test_low_anomaly_rate.py +import subprocess, sys, pandas as pd +from pathlib import Path + +def run(script, cwd): + res = subprocess.run([sys.executable, "-u", script], + cwd=cwd, capture_output=True, text=True, timeout=180) + assert res.returncode == 0, f"Script failed:\nSTDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}" + return res.stdout + +def test_clean_dataset_has_low_flags(no_anomaly_dataset_tmpdir): + workdir, n, injected = no_anomaly_dataset_tmpdir + root = Path.cwd() + + run(str(root / "detect_iforest_pca.py"), cwd=workdir) + run(str(root / "detect_residuals_and_hybrid.py"), cwd=workdir) + + df = pd.read_csv(workdir / "out" / "dataset_hybrid_iforest_pca_residual.csv") + + if_rate = df["anomaly_iforest"].mean() + pca_rate = df["anomaly_pca_recon"].mean() + res_rate = df["anomaly_residual_general"].mean() + two_of_three_rate = df["anomaly_2of3"].mean() + + msg = (f"Too many 2/3 anomalies on a clean dataset: {two_of_three_rate:.3f} " + f"(IF={if_rate:.3f}, PCA={pca_rate:.3f}, RES={res_rate:.3f})") + + assert two_of_three_rate < 0.07, msg diff --git a/AgCloud/services/README.md b/AgCloud/services/README.md new file mode 100644 index 000000000..d420b65b0 --- /dev/null +++ b/AgCloud/services/README.md @@ -0,0 +1,21 @@ +# Services + +This folder contains all microservices of the project, one subfolder per service. + +## Structure +Each service should include: +- `src/` – service code +- `tests/` – unit/integration tests (pytest preferred) +- `Dockerfile` – container build +- `README.md` – short usage notes +- `requirements.txt` + +## Naming +- Use lowercase folder names (e.g., `sensors`, `sound`, `aerial-image`, `api-gateway`). +- Keep service scope focused and independent. + +## Contributing +When adding a new service: +1. Create a new subfolder here. +2. Follow the template above. +3. Document any special configuration in the service’s own `README.md`. diff --git a/AgCloud/services/__init__.py b/AgCloud/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/air/.gitignore b/AgCloud/services/air/.gitignore new file mode 100644 index 000000000..a2661ad0d --- /dev/null +++ b/AgCloud/services/air/.gitignore @@ -0,0 +1 @@ +certs/ \ No newline at end of file diff --git a/AgCloud/services/air/Dockerfile.flink b/AgCloud/services/air/Dockerfile.flink new file mode 100644 index 000000000..c89ca99ae --- /dev/null +++ b/AgCloud/services/air/Dockerfile.flink @@ -0,0 +1,58 @@ +FROM flink:1.19.3-scala_2.12-java11 + +USER root + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV LANG=C.UTF-8 +ENV LC_ALL=C.UTF-8 + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip \ + ca-certificates wget curl libgomp1; \ + apt-get clean; \ + rm -rf /var/lib/apt/lists/* + +COPY certs /app/certs +RUN cp /app/certs/*.crt /usr/local/share/ca-certificates/ && \ + update-ca-certificates && \ + echo "✅ NetFree certificates installed successfully" + +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +RUN mkdir -p /opt/flink/lib && \ + curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar && \ + curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +WORKDIR /opt/app +RUN mkdir -p /opt/app/tmp && chmod 777 /opt/app/tmp + +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" +ENV PYFLINK_PYTHON=/opt/venv/bin/python +ENV PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python3 +ENV PYTHONPATH="/opt/venv/lib/python3.10/site-packages:${PYTHONPATH}" + +RUN /opt/venv/bin/pip install --no-cache-dir --default-timeout=1000 \ + apache-flink==1.19.3 \ + requests \ + Pillow \ + minio \ + kafka-python \ + "protobuf<=3.20.3" \ + google-cloud-storage \ + numpy + +COPY job.py /opt/app/job.py + +ENV KAFKA_BROKERS=kafka:9092 \ + IN_TOPIC=image.new.aerial \ + OUT_TOPIC_OBJECT=aerial_image_object_detections \ + OUT_TOPIC_ANOMALY=aerial_image_anomaly_detections \ + OUT_TOPIC_SEGMENTATION=aerial_image_segmentation \ + OUT_TOPIC_ALERTS=alerts \ + KAFKA_GROUP_ID=flink-air-device-pipeline diff --git a/AgCloud/services/air/README.md b/AgCloud/services/air/README.md new file mode 100644 index 000000000..1b32cd6a4 --- /dev/null +++ b/AgCloud/services/air/README.md @@ -0,0 +1,64 @@ +# 📦 Model Installation Guide + +This project requires **three machine-learning models** that are not stored in the repository due to size limits. +Please download them from the following Google Drive folder: + +👉 **Models Download Folder:** +https://drive.google.com/drive/folders/1F0iMHbm3ahGOuoOWGWKY3frFxPw0mzRm?usp=sharing + +The folder contains the following model files: + +| Model Type | File Name | Used By Service | +|----------------------|------------------------------|---------------------------| +| Object Detection | object_detection_api.pt | infer-api | +| Anomaly Detection | anomaly_detection_api.pt | anomaly-api | +| Segmentation | segmentation_api.pth | segmentation-api | + +--- + +## 📥 1. Download the Models + +Download all three files from the Google Drive folder: + +- object_detection_api.pt +- anomaly_detection_api.pt +- segmentation_api.pth + +--- + +## 📁 2. Copy Each Model to Its Required Directory + +### 🔹 Object Detection Model +Copy to: +`services/air/object_detection_api/model/` + +Expected final path: +`services/air/object_detection_api/model/object_detection_api.pt` + +--- + +### 🔹 Anomaly Detection Model +Copy to: +`services/air/anomaly_detection_api/models/` + +Expected final path: +`services/air/anomaly_detection_api/models/anomaly_detection_api.pt` + +--- + +### 🔹 Segmentation Model +Copy to: +`services/air/segmentation_api/model/` + +Expected final path: +`services/air/segmentation_api/model/segmentation_api.pth` + +--- + +## ✅ 3. Verify the Installation + +Ensure all three files exist in these exact locations: + +- `services/air/object_detection_api/model/object_detection_api.pt` +- `services/air/anomaly_detection_api/models/anomaly_detection_api.pt` +- `services/air/segmentation_api/model/segmentation_api.pth` diff --git a/AgCloud/services/air/anomaly_detection_api/.gitignore b/AgCloud/services/air/anomaly_detection_api/.gitignore new file mode 100644 index 000000000..12cfcc3f3 --- /dev/null +++ b/AgCloud/services/air/anomaly_detection_api/.gitignore @@ -0,0 +1,2 @@ +certs/ +models/ \ No newline at end of file diff --git a/AgCloud/services/air/anomaly_detection_api/Dockerfile.anomaly b/AgCloud/services/air/anomaly_detection_api/Dockerfile.anomaly new file mode 100644 index 000000000..32a0609de --- /dev/null +++ b/AgCloud/services/air/anomaly_detection_api/Dockerfile.anomaly @@ -0,0 +1,27 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 libglib2.0-0 ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* + +COPY certs /app/certs +RUN cp /app/certs/*.crt /usr/local/share/ca-certificates/ && \ + update-ca-certificates && \ + echo "✅ NetFree certificates installed successfully" + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\n\tpytorch.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +WORKDIR /app + +COPY service.py . +COPY models ./models + +RUN pip install --no-cache-dir \ + torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu && \ + pip install --no-cache-dir ultralytics fastapi uvicorn pillow python-multipart + +EXPOSE 8010 +CMD ["uvicorn", "service:app", "--host", "0.0.0.0", "--port", "8010"] diff --git a/AgCloud/services/air/anomaly_detection_api/readme.md b/AgCloud/services/air/anomaly_detection_api/readme.md new file mode 100644 index 000000000..e10e4ad7d --- /dev/null +++ b/AgCloud/services/air/anomaly_detection_api/readme.md @@ -0,0 +1,7 @@ +docker build -t anomaly-api . + +docker run -d -p 8010:8000 anomaly-api + +docker compose up -d + +http://localhost:8010/docs diff --git a/AgCloud/services/air/anomaly_detection_api/service.py b/AgCloud/services/air/anomaly_detection_api/service.py new file mode 100644 index 000000000..11a4dfb73 --- /dev/null +++ b/AgCloud/services/air/anomaly_detection_api/service.py @@ -0,0 +1,67 @@ +from fastapi import FastAPI, File, UploadFile +from fastapi.responses import JSONResponse +from ultralytics import YOLO +from PIL import Image +import io, time +import os + +# =================================================== +# Load YOLO model once +# =================================================== +MODEL_PATH = "models/anomaly_detection_api.pt" + +if not os.path.exists(MODEL_PATH): + raise FileNotFoundError(f"❌ Model weights not found at: {MODEL_PATH}") + +model = YOLO(MODEL_PATH) + +app = FastAPI(title="Anomaly Detection API", version="1.0") + + +def run_inference(file: UploadFile): + """Run YOLO inference in memory and return results.""" + image = Image.open(io.BytesIO(file.file.read())) + return model.predict(image, conf=0.4, iou=0.25, save=False) + + +@app.post("/predict") +async def predict(file: UploadFile = File(...)): + """Accepts an image, runs inference, and returns JSON with detections.""" + t0 = time.time() + results = run_inference(file) + + if not results or len(results[0].boxes) == 0: + return JSONResponse({ + "anomaly": False, + "description": "No anomaly detected.", + "inference_time_sec": round(time.time() - t0, 3) + }) + + h, w = results[0].orig_shape + detections = [] + for box in results[0].boxes: + cls = int(box.cls[0]) + detections.append({ + "label": model.names[cls], + "confidence": round(float(box.conf[0]), 3), + "bbox": [round(float(x), 1) for x in box.xyxy[0].tolist()], + "center_xy": [round(float(x), 1) for x in box.xywh[0][:2].tolist()], + "image_size": [w, h] + }) + + return JSONResponse({ + "anomaly": True, + "detections": detections, + "inference_time_sec": round(time.time() - t0, 3) + }) + + +@app.get("/health") +async def health(): + """Simple healthcheck endpoint.""" + return {"status": "ok", "model": MODEL_PATH} + + +if __name__ == "__main__": + import uvicorn + uvicorn.run("service:app", host="0.0.0.0", port=8010) diff --git a/AgCloud/services/air/job.py b/AgCloud/services/air/job.py new file mode 100644 index 000000000..b988ea292 --- /dev/null +++ b/AgCloud/services/air/job.py @@ -0,0 +1,397 @@ +import os +import io +import json +import time +import logging +import imghdr +import requests +from requests.adapters import HTTPAdapter, Retry +from minio import Minio +from PIL import Image + +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.typeinfo import Types +from pyflink.common import WatermarkStrategy +from pyflink.datastream.connectors.kafka import ( + KafkaSource, + KafkaSink, + KafkaRecordSerializationSchema, + KafkaOffsetsInitializer +) +from pyflink.datastream.functions import RuntimeContext, ProcessFunction + + +# =============================================================== +# LOGGER CONFIGURATION +# =============================================================== +def setup_logger(): + logger = logging.getLogger("FlinkJob") + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s | %(levelname)-8s | %(message)s", + "%Y-%m-%d %H:%M:%S")) + if logger.hasHandlers(): + logger.handlers.clear() + logger.addHandler(handler) + logger.propagate = False + return logger + +def upload_to_minio(file_path, bucket_name, object_name): + try: + endpoint = os.getenv("MINIO_ENDPOINT", "minio-hot:9000") + access_key = os.getenv("MINIO_ACCESS_KEY", "minioadmin") + secret_key = os.getenv("MINIO_SECRET_KEY", "minioadmin123") + use_ssl = False + client = Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=use_ssl) + if not client.bucket_exists(bucket_name): + client.make_bucket(bucket_name) + logging.info(f"🪣 Created bucket '{bucket_name}'") + client.fput_object(bucket_name, object_name, file_path) + logging.info(f"✅ Uploaded {object_name} to bucket '{bucket_name}'") + except Exception as e: + logging.error(f"❌ MinIO upload failed: {e}") + + + +logger = setup_logger() + + +# =============================================================== +# DownloadAndInfer Process Function +# =============================================================== +class DownloadAndInfer(ProcessFunction): + """Kafka → MinIO → Segmentation API → Infer API → Anomaly API → Kafka""" + + def open(self, runtime_context: RuntimeContext): + self.minio_client = Minio( + endpoint=os.getenv("MINIO_ENDPOINT", "minio-hot:9000"), + access_key=os.getenv("MINIO_ACCESS_KEY", "minioadmin"), + secret_key=os.getenv("MINIO_SECRET_KEY", "minioadmin123"), + secure=False + ) + + self.segmentation_url = os.getenv("SEGMENTATION_URL", "http://segmentation-api:8500/infer") + self.infer_url = os.getenv("INFER_URL", "http://infer-api:8000/infer") + self.anomaly_url = os.getenv("ANOMALY_URL", "http://anomaly-api:8010/predict") + + self.conf = float(os.getenv("INFER_CONF", "0.25")) + self.iou = float(os.getenv("INFER_IOU", "0.45")) + self.tmp_dir = "/opt/app/tmp" + os.makedirs(self.tmp_dir, exist_ok=True) + + self.session = requests.Session() + retries = Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504]) + self.session.mount("http://", HTTPAdapter(max_retries=retries)) + self.timeout = (5, 180) + + logger.info("✅ DownloadAndInfer ready (Segmentation + Object + Anomaly)") + + def process_element(self, value, ctx): + infer_results, anomaly_results, segmentation_results = [], [], [] + logger.info("\n" + "=" * 70) + logger.info(f"📥 Received message: {value}") + + total_start = time.time() + + # === Parse Kafka message === + try: + data = json.loads(value) + image_key = data.get("img_key") + if not image_key: + logger.warning("⚠️ Missing 'key' in Kafka message") + return [] + except Exception as e: + logger.warning(f"⚠️ Invalid JSON: {e}") + return [] + + bucket, *path_parts = image_key.strip("/").split("/") + object_path = "/".join(path_parts) + local_filename = os.path.basename(object_path) + local_path = os.path.join(self.tmp_dir, local_filename) + + # === Step 1: Download from MinIO === + try: + self.minio_client.fget_object(bucket, object_path, local_path) + logger.info(f"✅ Downloaded: {bucket}/{object_path}") + except Exception as e: + logger.error(f"❌ Failed to download from MinIO: {e}") + return [] + + # === Step 2: Detect MIME === + try: + with open(local_path, "rb") as f: + img_bytes = f.read() + image_type = imghdr.what(None, h=img_bytes) + if not image_type: + img = Image.open(io.BytesIO(img_bytes)) + image_type = img.format.lower() + mime_type = f"image/{'jpeg' if image_type == 'jpg' else image_type}" + file_name = f"image.{image_type}" + logger.info(f"🧾 Detected image type: {image_type.upper()}") + except Exception as e: + logger.error(f"❌ Invalid image: {e}") + return [] + + # === Step 3: Segmentation API === + try: + files = {"file": (file_name, img_bytes, mime_type)} + logger.info(f"🛰️ Sending to Segmentation API: {self.segmentation_url}") + t0 = time.time() + r_seg = self.session.post(self.segmentation_url, files=files, timeout=self.timeout) + t1 = time.time() - t0 + r_seg.raise_for_status() + + base_name = os.path.splitext(local_filename)[0] + mask_filename = f"{base_name}_mask.png" + mask_path = os.path.join(self.tmp_dir, mask_filename) + mask_bytes = r_seg.content + if not r_seg.content: + logger.warning(f"⚠️ Segmentation API returned empty mask for {image_key}") + with open(mask_path, "wb") as f: + f.write(mask_bytes) + logger.info(f"🖼️ Saved segmentation mask: {mask_path}") + + # === Step 3.1: Upload mask to MinIO === + try: + bucket_name = os.getenv("MINIO_BUCKET", "imagery") + mask_object_key = f"air_mask/{mask_filename}" + upload_to_minio(mask_path, bucket_name, mask_object_key) + logger.info(f"☁️ Uploaded mask to MinIO at {bucket_name}/{mask_object_key}") + except Exception as e: + logger.error(f"❌ Failed to upload mask to MinIO: {e}") + + header_data = r_seg.headers.get("X-Class-Distribution") + if header_data: + dist = json.loads(header_data.replace("'", '"')) + else: + dist = {} + + row = { + "img_key": image_key, + "mask_path": f"{bucket_name}/{mask_object_key}", + "other": dist.get("Other", 0), + "bareland": dist.get("Bareland", 0), + "rangeland": dist.get("Rangeland", 0), + "developed_space": dist.get("Developed space", 0), + "road": dist.get("Road", 0), + "tree": dist.get("Tree", 0), + "water": dist.get("Water", 0), + "agriculture": dist.get("Agriculture land", 0), + "building": dist.get("Building", 0), + } + + segmentation_results.append(row) + logger.info(f"🧩 Segmentation done for {image_key}: {json.dumps(row, indent=2)}") + + except Exception as e: + logger.exception(f"❌ Segmentation API error: {e}") + mask_path = None + + # === Step 4: Object Detection (Infer API) === + try: + files = {"image": (file_name, img_bytes, mime_type)} + + if mask_path and os.path.exists(mask_path): + with open(mask_path, "rb") as f: + mask_bytes = f.read() + files["mask"] = (os.path.basename(mask_path), mask_bytes, "image/png") + logger.info(f"🧠 Using segmentation mask for Infer API: {mask_path}") + + params = {"conf": self.conf, "iou": self.iou} + t0 = time.time() + r = self.session.post(self.infer_url, files=files, params=params, timeout=self.timeout) + t1 = time.time() - t0 + + r.raise_for_status() + infer_data = r.json() if r.status_code != 204 else {} + infer_detections = infer_data.get("result", {}).get("detections", []) or infer_data.get("detections", []) + for det in infer_detections: + x1, y1, x2, y2 = det.get("bbox", [0, 0, 0, 0]) + infer_results.append({ + "img_key": image_key, + "label": det.get("class_name"), + "confidence": float(det.get("confidence", 0)), + "bbox_x1": float(x1), + "bbox_y1": float(y1), + "bbox_x2": float(x2), + "bbox_y2": float(y2) + }) + logger.info(f"🧠 {image_key}: {len(infer_detections)} detections ({t1:.2f}s)") + except Exception as e: + logger.exception(f"❌ Infer API error: {e}") + + # === Step 5: Anomaly API === + res = {} + try: + files = {"file": (file_name, img_bytes, mime_type)} + t2 = time.time() + r2 = self.session.post(self.anomaly_url, files=files, timeout=self.timeout) + t3 = time.time() - t2 + + r2.raise_for_status() + res = r2.json() + if res.get("anomaly", False): + detections = res.get("detections", []) + for det in detections: + x1, y1, x2, y2 = det.get("bbox", [0, 0, 0, 0]) + anomaly_results.append({ + "img_key": image_key, + "label": det.get("label", "anomaly"), + "confidence": float(det.get("confidence", 0)), + "bbox_x1": float(x1), + "bbox_y1": float(y1), + "bbox_x2": float(x2), + "bbox_y2": float(y2) + }) + logger.info(f"⚠️ Anomaly detection: {len(detections)} anomalies ({t3:.2f}s)") + else: + logger.info("✅ No anomalies detected.") + except Exception as e: + logger.exception(f"❌ Anomaly API error: {e}") + + # === step 6: alerts ==== + alert_events = [] + + if res.get("anomaly", False): + detections = res.get("detections", []) + + # Extract device_id from filename + filename = os.path.basename(image_key) + device_id = filename.split("_")[0] + + alert = { + "alert_id": f"{time.time_ns()}", + "alert_type": "aerial_anomaly_detected", + "device_id": device_id, + "started_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "confidence": max(d.get("confidence", 0) for d in detections), + "severity": 3, + "image_url": image_key, # key = url אצלך + "meta": { + "anomalies": detections + } + } + + alert_events.append((json.dumps(alert), "alert")) + logger.info(f"🚨 ALERT created: {json.dumps(alert, indent=2)}") + + # === Step 7: Clean temp === + try: + os.remove(local_path) + if mask_path and os.path.exists(mask_path): + os.remove(mask_path) + except Exception: + pass + + total_time = time.time() - total_start + + logger.info(f"📊 Summary: " + f"{len(segmentation_results)} segmentations, " + f"{len(infer_results)} detections, " + f"{len(anomaly_results)} anomalies") + + logger.info(f"⏱️ Total processing time for {image_key}: {total_time:.2f}s") + + return ([(json.dumps(r), "segmentation") for r in segmentation_results] + + [(json.dumps(r), "object") for r in infer_results] + + [(json.dumps(r), "anomaly") for r in anomaly_results]+ + alert_events) + + +# =============================================================== +# MAIN EXECUTION FUNCTION +# =============================================================== +def main(): + logger.info("🚀 Starting Kafka→MinIO→Segmentation→Infer→Anomaly→Kafka Job") + + bootstrap = os.getenv("KAFKA_BROKERS", "kafka:9092") + topic_in = os.getenv("IN_TOPIC", "image.new.aerial") + topic_out_segmentation = os.getenv("OUT_TOPIC_SEGMENTATION", "aerial_image_segmentation") + topic_out_objects = os.getenv("OUT_TOPIC_OBJECT", "aerial_image_object_detections") + topic_out_anomaly = os.getenv("OUT_TOPIC_ANOMALY", "aerial_image_anomaly_detections") + topic_out_alerts = os.getenv("OUT_TOPIC_ALERTS", "alerts") + group_id = os.getenv("KAFKA_GROUP_ID", f"flink-air-device-pipeline") + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(1) + + source = ( + KafkaSource.builder() + .set_bootstrap_servers(bootstrap) + .set_topics(topic_in) + .set_group_id(group_id) + .set_starting_offsets(KafkaOffsetsInitializer.earliest()) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + sink_segmentation = ( + KafkaSink.builder() + .set_bootstrap_servers(bootstrap) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(topic_out_segmentation) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .build() + ) + + sink_objects = ( + KafkaSink.builder() + .set_bootstrap_servers(bootstrap) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(topic_out_objects) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .build() + ) + + sink_anomalies = ( + KafkaSink.builder() + .set_bootstrap_servers(bootstrap) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(topic_out_anomaly) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .build() + ) + + sink_alerts = ( + KafkaSink.builder() + .set_bootstrap_servers(bootstrap) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(topic_out_alerts) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .build() + ) + + + stream = env.from_source(source, WatermarkStrategy.no_watermarks(), "Kafka Source") + + processed = stream.process(DownloadAndInfer(), output_type=Types.TUPLE([Types.STRING(), Types.STRING()])) + + segmentation_stream = processed.filter(lambda x: x[1] == "segmentation").map(lambda x: x[0], output_type=Types.STRING()) + objects_stream = processed.filter(lambda x: x[1] == "object").map(lambda x: x[0], output_type=Types.STRING()) + anomalies_stream = processed.filter(lambda x: x[1] == "anomaly").map(lambda x: x[0], output_type=Types.STRING()) + alerts_stream = processed.filter(lambda x: x[1] == "alert").map(lambda x: x[0], output_type=Types.STRING()) + + segmentation_stream.sink_to(sink_segmentation) + objects_stream.sink_to(sink_objects) + anomalies_stream.sink_to(sink_anomalies) + alerts_stream.sink_to(sink_alerts) + + + env.execute("Kafka-MinIO-Segmentation-Infer-Anomaly-Job") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/air/object_detection_api/.gitignore b/AgCloud/services/air/object_detection_api/.gitignore new file mode 100644 index 000000000..33534c66b --- /dev/null +++ b/AgCloud/services/air/object_detection_api/.gitignore @@ -0,0 +1,2 @@ +certs/ +model/ \ No newline at end of file diff --git a/AgCloud/services/air/object_detection_api/Dockerfile.infer b/AgCloud/services/air/object_detection_api/Dockerfile.infer new file mode 100644 index 000000000..2bcf7c91f --- /dev/null +++ b/AgCloud/services/air/object_detection_api/Dockerfile.infer @@ -0,0 +1,35 @@ +FROM python:3.10-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 \ + curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY certs /app/certs + +RUN apt-get update && \ + apt-get install -y ca-certificates && \ + cp /app/certs/*.crt /usr/local/share/ca-certificates/ && \ + update-ca-certificates && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +WORKDIR /app + +RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu \ + torch torchvision torchaudio + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --default-timeout=1000 -r /app/requirements.txt + +COPY app.py model_wrapper.py /app/ + +ENV WEIGHTS_PATH=/app/object_detection_api.pt + +EXPOSE 8000 +CMD ["uvicorn","app:app","--host","0.0.0.0","--port","8000","--workers","1"] diff --git a/AgCloud/services/air/object_detection_api/README.md b/AgCloud/services/air/object_detection_api/README.md new file mode 100644 index 000000000..78bd5bda6 --- /dev/null +++ b/AgCloud/services/air/object_detection_api/README.md @@ -0,0 +1,203 @@ +# Aerial Object Counter API + +A lightweight FastAPI service that runs an object-detection model on aerial images and returns **counts per category** plus a **human‑readable summary**. +Optionally, the service can **save annotated images** (with bounding boxes) to disk. + +> **Outputs are English-only.** Every response includes: +> +> - `counts` — a JSON dictionary of `{class_name: count}` +> - `summary_text` — a compact sentence (e.g., `177 buildings, 19 vehicles`) + +--- + +## Overview + +- **Model**: Ultralytics YOLO (weights file: `best.pt`). +- **Wrapper**: `model_wrapper.py` normalizes class names to singular, clean keys. +- **API**: `app.py` exposes two endpoints: + - `POST /infer` — single image + - `POST /infer_dir` — process an entire folder of images + +Class set (12): `agri equipment, agri infra, building, rail, vessel, aviation, construction site, crane, tower, vehicle, container, yard`. + +--- + +## Project Structure + +``` +object_counter_api/ +├─ app.py # FastAPI server + endpoints +├─ model_wrapper.py # YOLOv8 loader + inference + draw/save utils +└─ requirements.txt # dependencies +``` + +--- + +## Installation + +> Recommended on Windows inside a virtual environment. + +```bat +python -m venv .venv +.venv\Scripts\activate +pip install -r requirements.txt +``` + +Place your trained weights file `best.pt` next to `app.py` (or change `WEIGHTS_PATH` at the top of `app.py`). + +--- + +## Run the Server + +```bat +uvicorn app:app --host 0.0.0.0 --port 8000 --reload +``` + +Health check (should return `{"status":"ok", ...}`): +``` +http://127.0.0.1:8000/health +``` + +--- + +## Endpoints + +### 1) `POST /infer` — Single Image + +- **Always returns**: `counts` and `summary_text` +- **Optional**: save an annotated image to disk (`outputs/` by default) + +**Query parameters** + +| Name | Type | Default | Description | +|-------------|---------|---------|-------------------------------------------| +| `conf` | float | 0.25 | Confidence threshold | +| `iou` | float | 0.45 | NMS IoU threshold | +| `min_area` | int? | null | Filter out tiny detections by pixel area | +| `save_image`| bool | false | Save annotated image to disk | +| `save_name` | string? | null | Optional filename for saved image (PNG) | + +**Request (Windows CMD / Git Bash)** +```bash +curl -s -X POST "http://127.0.0.1:8000/infer?save_image=true&save_name=annotated_test.png" \ + -F "file=@C:/Users/USER/Desktop/only_yolo/dataset/xView_12cls_en/images/test/img_99_640_0.jpg" +``` + +**Response** +```json +{ + "counts": { "building": 177, "vehicle": 19 }, + "summary_text": "177 buildings, 19 vehicles" +} +``` + +If `save_image=true`, the annotated PNG is saved on the server (default folder `outputs/`), filename as provided via `save_name` or `annotated_.png`. + +--- + +### 2) `POST /infer_dir` — Entire Folder + +Runs inference over all supported images in a directory. +**Always returns**: aggregated `counts` and `summary_text`. +**Optional**: save annotated images for each file. + +**Body (JSON)** +```json +{ + "dir_path": "C:/path/to/images", + "recursive": true, + "save_images": true, + "save_dir": "C:/path/to/outputs/batch1", + "conf": 0.25, + "iou": 0.45, + "min_area": 400 +} +``` + +**Example (Windows PowerShell / CMD)** +```bash +curl -s -X POST "http://127.0.0.1:8000/infer_dir" ^ + -H "Content-Type: application/json" ^ + -d "{\"dir_path\":\"C:/Users/USER/Desktop/only_yolo/dataset/images\",\"recursive\":true,\"save_images\":true,\"save_dir\":\"C:/Users/USER/Desktop/only_yolo/dataset/outputs/batch1\"}" +``` + +**Response** +```json +{ + "counts": { "building": 5200, "vehicle": 430 }, + "summary_text": "5200 buildings, 430 vehicles" +} +``` + +**Notes** + +- Allowed extensions: `.jpg .jpeg .png .tif .tiff` +- If `save_images=true` and no detections found for a file, no image is saved for that file. + +--- + +## Labels + +``` +GET /labels +``` + +Returns the model’s id→name mapping (normalized to clean, singular names). + +--- + +## Configuration + +At the top of `app.py`: + +```python +WEIGHTS_PATH = "best.pt" # path to your trained weights +OUTPUT_DIR = Path("outputs") # where annotated images are stored +``` + +You can change these to absolute paths if preferred. + +--- + +## Troubleshooting + +- **`ModuleNotFoundError: ultralytics`** + Ensure you run the server with the same Python where packages were installed. On Windows, check: + ```bat + where python + python --version + ``` + Then reinstall if needed: + ```bat + python -m pip install ultralytics + ``` + +- **Different Python versions** (e.g., installed under Python 3.10 but server runs under 3.11) + Create a virtual env (see Installation) and run everything inside it. + +- **Weights not found** + Verify that `best.pt` path matches `WEIGHTS_PATH` or place the file next to `app.py`. + +--- + +## Security / Safety + +- The API accepts local file paths (for `/infer_dir`). Do **not** expose this server publicly without proper authentication and sandboxing. +- For public deployments, add authentication, request size limits, and path validation. + +--- + +## License + +This repository contains example code and is provided “as is”, without warranty. +Your trained model weights remain your property under your chosen license. + +--- + +## Quick Checklist + +- [ ] Put `best.pt` next to `app.py` (or change `WEIGHTS_PATH`). +- [ ] `pip install -r requirements.txt` in a virtual environment. +- [ ] `uvicorn app:app --host 0.0.0.0 --port 8000 --reload` +- [ ] Test single image with `/infer`. +- [ ] Test folder mode with `/infer_dir`. diff --git a/AgCloud/services/air/object_detection_api/app.py b/AgCloud/services/air/object_detection_api/app.py new file mode 100644 index 000000000..686d6edc2 --- /dev/null +++ b/AgCloud/services/air/object_detection_api/app.py @@ -0,0 +1,131 @@ +from fastapi import FastAPI, UploadFile, File, Query +from fastapi.responses import JSONResponse +from typing import Dict +from io import BytesIO +from PIL import Image +import numpy as np +import logging, os +from pathlib import Path + +from model_wrapper import ( + ModelWrapper, + mask_to_detections, + merge_detections, +) + +# ----------------------------------------------------------- +# Logger setup +# ----------------------------------------------------------- +logger = logging.getLogger("fusion_api") +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") + +# ----------------------------------------------------------- +# App initialization +# ----------------------------------------------------------- +app = FastAPI(title="YOLO + Segmentation Fusion API", version="2.2") + +WEIGHTS_PATH = os.getenv("WEIGHTS_PATH", "/app/object_detection_api.pt") +model = ModelWrapper(weights_path=WEIGHTS_PATH, conf=0.25, iou=0.45) + +OUTPUT_DIR = Path("outputs") +OUTPUT_DIR.mkdir(exist_ok=True) + +if not os.path.exists(WEIGHTS_PATH): + raise FileNotFoundError(f"❌ Model weights not found at: {WEIGHTS_PATH}") +# ----------------------------------------------------------- +# Utility: convert NumPy types to JSON serializable types +# ----------------------------------------------------------- +def convert_numpy(obj): + """Recursively convert NumPy data types to native Python types.""" + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + elif isinstance(obj, (list, tuple)): + return [convert_numpy(i) for i in obj] + elif isinstance(obj, dict): + return {k: convert_numpy(v) for k, v in obj.items()} + else: + return obj + + +# ----------------------------------------------------------- +# Count detections by class +# ----------------------------------------------------------- +def count_by_class(detections): + counts = {} + for _, name, _, *_ in detections: + counts[name] = counts.get(name, 0) + 1 + return counts + + +# ----------------------------------------------------------- +# Build readable summary text +# ----------------------------------------------------------- +def build_summary(counts: Dict[str, int]) -> str: + total = sum(counts.values()) + details = ", ".join([f"{v} {k}" for k, v in counts.items()]) + return f"Total: {total} detections — {details}" + + +# ----------------------------------------------------------- +# Health check +# ----------------------------------------------------------- +@app.get("/health") +def health(): + return {"status": "ok", "weights": str(WEIGHTS_PATH)} + + +# ----------------------------------------------------------- +# Inference route +# ----------------------------------------------------------- +@app.post("/infer") +async def infer( + image: UploadFile = File(...), + mask: UploadFile = File(...), + conf: float = Query(0.25, ge=0.0, le=1.0), + iou: float = Query(0.45, ge=0.0, le=1.0), +): + try: + img_data = await image.read() + mask_data = await mask.read() + pil_image = Image.open(BytesIO(img_data)).convert("RGB") + mask_image = Image.open(BytesIO(mask_data)).convert("RGB") + + logger.info(f"📸 Loaded image: {image.filename}, mask size: {mask_image.size}") + + model.conf = conf + model.iou = iou + yolo_dets = model.predict(pil_image) + logger.info(f"🔹 YOLO detections: {len(yolo_dets)}") + + mask_dets = mask_to_detections(mask_image) + logger.info(f"🔹 Mask detections: {len(mask_dets)}") + + final_dets = merge_detections(yolo_dets, mask_dets) + logger.info(f"🔸 After merge: {len(final_dets)} total detections") + + counts = count_by_class(final_dets) + summary_text = build_summary(counts) + + result = { + "summary": summary_text, + "counts_per_class": counts, + "detections": [ + { + "class_id": d[0], + "class_name": d[1], + "confidence": d[2], + "bbox": [d[3], d[4], d[5], d[6]] + } + for d in final_dets + ], + } + + return JSONResponse(content=convert_numpy(result)) + + except Exception as e: + logger.exception(f"❌ Error: {e}") + return JSONResponse(status_code=500, content={"error": str(e)}) \ No newline at end of file diff --git a/AgCloud/services/air/object_detection_api/model_wrapper.py b/AgCloud/services/air/object_detection_api/model_wrapper.py new file mode 100644 index 000000000..1a355887e --- /dev/null +++ b/AgCloud/services/air/object_detection_api/model_wrapper.py @@ -0,0 +1,154 @@ +from __future__ import annotations +from typing import Dict, List, Tuple +from pathlib import Path +import numpy as np +from ultralytics import YOLO +import cv2 + +# ============================================================== +# STRUCTURE AND MAPPINGS +# ============================================================== + +Detection = Tuple[int, str, float, int, int, int, int] + +RAW_ID2NAME: Dict[int, str] = { + 0: "Agri equipment", 1: "Agri infra", 2: "Buildings", 3: "Rail", + 4: "Vessels", 5: "Aviation", 6: "Construction site", 7: "Cranes", + 8: "Towers", 9: "Vehicles", 10: "Containers", 11: "Yards", +} + +PALETTE = { + (210, 180, 140): (12, "Bareland"), + (152, 251, 152): (13, "Rangeland"), + (128, 128, 128): (14, "Developed space"), + (255, 255, 255): (15, "Road"), + (0, 100, 0): (16, "Tree"), + (30, 144, 255): (17, "Water"), + (255, 215, 0): (18, "Agriculture"), + (178, 34, 34): (19, "Buildings"), + (0, 0, 0): (20, "Other"), +} + +MODEL_PRIORITY = { + "vehicles": "yolo", "cranes": "yolo", "towers": "yolo", "containers": "yolo", + "yards": "yolo", "buildings": "yolo", + "road": "mask", "tree": "mask", "water": "mask", "agriculture": "mask", + "bareland": "mask", "rangeland": "mask", "developed space": "mask", "other": "mask", +} + +# ============================================================== +# YOLO MODEL WRAPPER +# ============================================================== + +class ModelWrapper: + def __init__(self, weights_path: str = "best.pt", conf: float = 0.25, iou: float = 0.45): + wp = Path(weights_path) + if not wp.exists(): + raise FileNotFoundError(f"Weights not found: {wp.resolve()}") + self.model = YOLO(str(wp)) + self.id2name = {int(i): str(n) for i, n in self.model.names.items()} if hasattr(self.model, "names") else RAW_ID2NAME + self.conf = conf + self.iou = iou + + def labels(self) -> Dict[int, str]: + return {i: n for i, n in self.id2name.items()} + + def predict(self, image: Image.Image) -> List[Detection]: + results = self.model.predict(source=np.array(image), conf=self.conf, iou=self.iou, verbose=False) + dets: List[Detection] = [] + for r in results: + if r.boxes is None: + continue + xyxy = r.boxes.xyxy.cpu().numpy() + confs = r.boxes.conf.cpu().numpy() + clss = r.boxes.cls.cpu().numpy().astype(int) + for (x1, y1, x2, y2), conf, cls in zip(xyxy, confs, clss): + name = self.id2name.get(int(cls), f"class_{cls}") + dets.append((int(cls), name, float(conf), int(x1), int(y1), int(x2), int(y2))) + return dets + + +# ============================================================== +# SEGFORMER MASK PROCESSING +# ============================================================== + +def color_mask_to_class_map(mask_rgb: Image.Image) -> np.ndarray: + mask = np.array(mask_rgb.convert("RGB")) + class_map = np.zeros(mask.shape[:2], dtype=np.uint8) + for color, (cid, _) in PALETTE.items(): + match = np.all(mask == color, axis=-1) + class_map[match] = cid + return class_map + + +def mask_to_detections(mask_rgb: Image.Image) -> List[Detection]: + mask = color_mask_to_class_map(mask_rgb) + dets: List[Detection] = [] + GENERAL_MIN = 100 + ROAD_MIN = 1000 + for cid in np.unique(mask): + if cid == 0: + continue + binary = (mask == cid).astype(np.uint8) * 255 + contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + _, cname = next(v for v in PALETTE.values() if v[0] == cid) + for c in contours: + x, y, w, h = cv2.boundingRect(c) + area = w * h + if cname.lower() == "road" and area < ROAD_MIN: + continue + if area < GENERAL_MIN: + continue + dets.append((cid, cname, 1.0, x, y, x + w, y + h)) + return dets + + +# ============================================================== +# MERGING LOGIC (Smart) +# ============================================================== + +def iou(a: Detection, b: Detection) -> float: + xA, yA = max(a[3], b[3]), max(a[4], b[4]) + xB, yB = min(a[5], b[5]), min(a[6], b[6]) + inter = max(0, xB - xA) * max(0, yB - yA) + areaA = (a[5] - a[3]) * (a[6] - a[4]) + areaB = (b[5] - b[3]) * (b[6] - b[4]) + union = areaA + areaB - inter + return inter / union if union > 0 else 0.0 + + +def merge_detections(yolo_dets: List[Detection], mask_dets: List[Detection], iou_thresh=0.4) -> List[Detection]: + final = list(yolo_dets) + + for md in mask_dets: + m_name = md[1].lower() + x1, y1, x2, y2 = md[3:7] + area = (x2 - x1) * (y2 - y1) + + if m_name == "tree" and area > 10000: + md = (md[0], "Forest", md[2], x1, y1, x2, y2) + + overlap = False + + for yd in list(final): + y_name = yd[1].lower() + ov = iou(md, yd) + if ov > iou_thresh: + if y_name in ("building", "buildings") and m_name != "building": + final.remove(yd) + continue + + y_pri = MODEL_PRIORITY.get(y_name, "yolo") + m_pri = MODEL_PRIORITY.get(m_name, "mask") + if m_pri == "mask" and y_pri == "yolo": + final.remove(yd) + final.append(md) + overlap = True + break + else: + overlap = True + + if not overlap: + final.append(md) + + return final diff --git a/AgCloud/services/air/object_detection_api/requirements.txt b/AgCloud/services/air/object_detection_api/requirements.txt new file mode 100644 index 000000000..630c65412 --- /dev/null +++ b/AgCloud/services/air/object_detection_api/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.115.0 +uvicorn==0.30.6 +pillow==10.4.0 +numpy==2.1.1 +pydantic==1.10.15 +python-multipart==0.0.9 +ultralytics==8.3.29 +opencv-python-headless==4.10.0.84 + diff --git a/AgCloud/services/air/segmentation_api/.gitignore b/AgCloud/services/air/segmentation_api/.gitignore new file mode 100644 index 000000000..3a8be3108 Binary files /dev/null and b/AgCloud/services/air/segmentation_api/.gitignore differ diff --git a/AgCloud/services/air/segmentation_api/dockerfile.segmentation b/AgCloud/services/air/segmentation_api/dockerfile.segmentation new file mode 100644 index 000000000..c2a62168a --- /dev/null +++ b/AgCloud/services/air/segmentation_api/dockerfile.segmentation @@ -0,0 +1,67 @@ +# ========================================================= +# 1️⃣ Base image – Lightweight Python 3.11 +# ========================================================= +FROM python:3.11-slim + +# ========================================================= +# 2️⃣ Environment setup +# ========================================================= +WORKDIR /app +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 + +# ========================================================= +# 3️⃣ Install system dependencies +# ========================================================= +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libgl1 \ + libglib2.0-0 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# ========================================================= +# 4️⃣ Copy NetFree SSL certificates (אם יש) +# ========================================================= +COPY certs /app/certs +RUN cp /app/certs/*.crt /usr/local/share/ca-certificates/ && \ + update-ca-certificates && \ + echo "✅ NetFree certificates installed successfully" + +# ========================================================= +# 5️⃣ Copy project files +# ========================================================= +COPY . /app + +# ========================================================= +# 6️⃣ Install Python dependencies +# ========================================================= +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +RUN pip install --no-cache-dir --upgrade pip && \ + pip config set global.cert /etc/ssl/certs/ca-certificates.crt && \ + pip install --no-cache-dir \ + fastapi \ + uvicorn[standard] \ + opencv-python-headless \ + numpy \ + pillow \ + transformers \ + scipy \ + python-multipart # ← הוסיפי את זה כאן ✅ + +# שלב 2: התקנת PyTorch בלבד עם אינדקס ייעודי +RUN pip install --no-cache-dir \ + torch==2.3.0+cpu \ + torchvision==0.18.0+cpu \ + --index-url https://download.pytorch.org/whl/cpu + +# ========================================================= +# 7️⃣ Expose port +# ========================================================= +EXPOSE 8500 + +# ========================================================= +# 8️⃣ Run the API +# ========================================================= +CMD ["uvicorn", "infer_api:app", "--host", "0.0.0.0", "--port", "8500"] diff --git a/AgCloud/services/air/segmentation_api/infer_api.py b/AgCloud/services/air/segmentation_api/infer_api.py new file mode 100644 index 000000000..d3e895bea --- /dev/null +++ b/AgCloud/services/air/segmentation_api/infer_api.py @@ -0,0 +1,329 @@ +from fastapi import FastAPI, UploadFile, File +from fastapi.responses import Response, JSONResponse +import torch, torch.nn.functional as F +import cv2, numpy as np, tempfile, math +from transformers import SegformerForSemanticSegmentation, SegformerConfig +from oem_palette import NUM_CLASSES, PALETTE +from scipy import ndimage +import json +import logging +import os + +logger = logging.getLogger("segformer_api") +logger.setLevel(logging.INFO) +formatter = logging.Formatter("%(asctime)s | %(levelname)-8s | %(message)s") +console_handler = logging.StreamHandler() +console_handler.setFormatter(formatter) +if not logger.hasHandlers(): + logger.addHandler(console_handler) + +logger.propagate = False + +# ========================================================= +# ⚙️ General Settings +# ========================================================= +app = FastAPI(title="🛰️ SegFormer-B3 Smart Inference API (Enhanced Road Logic)") + +@app.get("/health") +def health(): + return {"status": "ok"} + +MODEL_PATH = "model/segmentation_api.pth" +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +logger.info(f"✅ Using device: {device}") + +# ========================================================= +# 🧠 Model Loading +# ========================================================= +if not os.path.exists(MODEL_PATH): + raise FileNotFoundError(f"❌ Model not found at: {MODEL_PATH}") + +config = SegformerConfig( + backbone="mit_b3", + num_labels=NUM_CLASSES, + hidden_sizes=[64, 128, 320, 512], + depths=[3, 4, 18, 3], + decoder_hidden_size=768, + ignore_mismatched_sizes=True +) +model = SegformerForSemanticSegmentation(config) +state_dict = torch.load(MODEL_PATH, map_location=device) +model.load_state_dict(state_dict, strict=False) +model.to(device).eval() +logger.info("✅ SegFormer-B3 model loaded successfully!") + + +# ========================================================= +# 🎨 Helper Functions +# ========================================================= +def preprocess_image(img: np.ndarray): + img = cv2.resize(img, (512, 512)) + img = img.astype(np.float32) / 255.0 + img = img.transpose(2, 0, 1) + return torch.from_numpy(img).unsqueeze(0).to(device) + + +def predict_probs(img: np.ndarray): + inputs = preprocess_image(img) + with torch.no_grad(): + outputs = model(pixel_values=inputs) + logits = outputs.logits + logits = F.interpolate(logits, size=img.shape[:2], mode="bilinear", align_corners=False) + probs = torch.softmax(logits, dim=1)[0].cpu().numpy() + return probs + + +def compute_class_distribution(mask): + h, w = mask.shape + total = h * w + counts = {} + index_to_name = {v[0]: v[1] for v in PALETTE.values()} + for cls_idx, cls_name in index_to_name.items(): + cls_pixels = np.sum(mask == cls_idx) + percent = round(100 * cls_pixels / total, 2) + if percent > 0: + counts[cls_name] = percent + return counts + + +def colorize_mask(mask): + color_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8) + for color, (cls_idx, _) in PALETTE.items(): + color_mask[mask == cls_idx] = color + return color_mask + + +def refine_water_only(mask, probs, water_conf_thresh=0.8): + refined_mask = mask.copy() + top2 = np.argsort(-probs, axis=2) + second_best = top2[:, :, 1] + best_conf = np.max(probs, axis=2) + + WATER_CLASS = next((v[0] for k, v in PALETTE.items() if v[1].lower() == "water"), None) + if WATER_CLASS is not None: + low_conf_water = (mask == WATER_CLASS) & (best_conf < water_conf_thresh) + refined_mask[low_conf_water] = second_best[low_conf_water] + logger.info(f"💧 Replaced {np.sum(low_conf_water)} low-confidence water pixels") + + return refined_mask + + +def remove_small_roads(mask, probs, min_road_area=600, debug_visualize=True): + refined_mask = mask.copy() + ROAD_CLASS = next((v[0] for k, v in PALETTE.items() if v[1].lower() == "road"), None) + if ROAD_CLASS is None: + logger.warning("⚠️ ROAD_CLASS not found in PALETTE") + return refined_mask + + road_mask = (refined_mask == ROAD_CLASS).astype(np.uint8) + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(road_mask, connectivity=8) + + logger.info(f"🛣️ Found {num_labels - 1} road regions (min_road_area={min_road_area})") + + if debug_visualize: + color_debug = colorize_mask(mask).copy() + + removed = 0 + for i in range(1, num_labels): + area = stats[i, cv2.CC_STAT_AREA] + x, y = stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP] + w, h = stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] + cx, cy = centroids[i] + region_mask = (labels == i) + + logger.info(f"🚗 Road #{i:02d} | area={area:7.1f}px | bbox=({x},{y},{w},{h})") + + color = (0, 255, 0) if area >= min_road_area else (0, 0, 255) + if debug_visualize: + cv2.rectangle(color_debug, (x, y), (x + w, y + h), color, 2) + cv2.putText(color_debug, f"{i}", (x + 5, y + 20), + cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, cv2.LINE_AA) + + if area < min_road_area: + dilated = ndimage.binary_dilation(region_mask, iterations=10) + neighbors = refined_mask[dilated & (~region_mask)] + + if len(neighbors) > 0: + dominant_class = np.bincount(neighbors).argmax() + else: + dominant_class = 0 + + refined_mask[region_mask] = dominant_class + logger.info(f" 🧭 Replaced with surrounding class: {dominant_class}") + removed += 1 + + logger.info(f"✅ Finished checking all roads — removed {removed}/{num_labels - 1}") + + return refined_mask + +def connect_roads_perfect( + color_mask, + road_color=(255, 255, 255), + max_distance=200, + angle_threshold=35, + connection_angle_limit=45, + line_thickness=6, + min_area=50, + connect_extend=15, + debug=True +): + tolerance = 20 + low = np.array([max(0, c - tolerance) for c in road_color]) + high = np.array([min(255, c + tolerance) for c in road_color]) + road_mask = cv2.inRange(color_mask, low, high) + + kernel = np.ones((5, 5), np.uint8) + road_mask = cv2.morphologyEx(road_mask, cv2.MORPH_CLOSE, kernel, iterations=2) + + contours, _ = cv2.findContours(road_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + connected_mask = color_mask.copy() + directions = [] + + if debug: + logger.info(f"✅ Found {len(contours)} road segments") + + for idx, cnt in enumerate(contours): + area = cv2.contourArea(cnt) + if area < min_area: + directions.append(None) + continue + + data_pts = np.array(cnt[:, 0, :], dtype=np.float64) + _, eigenvectors = cv2.PCACompute(data_pts, mean=np.array([])) + vx, vy = eigenvectors[0] + angle = math.degrees(math.atan2(vy, vx)) + directions.append((vx, vy, angle)) + if debug: + logger.info(f" 🟡 Segment {idx}: area={area:.1f}, angle={angle:.1f}°") + + connections = 0 + + for i in range(len(contours)): + if directions[i] is None: + continue + for j in range(i + 1, len(contours)): + if directions[j] is None: + continue + + cnt1, cnt2 = contours[i], contours[j] + min_dist = 1e9 + pt1_min, pt2_min = None, None + + for p1 in cnt1: + for p2 in cnt2: + d = np.linalg.norm(p1[0] - p2[0]) + if d < min_dist: + min_dist = d + pt1_min, pt2_min = tuple(p1[0]), tuple(p2[0]) + + if min_dist > max_distance: + continue + + (vx1, vy1, angle1) = directions[i] + (vx2, vy2, angle2) = directions[j] + avg_angle = (angle1 + angle2) / 2 + diff_angle = abs(angle1 - angle2) + diff_angle = min(diff_angle, 180 - diff_angle) + + dx, dy = pt2_min[0] - pt1_min[0], pt2_min[1] - pt1_min[1] + length = math.hypot(dx, dy) + if length == 0: + continue + ux, uy = dx / length, dy / length + connection_angle = math.degrees(math.atan2(uy, ux)) + + def angle_between(vx, vy, ux, uy): + dot = vx * ux + vy * uy + cross = vx * uy - vy * ux + ang = abs(math.degrees(math.atan2(cross, dot))) + return min(ang, 180 - ang) + + ang_to_road1 = angle_between(vx1, vy1, ux, uy) + ang_to_road2 = angle_between(vx2, vy2, ux, uy) + + if debug: + logger.info(f"🔹 {i}↔{j} | dist={min_dist:.1f}px | Δdir={diff_angle:.1f}° | " + f"Δconn1={ang_to_road1:.1f}° | Δconn2={ang_to_road2:.1f}°") + + if ( + diff_angle < angle_threshold and + ang_to_road1 < connection_angle_limit and + ang_to_road2 < connection_angle_limit + ): + p1, p2 = np.array(pt1_min, np.float32), np.array(pt2_min, np.float32) + p1_ext = (p1 - np.array([ux, uy]) * connect_extend).astype(int) + p2_ext = (p2 + np.array([ux, uy]) * connect_extend).astype(int) + + cv2.line(connected_mask, tuple(p1_ext), tuple(p2_ext), + road_color, line_thickness, lineType=cv2.LINE_8) + connections += 1 + if debug: + logger.info(f" ✅ Connected {i}↔{j}") + + if debug: + logger.info(f"✅ Total valid connections: {connections}") + if connections == 0: + print("⚠️ No connections made — try increasing max_distance slightly.") + + return connected_mask + + +def apply_confidence_threshold(probs, mask, threshold=0.6): + best_conf = np.max(probs, axis=2) + low_conf_mask = best_conf < threshold + mask[low_conf_mask] = 0 + logger.info(f"⚙️ Converted {np.sum(low_conf_mask)} low-confidence pixels to class 0 (Other)") + return mask + + +@app.post("/infer") +async def infer_image(file: UploadFile = File(...), threshold: float = 0.3): + try: + with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as tmp: + tmp.write(await file.read()) + tmp_path = tmp.name + + img = cv2.cvtColor(cv2.imread(tmp_path), cv2.COLOR_BGR2RGB) + os.remove(tmp_path) + probs = predict_probs(img) + mask = np.argmax(probs, axis=0) + probs = np.transpose(probs, (1, 2, 0)) + mask = apply_confidence_threshold(probs, mask, threshold=threshold) + + + mask = refine_water_only(mask, probs, water_conf_thresh=1) + + color_mask = colorize_mask(mask) + + logger.info("🚗 Connecting roads before removing small ones...") + connected_color = connect_roads_perfect( + color_mask, + max_distance=120, + angle_threshold=35, + debug=True + ) + + connected_mask = np.zeros(mask.shape, dtype=np.uint8) + + for color, (cls_idx, _) in PALETTE.items(): + lower = np.clip(np.array(color) - 20, 0, 255).astype(np.uint8) + upper = np.clip(np.array(color) + 20, 0, 255).astype(np.uint8) + mask_area = cv2.inRange(connected_color, lower, upper) + connected_mask[mask_area > 0] = cls_idx + + logger.info(f"🖼️ mask shape: {mask.shape}, unique values: {np.unique(mask)}") + logger.info(f"🖼️ connected_mask shape: {connected_mask.shape}, unique values: {np.unique(connected_mask)}") + logger.info("🚗 sending to remove_small_roads() ...") + + refined_mask = remove_small_roads(connected_mask, probs, min_road_area=1000) + + final_color_mask = colorize_mask(refined_mask) + dist = compute_class_distribution(refined_mask) + + _, buffer = cv2.imencode(".png", cv2.cvtColor(final_color_mask, cv2.COLOR_RGB2BGR)) + return Response(content=buffer.tobytes(), media_type="image/png", + headers={"X-Class-Distribution": json.dumps(dist)}) + + except Exception as e: + return JSONResponse(content={"error": str(e)}, status_code=500) + diff --git a/AgCloud/services/air/segmentation_api/oem_palette.py b/AgCloud/services/air/segmentation_api/oem_palette.py new file mode 100644 index 000000000..711b737b0 --- /dev/null +++ b/AgCloud/services/air/segmentation_api/oem_palette.py @@ -0,0 +1,14 @@ +NUM_CLASSES = 9 + +PALETTE = { + (0, 0, 0): (0, "Other"), + (210, 180, 140): (1, "Bareland"), + (152, 251, 152): (2, "Rangeland"), + (128, 128, 128): (3, "Developed space"), + (255, 255, 255): (4, "Road"), + (0, 100, 0): (5, "Tree"), + (30, 144, 255): (6, "Water"), + (255, 215, 0): (7, "Agriculture"), + (178, 34, 34): (8, "Building"), +} + diff --git a/AgCloud/services/alertmanager_service/README.md b/AgCloud/services/alertmanager_service/README.md new file mode 100644 index 000000000..743bef952 --- /dev/null +++ b/AgCloud/services/alertmanager_service/README.md @@ -0,0 +1,201 @@ +# 🚨 AgGuard AlertManager Service + +The **AgGuard AlertManager Service** acts as a bridge between AgCloud’s detection pipelines and **Prometheus Alertmanager**. +It receives structured alert JSON payloads, renders descriptive messages using YAML templates, and forwards the alerts to Alertmanager’s `/api/v2/alerts` endpoint. + +--- + +## 🧩 Overview + +- **Framework:** FastAPI +- **Purpose:** Converts raw alerts from detection systems into human-readable, templated messages and sends them to Alertmanager +- **Output:** Properly structured Alertmanager v2 JSON alerts +- **Version:** `1.3` + +--- + +## ⚙️ Environment Variables + +| Variable | Description | Default | +|-----------|--------------|----------| +| `CFG_PATH` | Path to the YAML file containing alert templates | `/app/templates/templates/templates.yml` | +| `ALERTMANAGER_URL` | Base URL of the Alertmanager API | `http://alertmanager:9093` | +| `LOG_LEVEL` | Optional logging verbosity (e.g., `INFO`, `DEBUG`) | `INFO` | + +--- + +## 🚀 Endpoints + +### `POST /alerts` + +Accepts an alert JSON payload and forwards it to Alertmanager after rendering its template. + +**Example request:** + +```bash +curl -X POST http://localhost:8000/alerts \ + -H "Content-Type: application/json" \ + -d '{ + "alert_id": "alert-67", + "alert_type": "smoke_detected", + "device_id": "camera-12", + "started_at": "2025-10-30T14:45:00Z", + "ended_at": "2025-10-30T15:10:00Z", + "confidence": 0.91, + "severity": 2, + "area": "south_field", + "lat": 31.900215, + "lon": 34.850921, + "image_url": "https://s3.farm/agguard/smoke_20251030_1445.jpg", + "vod": "https://s3.farm/agguard/smoke_clip_1445.mp4" + }' +``` + +**Example response:** + +```json +{ + "status": "sent", + "alert": { + "labels": { + "alertname": "smoke_detected", + "alert_id": "alert-67", + "device": "camera-12", + "source": "agcloud-alerts" + }, + "annotations": { + "summary": "🚨 Smoke detected by camera-12 near south_field (confidence 0.91)", + "recommendation": "Inspect the south_field immediately. If fire is confirmed, contact emergency services.", + "category": "environmental", + "severity": "2", + "lat": "31.900215", + "lon": "34.850921", + "image_url": "https://s3.farm/agguard/smoke_20251030_1445.jpg", + "vod": "https://s3.farm/agguard/smoke_clip_1445.mp4" + }, + "startsAt": "2025-10-30T14:45:00Z", + "endsAt": "2025-10-30T15:10:00Z" + } +} +``` + +--- + +### `GET /health` + +Simple health check endpoint. + +**Response:** + +```json +{ "status": "ok" } +``` + +--- + +## 📄 Template Configuration + +Templates are defined in a YAML file (default: `/app/templates/templates/templates.yml`). +Each key corresponds to an `alert_type` and defines the message text and metadata. + +**Example:** + +```yaml +templates: + smoke_detected: + category: environmental + summary: "🚨 Smoke detected by ${device_id} near ${area} (confidence ${confidence})" + recommendation: "Inspect the ${area} immediately. If fire is confirmed, contact emergency services." + + masked_person: + category: security + summary: "Person wearing a mask detected by ${device_id} at ${timestamp}" + recommendation: "Verify the person’s authorization using the live feed." +``` + +### 🧠 Template Variables + +Template values use Python’s `string.Template` syntax (`${variable}`). +Any key present in the incoming alert JSON can be substituted dynamically. + +| Common variable | Description | +|------------------|-------------| +| `${device_id}` | Unique device identifier | +| `${area}` | Detected area/zone | +| `${confidence}` | Detection confidence | +| `${timestamp}` | ISO time string (optional) | +| `${alert_type}` | Type of alert | +| `${severity}` | Numeric severity or category | + +If a template variable is missing in the payload, it is safely ignored (not replaced). + +--- + +## 💬 How Templates Are Used in UI and Slack + +- The `summary` field defined in the template is **displayed directly in the AgGuard UI alert panels**, providing human-readable context (e.g., _“🚨 Smoke detected by camera-12 near south_field”_). +- The same `summary` text is also included in **Slack notifications** sent by Alertmanager, ensuring consistent and recognizable messages across interfaces. +- `recommendation` text is used as an actionable suggestion in both the UI and Slack alerts (e.g., _“Inspect the south_field immediately.”_) + +--- + +## 🧱 Expected JSON Fields + +| Field | Required | Description | +|--------|-----------|-------------| +| `alert_id` | ✅ | Unique alert identifier | +| `alert_type` | ✅ | Type of alert (matches template name) | +| `device_id` | ✅ | Source device ID | +| `started_at` | ✅ | ISO timestamp (`Z` or timezone-aware) | +| `ended_at` | ❌ | ISO timestamp for resolution (optional) | +| `severity` | ❌ | Numeric or string-based severity | +| `confidence`, `area`, `lat`, `lon`, `image_url`, `vod`, `hls`, `meta` | ❌ | Optional metadata | + +--- + +## 🧾 Example Alert Flow + +1. **AgCloud Detector** sends a JSON alert to `/alerts`. +2. The service loads the corresponding YAML template (based on `alert_type`). +3. It renders the `summary`, `recommendation`, and `category` using `${variables}`. +4. A properly formatted payload is sent to **Alertmanager v2 API**. +5. Alertmanager handles grouping, silencing, and routing to receivers (e.g., Slack, email). +6. The same summary is displayed in both **Slack messages** and the **AgGuard UI alerts**. + +--- + +## 🧰 Local Run + +```bash +# Install dependencies +pip install fastapi uvicorn pyyaml + +# Run the service +uvicorn main:app --reload --host 0.0.0.0 --port 8000 +``` + +Environment variables can be provided via `.env` or Docker Compose: + +```yaml +environment: + - CFG_PATH=/app/templates/templates/templates.yml + - ALERTMANAGER_URL=http://alertmanager:9093 +``` + +--- + +## 🪶 Logging + +Logs include alert processing details and delivery status: + +``` +2025-11-02 15:34:12 | INFO | [ALERT PAYLOAD] { + "labels": { "alertname": "smoke_detected", ... }, + "annotations": { "summary": "...", ... }, + "startsAt": "..." +} +2025-11-02 15:34:12 | INFO | [Alertmanager] Sent alerts (HTTP 200) +``` + +--- + diff --git a/AgCloud/services/alertmanager_service/compose/alertmanager.yml b/AgCloud/services/alertmanager_service/compose/alertmanager.yml new file mode 100644 index 000000000..84acb4590 --- /dev/null +++ b/AgCloud/services/alertmanager_service/compose/alertmanager.yml @@ -0,0 +1,81 @@ + +global: + resolve_timeout: 24h + +route: + receiver: "null" + group_by: ["alertname", "device", "alert_id"] + group_wait: 0s + group_interval: 1s + repeat_interval: 2h + routes: + # 1️⃣ Gateway route + - receiver: "gateway" + continue: true + matchers: + - source="agcloud-alerts" + + # 2️⃣ Slack route + - receiver: "slack" + continue: false + matchers: + - source="agcloud-alerts" + +receivers: + - name: "null" + + - name: "gateway" + webhook_configs: + - url: "http://alerts-gateway:8000/internal/alert" + send_resolved: true + + - name: "slack" + slack_configs: + - api_url_file: /run/secrets/slack_webhook + channel: "all-agcloud" # FIXED (no "#") + send_resolved: true + username: "AgCloud Alerts" + icon_emoji: ":rotating_light:" + + title: >- + {{ if eq .Status "resolved" }} + ⚪ EVENT CLOSED: {{ .CommonLabels.alertname }} on {{ .CommonLabels.device }} + {{ else }} + 🔴 ACTIVE ALERT: {{ .CommonLabels.alertname }} on {{ .CommonLabels.device }} + {{ end }} + + text: |- + {{ if eq .Status "resolved" }} + *Event closed.* + + *Type:* {{ .CommonLabels.alertname }} + *Device:* {{ .CommonLabels.device }} + *Alert ID:* {{ .CommonLabels.alert_id }} + + *Duration:* {{ (index .Alerts 0).StartsAt }} → {{ (index .Alerts 0).EndsAt }} + + *Summary:* {{ .CommonAnnotations.summary }} + + _No further activity detected. The alert has been automatically closed._ + + {{ else }} + *Summary:* {{ .CommonAnnotations.summary }} + *Recommendation:* {{ .CommonAnnotations.recommendation }} + *Severity:* {{ .CommonAnnotations.severity }} + *Category:* {{ .CommonAnnotations.category }} + + {{- if .CommonAnnotations.image_url }} + <{{ .CommonAnnotations.image_url }}|📷 Image> + {{- end }} + + {{- if .CommonAnnotations.vod }} + | <{{ .CommonAnnotations.vod }}|🎞️ VOD> + {{- end }} + + {{- if .CommonAnnotations.hls }} + | <{{ .CommonAnnotations.hls }}|📺 HLS> + {{- end }} + + _🕒 ID: {{ .CommonLabels.alert_id }}_ + {{ end }} + diff --git a/AgCloud/services/alertmanager_service/compose/docker-compose.yml b/AgCloud/services/alertmanager_service/compose/docker-compose.yml new file mode 100644 index 000000000..91100a5b1 --- /dev/null +++ b/AgCloud/services/alertmanager_service/compose/docker-compose.yml @@ -0,0 +1,46 @@ +version: "3.9" + +services: + alertmanager: + image: prom/alertmanager:v0.27.0 + container_name: alertmanager + command: + - "--config.file=/etc/alertmanager/alertmanager.yml" + - "--storage.path=/alertmanager" + - "--log.level=debug" + volumes: + - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro + ports: + - "9093:9093" + restart: always + + alertmanager_service: + build: + context: ../src + dockerfile: Dockerfile + container_name: alertmanager_service + ports: + - "8090:8090" + command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090"] + environment: + - CFG_PATH=/app/templates.yml + - ALERTMANAGER_URL=http://alertmanager:9093 + - GATEWAY_URL=http://alerts-gateway:8000/internal/alert + depends_on: + - alertmanager + - alerts-gateway + + alerts-gateway: + build: + context: ../src + dockerfile: Dockerfile + container_name: alerts_gateway + command: ["uvicorn", "gateway:app", "--host", "0.0.0.0", "--port", "8000"] + ports: + - "8010:8000" # host:container + +networks: + default: + external: true + name: agcloud_ag_cloud + diff --git a/AgCloud/services/alertmanager_service/src/Dockerfile b/AgCloud/services/alertmanager_service/src/Dockerfile new file mode 100644 index 000000000..76b1b51db --- /dev/null +++ b/AgCloud/services/alertmanager_service/src/Dockerfile @@ -0,0 +1,57 @@ +# ───────────────────────────────────────────── +# Dockerfile (used for both alert_service and gateway) +# ───────────────────────────────────────────── +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +COPY certs/*.crt /usr/local/share/ca-certificates/ +RUN chmod 644 /usr/local/share/ca-certificates/*.crt \ + && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +RUN python -m pip install --upgrade --no-cache-dir pip setuptools wheel + +# Copy code +COPY . . + +# If ./certs has any *.crt (at any depth) → add to system trust store; otherwise skip +RUN if [ -d /app/certs ] && [ -n "$(find /app/certs -type f -name '*.crt' -print -quit)" ]; then \ + find /app/certs -type f -name '*.crt' -exec cp {} /usr/local/share/ca-certificates/ \; && \ + update-ca-certificates; \ + else \ + echo "No extra CA certs found. Skipping CA update."; \ + fi + +# Ensure pip/requests use the system CA bundle (works with/without NetFree) +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Persist pip setting as fallback +RUN printf "[global]\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +# Install deps +RUN pip install --no-cache-dir fastapi "uvicorn[standard]" pyyaml aiohttp asyncpg + +# Default port — can be overridden by compose +EXPOSE 8090 + +# Default environment +ENV CFG_PATH=/app/templates.yml +ENV ALERTMANAGER_URL=http://alertmanager:9093 +ENV GATEWAY_URL=http://alerts-gateway:8000/internal/alert + +# Default command (can be overridden in docker-compose) +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090"] diff --git a/AgCloud/services/alertmanager_service/src/alert_service.py b/AgCloud/services/alertmanager_service/src/alert_service.py new file mode 100644 index 000000000..27b6e1978 --- /dev/null +++ b/AgCloud/services/alertmanager_service/src/alert_service.py @@ -0,0 +1,121 @@ +from __future__ import annotations +import yaml, json, logging, string +from typing import Dict, Any, Sequence +from datetime import datetime, timezone +import urllib.request + +log = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s") + + +# ───────────────────────────────────────────── +# Template Renderer +# ───────────────────────────────────────────── +class AlertTemplateRenderer: + def __init__(self, cfg_path: str): + with open(cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + self.templates = cfg.get("templates", {}) + + def render(self, alert_type: str, context: dict) -> dict: + tpl = self.templates.get(alert_type) + if not tpl: + raise ValueError(f"No template found for alert type '{alert_type}'") + return {k: string.Template(str(v)).safe_substitute(context) for k, v in tpl.items()} + + +# ───────────────────────────────────────────── +# Alertmanager HTTP Client +# ───────────────────────────────────────────── +class AlertmanagerClient: + def __init__(self, base_url: str, timeout: float = 3.0): + self.base_url = base_url.rstrip("/") + self.timeout = timeout + + def send(self, alerts: Sequence[Dict[str, Any]]) -> None: + """Send alerts to Alertmanager v2 API endpoint.""" + url = f"{self.base_url}/api/v2/alerts" + data = json.dumps(list(alerts)).encode("utf-8") + req = urllib.request.Request(url, data=data, method="POST", + headers={"Content-Type": "application/json"}) + try: + with urllib.request.urlopen(req, timeout=self.timeout) as resp: + log.info(f"[Alertmanager] Sent alerts (HTTP {resp.status})") + except Exception as e: + log.error(f"[Alertmanager] Failed to send: {e}") + raise + + +# ───────────────────────────────────────────── +# Main Service Logic +# ───────────────────────────────────────────── +class AlertManagerService: + def __init__(self, cfg_path: str, alertmanager_url: str): + self.renderer = AlertTemplateRenderer(cfg_path) + self.client = AlertmanagerClient(alertmanager_url) + + def process_alert(self, data: dict): + """Validate, render, and send an alert to Alertmanager.""" + required_fields = ["alert_id", "alert_type", "device_id", "started_at"] + for field in required_fields: + if field not in data: + raise ValueError(f"Missing required field: {field}") + + tpl = self.renderer.render(data["alert_type"], data) + + # ───── Stable labels ───── + labels = { + "alertname": data["alert_type"], + "alert_id": data["alert_id"], + "device": data["device_id"], + "source": "agcloud-alerts", + } + + # ───── Descriptive annotations ───── + annotations = { + "summary": tpl.get("summary"), + "recommendation": tpl.get("recommendation"), + "category": tpl.get("category"), + "severity": str(data.get("severity", tpl.get("severity", "unknown"))), + } + + # Optional dynamic fields + optional_fields = [ + "confidence", "area", "lat", "lon", + "image_url", "vod", "hls", "meta" + ] + for f in optional_fields: + if f in data and data[f] is not None: + annotations[f] = str(data[f]) + + # ───── Timestamp normalization ───── + def to_utc_iso(s: str | None) -> str | None: + if not s: + return None + dt = datetime.fromisoformat(s.replace("Z", "+00:00")) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + starts_at = to_utc_iso(data.get("started_at")) + ends_at = to_utc_iso(data.get("ended_at")) + + now_utc = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + if starts_at and starts_at > now_utc: + log.warning(f"Start time {starts_at} is in the future, adjusting to now: {now_utc}") + starts_at = now_utc + + + payload = { + "labels": labels, + "annotations": annotations, + "startsAt": starts_at, + } + if ends_at: + payload["endsAt"] = ends_at + + # ───── Send to Alertmanager ───── + self.client.send([payload]) + log.info(f"[ALERT PAYLOAD] {json.dumps(payload, indent=2)}") + + return payload diff --git a/AgCloud/services/alertmanager_service/src/app.py b/AgCloud/services/alertmanager_service/src/app.py new file mode 100644 index 000000000..b9fc3447a --- /dev/null +++ b/AgCloud/services/alertmanager_service/src/app.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import JSONResponse +from alert_service import AlertManagerService +import os, logging + +app = FastAPI(title="AgGuard AlertManager Service", version="1.3") +log = logging.getLogger(__name__) + +# CFG_PATH = os.getenv("CFG_PATH", "templates.yml") +CFG_PATH = os.getenv("CFG_PATH", "/app/templates/templates.yml") + +ALERTMANAGER_URL = os.getenv("ALERTMANAGER_URL", "http://alertmanager:9093") + +service = AlertManagerService(CFG_PATH, ALERTMANAGER_URL) + + +@app.post("/alerts") +async def post_alert(request: Request): + """ + Receive an alert JSON payload and forward it to Alertmanager. + """ + try: + data = await request.json() + result = service.process_alert(data) + return JSONResponse({"status": "sent", "alert": result}) + except Exception as e: + log.exception("Failed to process alert") + raise HTTPException(status_code=400, detail=str(e)) + + +@app.get("/health") +async def health(): + """Simple health check endpoint.""" + return {"status": "ok"} diff --git a/AgCloud/services/alertmanager_service/src/gateway.py b/AgCloud/services/alertmanager_service/src/gateway.py new file mode 100644 index 000000000..872b3704d --- /dev/null +++ b/AgCloud/services/alertmanager_service/src/gateway.py @@ -0,0 +1,57 @@ +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi.responses import JSONResponse +import json, asyncio, logging + + +log = logging.getLogger(__name__) +app = FastAPI(title="AgGuard Alerts Gateway", version="1.0") + +CLIENTS = set() + +@app.websocket("/ws/alerts") +async def ws_alerts(ws: WebSocket): + await ws.accept() + CLIENTS.add(ws) + log.info("Client connected.") + # active = await fetch_active_alerts() + try: + # Send initial snapshot + + while True: + # Wait for pings or keepalive from client + try: + msg = await asyncio.wait_for(ws.receive_text(), timeout=30) + log.debug(f"Received message: {msg}") + except asyncio.TimeoutError: + # No message from client — send ping + await ws.send_json({"type": "ping"}) + continue + except WebSocketDisconnect: + log.info("Client disconnected.") + except Exception as e: + log.exception(f"Error in WebSocket: {e}") + finally: + CLIENTS.discard(ws) + + +@app.post("/internal/alert") +async def internal_alert(request: Request): + """Called by alert_service when a new alert is received.""" + alert = await request.json() + msg = json.dumps({"type": "alert", "data": alert}) + dead = [] + for ws in CLIENTS: + try: + await ws.send_text(msg) + except Exception: + dead.append(ws) + for ws in dead: + CLIENTS.discard(ws) + return {"status": "broadcasted"} + + + +@app.get("/health") +async def health(): + return {"status": "ok"} + diff --git a/AgCloud/services/alerts_forwarder/Dockerfile.flink b/AgCloud/services/alerts_forwarder/Dockerfile.flink new file mode 100644 index 000000000..fbb7026c4 --- /dev/null +++ b/AgCloud/services/alerts_forwarder/Dockerfile.flink @@ -0,0 +1,41 @@ + +FROM flink:1.20.0-scala_2.12-java11 + +USER root + +# Add local CA (place netfree-ca.crt next to this Dockerfile before building) +# COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +# RUN chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Python & tools +RUN apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3-pip curl ca-certificates && rm -rf /var/lib/apt/lists/* + +# Create venv and install pyflink +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Install PyFlink (DataStream API) and requests +RUN pip install --upgrade pip certifi && pip install --prefer-binary apache-flink==2.1.0 requests urllib3 + +# Kafka connector jar matching Flink 1.20 (connector v3 series) +# Reference: flink-connector-kafka-3.x for Flink 1.20 +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar && \ + curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +RUN mkdir -p /opt/app/secrets && chmod -R 777 /opt/app + +WORKDIR /opt/app +COPY alerts_forwarder.py /opt/app/alerts_forwarder.py + +# Flink Python env vars +ENV PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python PYFLINK_PYTHON=/opt/venv/bin/python PYTHONPATH=/opt/app + +# Default command is provided by docker-compose (jobmanager/taskmanager), but keep a convenient default +CMD ["bash", "-lc", "python alerts_forwarder.py"] + diff --git a/AgCloud/services/alerts_forwarder/alerts_forwarder.py b/AgCloud/services/alerts_forwarder/alerts_forwarder.py new file mode 100644 index 000000000..537aa5815 --- /dev/null +++ b/AgCloud/services/alerts_forwarder/alerts_forwarder.py @@ -0,0 +1,53 @@ +import os, json, requests +from pyflink.common import Types +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import KafkaSource, KafkaOffsetsInitializer +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy + +ALERTMANAGER_SERVICE_URL = "http://alertmanager_service:8090/alerts" +KAFKA_BROKERS = "kafka:9092" +TOPIC = "alerts" + +def send_to_alertmanager(alert_json: str): + try: + data = json.loads(alert_json) + resp = requests.post(ALERTMANAGER_SERVICE_URL, json=data, timeout=5) + if resp.status_code == 200: + print(f"✅ Sent alert {data.get('alert_id')}", flush=True) + else: + print(f"❌ {resp.status_code}: {resp.text}", flush=True) + except Exception as e: + print(f"Failed to send alert: {e}", flush=True) + +def main(): + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(int(os.getenv("FLINK_PARALLELISM", "1"))) + + print(f"[FLINK] Listening on topic: {TOPIC}", flush=True) + + source = ( + KafkaSource.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_topics(TOPIC) + .set_group_id("flink-alerts-to-alertmanager") + .set_starting_offsets(KafkaOffsetsInitializer.latest()) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + stream = env.from_source(source, WatermarkStrategy.no_watermarks(), "Kafka Alerts Source") + + # ✅ Must have a terminal operator + stream.map( + lambda raw: (send_to_alertmanager(raw) or True), + output_type=Types.BOOLEAN() + ).print() + + env.execute("Flink Alerts → AlertManager Forwarder") + +if __name__ == "__main__": + main() + + + diff --git a/AgCloud/services/alerts_forwarder/docker-compose.yml b/AgCloud/services/alerts_forwarder/docker-compose.yml new file mode 100644 index 000000000..4f45542b7 --- /dev/null +++ b/AgCloud/services/alerts_forwarder/docker-compose.yml @@ -0,0 +1,19 @@ +services: + flink-alerts-job: + build: + context: . + dockerfile: Dockerfile.flink + container_name: alerts-forwarder + depends_on: + - kafka + - alertmanager_service + environment: + - PYTHONPATH=/opt/app + - KAFKA_BROKERS=kafka:9092 + - ALERTMANAGER_SERVICE_URL=http://alertmanager_service:8090/alerts + command: ["python", "/opt/app/alerts_forwarder.py"] + +networks: + default: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/services/compression/Dockerfile b/AgCloud/services/compression/Dockerfile new file mode 100644 index 000000000..44deed463 --- /dev/null +++ b/AgCloud/services/compression/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.12-slim + +# Install ffmpeg, cron, and ca-certificates +RUN apt-get update && \ + apt-get install -y ffmpeg cron ca-certificates dos2unix && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy certificates +COPY certs /app/certs + +# Install certificates +RUN cp /app/certs/*.crt /usr/local/share/ca-certificates/ && \ + update-ca-certificates + +# Copy requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy source code +COPY src/ ./src/ + +# Create logs directory +RUN mkdir -p /app/src/logs + +# Copy and fix both scripts +COPY docker-entrypoint.sh /docker-entrypoint.sh +RUN dos2unix /docker-entrypoint.sh && \ + chmod +x /docker-entrypoint.sh && \ + dos2unix /app/src/run_tiering.sh && \ + chmod +x /app/src/run_tiering.sh + +WORKDIR /app/src + +ENTRYPOINT ["/docker-entrypoint.sh"] diff --git a/AgCloud/services/compression/README.md b/AgCloud/services/compression/README.md new file mode 100644 index 000000000..9b43c7487 --- /dev/null +++ b/AgCloud/services/compression/README.md @@ -0,0 +1,52 @@ +# Sound 2 – Compression & Retention Policy + +## Implements + +1. Prototype compression (FLAC, Opus). +2. Measure compression ratio, encoding time, CPU usage. +3. Two-tier storage: `data/raw` (short-term) → `data/compressed` (long-term) with retention, directly on MinIO. + +## MionIO Layout + +- Bucket: compression +- Folder raw/ – input audio. +- Folder compressed/ – compressed outputs. +- results/ – benchmark results (local). +- scripts/ – Python code. + +## Usage + +```bash +# Python +pip install psutil pandas minio + +# Benchmarks: encode all raw files and measure CPU/time/ratio +python scripts/run_bench.py + +# Tiering job: compress raw files older than 24h, encode to Opus, delete raw, retention 90 days +python scripts/tiering_job.py --raw-max-age-hours 24 --codec opus --delete-raw-after --compressed-max-age-days 90 + +# Dry run example +python scripts/tiering_job.py --raw-max-age-hours 24 --codec opus --dry-run +``` + +## Notes - compression task + +- FLAC = lossless, may be larger than MP3. + +- Opus = smaller, low-loss, higher CPU. + +- Retention controlled with --compressed-max-age-days. + +- All files are stored directly on MinIO, no local copy required. + +## MinIO Client Configuration + +```bash +client = Minio( + "localhost:9001", + access_key="minioadmin", + secret_key="minioadmin123", + secure=False +) +``` diff --git a/AgCloud/services/compression/compressed/cat.flac b/AgCloud/services/compression/compressed/cat.flac new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/compression/docker-entrypoint.sh b/AgCloud/services/compression/docker-entrypoint.sh new file mode 100644 index 000000000..997109f08 --- /dev/null +++ b/AgCloud/services/compression/docker-entrypoint.sh @@ -0,0 +1,64 @@ +#!/bin/bash +set -e + +echo "Setting up cron job..." + +# Write environment variables with export prefix +printenv | grep -E '^(RAW_MAX_AGE_DAYS|COMPRESSION_CODEC|COMPRESSED_MAX_AGE_DAYS|MINIO_ENDPOINT|ACCESS_KEY|SECRET_KEY|BUCKET_NAME)=' | sed 's/^/export /' > /app/cron.env + +# Make scripts executable +chmod +x /app/src/run_tiering.sh + +# Create crontab +cat > /tmp/crontab.txt << 'EOF' +SHELL=/bin/bash +PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin + +# Run every 2 minutes +*/2 * * * * . /app/cron.env && /app/src/run_tiering.sh >> /app/src/logs/cron.log 2>&1 + +# Debug: Log cron is alive every 10 minutes +*/10 * * * * echo "[$(date)] Cron is alive" >> /app/src/logs/cron.log +EOF + +# Install crontab +crontab /tmp/crontab.txt + +echo "===================================" +echo "Audio Compression Service Started" +echo "===================================" +echo "Cron schedule:" +crontab -l +echo "===================================" +echo "Environment variables saved to /app/cron.env:" +cat /app/cron.env +echo "===================================" + +# Create logs directory +mkdir -p /app/src/logs + +# Initial log entry +echo "[$(date)] Cron daemon starting..." >> /app/src/logs/cron.log + +echo "Waiting for MinIO to be ready..." +sleep 10 + +echo "===================================" +echo "Running initial test..." +echo "===================================" + +# Run initial test (this will show if there are any immediate errors) +if /app/src/run_tiering.sh >> /app/src/logs/cron.log 2>&1; then + echo "✓ Initial test completed successfully" +else + echo "✗ Initial test failed - check /app/src/logs/cron.log" +fi + +echo "===================================" +echo "Service is now running." +echo "Compression will run every 2 minutes." +echo "Check logs: docker exec audio_compression tail -f /app/src/logs/cron.log" +echo "===================================" + +# Start cron in foreground +exec cron -f \ No newline at end of file diff --git a/AgCloud/services/compression/pytest.ini b/AgCloud/services/compression/pytest.ini new file mode 100644 index 000000000..fcccae197 --- /dev/null +++ b/AgCloud/services/compression/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = src diff --git a/AgCloud/services/compression/requirements.txt b/AgCloud/services/compression/requirements.txt new file mode 100644 index 000000000..68cfe7aa3 --- /dev/null +++ b/AgCloud/services/compression/requirements.txt @@ -0,0 +1,4 @@ +minio==7.2.18 +# ffmpeg-python==4.4 +# argparse==1.4.0 +# statistics==1.0.3.5 # This module is built-in, but can be added for compatibility diff --git a/AgCloud/services/compression/src/__init__.py b/AgCloud/services/compression/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/compression/src/minio_client.py b/AgCloud/services/compression/src/minio_client.py new file mode 100644 index 000000000..6e970881b --- /dev/null +++ b/AgCloud/services/compression/src/minio_client.py @@ -0,0 +1,17 @@ +from minio import Minio +import os + +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9001") +ACCESS_KEY = os.getenv("ACCESS_KEY", "minioadmin") +SECRET_KEY = os.getenv("SECRET_KEY", "minioadmin123") +BUCKET_NAME = os.getenv("BUCKET_NAME", "telemetry") + +client = Minio( + MINIO_ENDPOINT, + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + secure=False, +) + +if not client.bucket_exists(BUCKET_NAME): + client.make_bucket(BUCKET_NAME) diff --git a/AgCloud/services/compression/src/prototype_lib.py b/AgCloud/services/compression/src/prototype_lib.py new file mode 100644 index 000000000..d2ef512af --- /dev/null +++ b/AgCloud/services/compression/src/prototype_lib.py @@ -0,0 +1,157 @@ +from pathlib import Path, PurePosixPath +import subprocess +import tempfile +import time +from datetime import datetime +import re +from minio_client import client, BUCKET_NAME + +# Supported audio formats for compression +AUDIO_EXTS = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"} + +RAW_PREFIX = "sound/" + +def is_audio_file(filename: str) -> bool: + """Check if file is an audio file that should be compressed.""" + if filename.lower().endswith((".flac", ".opus")): + return False # If it's already compressed, don't compress again + return any(filename.lower().endswith(ext) for ext in AUDIO_EXTS) + +def iter_audio_files(): + """Yield MinIO object names in RAW_PREFIX that are audio files.""" + for obj in client.list_objects(BUCKET_NAME, prefix=RAW_PREFIX, recursive=True): + if is_audio_file(obj.object_name): + yield obj.object_name + +def parse_timestamp_from_filename(filename: str) -> datetime: + """ + Extract timestamp from filename pattern: sensor-id_timestamp.ext + """ + # Pattern: anything_YYYYMMDDtHHMMSSz.ext + # Case-insensitive for 't' and 'z' + pattern = r'_(\d{8})[tT](\d{6})[zZ]\.' + + match = re.search(pattern, filename) + if not match: + print(f"[WARN] Cannot parse timestamp from filename: {filename}") + return None + + date_part = match.group(1) # YYYYMMDD + time_part = match.group(2) # HHMMSS + + try: + # Parse: 20240901 120000 + dt = datetime.strptime(f"{date_part}{time_part}", "%Y%m%d%H%M%S") + # Assume UTC (because of 'z' suffix) + dt = dt.replace(tzinfo=None) + return dt + except ValueError as e: + print(f"[WARN] Invalid timestamp in filename {filename}: {e}") + return None + +def get_file_age_seconds(obj_name: str) -> float: + """ + Get age of file in seconds based on timestamp in filename. + + Returns: + Age in seconds, or 0 if timestamp cannot be parsed + """ + dt = parse_timestamp_from_filename(obj_name) + if dt is None: + return 0 + + now = datetime.utcnow() + age = now - dt + return age.total_seconds() + +def is_older_than(obj_name: str, age_seconds: int) -> bool: + """Check if file is older than specified age based on filename timestamp.""" + return get_file_age_seconds(obj_name) >= age_seconds + +def build_ffmpeg_cmds(in_local_path: Path, codec="all", flac_level="5", opus_bitrate="96k"): + """ + Return ffmpeg commands to encode a local audio file. + Output will be a temporary file (to upload after encode). + """ + cmds = [] + temp_dir = Path(tempfile.gettempdir()) + + if codec in ("flac", "all"): + flac_out = temp_dir / f"{in_local_path.stem}.flac" + flac_cmd = [ + "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", + "-i", str(in_local_path), + "-c:a", "flac", "-compression_level", flac_level, + str(flac_out) + ] + cmds.append(("flac", flac_cmd, flac_out)) + + if codec in ("opus", "all"): + opus_out = temp_dir / f"{in_local_path.stem}.opus" + opus_cmd = [ + "ffmpeg", "-y", "-hide_banner", "-loglevel", "error", + "-i", str(in_local_path), + "-c:a", "libopus", "-b:a", opus_bitrate, + str(opus_out) + ] + cmds.append(("opus", opus_cmd, opus_out)) + + return cmds + +def download_raw_to_temp(obj_name: str) -> Path: + """Download MinIO raw object to temporary file.""" + local_path = Path(tempfile.gettempdir()) / Path(obj_name).name + client.fget_object(BUCKET_NAME, obj_name, str(local_path)) + return local_path + +def replace_with_compressed(original_obj_name: str, compressed_local_path: Path): + """ + Replace the original file in MinIO with the compressed version. + Keeps the same path, only changes the extension. + + Example: + sound/drone-01_20251102t010618z.wav -> sound/drone-01_20251102t010618z.opus + """ + # Use PurePosixPath to always get forward slashes for MinIO paths + from pathlib import PurePosixPath + + # Parse original path (use PurePosixPath for consistency) + obj_path = PurePosixPath(original_obj_name) + stem = obj_path.stem # e.g., "drone-01_20251102t010618z" + parent = obj_path.parent # e.g., "sound" + + # New compressed extension + compressed_ext = compressed_local_path.suffix # e.g., ".opus" + new_obj_name = str(parent / f"{stem}{compressed_ext}") + + # Upload compressed file to the new path + client.fput_object(BUCKET_NAME, new_obj_name, str(compressed_local_path)) + + # Delete original file + client.remove_object(BUCKET_NAME, original_obj_name) + + return new_obj_name + +def delete_object(obj_name: str): + """Delete an object from MinIO.""" + client.remove_object(BUCKET_NAME, obj_name) + +def get_compressed_variants(obj_name: str) -> list: + """ + Given an object name, return possible compressed variants. + + Example: + sound/drone-01_20251102t010618z.wav -> + [ + "sound/drone-01_20251102t010618z.opus", + "sound/drone-01_20251102t010618z.flac" + ] + """ + obj_path = PurePosixPath(obj_name) + stem = obj_path.stem + parent = obj_path.parent + + return [ + str(parent / f"{stem}.opus"), + str(parent / f"{stem}.flac") + ] \ No newline at end of file diff --git a/AgCloud/services/compression/src/run_bench.py b/AgCloud/services/compression/src/run_bench.py new file mode 100644 index 000000000..19d39568d --- /dev/null +++ b/AgCloud/services/compression/src/run_bench.py @@ -0,0 +1,134 @@ +from pathlib import Path +import time +import csv +from statistics import mean +import subprocess +from prototype_lib import ( + iter_audio_files, + build_ffmpeg_cmds, + download_raw_to_temp, + replace_with_compressed, + parse_timestamp_from_filename, + get_file_age_seconds +) +from minio_client import BUCKET_NAME, client + +RES_DIR = Path("results") +RES_DIR.mkdir(exist_ok=True) + +def file_size_minio(obj_name: str) -> int: + """Return object size in bytes.""" + try: + stat = client.stat_object(BUCKET_NAME, obj_name) + return stat.size + except: + return 0 + +def run_and_profile(cmd): + """Run command and profile CPU usage.""" + import psutil + start = time.time() + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + parent = psutil.Process(proc.pid) + samples = [] + while proc.poll() is None: + cpu_total = 0.0 + for pr in [parent] + parent.children(recursive=True): + try: + cpu_total += pr.cpu_percent(interval=0.1) + except psutil.NoSuchProcess: + continue + samples.append(cpu_total) + out, err = proc.communicate() + wall = time.time() - start + avg_cpu = mean(samples) if samples else 0.0 + return proc.returncode, wall, avg_cpu, (out or b"") + (err or b"") + +def main(): + rows = [] + files = list(iter_audio_files()) + if not files: + print("No audio files found in MinIO") + return + + print("=" * 70) + print("AUDIO COMPRESSION BENCHMARK") + print("=" * 70) + print(f"Bucket: {BUCKET_NAME}") + print(f"Files to process: {len(files)}") + print("=" * 70) + + for obj_name in files: + # Parse timestamp from filename + dt = parse_timestamp_from_filename(obj_name) + age_seconds = get_file_age_seconds(obj_name) + + print(f"\n[PROCESSING] {obj_name}") + if dt: + print(f" Timestamp: {dt.strftime('%Y-%m-%d %H:%M:%S')} UTC") + print(f" Age: {age_seconds/86400:.1f} days") + + # Download original file + local_file = download_raw_to_temp(obj_name) + orig_size = local_file.stat().st_size + + # Test each codec + for codec, cmd, outp in build_ffmpeg_cmds(local_file): + rc, wall, cpu, _ = run_and_profile(cmd) + if rc != 0: + print(f" [FAIL] {codec.upper()} encoding failed") + continue + + # Replace original with compressed version (same path, different extension) + try: + new_obj_name = replace_with_compressed(obj_name, outp) + enc_size = file_size_minio(new_obj_name) + ratio = (orig_size / enc_size) if enc_size else 0.0 + + print(f" [OK] {codec.upper()}: {new_obj_name}") + print(f" Size: {enc_size:,} bytes ({enc_size/(1024**2):.2f} MB)") + print(f" Ratio: {ratio:.2f}x") + print(f" Time: {wall:.2f}s, CPU: {cpu:.1f}%") + + rows.append({ + "file": Path(obj_name).name, + "codec": codec, + "orig_bytes": orig_size, + "encoded_bytes": enc_size, + "compression_ratio_orig_over_encoded": round(ratio, 3), + "encode_time_sec": round(wall, 3), + "encode_cpu_avg_percent": round(cpu, 1), + "timestamp": dt.isoformat() if dt else "unknown", + "age_days": round(age_seconds / 86400, 1) if age_seconds > 0 else 0, + }) + + # Clean up local encoded file + outp.unlink() + + except Exception as e: + print(f" [FAIL] {codec.upper()}: {e}") + outp.unlink(missing_ok=True) + + # Clean up local original file + local_file.unlink() + + # Save results + if rows: + out_csv = RES_DIR / "benchmarks.csv" + with open(out_csv, "w", newline="", encoding="utf-8") as fh: + writer = csv.DictWriter(fh, fieldnames=list(rows[0].keys())) + writer.writeheader() + writer.writerows(rows) + + print("\n" + "=" * 70) + print("SUMMARY") + print("=" * 70) + print(f"Total files benchmarked: {len(files)}") + print(f"Total tests: {len(rows)}") + print(f"Results saved: {out_csv}") + print("=" * 70) + else: + print("\n[WARN] No successful encodings to save") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/AgCloud/services/compression/src/run_tiering.sh b/AgCloud/services/compression/src/run_tiering.sh new file mode 100644 index 000000000..2ea1fc7ab --- /dev/null +++ b/AgCloud/services/compression/src/run_tiering.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +SCRIPT_DIR="/app/src" +LOG_DIR="$SCRIPT_DIR/logs" +LOG_FILE="$LOG_DIR/tiering_$(date +%Y%m%d_%H%M%S).log" + +mkdir -p "$LOG_DIR" +cd "$SCRIPT_DIR" + +# In Docker, python3 is in PATH, no venv needed +PYTHON="python3" + +# ffmpeg is already in PATH from Dockerfile +export PATH="/usr/bin:/usr/local/bin:$PATH" + +echo "=== Starting at $(date) ===" >> "$LOG_FILE" +"$PYTHON" tiering_job.py \ + --raw-max-age-days ${RAW_MAX_AGE_DAYS:-30} \ + --codec ${COMPRESSION_CODEC:-opus} \ + --compressed-max-age-days ${COMPRESSED_MAX_AGE_DAYS:-90} \ + >> "$LOG_FILE" 2>&1 +echo "=== Finished at $(date) ===" >> "$LOG_FILE" + +# Clean old logs (older than 7 days) +find "$LOG_DIR" -name "tiering_*.log" -mtime +7 -delete diff --git a/AgCloud/services/compression/src/tiering_job.py b/AgCloud/services/compression/src/tiering_job.py new file mode 100644 index 000000000..ef42665bf --- /dev/null +++ b/AgCloud/services/compression/src/tiering_job.py @@ -0,0 +1,239 @@ +from pathlib import Path +import time +import argparse +import subprocess +from prototype_lib import ( + iter_audio_files, + build_ffmpeg_cmds, + download_raw_to_temp, + replace_with_compressed, + delete_object, + get_file_age_seconds, + is_older_than, + RAW_PREFIX # Import the prefix to ensure consistency +) +from minio_client import client, BUCKET_NAME + +DEFAULT_RAW_MAX_AGE_DAYS = 30 +DEFAULT_COMP_MAX_AGE_DAYS = 90 +DEFAULT_LONG_TERM_CODEC = "opus" + +def encode_and_replace(obj_name: str, codec: str) -> str: + """ + Download audio file, encode it, and replace the original in MinIO. + + Returns: + The new object name (with compressed extension) + """ + # Skip already compressed files + if obj_name.lower().endswith((".flac", ".opus")): + print(f"[SKIP] {obj_name} - Already compressed, skipping.") + return obj_name + + # Download original + local_file = download_raw_to_temp(obj_name) + + # Build encode commands + encode_cmds = build_ffmpeg_cmds(local_file, codec=codec) + + if not encode_cmds: + local_file.unlink() + raise RuntimeError(f"No encode commands generated for {obj_name}") + + # Take the first codec result + codec_name, cmd, output_path = encode_cmds[0] + + # Run encoding + print(f"[ENC] Encoding {obj_name} -> {codec_name.upper()}...") + rc = subprocess.call(cmd) + + if rc != 0: + local_file.unlink() + output_path.unlink(missing_ok=True) + raise RuntimeError(f"Encode failed: {obj_name} -> {codec}") + + # Replace in MinIO (same path, different extension) + new_obj_name = replace_with_compressed(obj_name, output_path) + + # Cleanup local files + output_path.unlink() + local_file.unlink() + + return new_obj_name + +def cleanup_compressed(max_age_days: int, dry_run: bool) -> int: + """ + Delete very old compressed files that exceeded retention period. + Uses timestamp from filename (same logic as compression). + """ + if max_age_days <= 0: + print("[INFO] Compressed cleanup disabled (max_age_days <= 0)") + return 0 + + cutoff_sec = max_age_days * 86400 + deleted = 0 + + print(f"\n[CLEANUP] Checking for compressed files older than {max_age_days} days...") + print(f"[CLEANUP] Looking in prefix: {RAW_PREFIX}") + print(f"[CLEANUP] Using timestamp from filename") + + # Only check files in the RAW_PREFIX (sound/) directory + for obj in client.list_objects(BUCKET_NAME, prefix=RAW_PREFIX, recursive=True): + # Only consider compressed files + if not obj.object_name.lower().endswith(('.opus', '.flac')): + continue + + # Use timestamp from filename (consistent with compression logic) + file_age_sec = get_file_age_seconds(obj.object_name) + + if file_age_sec == 0: + print(f"[SKIP] {obj.object_name} - Cannot parse timestamp from filename") + continue + + file_age_days = file_age_sec / 86400 + + print(f"[CHECK] {obj.object_name}: {file_age_days:.1f} days old (from filename)") + + if file_age_sec >= cutoff_sec: + if dry_run: + print(f"[DRY] Would delete: {obj.object_name} (age={file_age_days:.1f} days)") + deleted += 1 + else: + try: + delete_object(obj.object_name) + deleted += 1 + print(f"[DEL] ✓ Deleted: {obj.object_name} (age={file_age_days:.1f} days)") + except Exception as e: + print(f"[ERROR] Failed to delete {obj.object_name}: {e}") + else: + remaining_days = max_age_days - file_age_days + print(f"[KEEP] {obj.object_name} - will be deleted in {remaining_days:.1f} days") + + return deleted + +def main(): + ap = argparse.ArgumentParser( + description="Two-tier audio compression job - compresses files based on filename timestamp" + ) + ap.add_argument( + "--raw-max-age-days", + type=int, + default=DEFAULT_RAW_MAX_AGE_DAYS, + help=f"Age threshold in days for audio files to be compressed (default: {DEFAULT_RAW_MAX_AGE_DAYS})" + ) + ap.add_argument( + "--codec", + choices=["opus", "flac"], + default=DEFAULT_LONG_TERM_CODEC, + help="Compression codec to use" + ) + ap.add_argument( + "--compressed-max-age-days", + type=int, + default=DEFAULT_COMP_MAX_AGE_DAYS, + help="Delete compressed files older than this many days (0 to disable)" + ) + ap.add_argument( + "--dry-run", + action="store_true", + help="Simulate operations without making changes" + ) + args = ap.parse_args() + + # Calculate age threshold + raw_age_seconds = args.raw_max_age_days * 86400 + age_desc = f"{args.raw_max_age_days} days" + + print("=" * 70) + print("AUDIO COMPRESSION & TIERING JOB") + print("=" * 70) + print(f"Bucket: {BUCKET_NAME}") + print(f"Age threshold: {age_desc} (based on filename timestamp)") + print(f"Codec: {args.codec.upper()}") + print(f"Compressed retention: {args.compressed_max_age_days} days") + print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE'}") + print("=" * 70) + + processed = 0 + skipped = 0 + errors = 0 + total_orig_size = 0 + total_comp_size = 0 + + # Process audio files only + for obj_name in iter_audio_files(): + + # Check age based on filename timestamp + age = get_file_age_seconds(obj_name) + + if age == 0: + print(f"[SKIP] {obj_name} - Cannot parse timestamp from filename") + skipped += 1 + continue + + if not is_older_than(obj_name, raw_age_seconds): + skipped += 1 + continue + + age_days = age / 86400 + + if args.dry_run: + print(f"[DRY] Would compress: {obj_name} (age={age_days:.1f} days) -> {args.codec.upper()}") + processed += 1 + continue + + # Get original size + try: + orig_stat = client.stat_object(BUCKET_NAME, obj_name) + orig_size = orig_stat.size + total_orig_size += orig_size + except: + orig_size = 0 + + try: + new_obj_name = encode_and_replace(obj_name, args.codec) + + # Get compressed size + try: + comp_stat = client.stat_object(BUCKET_NAME, new_obj_name) + comp_size = comp_stat.size + total_comp_size += comp_size + saved = orig_size - comp_size + ratio = orig_size / comp_size if comp_size > 0 else 0 + + print(f"[OK] Compressed: {obj_name} -> {new_obj_name}") + print(f" Age: {age_days:.1f} days") + print(f" Original: {orig_size:,} bytes ({orig_size/(1024**2):.2f} MB)") + print(f" Compressed: {comp_size:,} bytes ({comp_size/(1024**2):.2f} MB)") + print(f" Ratio: {ratio:.2f}x, Saved: {saved:,} bytes ({saved/(1024**2):.2f} MB)") + except: + print(f"[OK] Compressed: {obj_name} -> {new_obj_name}") + + processed += 1 + + except Exception as e: + errors += 1 + print(f"[FAIL] {obj_name}: {e}") + + # Cleanup very old compressed files + comp_deleted = cleanup_compressed(args.compressed_max_age_days, args.dry_run) + + print("\n" + "=" * 70) + print("SUMMARY") + print("=" * 70) + print(f"Audio files compressed: {processed}") + print(f"Files skipped (too new): {skipped}") + print(f"Errors: {errors}") + print(f"Old compressed deleted: {comp_deleted}") + if total_orig_size > 0 and total_comp_size > 0: + total_saved = total_orig_size - total_comp_size + total_ratio = total_orig_size / total_comp_size + print(f"Total original size: {total_orig_size:,} bytes ({total_orig_size/(1024**2):.2f} MB)") + print(f"Total compressed size: {total_comp_size:,} bytes ({total_comp_size/(1024**2):.2f} MB)") + print(f"Total saved: {total_saved:,} bytes ({total_saved/(1024**2):.2f} MB)") + print(f"Overall compression ratio: {total_ratio:.2f}x") + print("=" * 70) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/AgCloud/services/compression/tests/__init__.py b/AgCloud/services/compression/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/compression/tests/test_prototype_lib.py b/AgCloud/services/compression/tests/test_prototype_lib.py new file mode 100644 index 000000000..5ecaa127d --- /dev/null +++ b/AgCloud/services/compression/tests/test_prototype_lib.py @@ -0,0 +1,435 @@ +""" +Tests for prototype_lib.py - Audio compression library +Updated to match the new implementation with same-path compression +""" + +from pathlib import Path +import pytest +import tempfile +from datetime import datetime, timedelta +from unittest.mock import patch, MagicMock, Mock +from prototype_lib import ( + is_audio_file, + iter_audio_files, + parse_timestamp_from_filename, + get_file_age_seconds, + is_older_than, + build_ffmpeg_cmds, + get_compressed_variants, + find_file_with_fallback, + AUDIO_EXTS, + RAW_PREFIX +) + + +class TestIsAudioFile: + """Tests for is_audio_file function""" + + def test_valid_audio_extensions(self): + """Test that valid audio files are recognized""" + valid_files = [ + "audio.wav", "music.mp3", "sound.flac", "voice.opus", + "song.ogg", "track.m4a", "audio.aac", "sound.wma" + ] + + for filename in valid_files: + assert is_audio_file(filename), f"{filename} should be recognized as audio" + + def test_case_insensitive(self): + """Test case insensitivity""" + assert is_audio_file("AUDIO.WAV") + assert is_audio_file("Music.MP3") + assert is_audio_file("SoUnD.fLaC") + + def test_invalid_extensions(self): + """Test that non-audio files are rejected""" + invalid_files = [ + "doc.txt", "image.jpg", "video.mp4", "data.csv", "script.py" + ] + + for filename in invalid_files: + assert not is_audio_file(filename), f"{filename} should not be recognized as audio" + + def test_audio_exts_constant(self): + """Test that AUDIO_EXTS contains expected formats""" + expected = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"} + assert AUDIO_EXTS == expected + + +class TestIterAudioFiles: + """Tests for iter_audio_files function""" + + @patch('prototype_lib.client') + def test_iter_audio_files_filters_correctly(self, mock_client): + """Test that only audio files are returned""" + # Mock MinIO objects + mock_objects = [ + Mock(object_name=f"{RAW_PREFIX}drone-01_20251102t010618z.wav"), + Mock(object_name=f"{RAW_PREFIX}sensor-02_20251102t020000z.mp3"), + Mock(object_name=f"{RAW_PREFIX}data.txt"), # Should be filtered out + Mock(object_name=f"{RAW_PREFIX}image.jpg"), # Should be filtered out + ] + mock_client.list_objects.return_value = mock_objects + + files = list(iter_audio_files()) + + assert len(files) == 2 + assert all(is_audio_file(f) for f in files) + mock_client.list_objects.assert_called_once() + + @patch('prototype_lib.client') + def test_iter_audio_files_empty_bucket(self, mock_client): + """Test with empty bucket""" + mock_client.list_objects.return_value = [] + + files = list(iter_audio_files()) + + assert len(files) == 0 + + +class TestParseTimestampFromFilename: + """Tests for parse_timestamp_from_filename function""" + + def test_valid_timestamp_lowercase(self): + """Test parsing valid timestamp (lowercase t and z)""" + filename = "drone-01_20251102t010618z.wav" + dt = parse_timestamp_from_filename(filename) + + assert dt is not None + assert dt.year == 2025 + assert dt.month == 11 + assert dt.day == 2 + assert dt.hour == 1 + assert dt.minute == 6 + assert dt.second == 18 + + def test_valid_timestamp_uppercase(self): + """Test parsing valid timestamp (uppercase T and Z)""" + filename = "sensor-06_20251028T080000Z.opus" + dt = parse_timestamp_from_filename(filename) + + assert dt is not None + assert dt.year == 2025 + assert dt.month == 10 + assert dt.day == 28 + assert dt.hour == 8 + assert dt.minute == 0 + assert dt.second == 0 + + def test_valid_timestamp_mixed_case(self): + """Test parsing with mixed case""" + filename = "device_20240101T120000z.mp3" + dt = parse_timestamp_from_filename(filename) + + assert dt is not None + assert dt.year == 2024 + + def test_invalid_timestamp_format(self): + """Test with invalid timestamp format""" + invalid_filenames = [ + "audio_without_timestamp.wav", + "audio_20251102.wav", # Missing time + "audio_2025-11-02t01:06:18z.wav", # Wrong format (hyphens and colons) + ] + + for filename in invalid_filenames: + dt = parse_timestamp_from_filename(filename) + assert dt is None, f"Should return None for {filename}" + + def test_invalid_date_values(self): + """Test with invalid date values""" + # Invalid month (13) + filename = "audio_20251302t010618z.wav" + dt = parse_timestamp_from_filename(filename) + assert dt is None + + def test_full_path(self): + """Test parsing from full MinIO path""" + full_path = "sound/drone-01_20251102t010618z.wav" + dt = parse_timestamp_from_filename(full_path) + + assert dt is not None + assert dt.year == 2025 + + +class TestGetFileAgeSeconds: + """Tests for get_file_age_seconds function""" + + @patch('prototype_lib.datetime') + def test_get_file_age_recent_file(self, mock_datetime_module): + """Test age calculation for recent file""" + # Mock current time + now = datetime(2025, 11, 2, 12, 0, 0) + mock_datetime_module.utcnow.return_value = now + mock_datetime_module.strptime = datetime.strptime + + # File timestamp: 1 hour ago + filename = "audio_20251102t110000z.wav" + + age = get_file_age_seconds(filename) + + assert age == 3600 # 1 hour in seconds + + @patch('prototype_lib.datetime') + def test_get_file_age_old_file(self, mock_datetime_module): + """Test age calculation for old file""" + now = datetime(2025, 11, 2, 12, 0, 0) + mock_datetime_module.utcnow.return_value = now + mock_datetime_module.strptime = datetime.strptime + + # File timestamp: 30 days ago + filename = "audio_20251003t120000z.wav" + + age = get_file_age_seconds(filename) + + expected_age = 30 * 86400 # 30 days in seconds + assert abs(age - expected_age) < 60 # Allow 1 minute tolerance + + def test_get_file_age_no_timestamp(self): + """Test with file that has no parseable timestamp""" + filename = "audio_no_timestamp.wav" + + age = get_file_age_seconds(filename) + + assert age == 0 + + +class TestIsOlderThan: + """Tests for is_older_than function""" + + @patch('prototype_lib.get_file_age_seconds') + def test_is_older_than_true(self, mock_get_age): + """Test when file is older than threshold""" + mock_get_age.return_value = 7200 # 2 hours + + result = is_older_than("audio.wav", 3600) # 1 hour threshold + + assert result is True + + @patch('prototype_lib.get_file_age_seconds') + def test_is_older_than_false(self, mock_get_age): + """Test when file is younger than threshold""" + mock_get_age.return_value = 1800 # 30 minutes + + result = is_older_than("audio.wav", 3600) # 1 hour threshold + + assert result is False + + @patch('prototype_lib.get_file_age_seconds') + def test_is_older_than_equal(self, mock_get_age): + """Test when file age equals threshold""" + mock_get_age.return_value = 3600 + + result = is_older_than("audio.wav", 3600) + + assert result is True # >= should return True + + +class TestBuildFfmpegCmds: + """Tests for build_ffmpeg_cmds function""" + + def test_build_commands_all_codecs(self): + """Test building commands for all codecs""" + input_path = Path("test_audio.wav") + + cmds = build_ffmpeg_cmds(input_path, codec="all") + + assert len(cmds) == 2 + codec_names = [cmd[0] for cmd in cmds] + assert "flac" in codec_names + assert "opus" in codec_names + + def test_build_commands_flac_only(self): + """Test building commands for FLAC only""" + input_path = Path("test_audio.wav") + + cmds = build_ffmpeg_cmds(input_path, codec="flac") + + assert len(cmds) == 1 + assert cmds[0][0] == "flac" + assert "-c:a" in cmds[0][1] + assert "flac" in cmds[0][1] + + def test_build_commands_opus_only(self): + """Test building commands for Opus only""" + input_path = Path("test_audio.wav") + + cmds = build_ffmpeg_cmds(input_path, codec="opus") + + assert len(cmds) == 1 + assert cmds[0][0] == "opus" + assert "-c:a" in cmds[0][1] + assert "libopus" in cmds[0][1] + + def test_build_commands_custom_parameters(self): + """Test custom compression parameters""" + input_path = Path("test.wav") + + cmds = build_ffmpeg_cmds(input_path, codec="all", + flac_level="8", opus_bitrate="128k") + + flac_cmd = [c for c in cmds if c[0] == "flac"][0] + opus_cmd = [c for c in cmds if c[0] == "opus"][0] + + assert "8" in flac_cmd[1] + assert "128k" in opus_cmd[1] + + def test_build_commands_output_extensions(self): + """Test that output files have correct extensions""" + input_path = Path("audio.wav") + + cmds = build_ffmpeg_cmds(input_path, codec="all") + + for codec, _, output_path in cmds: + if codec == "flac": + assert output_path.suffix == ".flac" + elif codec == "opus": + assert output_path.suffix == ".opus" + + def test_build_commands_preserves_stem(self): + """Test that file stem is preserved""" + input_path = Path("my_audio_file.wav") + + cmds = build_ffmpeg_cmds(input_path) + + for _, _, output_path in cmds: + assert output_path.stem == "my_audio_file" + + +class TestGetCompressedVariants: + """Tests for get_compressed_variants function""" + + def test_get_variants_basic(self): + """Test getting compressed variants""" + obj_name = "sound/drone-01_20251102t010618z.wav" + + variants = get_compressed_variants(obj_name) + + assert len(variants) == 2 + assert "sound/drone-01_20251102t010618z.opus" in variants + assert "sound/drone-01_20251102t010618z.flac" in variants + + def test_get_variants_already_compressed(self): + """Test variants for already compressed file""" + obj_name = "sound/audio.opus" + + variants = get_compressed_variants(obj_name) + + assert "sound/audio.opus" in variants + assert "sound/audio.flac" in variants + + def test_get_variants_nested_path(self): + """Test with nested directory structure""" + obj_name = "sound/folder1/folder2/audio.mp3" + + variants = get_compressed_variants(obj_name) + + assert all("folder1/folder2" in v for v in variants) + + +class TestFindFileWithFallback: + """Tests for find_file_with_fallback function""" + + @patch('prototype_lib.client') + def test_find_original_exists(self, mock_client): + """Test when original file exists""" + obj_name = "sound/audio.wav" + mock_client.stat_object.return_value = Mock() + + found, exists = find_file_with_fallback(obj_name) + + assert exists is True + assert found == obj_name + mock_client.stat_object.assert_called_once() + + @patch('prototype_lib.client') + def test_find_compressed_variant(self, mock_client): + """Test fallback to compressed variant""" + obj_name = "sound/audio.wav" + + # First call (original) fails, second call (opus variant) succeeds + mock_client.stat_object.side_effect = [ + Exception("Not found"), # Original doesn't exist + Mock(), # Opus variant exists + ] + + found, exists = find_file_with_fallback(obj_name) + + assert exists is True + assert found == "sound/audio.opus" + assert mock_client.stat_object.call_count == 2 + + @patch('prototype_lib.client') + def test_find_no_variants_exist(self, mock_client): + """Test when no variants exist""" + obj_name = "sound/audio.wav" + mock_client.stat_object.side_effect = Exception("Not found") + + found, exists = find_file_with_fallback(obj_name) + + assert exists is False + assert found == obj_name + assert mock_client.stat_object.call_count == 3 # Original + 2 variants + + +class TestEdgeCases: + """Test edge cases and error handling""" + + def test_parse_timestamp_edge_dates(self): + """Test with edge case dates""" + # New Year + dt = parse_timestamp_from_filename("audio_20250101t000000z.wav") + assert dt.month == 1 and dt.day == 1 + + # End of year + dt = parse_timestamp_from_filename("audio_20251231t235959z.wav") + assert dt.month == 12 and dt.day == 31 + + def test_build_ffmpeg_special_characters(self): + """Test with special characters in filename""" + special_names = [ + Path("file with spaces.wav"), + Path("file-with-dashes.mp3"), + Path("file_with_underscores.flac"), + ] + + for input_path in special_names: + cmds = build_ffmpeg_cmds(input_path) + assert len(cmds) > 0 + + def test_audio_extensions_lowercase(self): + """Ensure all extensions in AUDIO_EXTS are lowercase""" + for ext in AUDIO_EXTS: + assert ext.islower(), f"Extension {ext} should be lowercase" + assert ext.startswith("."), f"Extension {ext} should start with dot" + + +class TestIntegration: + """Integration tests""" + + @patch('prototype_lib.datetime') + @patch('prototype_lib.client') + def test_full_workflow_simulation(self, mock_client, mock_datetime_module): + """Simulate full workflow: list -> filter -> check age -> find""" + # Setup + now = datetime(2025, 11, 2, 12, 0, 0) + mock_datetime_module.utcnow.return_value = now + mock_datetime_module.strptime = datetime.strptime + + # Mock MinIO files (one old, one new) + mock_objects = [ + Mock(object_name="sound/old_20251001t120000z.wav"), # 32 days old + Mock(object_name="sound/new_20251102t100000z.wav"), # 2 hours old + ] + mock_client.list_objects.return_value = mock_objects + + # Get all audio files + files = list(iter_audio_files()) + assert len(files) == 2 + + # Check which are old (30+ days) + threshold = 30 * 86400 + old_files = [f for f in files if is_older_than(f, threshold)] + + assert len(old_files) == 1 + assert "old_" in old_files[0] diff --git a/AgCloud/services/compression/tests/test_run_bench.py b/AgCloud/services/compression/tests/test_run_bench.py new file mode 100644 index 000000000..3697749ac --- /dev/null +++ b/AgCloud/services/compression/tests/test_run_bench.py @@ -0,0 +1,199 @@ +from pathlib import Path +from run_bench import run_and_profile, file_size_minio, main +from unittest.mock import patch, Mock, MagicMock, call +import subprocess +import csv +import pytest + +# Test the `run_and_profile` function +@patch('psutil.Process') +@patch('psutil.NoSuchProcess', new=Exception) +@patch('run_bench.subprocess.Popen') +def test_run_and_profile(mock_popen, mock_process_class): + """Test run_and_profile function with mocked subprocess and psutil""" + # Setup mocks + mock_proc = Mock() + mock_proc.pid = 12345 + mock_proc.poll.side_effect = [None, None, 0] # Simulate process running then finishing + mock_proc.communicate.return_value = (b"ffmpeg version", b"") + mock_proc.returncode = 0 + mock_popen.return_value = mock_proc + + # Mock psutil Process + mock_parent = Mock() + mock_parent.cpu_percent.return_value = 50.0 + mock_parent.children.return_value = [] + mock_process_class.return_value = mock_parent + + # Set up a test command for profiling + cmd = ["ffmpeg", "-version"] + + # Run the command and get results + rc, wall_time, cpu, output = run_and_profile(cmd) + + # Test if the command ran successfully + assert rc == 0, f"Command failed with return code {rc}" + assert wall_time > 0, "Wall time should be greater than zero" + assert cpu >= 0, "CPU usage should be non-negative" + assert b"ffmpeg" in output, "Expected output not found" + + +# Test the `file_size_minio` function +@patch('run_bench.client') +def test_file_size(mock_client): + """Test file_size_minio function with mocked MinIO client""" + test_obj_name = "sound/drone-01_20251102t010618z.wav" + + # Mock stat_object to return a size + mock_stat = Mock() + mock_stat.size = 1024000 # 1MB + mock_client.stat_object.return_value = mock_stat + + # Get the file size + size = file_size_minio(test_obj_name) + + # Test if the file size is correct + assert size == 1024000, f"Expected file size 1024000, but got {size}" + mock_client.stat_object.assert_called_once() + + +# Test the `main` function to check if the CSV is created correctly +@patch('run_bench.client') +@patch('run_bench.iter_audio_files') +@patch('run_bench.download_raw_to_temp') +@patch('run_bench.replace_with_compressed') +@patch('run_bench.run_and_profile') +@patch('run_bench.parse_timestamp_from_filename') +@patch('run_bench.get_file_age_seconds') +@patch('run_bench.build_ffmpeg_cmds') +def test_main(mock_build, mock_get_age, mock_parse_ts, mock_profile, + mock_replace, mock_download, mock_iter, mock_client): + """Test main function with all dependencies mocked""" + from datetime import datetime + + # Setup mocks + test_obj = "sound/test_20251102t120000z.wav" + mock_iter.return_value = [test_obj] + + mock_dt = datetime(2025, 11, 2, 12, 0, 0) + mock_parse_ts.return_value = mock_dt + mock_get_age.return_value = 86400 # 1 day + + # Mock local file + mock_local = Mock() + mock_local.stat.return_value.st_size = 2000000 # 2MB original + mock_local.unlink = Mock() + mock_download.return_value = mock_local + + # Mock encoding + mock_profile.return_value = (0, 5.0, 75.0, b"") # success, 5s, 75% CPU + + # Mock MinIO client for file_size_minio + mock_stat = Mock() + mock_stat.size = 500000 # 500KB compressed + mock_client.stat_object.return_value = mock_stat + + # Mock replace operation + mock_replace.return_value = "sound/test_20251102t120000z.opus" + + # Mock output file + mock_output = Mock() + mock_output.unlink = Mock() + + # Mock build_ffmpeg_cmds + mock_build.return_value = [ + ("opus", ["ffmpeg", "-i", "test.wav", "test.opus"], mock_output) + ] + + # Run main + result_path = Path("results/benchmarks.csv") + if result_path.exists(): + result_path.unlink() + + main() + + # Check if the results file was created + assert result_path.exists(), "The result CSV file was not created" + + # Check if it contains rows + with open(result_path, "r") as file: + reader = csv.DictReader(file) + rows = list(reader) + assert len(rows) > 0, "No rows in the results CSV" + + # Verify row contents + row = rows[0] + assert row["codec"] == "opus" + assert float(row["compression_ratio_orig_over_encoded"]) == 4.0 # 2MB / 500KB + + +# Additional tests for edge cases +@patch('run_bench.client') +def test_file_size_invalid_file(mock_client): + """Test the file size function with a non-existent file""" + invalid_obj = "sound/nonexistent_file.wav" + + # Mock stat_object to raise exception + mock_client.stat_object.side_effect = Exception("Object not found") + + size = file_size_minio(invalid_obj) + assert size == 0, f"Expected file size to be 0, but got {size}" + + +@patch('run_bench.client') +@patch('run_bench.iter_audio_files') +def test_main_no_files(mock_iter, mock_client, capsys): + """Test main function when no files are found""" + mock_iter.return_value = [] + + main() + + captured = capsys.readouterr() + assert "No audio files found" in captured.out + + +@patch('run_bench.client') +@patch('run_bench.iter_audio_files') +@patch('run_bench.download_raw_to_temp') +@patch('run_bench.run_and_profile') +@patch('run_bench.parse_timestamp_from_filename') +@patch('run_bench.get_file_age_seconds') +@patch('run_bench.build_ffmpeg_cmds') +def test_main_encoding_failure(mock_build, mock_get_age, mock_parse_ts, + mock_profile, mock_download, mock_iter, mock_client): + """Test main function when encoding fails""" + from datetime import datetime + + test_obj = "sound/test.wav" + mock_iter.return_value = [test_obj] + + mock_parse_ts.return_value = datetime(2025, 11, 2, 12, 0, 0) + mock_get_age.return_value = 0 + + mock_local = Mock() + mock_local.stat.return_value.st_size = 1000000 + mock_local.unlink = Mock() + mock_download.return_value = mock_local + + # Mock failed encoding (return code != 0) + mock_profile.return_value = (1, 0.0, 0.0, b"error") + + mock_output = Mock() + mock_output.unlink = Mock() + + mock_build.return_value = [ + ("opus", ["ffmpeg", "-i", "test.wav", "test.opus"], mock_output) + ] + + result_path = Path("results/benchmarks.csv") + if result_path.exists(): + result_path.unlink() + + main() + + # CSV should not be created or should be empty + if result_path.exists(): + with open(result_path, "r") as file: + reader = csv.DictReader(file) + rows = list(reader) + assert len(rows) == 0, "Should have no successful encodings" diff --git a/AgCloud/services/compression/tests/test_tiering_job.py b/AgCloud/services/compression/tests/test_tiering_job.py new file mode 100644 index 000000000..6bdb133be --- /dev/null +++ b/AgCloud/services/compression/tests/test_tiering_job.py @@ -0,0 +1,495 @@ +""" +Tests for tiering_job.py - Audio compression and tiering +Updated to match the new implementation with same-path compression +""" + +import pytest +from pathlib import Path +from unittest.mock import patch, Mock, MagicMock, call +from tiering_job import ( + encode_and_replace, + cleanup_compressed, + main, + DEFAULT_RAW_MAX_AGE_DAYS, + DEFAULT_COMP_MAX_AGE_DAYS, + DEFAULT_LONG_TERM_CODEC +) + + +class TestEncodeAndReplace: + """Tests for encode_and_replace function""" + + @patch('tiering_job.download_raw_to_temp') + @patch('tiering_job.build_ffmpeg_cmds') + @patch('tiering_job.subprocess.call') + @patch('tiering_job.replace_with_compressed') + def test_encode_and_replace_success( + self, mock_replace, mock_subprocess, mock_build, mock_download + ): + """Test successful encoding and replacement""" + # Setup mocks + mock_local = Mock() + mock_local.unlink = Mock() + mock_download.return_value = mock_local + + mock_output = Mock() + mock_output.unlink = Mock() + mock_build.return_value = [ + ("opus", ["ffmpeg", "-i", "input", "output"], mock_output) + ] + + mock_subprocess.return_value = 0 # Success + mock_replace.return_value = "sound/audio.opus" + + # Execute + result = encode_and_replace("sound/audio.wav", "opus") + + # Verify + assert result == "sound/audio.opus" + mock_download.assert_called_once_with("sound/audio.wav") + mock_build.assert_called_once() + mock_subprocess.assert_called_once() + mock_replace.assert_called_once() + mock_local.unlink.assert_called_once() + mock_output.unlink.assert_called_once() + + @patch('tiering_job.download_raw_to_temp') + @patch('tiering_job.build_ffmpeg_cmds') + @patch('tiering_job.subprocess.call') + def test_encode_and_replace_encode_failure( + self, mock_subprocess, mock_build, mock_download + ): + """Test handling of encoding failure""" + mock_local = Mock() + mock_local.unlink = Mock() + mock_download.return_value = mock_local + + mock_output = Mock() + mock_output.unlink = Mock() + mock_build.return_value = [ + ("opus", ["ffmpeg"], mock_output) + ] + + mock_subprocess.return_value = 1 # Failure + + with pytest.raises(RuntimeError, match="Encode failed"): + encode_and_replace("sound/audio.wav", "opus") + + # Verify cleanup happened + mock_local.unlink.assert_called_once() + mock_output.unlink.assert_called_once() + + @patch('tiering_job.download_raw_to_temp') + @patch('tiering_job.build_ffmpeg_cmds') + def test_encode_and_replace_no_commands(self, mock_build, mock_download): + """Test when no encode commands are generated""" + mock_local = Mock() + mock_local.unlink = Mock() + mock_download.return_value = mock_local + + mock_build.return_value = [] # No commands + + with pytest.raises(RuntimeError, match="No encode commands"): + encode_and_replace("sound/audio.wav", "opus") + + mock_local.unlink.assert_called_once() + + @patch('tiering_job.download_raw_to_temp') + @patch('tiering_job.build_ffmpeg_cmds') + @patch('tiering_job.subprocess.call') + @patch('tiering_job.replace_with_compressed') + def test_encode_and_replace_flac_codec( + self, mock_replace, mock_subprocess, mock_build, mock_download + ): + """Test encoding with FLAC codec""" + mock_local = Mock() + mock_local.unlink = Mock() + mock_download.return_value = mock_local + + mock_output = Mock() + mock_output.unlink = Mock() + mock_build.return_value = [ + ("flac", ["ffmpeg"], mock_output) + ] + + mock_subprocess.return_value = 0 + mock_replace.return_value = "sound/audio.flac" + + result = encode_and_replace("sound/audio.wav", "flac") + + assert result == "sound/audio.flac" + mock_build.assert_called_once_with(mock_local, codec="flac") + + +class TestCleanupCompressed: + """Tests for cleanup_compressed function""" + + @patch('tiering_job.client') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.delete_object') + def test_cleanup_compressed_deletes_old_files( + self, mock_delete, mock_age, mock_client + ): + """Test deletion of old compressed files""" + # Mock old compressed files + mock_obj1 = Mock() + mock_obj1.object_name = "compressed/old1.opus" + mock_obj2 = Mock() + mock_obj2.object_name = "compressed/old2.flac" + + mock_client.list_objects.return_value = [mock_obj1, mock_obj2] + mock_age.side_effect = [100 * 86400, 95 * 86400] # Both > 90 days + + result = cleanup_compressed(90, dry_run=False) + + assert result == 2 + assert mock_delete.call_count == 2 + mock_delete.assert_any_call("compressed/old1.opus") + mock_delete.assert_any_call("compressed/old2.flac") + + @patch('tiering_job.client') + @patch('tiering_job.get_file_age_seconds') + def test_cleanup_compressed_keeps_new_files( + self, mock_age, mock_client + ): + """Test that new files are not deleted""" + mock_obj = Mock() + mock_obj.object_name = "compressed/new.opus" + + mock_client.list_objects.return_value = [mock_obj] + mock_age.return_value = 10 * 86400 # 10 days old + + result = cleanup_compressed(90, dry_run=False) + + assert result == 0 + + @patch('tiering_job.client') + @patch('tiering_job.get_file_age_seconds') + def test_cleanup_compressed_dry_run( + self, mock_age, mock_client, capsys + ): + """Test dry run mode""" + mock_obj = Mock() + mock_obj.object_name = "compressed/old.opus" + + mock_client.list_objects.return_value = [mock_obj] + mock_age.return_value = 100 * 86400 + + result = cleanup_compressed(90, dry_run=True) + + assert result == 0 # Nothing actually deleted + captured = capsys.readouterr() + assert "[DRY]" in captured.out + assert "Would delete" in captured.out + + def test_cleanup_compressed_disabled(self): + """Test when cleanup is disabled (max_age <= 0)""" + result = cleanup_compressed(0, dry_run=False) + assert result == 0 + + result = cleanup_compressed(-1, dry_run=False) + assert result == 0 + + +class TestMain: + """Tests for main function""" + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.cleanup_compressed') + def test_main_no_files(self, mock_cleanup, mock_iter, capsys): + """Test when no files need processing""" + mock_iter.return_value = [] + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "Audio files compressed: 0" in captured.out + mock_cleanup.assert_called_once() + + @patch('sys.argv', ['tiering_job.py', '--dry-run']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.cleanup_compressed') + def test_main_dry_run( + self, mock_cleanup, mock_older, mock_age, mock_iter, capsys + ): + """Test dry run mode""" + mock_iter.return_value = ["sound/audio.wav"] + mock_age.return_value = 35 * 86400 # 35 days + mock_older.return_value = True + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "[DRY]" in captured.out + assert "Would compress" in captured.out + + @patch('sys.argv', ['tiering_job.py', '--codec', 'flac', '--raw-max-age-days', '7']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.encode_and_replace') + @patch('tiering_job.client') + @patch('tiering_job.cleanup_compressed') + def test_main_custom_settings( + self, mock_cleanup, mock_client, mock_encode, + mock_older, mock_age, mock_iter + ): + """Test with custom codec and age threshold""" + mock_iter.return_value = ["sound/audio.wav"] + mock_age.return_value = 10 * 86400 # 10 days + mock_older.return_value = True + + # Mock file stats + mock_orig_stat = Mock() + mock_orig_stat.size = 10000000 + mock_comp_stat = Mock() + mock_comp_stat.size = 5000000 + mock_client.stat_object.side_effect = [mock_orig_stat, mock_comp_stat] + + mock_encode.return_value = "sound/audio.flac" + mock_cleanup.return_value = 0 + + main() + + # Verify encode was called with FLAC + mock_encode.assert_called_once_with("sound/audio.wav", "flac") + # Verify age threshold was 7 days + mock_older.assert_called_with("sound/audio.wav", 7 * 86400) + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.encode_and_replace') + @patch('tiering_job.client') + @patch('tiering_job.cleanup_compressed') + def test_main_successful_compression( + self, mock_cleanup, mock_client, mock_encode, + mock_older, mock_age, mock_iter, capsys + ): + """Test successful compression workflow""" + mock_iter.return_value = ["sound/audio.wav"] + mock_age.return_value = 35 * 86400 # 35 days + mock_older.return_value = True + mock_encode.return_value = "sound/audio.opus" + + mock_orig_stat = Mock() + mock_orig_stat.size = 10000000 + mock_comp_stat = Mock() + mock_comp_stat.size = 500000 + mock_client.stat_object.side_effect = [mock_orig_stat, mock_comp_stat] + + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "[OK]" in captured.out + assert "Compressed:" in captured.out + assert "Audio files compressed: 1" in captured.out + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.encode_and_replace') + @patch('tiering_job.client') + @patch('tiering_job.cleanup_compressed') + def test_main_encoding_failure( + self, mock_cleanup, mock_client, mock_encode, mock_older, mock_age, mock_iter, capsys + ): + """Test handling of encoding failure""" + mock_iter.return_value = ["sound/audio.wav"] + mock_age.return_value = 35 * 86400 + mock_older.return_value = True + + # Mock the stat_object to return a size for original file + mock_orig_stat = Mock() + mock_orig_stat.size = 10000000 + mock_client.stat_object.return_value = mock_orig_stat + + mock_encode.side_effect = RuntimeError("Encoding failed") + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "[FAIL]" in captured.out + assert "Encoding failed" in captured.out + assert "Errors: 1" in captured.out + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.cleanup_compressed') + def test_main_skip_young_files( + self, mock_cleanup, mock_older, mock_age, mock_iter, capsys + ): + """Test that young files are skipped""" + mock_iter.return_value = ["sound/new_audio.wav"] + mock_age.return_value = 5 * 86400 # 5 days old + mock_older.return_value = False # Not old enough + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "Files skipped (too new):" in captured.out + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.cleanup_compressed') + def test_main_skip_files_without_timestamp( + self, mock_cleanup, mock_older, mock_age, mock_iter, capsys + ): + """Test skipping files without parseable timestamp""" + mock_iter.return_value = ["sound/no_timestamp.wav"] + mock_age.return_value = 0 # No parseable timestamp + mock_older.return_value = False + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "[SKIP]" in captured.out + assert "Cannot parse timestamp" in captured.out + + @patch('sys.argv', ['tiering_job.py', '--compressed-max-age-days', '60']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.cleanup_compressed') + def test_main_cleanup_old_compressed( + self, mock_cleanup, mock_iter + ): + """Test cleanup of old compressed files""" + mock_iter.return_value = [] + mock_cleanup.return_value = 5 + + main() + + mock_cleanup.assert_called_once_with(60, False) + + +class TestDefaultConstants: + """Test default configuration constants""" + + def test_default_values(self): + """Test that default values are reasonable""" + assert DEFAULT_RAW_MAX_AGE_DAYS > 0 + assert DEFAULT_COMP_MAX_AGE_DAYS > 0 + assert DEFAULT_COMP_MAX_AGE_DAYS >= DEFAULT_RAW_MAX_AGE_DAYS + assert DEFAULT_LONG_TERM_CODEC in ["opus", "flac"] + + +class TestArgumentParsing: + """Test command-line argument parsing""" + + @patch('sys.argv', ['tiering_job.py', '--help']) + def test_help_argument(self): + """Test that help argument works""" + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 0 + + @patch('sys.argv', ['tiering_job.py', '--codec', 'invalid']) + def test_invalid_codec(self): + """Test error with invalid codec""" + with pytest.raises(SystemExit): + main() + + +class TestEdgeCases: + """Test edge cases""" + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.encode_and_replace') + @patch('tiering_job.client') + @patch('tiering_job.cleanup_compressed') + def test_main_multiple_files( + self, mock_cleanup, mock_client, mock_encode, + mock_older, mock_age, mock_iter + ): + """Test processing multiple files""" + mock_iter.return_value = [ + "sound/audio1.wav", + "sound/audio2.wav", + "sound/audio3.wav" + ] + mock_age.return_value = 35 * 86400 + mock_older.return_value = True + mock_encode.side_effect = [ + "sound/audio1.opus", + "sound/audio2.opus", + "sound/audio3.opus" + ] + + mock_stat = Mock() + mock_stat.size = 1000000 + mock_client.stat_object.return_value = mock_stat + + mock_cleanup.return_value = 0 + + main() + + assert mock_encode.call_count == 3 + + @patch('sys.argv', ['tiering_job.py']) + @patch('tiering_job.iter_audio_files') + @patch('tiering_job.get_file_age_seconds') + @patch('tiering_job.is_older_than') + @patch('tiering_job.encode_and_replace') + @patch('tiering_job.client') + @patch('tiering_job.cleanup_compressed') + def test_main_size_calculation( + self, mock_cleanup, mock_client, mock_encode, + mock_older, mock_age, mock_iter, capsys + ): + """Test size and ratio calculations""" + mock_iter.return_value = ["sound/audio.wav"] + mock_age.return_value = 35 * 86400 + mock_older.return_value = True + mock_encode.return_value = "sound/audio.opus" + + # Original: 10MB, Compressed: 1MB (10x ratio) + mock_orig = Mock() + mock_orig.size = 10 * 1024 * 1024 + mock_comp = Mock() + mock_comp.size = 1 * 1024 * 1024 + + mock_client.stat_object.side_effect = [mock_orig, mock_comp] + mock_cleanup.return_value = 0 + + main() + + captured = capsys.readouterr() + assert "Ratio: 10.00x" in captured.out + assert "Saved: 9,437,184 bytes" in captured.out + + +class TestIntegration: + """Integration-like tests""" + + def test_imports(self): + """Test that all required imports work""" + from tiering_job import ( + encode_and_replace, + cleanup_compressed, + main, + DEFAULT_RAW_MAX_AGE_DAYS, + DEFAULT_COMP_MAX_AGE_DAYS, + DEFAULT_LONG_TERM_CODEC + ) + + assert callable(encode_and_replace) + assert callable(cleanup_compressed) + assert callable(main) \ No newline at end of file diff --git a/AgCloud/services/db_api_service/.gitignore b/AgCloud/services/db_api_service/.gitignore new file mode 100644 index 000000000..7f5d2fbc0 --- /dev/null +++ b/AgCloud/services/db_api_service/.gitignore @@ -0,0 +1,38 @@ +# Virtual environments +.venv/ +env/ +venv/ + +# Byte-compiled / cache +__pycache__/ +*.py[cod] +*.pyo +*.pyd + +# Pytest / coverage +.pytest_cache/ +.coverage +htmlcov/ + +# IDE / editor +.vscode/ +.idea/ + +# OS metadata +.DS_Store +Thumbs.db + +# Certificates / secrets +*.crt +*.pem +*.key +.env +.env.* + +# Build / distribution +build/ +dist/ +*.egg-info/ + +# Logs +*.log diff --git a/AgCloud/services/db_api_service/Dockerfile b/AgCloud/services/db_api_service/Dockerfile new file mode 100644 index 000000000..f10cb2ab4 --- /dev/null +++ b/AgCloud/services/db_api_service/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +COPY *.crt /usr/local/share/ca-certificates/ +RUN chmod 644 /usr/local/share/ca-certificates/*.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + + +COPY requirements.txt . +RUN pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + -r requirements.txt + +COPY app ./app + +EXPOSE 8001 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] \ No newline at end of file diff --git a/AgCloud/services/db_api_service/README.md b/AgCloud/services/db_api_service/README.md new file mode 100644 index 000000000..254588308 --- /dev/null +++ b/AgCloud/services/db_api_service/README.md @@ -0,0 +1,199 @@ +# Storage DB API + +A FastAPI microservice for managing image/file metadata in the **AgCloud** platform. + +## Quickstart (Docker Compose) + +Build: +```bash +docker compose up -d --build +``` + +Check health: +```bash +curl http://localhost:8001/healthz +curl http://localhost:8001/ready +``` + +Stop and clean up: +```bash +docker compose down +``` + +## Generic API Support +The service now includes a Generic API layer that automatically exposes CRUD endpoints for allowed database tables. + +To enable a table: + +Add the table name under the ALLOWED_TABLES list in the ENV file + +## Authentication + +### Dev bootstrap +For local development only – creates default user (`admin`) and service account (`db-api`): + +```bash +curl -X POST http://localhost:8001/auth/_dev_bootstrap +``` + +Response includes: +- User and Service Account (created if missing). +- JWT access & refresh tokens. +- Raw service token (only shown once if newly created). + +--- + +### Human users (username/password) + +Login: +```bash +curl -s -X POST http://localhost:8001/auth/login -H "Content-Type: application/x-www-form-urlencoded" -d "username=admin&password=admin123" +``` + +Use the returned `access_token` in the `Authorization` header: + +```http +Authorization: Bearer +``` + +Refresh: +```bash +curl -s -X POST http://localhost:8001/auth/refresh -H "Content-Type: application/json" -d '{"refresh_token":""}' +``` + +--- + +### Service-to-service + +Use the `X-Service-Token` header with the raw token received during bootstrap (or after manual rotation): + +```http +X-Service-Token: +``` + +--- + +## Example API call + +With JWT (user): +```powershell +$boot = Invoke-WebRequest -Method POST "http://localhost:8001/auth/_dev_bootstrap" +$j = $boot.Content | ConvertFrom-Json +$access = $j.tokens.access_token + +Invoke-WebRequest "http://localhost:8001/api/files?limit=2" ` + -Headers @{ Authorization = ("Bearer {0}" -f $access) } +``` + +With Service Token (service account): +```powershell +Invoke-WebRequest "http://localhost:8001/api/files?limit=2" ` + -Headers @{ "X-Service-Token" = "" } +``` + +--- + +## Testing readme in /test + + +--- + +## Notes +- Changing `JWT_SECRET` invalidates all existing JWTs. +- Service tokens are **write-once**: only the raw token (from bootstrap or rotation) can be used; the DB only stores its SHA-256 hash. +- `/auth/_dev_bootstrap` is intended for development only – do not enable in production. + +## API Examples (CRUD) + +### Available endpoints +| Method | Endpoint | Description | +|--------|-----------|-------------| +| GET | `/api/tables/{resource}/schema` | Get table schema | +| GET | `/api/tables/{resource}` | List rows | +| POST | `/api/tables/{resource}` | Create a single row | +| POST | `/api/tables/{resource}/rows:batch` | Create multiple rows in one request | + +Base URL: http://localhost:8001 (adjust if different) +Prefix used below: /api/tables/{resource} + +Replace `{resource}` with the table name (e.g. `event_logs_sensors`). Ensure `Content-Type: application/json` header. + +Create — single row (POST /api/tables/{resource}) +```bash +curl -X POST "http://localhost:8001/api/tables/event_logs_sensors" \ + -H "Content-Type: application/json" \ + -d '{ + "log_id": 4, + "device_id": "dev-c", + "status": "ok", + "value": 12.3 + }' +# Response: {"affected_rows":1,"returning":{...}} +``` + +Create — batch (POST /api/tables/{resource}/rows:batch) +```bash +curl -X POST "http://localhost:8001/api/tables/event_logs_sensors/rows:batch" \ + -H "Content-Type: application/json" \ + -d '[ + {"log_id": 5, "device_id":"dev-a", "status":"ok"}, + {"log_id": 6, "device_id":"dev-b", "status":"error"} + ]' +# Response: {"affected_rows":2} +``` + +Read — list rows (GET /api/tables/{resource}) +```bash +curl "http://localhost:8001/api/tables/event_logs_sensors?limit=20&offset=0&order_by=log_id&order_dir=asc" +# Response: {"rows":[...],"count":N} +``` + +Read — describe table / schema (GET /api/tables/{resource}/schema) +```bash +curl "http://localhost:8001/api/tables/event_logs_sensors/schema" +# Response: {"table":"event_logs_sensors","contract":{...},"columns":[...]} +``` + +Update — partial (PATCH /api/tables/{resource}) +- body must include `keys` (identifying fields) and `data` (fields to update) +```bash +curl -X PATCH "http://localhost:8001/api/tables/event_logs_sensors" \ + -H "Content-Type: application/json" \ + -d '{ + "keys": {"log_id": 4, "device_id": "dev-c"}, + "data": {"status": "resolved", "value": 15.0} + }' +# Response: {"affected_rows":1,"returning":{...}} +``` + +Replace / full update (PUT /api/tables/{resource}) +- body includes `keys` and a full `data` payload validated against the contract +```bash +curl -X PUT "http://localhost:8001/api/tables/event_logs_sensors" \ + -H "Content-Type: application/json" \ + -d '{ + "keys": {"log_id": 4, "device_id": "dev-c"}, + "data": { + "log_id": 4, + "device_id": "dev-c", + "status": "resolved", + "value": 15.0, + "ts": "2025-10-22T10:00:00Z" + } + }' +# Response: {"affected_rows":1,"returning":{...}} +``` + +Delete (DELETE /api/tables/{resource}) +- body must include `keys` object +```bash +curl -X DELETE "http://localhost:8001/api/tables/event_logs_sensors" \ + -H "Content-Type: application/json" \ + -d '{"keys": {"log_id": 4, "device_id": "dev-c"}}' +# Response: {"affected_rows":1} +``` + +Notes +- `keys` fields are determined by the contract's `x-keyFields` (fallback to `id` if not present). Use the schema endpoint to confirm. +- All validation is performed against the JSON contract; unknown fields are rejected. +- Adjust base URL, auth headers and query params as needed for your deployment. diff --git a/AgCloud/services/db_api_service/app/__init__.py b/AgCloud/services/db_api_service/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/auth.py b/AgCloud/services/db_api_service/app/auth.py new file mode 100644 index 000000000..3995f9ee2 --- /dev/null +++ b/AgCloud/services/db_api_service/app/auth.py @@ -0,0 +1,187 @@ +# app/auth.py +from __future__ import annotations + +import os +import uuid +import hashlib +from datetime import datetime, timedelta, timezone +from typing import Optional, Tuple + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import jwt, JWTError +from passlib.context import CryptContext +from sqlalchemy.orm import Session +from pydantic import BaseModel + +from .db import session_scope +from .models import User, ServiceAccount, RefreshToken + +ENV = os.getenv("ENV", "dev") +JWT_SECRET = os.getenv("JWT_SECRET", "change-me-please-very-secret") +JWT_ALGO = os.getenv("JWT_ALGO", "HS256") +ACCESS_TTL_MIN = int(os.getenv("ACCESS_TTL_MIN", "15")) +REFRESH_TTL_DAYS = int(os.getenv("REFRESH_TTL_DAYS", "14")) + +DEV_ADMIN_USER = os.getenv("DEV_ADMIN_USER", "admin") +DEV_ADMIN_PASS = os.getenv("DEV_ADMIN_PASS", "admin123") +DEV_SA_NAME = os.getenv("DEV_SA_NAME", "db-api") + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login", auto_error=False) +router = APIRouter(prefix="/auth", tags=["auth"]) + +def get_db(): + with session_scope() as s: + yield s + +# ---------- Hashing / Verify ---------- +def hash_password(raw: str) -> str: + return pwd_context.hash(raw) + +def verify_password(raw: str, hashed: str) -> bool: + return pwd_context.verify(raw, hashed) + +def hash_sa_token(raw: str) -> str: + return hashlib.sha256(raw.encode()).hexdigest() + +# ---------- JWT helpers ---------- +def _encode_token(payload: dict) -> str: + return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGO) + +def _decode_token(token: str) -> dict: + return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGO]) + +def create_access_token(sub: str, subject_type: str = "user") -> str: + now = datetime.now(timezone.utc) + exp = now + timedelta(minutes=ACCESS_TTL_MIN) + payload = {"sub": sub, "sub_type": subject_type, "iat": int(now.timestamp()), "exp": int(exp.timestamp())} + return _encode_token(payload) + +def create_refresh_token(user_id: int) -> tuple[str, datetime]: + token = str(uuid.uuid4()) + expires = datetime.now(timezone.utc) + timedelta(days=REFRESH_TTL_DAYS) + return token, expires + +# ---------- Guard: require_auth ---------- +def require_auth( + request: Request, + db: Session = Depends(get_db), + bearer_token: Optional[str] = Depends(oauth2_scheme), +) -> Tuple[str, object]: + raw_sa = request.headers.get("X-Service-Token") + if raw_sa: + h = hash_sa_token(raw_sa) + sa = db.query(ServiceAccount).filter(ServiceAccount.token_hash == h).first() + if not sa: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid service token") + return ("service", sa) + + if not bearer_token: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing token") + + try: + payload = _decode_token(bearer_token) + except JWTError: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid token") + + if payload.get("sub_type") != "user": + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid subject type") + + user_id = int(payload["sub"]) if str(payload.get("sub", "")).isdigit() else None + if not user_id: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid subject") + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found") + + return ("user", user) + +# ---------- Endpoints ---------- +@router.post("/login") +def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == form.username).first() + if not user or not verify_password(form.password, user.password_hash): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="bad credentials") + access = create_access_token(str(user.id), "user") + refresh_token, expires = create_refresh_token(user.id) + db.add(RefreshToken(user_id=user.id, token=refresh_token, expires_at=expires)) + return {"access_token": access, "token_type": "bearer", "refresh_token": refresh_token} + +class RefreshIn(BaseModel): + refresh_token: str + +@router.post("/refresh") +def refresh_token(body: RefreshIn, db: Session = Depends(get_db)): + rt = db.query(RefreshToken).filter(RefreshToken.token == body.refresh_token).first() + if not rt: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid refresh token") + + user = rt.user + if not user.is_active: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="inactive user") + + if rt.expires_at < datetime.now(timezone.utc): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="refresh token expired") + + access = create_access_token(str(user.id), "user") + + # rotate refresh token + db.delete(rt) + new_refresh, new_expires = create_refresh_token(user.id) + db.add(RefreshToken(user_id=user.id, token=new_refresh, expires_at=new_expires)) + db.commit() + + return {"access_token": access, "refresh_token": new_refresh, "token_type": "bearer"} +class DevBootstrapIn(BaseModel): + service_name: str | None = None + rotate_if_exists: bool = False + +@router.post("/_dev_bootstrap", status_code=status.HTTP_201_CREATED) +def dev_bootstrap(body: DevBootstrapIn | None = None, db: Session = Depends(get_db)): + if ENV != "dev": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="for dev only") + + service_name = (body.service_name.strip() if body and body.service_name else DEV_SA_NAME).strip() + rotate = bool(body.rotate_if_exists) if body else False + if not service_name: + raise HTTPException(status_code=400, detail="service_name required") + + user = db.query(User).filter(User.username == DEV_ADMIN_USER).first() + created_user = False + if not user: + user = User(username=DEV_ADMIN_USER, password_hash=hash_password(DEV_ADMIN_PASS)) + db.add(user) + db.flush() + created_user = True + + sa = db.query(ServiceAccount).filter(ServiceAccount.name == service_name).first() + raw_sa_token: Optional[str] = None + + if not sa: + raw_sa_token = str(uuid.uuid4()) + sa = ServiceAccount(name=service_name, token_hash=hash_sa_token(raw_sa_token)) + db.add(sa) + else: + if rotate: + raw_sa_token = str(uuid.uuid4()) + sa.token_hash = hash_sa_token(raw_sa_token) + + access = create_access_token(str(user.id), "user") + refresh_token, expires = create_refresh_token(user.id) + db.add(RefreshToken(user_id=user.id, token=refresh_token, expires_at=expires)) + + return { + "created_user": created_user, + "service_account": { + "name": sa.name, + "raw_token": raw_sa_token or None, + "token": (raw_sa_token or "*** (already exists)"), + }, + "tokens": { + "access_token": access, + "refresh_token": refresh_token, + "token_type": "bearer", + }, + } diff --git a/AgCloud/services/db_api_service/app/config.py b/AgCloud/services/db_api_service/app/config.py new file mode 100644 index 000000000..21190041d --- /dev/null +++ b/AgCloud/services/db_api_service/app/config.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from pathlib import Path +from typing import List + +from pydantic import field_validator +from pydantic_settings import BaseSettings + + +# Application configuration validated via Pydantic. +# Holds paths and runtime flags used across the service. +class Settings(BaseSettings): + CONTRACTS_DIR: Path = Path("app/contracts") + ALLOWED_TABLES: List[str] = [] # provided via ENV or .env (comma-separated) + STRICT_UNKNOWN_FIELDS: bool = True + + # Accept comma-separated string or iterable; normalize to lowercase unique list. + @field_validator("ALLOWED_TABLES", mode="before") + @classmethod + def _normalize_allowed_tables(cls, v): + if isinstance(v, str): + v = [x.strip() for x in v.split(",") if x.strip()] + seen = set() + result: List[str] = [] + for name in v: + key = name.strip().lower() + if key and key not in seen: + seen.add(key) + result.append(key) + return result + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +# Global settings instance used by the application modules. +settings = Settings() + + +# Utility: centralized check whether a table name is allowed. +def is_table_allowed(table_name: str) -> bool: + """Centralized check used by routers/repos.""" + return table_name.strip().lower() in settings.ALLOWED_TABLES diff --git a/AgCloud/services/db_api_service/app/contracts/Dockerfile b/AgCloud/services/db_api_service/app/contracts/Dockerfile new file mode 100644 index 000000000..ed6d31775 --- /dev/null +++ b/AgCloud/services/db_api_service/app/contracts/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +COPY *.crt /usr/local/share/ca-certificates/ +RUN chmod 644 /usr/local/share/ca-certificates/*.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + -r requirements.txt + +COPY app ./app + +ENTRYPOINT ["python", "-m", "app.tools.generate_contracts"] diff --git a/AgCloud/services/db_api_service/app/contracts/__init__.py b/AgCloud/services/db_api_service/app/contracts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/contracts/loader.py b/AgCloud/services/db_api_service/app/contracts/loader.py new file mode 100644 index 000000000..fb9f3d685 --- /dev/null +++ b/AgCloud/services/db_api_service/app/contracts/loader.py @@ -0,0 +1,59 @@ +from __future__ import annotations +import json +from pathlib import Path +from typing import Dict, Any, Optional + +# Lightweight in-memory contract store. +# Loads JSON contracts from a directory and provides case-insensitive lookup by table/name. +class ContractStore: + # Initialize store with resolved base directory and empty index mapping. + def __init__(self, base_dir: Path): + self.base_dir = Path(base_dir).resolve() + self._by_table: Dict[str, Dict[str, Any]] = {} + + # Compute lookup keys for a contract file: filename stem, schema title, and title suffix. + def _index_keys(self, file_path: Path, schema: Dict[str, Any]) -> set[str]: + keys = set() + stem = file_path.stem.strip().lower() + keys.add(stem) + + title = str(schema.get("title", "")).strip().lower() + if title: + keys.add(title) + if "." in title: + keys.add(title.split(".", 1)[1]) + return keys + + # Load all JSON contract files from base_dir into the in-memory index. + # Creates the directory if missing and prints a short load summary. + def load_all(self) -> None: + self._by_table.clear() + print(f"[contract-store] trying to load from: {self.base_dir}") + self.base_dir.mkdir(parents=True, exist_ok=True) + + loaded_files = 0 + for p in self.base_dir.glob("*.json"): + try: + with p.open("r", encoding="utf-8") as f: + schema = json.load(f) + except Exception as e: + print(f"[contract-store] failed to load {p}: {e}") + continue + + for key in self._index_keys(p, schema): + self._by_table[key] = schema + loaded_files += 1 + + print( + f"[contract-store] loaded_files={loaded_files} " + f"mapped_keys={len(self._by_table)} " + f"keys={sorted(self._by_table.keys())}" + ) + + # Return the contract dict for the given table/key (case-insensitive), or None if not found. + def get(self, table: str) -> Optional[Dict[str, Any]]: + return self._by_table.get(table.strip().lower()) + + # Return True if a contract exists for the given table/key. + def has(self, table: str) -> bool: + return self.get(table) is not None diff --git a/AgCloud/services/db_api_service/app/contracts/requirements.txt b/AgCloud/services/db_api_service/app/contracts/requirements.txt new file mode 100644 index 000000000..fac21d5c5 --- /dev/null +++ b/AgCloud/services/db_api_service/app/contracts/requirements.txt @@ -0,0 +1,4 @@ +SQLAlchemy==2.0.36 +pathlib + + diff --git a/AgCloud/services/db_api_service/app/db.py b/AgCloud/services/db_api_service/app/db.py new file mode 100644 index 000000000..7ebca876e --- /dev/null +++ b/AgCloud/services/db_api_service/app/db.py @@ -0,0 +1,50 @@ + +from dotenv import load_dotenv +load_dotenv() + +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from contextlib import contextmanager +from sqlalchemy.pool import StaticPool + +from app.models import Base + +DB_DSN = os.getenv( + "DB_DSN", + "postgresql+psycopg://missions_user:pg123@localhost:5432/missions_db" +) + +# --- Engine setup --- +if DB_DSN.startswith("sqlite"): + + engine = create_engine( + DB_DSN, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + echo=False + ) +else: + pool_size = int(os.getenv("DB_POOL_SIZE", "5")) + max_overflow = int(os.getenv("DB_MAX_OVERFLOW", "10")) + engine = create_engine( + DB_DSN, + pool_size=pool_size, + max_overflow=max_overflow, + pool_pre_ping=True, + echo=False + ) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +@contextmanager +def session_scope(): + s = SessionLocal() + try: + yield s + s.commit() + except Exception: + s.rollback() + raise + finally: + s.close() diff --git a/AgCloud/services/db_api_service/app/main.py b/AgCloud/services/db_api_service/app/main.py new file mode 100644 index 000000000..0a5c6463b --- /dev/null +++ b/AgCloud/services/db_api_service/app/main.py @@ -0,0 +1,36 @@ +# from app.router import api + +# app.include_router(api) + +from fastapi import FastAPI +from app.auth import router as auth_router +from app.db import engine +from app.models import Base +from app.contracts.loader import ContractStore +from app.router import build_router +from app.tables.generic import repo +from .config import settings + +contract_store = ContractStore(settings.CONTRACTS_DIR) +repo.set_contract_store(contract_store) + +app = FastAPI(title="Storage DB API", version="1.1.0") + +@app.on_event("startup") +def load_contracts_on_startup(): + contract_store.load_all() + +@app.get("/healthz") +def healthz(): + return {"status": "ok"} + +@app.get("/ready") +def ready(): + Base.metadata.create_all(bind=engine) + return {"ready": True} + +app.include_router(auth_router) + +app.include_router(build_router(contract_store)) + + diff --git a/AgCloud/services/db_api_service/app/models.py b/AgCloud/services/db_api_service/app/models.py new file mode 100644 index 000000000..00658c417 --- /dev/null +++ b/AgCloud/services/db_api_service/app/models.py @@ -0,0 +1,46 @@ +from __future__ import annotations +from typing import Optional, List +from datetime import datetime + +from sqlalchemy import String, Integer, DateTime, ForeignKey, func, Text, Boolean +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + pass + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, index=True) + username: Mapped[str] = mapped_column(String(150), unique=True, index=True, nullable=False) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + refresh_tokens: Mapped[List["RefreshToken"]] = relationship( + back_populates="user", + cascade="all, delete-orphan", + passive_deletes=True, + ) + +class ServiceAccount(Base): + __tablename__ = "service_accounts" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, index=True) + name: Mapped[str] = mapped_column(String(150), unique=True, index=True, nullable=False) + token_hash: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + +class RefreshToken(Base): + __tablename__ = "refresh_tokens" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, index=True) + user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + token: Mapped[str] = mapped_column(Text, unique=True, nullable=False) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + user: Mapped["User"] = relationship(back_populates="refresh_tokens") diff --git a/AgCloud/services/db_api_service/app/router.py b/AgCloud/services/db_api_service/app/router.py new file mode 100644 index 000000000..f775341ef --- /dev/null +++ b/AgCloud/services/db_api_service/app/router.py @@ -0,0 +1,25 @@ +# app/router.py +# from app.auth import require_uth +from fastapi import APIRouter, Depends +from app.auth import require_auth +from app.tables.files.router import router as files_router + +from app.tables.generic.router import build_generic_router +from app.tables.task_thresholds.router import router as task_thresholds_router +from app.tables.ripeness_weekly_rollups_ts.router import router as ripeness_weekly_router + + +def build_router(contract_store) -> APIRouter: + api = APIRouter( + prefix="/api", + tags=["api"], + dependencies=[Depends(require_auth)], + ) + + + api.include_router(files_router) + api.include_router(task_thresholds_router) + api.include_router(ripeness_weekly_router) + api.include_router(build_generic_router(contract_store)) + + return api diff --git a/AgCloud/services/db_api_service/app/schemas.py b/AgCloud/services/db_api_service/app/schemas.py new file mode 100644 index 000000000..0125b9a9c --- /dev/null +++ b/AgCloud/services/db_api_service/app/schemas.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel +from typing import Optional + +class LoginRequest(BaseModel): + username: str + password: str + +class TokenPair(BaseModel): + access_token: str + refresh_token: str + token_type: str = "Bearer" + expires_in: int + +class RefreshRequest(BaseModel): + refresh_token: str + +class ClientCredentialsRequest(BaseModel): + client_id: str + client_secret: str + scope: Optional[str] = None + +class AccessTokenResponse(BaseModel): + access_token: str + token_type: str = "Bearer" + expires_in: int diff --git a/AgCloud/services/db_api_service/app/tables/__init__.py b/AgCloud/services/db_api_service/app/tables/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/tables/devices/repo.py b/AgCloud/services/db_api_service/app/tables/devices/repo.py new file mode 100644 index 000000000..ad449d931 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/devices/repo.py @@ -0,0 +1,86 @@ +from typing import Optional, Dict, Any +from sqlalchemy import text +from app.db import session_scope + + +def list_devices( + limit: int = 50, + offset: int = 0, + q: Optional[str] = None, + active: Optional[bool] = None, +) -> Dict[str, Any]: + """ + Retrieve a paginated list of devices with optional filtering. + + Args: + limit (int): Maximum number of devices to return. Defaults to 50. + offset (int): Number of records to skip (for pagination). Defaults to 0. + q (Optional[str]): Search string to filter by device_id, model, or owner. + active (Optional[bool]): Filter by active status if provided. + + Returns: + Dict[str, Any]: A dictionary containing: + - "total": total number of matching devices. + - "items": list of matching device records as dictionaries. + """ + filters = [] + params: Dict[str, Any] = {"limit": limit, "offset": offset} + + # Free-text search filter + if q: + filters.append( + "(device_id ILIKE :q OR model ILIKE :q OR owner ILIKE :q)" + ) + params["q"] = f"%{q}%" + + # Active status filter + if active is not None: + filters.append("active = :active") + params["active"] = active + + # Build WHERE clause dynamically based on filters + where_sql = f"WHERE {' AND '.join(filters)}" if filters else "" + + # SQL for fetching paginated list + list_sql = text(f""" + SELECT device_id, model, owner, active + FROM public.devices + {where_sql} + ORDER BY device_id + LIMIT :limit OFFSET :offset + """) + + # SQL for total count + count_sql = text(f""" + SELECT COUNT(*)::int AS total + FROM public.devices + {where_sql} + """) + + # Execute both queries within a session scope + with session_scope() as s: + total = s.execute(count_sql, params).scalar_one() + rows = s.execute(list_sql, params).mappings().all() + + return {"total": total, "items": [dict(r) for r in rows]} + + +def get_device(device_id: str) -> Optional[Dict[str, Any]]: + """ + Retrieve a single device by its ID. + + Args: + device_id (str): Unique identifier of the device. + + Returns: + Optional[Dict[str, Any]]: Dictionary containing device details + if found, otherwise None. + """ + sql = text(""" + SELECT device_id, model, owner, active + FROM public.devices + WHERE device_id = :device_id + """) + with session_scope() as s: + row = s.execute(sql, {"device_id": device_id}).mappings().first() + return dict(row) if row else None \ No newline at end of file diff --git a/AgCloud/services/db_api_service/app/tables/devices/router.py b/AgCloud/services/db_api_service/app/tables/devices/router.py new file mode 100644 index 000000000..be950ebf3 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/devices/router.py @@ -0,0 +1,49 @@ +from typing import Optional +from fastapi import APIRouter, HTTPException, Query +from .schemas import DeviceOut, DeviceList +from . import repo + +# Create API router for devices +router = APIRouter(prefix="/devices", tags=["devices"]) + + +@router.get("", response_model=DeviceList) +def list_devices( + limit: int = Query(50, ge=1, le=500, description="Maximum number of devices to return"), + offset: int = Query(0, ge=0, description="Number of devices to skip for pagination"), + q: Optional[str] = Query(None, description="Free text search in device_id, model, or owner"), + active: Optional[bool] = Query(None, description="Filter by active status"), +): + """ + API endpoint to retrieve a list of devices with optional filters. + + Query Parameters: + - limit: Maximum number of records (default: 50, max: 500). + - offset: Records to skip for pagination (default: 0). + - q: Free-text search across device_id, model, and owner. + - active: Filter devices by active status. + + Returns: + DeviceList: Paginated list of devices with total count. + """ + return repo.list_devices(limit=limit, offset=offset, q=q, active=active) + + +@router.get("/{device_id}", response_model=DeviceOut) +def get_device(device_id: str): + """ + API endpoint to retrieve a single device by its ID. + + Args: + device_id (str): Unique identifier of the device. + + Raises: + HTTPException: 404 if the device is not found. + + Returns: + DeviceOut: Device details if found. + """ + row = repo.get_device(device_id) + if not row: + raise HTTPException(status_code=404, detail="Device not found") + return row \ No newline at end of file diff --git a/AgCloud/services/db_api_service/app/tables/devices/schemas.py b/AgCloud/services/db_api_service/app/tables/devices/schemas.py new file mode 100644 index 000000000..f3b4bf2b7 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/devices/schemas.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel +from typing import Optional, List + +class DeviceOut(BaseModel): + device_id: str + model: Optional[str] = None + owner: Optional[str] = None + active: Optional[bool] = None + +class DeviceList(BaseModel): + total: int + items: List[DeviceOut] \ No newline at end of file diff --git a/AgCloud/services/db_api_service/app/tables/files/__init__.py b/AgCloud/services/db_api_service/app/tables/files/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/tables/files/repo.py b/AgCloud/services/db_api_service/app/tables/files/repo.py new file mode 100644 index 000000000..35fd28f1a --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/files/repo.py @@ -0,0 +1,262 @@ +# app/tables/files/repo.py +import os +import json +import time +import pathlib +from typing import Any, Dict, List, Optional + +from sqlalchemy import text +from app.db import session_scope + +DRY_RUN = os.getenv("DB_DRY_RUN", "0") == "1" +SPOOL_DIR = os.getenv("DRY_RUN_SPOOL", "/tmp/api_spool") + + +def _spool(name: str, payload: Dict[str, Any]): + p = pathlib.Path(SPOOL_DIR) + p.mkdir(parents=True, exist_ok=True) + ts = int(time.time() * 1000) + (p / f"{ts}-{name}.json").write_text( + json.dumps(payload, ensure_ascii=False), encoding="utf-8" + ) + + +def _ensure_json_text(obj: Any) -> Optional[str]: + if obj is None: + return None + if isinstance(obj, (dict, list)): + return json.dumps(obj, ensure_ascii=False) + return obj + + +def upsert_file(payload: Dict[str, Any]) -> None: + if DRY_RUN: + _spool("files_upsert", payload) + return + + payload = dict(payload) + payload["metadata"] = _ensure_json_text(payload.get("metadata")) + + # optional footprint (WKT) -> geometry + fp = payload.get("footprint") + payload["footprint"] = (None if not fp else fp) + + q = text(""" + INSERT INTO files ( + bucket, object_key, content_type, size_bytes, etag, + mission_id, device_id, tile_id, footprint, metadata + ) + VALUES ( + :bucket, :object_key, :content_type, :size_bytes, :etag, + :mission_id, :device_id, :tile_id, + CASE + WHEN NULLIF(CAST(:footprint AS text), '') IS NULL THEN NULL::geometry + ELSE ST_GeomFromText(CAST(:footprint AS text), 4326) + END, + CAST(:metadata AS JSONB) + ) + ON CONFLICT (bucket, object_key) + DO UPDATE SET + content_type = EXCLUDED.content_type, + size_bytes = EXCLUDED.size_bytes, + etag = EXCLUDED.etag, + mission_id = EXCLUDED.mission_id, + device_id = EXCLUDED.device_id, + tile_id = EXCLUDED.tile_id, + footprint = EXCLUDED.footprint, + metadata = EXCLUDED.metadata; + """) + with session_scope() as s: + s.execute(q, payload) + + +def update_file(bucket: str, object_key: str, updates: Dict[str, Any]) -> bool: + if DRY_RUN: + _spool("files_update", {"bucket": bucket, "object_key": object_key, **updates}) + return True + + sets: List[str] = [] + params: Dict[str, Any] = {"bucket": bucket, "object_key": object_key} + + if "content_type" in updates and updates["content_type"] is not None: + sets.append("content_type=:content_type") + params["content_type"] = updates["content_type"] + + if "size_bytes" in updates and updates["size_bytes"] is not None: + sets.append("size_bytes=:size_bytes") + params["size_bytes"] = updates["size_bytes"] + + if "etag" in updates and updates["etag"] is not None: + sets.append("etag=:etag") + params["etag"] = updates["etag"] + + if "mission_id" in updates and updates["mission_id"] is not None: + sets.append("mission_id=:mission_id") + params["mission_id"] = updates["mission_id"] + + if "device_id" in updates and updates["device_id"] is not None: + sets.append("device_id=:device_id") + params["device_id"] = updates["device_id"] + + if "tile_id" in updates and updates["tile_id"] is not None: + sets.append("tile_id=:tile_id") + params["tile_id"] = updates["tile_id"] + + if "footprint" in updates: + fp = updates["footprint"] + params["footprint"] = (None if not fp else fp) + sets.append( + "footprint = CASE " + "WHEN NULLIF(CAST(:footprint AS text), '') IS NULL THEN NULL::geometry " + "ELSE ST_GeomFromText(CAST(:footprint AS text), 4326) " + "END" + ) + + if "metadata" in updates and updates["metadata"] is not None: + params["metadata"] = _ensure_json_text(updates["metadata"]) + sets.append("metadata=CAST(:metadata AS JSONB)") + + if not sets: + return True + + q = text(f""" + UPDATE files + SET {', '.join(sets)} + WHERE bucket=:bucket AND object_key=:object_key + RETURNING file_id; + """) + with session_scope() as s: + row = s.execute(q, params).first() + return bool(row) + + +def get_file(bucket: str, object_key: str) -> Optional[Dict[str, Any]]: + if DRY_RUN: + return None + + q = text(""" + SELECT + file_id, bucket, object_key, + object_key AS key, + content_type, size_bytes, etag, + mission_id, device_id, tile_id, + ST_AsText(footprint) AS footprint_wkt, + metadata, created_at + FROM files + WHERE bucket=:bucket AND object_key=:object_key + LIMIT 1; + """) + with session_scope() as s: + row = s.execute(q, {"bucket": bucket, "object_key": object_key}).mappings().first() + return dict(row) if row else None + + +# def get_file_by_id(file_id: int) -> Optional[Dict[str, Any]]: +# """Fetch by numeric file_id.""" +# if DRY_RUN: +# return None + +# q = text(""" +# SELECT +# file_id, bucket, object_key, +# object_key AS key, +# content_type, size_bytes, etag, +# mission_id, device_id, tile_id, +# ST_AsText(footprint) AS footprint_wkt, +# metadata, created_at +# FROM files +# WHERE file_id = :file_id +# LIMIT 1; +# """) +# with session_scope() as s: +# row = s.execute(q, {"file_id": file_id}).mappings().first() +# return dict(row) if row else None +def get_file_by_id(file_id: int) -> Optional[Dict[str, Any]]: + """New: fetch file metadata by numeric file_id using sound_new_sounds_connections (no files table).""" + if DRY_RUN: + return None + q = text(""" + SELECT + snsc.id AS file_id, + snsc.key AS key, -- combined bucket + path, e.g. "my-bucket/path/to/file.wav" + split_part(snsc.key, '/', 1) AS bucket, + regexp_replace(snsc.key, '^[^/]+/', '') AS object_key, + COALESCE(sm.file_name, snsc.file_name) AS filename, + sm.device_id AS device_id, + sm.content_type AS content_type, + sm.duration_sec AS duration_sec, + COALESCE(sm.capture_time, snsc.linked_time) AS capture_time, + snsc.metadata AS metadata, + snsc.linked_time AS linked_time + FROM public.sound_new_sounds_connections snsc + LEFT JOIN public.sounds_metadata sm + ON sm.file_name = snsc.file_name + WHERE snsc.id = :file_id + LIMIT 1; + """) + with session_scope() as s: + row = s.execute(q, {"file_id": file_id}).mappings().first() + return dict(row) if row else None + +def list_files(bucket: Optional[str], device_id: Optional[str], limit: int) -> List[Dict[str, Any]]: + if DRY_RUN: + return [] + + filters: List[str] = [] + params: Dict[str, Any] = {"limit": limit} + + if bucket: + filters.append("bucket=:bucket") + params["bucket"] = bucket + + if device_id: + filters.append("device_id=:device_id") + params["device_id"] = device_id + + where = f"WHERE {' AND '.join(filters)}" if filters else "" + + q = text(f""" + SELECT + file_id, bucket, object_key, + object_key AS key, + content_type, size_bytes, etag, + mission_id, device_id, tile_id, + ST_AsText(footprint) AS footprint_wkt, + metadata, created_at + FROM files + {where} + ORDER BY created_at DESC + LIMIT :limit; + """) + with session_scope() as s: + rows = s.execute(q, params).mappings().all() + return [dict(r) for r in rows] + + +def delete_file(bucket: str, object_key: str) -> bool: + if DRY_RUN: + _spool("files_delete", {"bucket": bucket, "object_key": object_key}) + return True + + q = text(""" + DELETE FROM files + WHERE bucket=:bucket AND object_key=:object_key + RETURNING file_id; + """) + with session_scope() as s: + row = s.execute(q, {"bucket": bucket, "object_key": object_key}).first() + return bool(row) + + +def db_query(query: str, params: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]: + """ + Generic helper to execute raw SQL and return rows as dictionaries. + This is used by the audio-aggregates and plant-predictions endpoints. + """ + if DRY_RUN: + return [] + + with session_scope() as s: + result = s.execute(text(query), params or {}) + rows = result.mappings().all() + return [dict(r) for r in rows] \ No newline at end of file diff --git a/AgCloud/services/db_api_service/app/tables/files/router.py b/AgCloud/services/db_api_service/app/tables/files/router.py new file mode 100644 index 000000000..7a606ff42 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/files/router.py @@ -0,0 +1,306 @@ +# app/tables/files/router.py +from typing import Optional, Any, Dict +from urllib.parse import unquote, quote +import os, json + +from fastapi import APIRouter, HTTPException, Query +from .schemas import FilesCreate, FilesUpdate +from . import repo + +router = APIRouter(prefix="/files", tags=["files"]) +PUBLIC_S3_BASE = os.getenv("PUBLIC_S3_BASE") + + +def _attach_url_if_possible(row: Dict[str, Any]) -> Dict[str, Any]: + """Attach a public URL to access the file from MinIO.""" + if not row: + return row + + # Try to extract URL from metadata first + meta = row.get("metadata") + if isinstance(meta, str): + try: + meta = json.loads(meta) + except Exception: + meta = None + if isinstance(meta, dict): + for k in ("url", "s3_url"): + if meta.get(k): + row.setdefault("url", meta[k]) + return row + if PUBLIC_S3_BASE and row.get("bucket") and (row.get("key") or row.get("object_key")): + bucket = str(row["bucket"]) + key = str(row.get("key") or row.get("object_key")) + built = f"{PUBLIC_S3_BASE.rstrip('/')}/{quote(bucket, safe='')}/{quote(key, safe='/')}" + row.setdefault("url", built) + return row + +def _is_compressed(filename: str) -> bool: + """Check if file is compressed (OPUS format)""" + if not filename: + return False + return filename.lower().endswith('.opus') + +@router.post("", status_code=201) +def create_or_upsert_file(payload: FilesCreate): + repo.upsert_file(payload.model_dump(by_alias=True)) + return {"status": "ok"} +@router.get("/audio-aggregates/", summary="List audio file aggregates (environment sounds)") +def list_audio_aggregates( + run_id: Optional[str] = None, + type: Optional[str] = Query(None, description="Predicted label (noise type)"), + date_from: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"), + date_to: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"), + search: Optional[str] = Query(None, description="Search by filename"), + device_ids: Optional[str] = Query(None, description="Comma-separated device IDs"), + sort_by: Optional[str] = Query("Date (Newest)", description="Sort field"), + limit: int = Query(100, ge=1, le=500), +): + conditions = [] + params: Dict[str, Any] = {} + + # Run filter + if run_id: + conditions.append("fa.run_id = :run_id") + params["run_id"] = run_id + + # Label filter + if type and type.lower() not in ("all types", "all signals"): + conditions.append("fa.head_pred_label ILIKE :type") + params["type"] = f"%{type}%" + + # Search filter on filename + if search: + conditions.append("snsc.file_name ILIKE :search") + params["search"] = f"%{search}%" + + # Device filter (based on snsc.file_name prefix) + if device_ids: + device_list = [d.strip() for d in device_ids.split(",") if d.strip()] + if device_list: + placeholders = ", ".join([f":dev_{i}" for i in range(len(device_list))]) + conditions.append(f"(split_part(snsc.file_name, '_', 1)) IN ({placeholders})") + for i, dev in enumerate(device_list): + params[f"dev_{i}"] = dev + + # Date filters (use capture_time from sounds_metadata or snsc.linked_time) + if date_from: + conditions.append("COALESCE(sm.capture_time, snsc.linked_time) >= CAST(:date_from AS timestamptz)") + params["date_from"] = date_from + + if date_to: + conditions.append("COALESCE(sm.capture_time, snsc.linked_time) < CAST(:date_to AS timestamptz) + INTERVAL '1 day'") + params["date_to"] = date_to + + where_clause = "WHERE " + " AND ".join(conditions) if conditions else "" + + # Sorting options + sort_map = { + "Date (Newest)": "COALESCE(sm.capture_time, snsc.linked_time) DESC", + "Date (Oldest)": "COALESCE(sm.capture_time, snsc.linked_time) ASC", + "Length": "sm.duration_sec DESC", # if you prefer duration from sounds_metadata + "Device": "snsc.file_name ASC", + "processing_ms": "fa.processing_ms DESC", + "filename": "snsc.file_name ASC", + } + order_clause = sort_map.get(sort_by, "COALESCE(sm.capture_time, snsc.linked_time) DESC") + + query = f""" + SELECT + fa.file_id, + snsc.key AS combined_key, + split_part(snsc.key, '/', 1) AS bucket, + regexp_replace(snsc.key, '^[^/]+/', '') AS object_key, + COALESCE(sm.file_name, snsc.file_name) AS filename, + sm.device_id AS device_id, + COALESCE(sm.capture_time, snsc.linked_time) AS capture_time, + fa.head_pred_label, + fa.head_pred_prob, + fa.agg_mode, + fa.num_windows + FROM agcloud_audio.file_aggregates fa + JOIN public.sound_new_sounds_connections snsc + ON fa.file_id = snsc.id + LEFT JOIN public.sounds_metadata sm + ON sm.file_name = snsc.file_name + {where_clause} + ORDER BY {order_clause} + LIMIT :limit; + """ + + params["limit"] = limit + + try: + rows = repo.db_query(query, params) + results = [] + + for r in rows: + # build URL server-side + bucket = r.get("bucket") + object_key = r.get("object_key") + url = None + if bucket and object_key: + # PUBLIC_S3_BASE should be like "https://minio.example.com" + url = f"{PUBLIC_S3_BASE.rstrip('/')}/{quote(bucket, safe='')}/{quote(object_key, safe='/')}" + + + results.append({ + "file_id": r.get("file_id"), + "filename": r.get("filename"), + "predicted_label": r.get("head_pred_label"), + "probability": r.get("head_pred_prob"), + "device_id": (r.get("filename") or "").split("_")[0], + "url": url, + }) + + return results + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + + +@router.get("/plant-predictions/", summary="List ultrasonic plant predictions") +def list_plant_predictions( + predicted_class: Optional[str] = Query(None, description="Filter by predicted class"), + date_from: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"), + date_to: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"), + search: Optional[str] = Query(None, description="Search by filename"), + device_ids: Optional[str] = Query(None, description="Comma-separated device IDs (if applicable)"), + sort_by: Optional[str] = Query("Date (Newest)", description="Sort field"), + limit: int = Query(100, ge=1, le=500), +): + """ + Returns ultrasonic plant predictions from public.ultrasonic_plant_predictions + + This endpoint serves plant stress/watering sounds (Tomato Cut, Tobacco Dry, etc.) + """ + conditions = [] + params: Dict[str, Any] = {} + + # Filter by predicted class + if predicted_class and predicted_class.lower() not in ("all signals", "all types"): + conditions.append("upp.predicted_class ILIKE :pred_class") + params["pred_class"] = f"%{predicted_class}%" + + # Search by filename + if search: + conditions.append("upp.file ILIKE :search") + params["search"] = f"%{search}%" + + # Date filters + if date_from: + conditions.append("upp.prediction_time >= CAST(:date_from AS timestamptz)") + params["date_from"] = date_from + if date_to: + conditions.append("upp.prediction_time < CAST(:date_to AS timestamptz) + INTERVAL '1 day'") + params["date_to"] = date_to + + where_clause = "WHERE " + " AND ".join(conditions) if conditions else "" + + # Sort mapping + sort_map = { + "Date (Newest)": "upp.prediction_time DESC", + "Date (Oldest)": "upp.prediction_time ASC", + "filename": "upp.file ASC" + } + order_clause = sort_map.get(sort_by, "upp.prediction_time DESC") + + # Join with files table to get bucket/key for URL construction + query = f""" + SELECT + upp.id, + upp.file, + upp.predicted_class, + upp.confidence, + upp.watering_status, + upp.status, + snpc.key AS combined_key, + split_part(snpc.key, '/', 1) AS bucket, + regexp_replace(snpc.key, '^[^/]+/', '') AS object_key, + COALESCE(sm.file_name, snpc.file_name) AS filename, + COALESCE(sm.capture_time, snpc.linked_time) AS capture_time + FROM public.ultrasonic_plant_predictions upp + LEFT JOIN public.sound_new_plants_connections snpc + ON snpc.file_name = upp.file + LEFT JOIN public.sounds_metadata sm + ON sm.file_name = snpc.file_name + {where_clause} + ORDER BY {order_clause} + LIMIT :limit; + """ + + params["limit"] = limit + + try: + rows = repo.db_query(query, params) + results = [] + + for r in rows: + url = None + if r.get("bucket") and r.get("object_key"): + bucket = str(r["bucket"]) + key = str(r["object_key"]) + url = f"{PUBLIC_S3_BASE.rstrip('/')}/{quote(bucket, safe='')}/{quote(key, safe='/')}" + results.append({ + "id": r.get("id"), + "file": r.get("file"), + "predicted_class": r.get("predicted_class"), + "confidence": r.get("confidence"), + "watering_status": r.get("watering_status"), + "status": r.get("status"), + "device_id": ((r.get("file") or "").split("_")[0] or "Unknown"), + "url": url + }) + + return results + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + + +@router.get("/{file_id:int}", summary="Get file by ID") +def get_file_by_id(file_id: int): + row = repo.get_file_by_id(file_id) + if not row: + raise HTTPException(status_code=404, detail="not found") + return _attach_url_if_possible(row) + + +@router.get("/{bucket}", summary="List files in bucket") +def list_files_in_bucket( + bucket: str, + device_id: Optional[str] = None, + limit: int = Query(50, ge=1, le=500), +): + bucket = unquote(bucket) + rows = repo.list_files(bucket, device_id, limit) + return [_attach_url_if_possible(r) for r in rows] + + +@router.get("/{bucket}/{object_key:path}", summary="Get file by bucket/key") +def get_file(bucket: str, object_key: str): + bucket = unquote(bucket) + object_key = unquote(object_key) + row = repo.get_file(bucket, object_key) + if not row: + raise HTTPException(status_code=404, detail="not found") + return _attach_url_if_possible(row) + + +@router.put("/{bucket}/{object_key:path}", summary="Update file metadata") +def update_file(bucket: str, object_key: str, payload: FilesUpdate): + bucket = unquote(bucket) + object_key = unquote(object_key) + ok = repo.update_file(bucket, object_key, payload.model_dump(exclude_unset=True)) + if not ok: + raise HTTPException(status_code=404, detail="not found") + return {"status": "ok"} + + +@router.delete("/{bucket}/{object_key:path}", summary="Delete file") +def delete_file(bucket: str, object_key: str): + bucket = unquote(bucket) + object_key = unquote(object_key) + ok = repo.delete_file(bucket, object_key) + if not ok: + raise HTTPException(status_code=404, detail="not found") + return {"status": "deleted"} diff --git a/AgCloud/services/db_api_service/app/tables/files/schemas.py b/AgCloud/services/db_api_service/app/tables/files/schemas.py new file mode 100644 index 000000000..e3d43810e --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/files/schemas.py @@ -0,0 +1,30 @@ +# app/tables/files/schemas.py +from typing import Optional, Any, Dict +from pydantic import BaseModel, Field, NonNegativeInt + + +class FilesCreate(BaseModel): + bucket: str + object_key: str = Field(alias="object_key") + content_type: Optional[str] = None + size_bytes: Optional[NonNegativeInt] = None + etag: Optional[str] = None + mission_id: Optional[int] = None + device_id: Optional[str] = None + tile_id: Optional[str] = None + footprint: Optional[str] = None # WKT + metadata: Optional[Dict[str, Any]] = None + + class Config: + populate_by_name = True + + +class FilesUpdate(BaseModel): + content_type: Optional[str] = None + size_bytes: Optional[NonNegativeInt] = None + etag: Optional[str] = None + mission_id: Optional[int] = None + device_id: Optional[str] = None + tile_id: Optional[str] = None + footprint: Optional[str] = None + metadata: Optional[Dict[str, Any]] = None diff --git a/AgCloud/services/db_api_service/app/tables/generic/_init_.py b/AgCloud/services/db_api_service/app/tables/generic/_init_.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/tables/generic/repo.py b/AgCloud/services/db_api_service/app/tables/generic/repo.py new file mode 100644 index 000000000..5b19aa6e2 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/generic/repo.py @@ -0,0 +1,442 @@ +from __future__ import annotations +# app/tables/generic/repo.py +# Schema-first repository: load JSON contracts, build in-memory SQLAlchemy Table, +# validate payloads, and perform read/insert operations (single + batch). +from typing import Any, Dict, List, Optional +from sqlalchemy.dialects.postgresql import insert as pg_insert +from functools import lru_cache +import json +import os + +from sqlalchemy import ( + Table, + Column, + MetaData, + String, + Integer, + Boolean, + Date, + DateTime, + Float, + Numeric, + insert, + select, + asc, + desc, + update, + delete, +) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.exc import IntegrityError, SQLAlchemyError + +from jsonschema import Draft202012Validator, ValidationError as JsonSchemaValidationError + +from app.db import session_scope +from app.config import settings + +ALLOWED_TABLES = set(settings.ALLOWED_TABLES) +CONTRACTS_DIR = settings.CONTRACTS_DIR + +# -------------------- Exceptions (repo-level) -------------------- # +class RepoError(Exception): + """Base repo error carrying optional payload for HTTP layer.""" + def __init__(self, message: str, payload: Optional[dict] = None): + super().__init__(message) + self.payload = payload or {} + +class NotAllowed(RepoError): + """Raised when a requested resource/table is not allowed or not found.""" + pass + +class ValidationFailed(RepoError): + """Raised when input validation against the JSON contract fails.""" + pass + +class DbConstraintError(RepoError): + """Raised on DB integrity/constraint errors (e.g. unique violation).""" + pass + +class DbSqlError(RepoError): + """Raised on general SQL/database errors.""" + pass + +# -------------------- Contract loading / table build -------------------- # + +_CONTRACT_STORE: Optional[Any] = None + +def set_contract_store(store: Any) -> None: + """ + Inject a ContractStore instance. After injection the repo will always + read contracts from that store. Clears the _load_contract cache. + """ + global _CONTRACT_STORE + _CONTRACT_STORE = store + # clear lru cache so previously cached file-based contracts (if any) do not linger + try: + _load_contract.cache_clear() + except Exception: + pass + +@lru_cache(maxsize=256) +def _load_contract(resource: str) -> Dict[str, Any]: + """ + Load and return the JSON contract for `resource`. + Always prefer the injected ContractStore. If no store was injected, + raise NotAllowed to force explicit wiring (prevents silent file reads). + """ + # require injected store + if _CONTRACT_STORE is None: + raise NotAllowed("no contract_store injected into repo; call set_contract_store(store) during startup") + + schema = _CONTRACT_STORE.get(resource) + if not schema: + raise NotAllowed(f"Contract for '{resource}' not found in injected contract_store") + return schema + +@lru_cache(maxsize=256) +def _build_table_from_contract(resource: str) -> Table: + """ + Build an in-memory SQLAlchemy Table object from the JSON contract. + This table is used only for SQL generation (no reflection). + """ + contract = _load_contract(resource) + md = MetaData() + props = contract.get("properties", {}) + required_set = set(contract.get("required", []) or []) + cols: List[Column] = [] + for name, prop in props.items(): + coltype = _jsonschema_type_to_sqla(prop) + prop_nullable = prop.get("nullable") + if prop_nullable is not None: + nullable = bool(prop_nullable) + else: + is_required = (name in required_set) or bool(prop.get("required", False)) + nullable = not is_required + cols.append(Column(name, coltype, nullable=nullable)) + return Table(resource, md, *cols) + +# -------------------- JSON Schema -> SQLAlchemy type mapping -------------------- # +def _jsonschema_type_to_sqla(prop: dict) -> Any: + """ + Map a JSON Schema property to a SQLAlchemy column type. + Conservative defaults are used. + """ + t = (prop.get("type") or "string").lower() + fmt = (prop.get("format") or "").lower() + + if t == "integer": + return Integer + if t == "number": + # prefer Numeric for exactness if format indicates decimal + return Numeric if fmt in {"decimal", "numeric"} else Float + if t == "boolean": + return Boolean + if t == "string": + if fmt in {"date"}: + return Date + if fmt in {"date-time", "datetime"}: + return DateTime + if fmt in {"uuid"}: + return String(36) + # default string + return String + if t == "object" or t == "array": + return JSONB + # fallback + return String + +# -------------------- Validation helpers -------------------- # +def _validate_with_contract(resource: str, payload: Dict[str, Any]) -> Dict[str, Any]: + """ + Validate the payload dict against the resource JSON contract using jsonschema. + Returns the input payload (possibly cleaned) or raises ValidationFailed. + """ + contract = _load_contract(resource) + # The contract may be a full JSON Schema for an object. + validator = Draft202012Validator(contract) + try: + validator.validate(payload) + # return payload as-is; pydantic-type coercion is not applied here. + return payload + except JsonSchemaValidationError as e: + # jsonschema exception doesn't provide a simple errors() like pydantic; + # provide the message and path for debugging. + raise ValidationFailed("validation error", {"detail": str(e), "path": list(e.path)}) + +def _validate(resource: str, payload: Dict[str, Any]) -> Dict[str, Any]: + """Compatibility wrapper used by router code.""" + return _validate_with_contract(resource, payload) + +def _validate_partial_against_props(props: Dict[str, Any], payload: Dict[str, Any]) -> None: + """ + Validate only the provided fields in payload using a temporary schema + built from the contract properties for those fields. + Raises ValidationFailed on error. + """ + # build minimal schema for the provided keys + subset_props = {k: props[k] for k in payload.keys() if k in props} + schema = {"type": "object", "properties": subset_props} + validator = Draft202012Validator(schema) + try: + validator.validate(payload) + except JsonSchemaValidationError as e: + raise ValidationFailed("validation error", {"detail": str(e), "path": list(e.path)}) + +# -------------------- Public repo API -------------------- # +def describe_table(resource: str) -> Dict[str, Any]: + """ + Return the JSON contract for the resource and a lightweight columns summary. + Raises NotAllowed if missing. + """ + contract = _load_contract(resource) + # build a simple columns list from properties for convenience + props = contract.get("properties", {}) + columns = [] + for name, p in props.items(): + columns.append({ + "name": name, + "type": p.get("type"), + "format": p.get("format"), + }) + return {"table": resource, "contract": contract, "columns": columns} + +def list_rows( + resource: str, + limit: int = 50, + offset: int = 0, + order_by: Optional[str] = None, + order_dir: str = "desc", + filters: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + """ + Select rows from the resource table. + - Validates order_by against contract columns. + - Accepts simple equality filters only (key=value). + - Returns dict {rows: [...], count: n} + + Note: 'count' is the number of rows returned in this page (i.e. len(rows)), + not the total number of matching rows in the table. This avoids an extra + COUNT(*) query and keeps the endpoint faster. + Raises NotAllowed / ValidationFailed / DbSqlError + """ + table = _build_table_from_contract(resource) + allowed_cols = {c.name for c in table.columns} + + # validate order_by + if order_by and order_by not in allowed_cols: + raise ValidationFailed("invalid order_by", {"allowed": sorted(allowed_cols)}) + + # build base select + stmt = select(table).limit(limit).offset(offset) + # ordering + if order_by: + col = table.c[order_by] + stmt = stmt.order_by(asc(col) if order_dir.lower().startswith("asc") else desc(col)) + # filters (only allow known columns) + if filters: + for k, v in filters.items(): + if k not in allowed_cols: + raise ValidationFailed("invalid filter key", {"key": k}) + stmt = stmt.where(table.c[k] == v) + + try: + with session_scope() as s: + res = s.execute(stmt) + rows = [dict(r) for r in res.mappings().all()] + return {"rows": rows, "count": len(rows)} + except SQLAlchemyError as e: + raise DbSqlError("sql error", {"detail": str(e)}) + +def insert_row(resource: str, payload: Dict[str, Any], returning: str = "keys") -> Dict[str, Any]: + """ + Insert a single row into resource after validating against the contract. + Returns {"affected_rows": 1, "returning": | None}. + """ + # load contract first and use its properties to determine allowed fields + contract = _load_contract(resource) + props = contract.get("properties", {}) + allowed_cols = set(props.keys()) + + unknown = set(payload) - allowed_cols + if unknown: + raise ValidationFailed("unknown fields", {"unknown_fields": sorted(unknown)}) + + # validate payload against contract (may add defaults / coerce) + try: + valid = _validate_with_contract(resource, payload) + except ValidationFailed as e: + raise e + + # build SQLAlchemy table afterwards for SQL generation + table = _build_table_from_contract(resource) + + key_fields = contract.get("x-keyFields") or (["id"] if "id" in props else []) + + if not key_fields: + raise ValidationFailed("no key fields", {"detail": "contract has no x-keyFields and no id"}) + + # Build UPSERT statement + stmt = pg_insert(table).values(**valid) + update_fields = {k: stmt.excluded[k] for k in valid.keys() if k not in key_fields} + + stmt = stmt.on_conflict_do_update( + index_elements=key_fields, + set_=update_fields, + ).returning(*table.columns) + + try: + with session_scope() as s: + res = s.execute(stmt) + row = res.mappings().first() if res.returns_rows else None + if not row: + return {"affected_rows": 1, "returning": None} + + full = dict(row) + if returning == "full": + return {"affected_rows": 1, "returning": full} + + key_fields = contract.get("x-keyFields") or (["id"] if "id" in props else []) + keys_obj = {k: full[k] for k in key_fields if k in full} if key_fields else None + return {"affected_rows": 1, "returning": keys_obj} + except IntegrityError as e: + raise DbConstraintError("integrity error", {"detail": str(e.orig)}) + except SQLAlchemyError as e: + raise DbSqlError("sql error", {"detail": str(e)}) + +def insert_batch(resource: str, payloads: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Insert multiple rows in a single DB call where possible. + Returns {"affected_rows": n}. Raises on validation/db errors. + """ + if not payloads: + return {"affected_rows": 0} + + # determine allowed columns from contract properties first + contract = _load_contract(resource) + props = contract.get("properties", {}) + allowed_cols = set(props.keys()) + table = _build_table_from_contract(resource) + + to_insert: List[Dict[str, Any]] = [] + for p in payloads: + unknown = set(p) - allowed_cols + if unknown: + raise ValidationFailed("unknown fields in batch", {"unknown_fields": sorted(unknown)}) + try: + valid = _validate_with_contract(resource, p) + to_insert.append(valid) + except ValidationFailed as e: + raise e + + try: + with session_scope() as s: + # use bulk insert using SQLAlchemy insert with multiple parameter sets + s.execute(insert(table), to_insert) + return {"affected_rows": len(to_insert)} + except IntegrityError as e: + raise DbConstraintError("integrity error", {"detail": str(e.orig)}) + except SQLAlchemyError as e: + raise DbSqlError("sql error", {"detail": str(e)}) + +def update_row(resource: str, keys: Dict[str, Any], payload: Dict[str, Any], replace: bool = False) -> Dict[str, Any]: + """ + Update (partial or full) a row identified by keys. + - keys: mapping of key-field -> value (must include contract x-keyFields) + - payload: fields to update + - replace: if True, expect full payload and validate against full contract (PUT semantics) + Returns {"affected_rows": n, "returning": | None} + """ + contract = _load_contract(resource) + props = contract.get("properties", {}) + allowed_cols = set(props.keys()) + + if not keys or not isinstance(keys, dict): + raise ValidationFailed("missing keys", {"detail": "provide keys dict to identify the row"}) + + # ensure key fields exist in contract (prefer x-keyFields if provided) + key_fields = contract.get("x-keyFields") or [] + if not key_fields: + # fallback: try "id" or first property + if "id" in props: + key_fields = ["id"] + else: + raise ValidationFailed("no key fields", {"detail": "contract has no x-keyFields and no id"}) + + missing_keys = [k for k in key_fields if k not in keys] + if missing_keys: + raise ValidationFailed("missing key fields", {"missing": missing_keys}) + + # check unknown fields in payload + unknown = set(payload) - allowed_cols + if unknown: + raise ValidationFailed("unknown fields", {"unknown_fields": sorted(unknown)}) + + # validate payload: + if replace: + # full validation against contract (may enforce required) + try: + _validate_with_contract(resource, payload) + except ValidationFailed as e: + raise e + else: + # partial validation only for provided fields + try: + _validate_partial_against_props(props, payload) + except ValidationFailed as e: + raise e + + table = _build_table_from_contract(resource) + + # build where clause from key_fields + where_clause = None + for k in key_fields: + cond = (table.c[k] == keys[k]) + where_clause = cond if where_clause is None else (where_clause & cond) + + stmt = update(table).where(where_clause).values(**payload).returning(*table.columns) + try: + with session_scope() as s: + res = s.execute(stmt) + row = res.mappings().first() if res.returns_rows else None + affected = res.rowcount if hasattr(res, "rowcount") else (1 if row else 0) + return {"affected_rows": int(affected), "returning": dict(row) if row else None} + except IntegrityError as e: + raise DbConstraintError("integrity error", {"detail": str(e.orig)}) + except SQLAlchemyError as e: + raise DbSqlError("sql error", {"detail": str(e)}) + +def delete_row(resource: str, keys: Dict[str, Any]) -> Dict[str, Any]: + """ + Delete a row identified by keys (contract x-keyFields required). + Returns {"affected_rows": n}. + """ + contract = _load_contract(resource) + props = contract.get("properties", {}) + key_fields = contract.get("x-keyFields") or [] + if not key_fields: + if "id" in props: + key_fields = ["id"] + else: + raise ValidationFailed("no key fields", {"detail": "contract has no x-keyFields and no id"}) + + missing_keys = [k for k in key_fields if k not in keys] + if missing_keys: + raise ValidationFailed("missing key fields", {"missing": missing_keys}) + + table = _build_table_from_contract(resource) + + where_clause = None + for k in key_fields: + cond = (table.c[k] == keys[k]) + where_clause = cond if where_clause is None else (where_clause & cond) + + stmt = delete(table).where(where_clause) + try: + with session_scope() as s: + res = s.execute(stmt) + affected = res.rowcount if hasattr(res, "rowcount") else 0 + return {"affected_rows": int(affected)} + except IntegrityError as e: + raise DbConstraintError("integrity error", {"detail": str(e.orig)}) + except SQLAlchemyError as e: + raise DbSqlError("sql error", {"detail": str(e)}) diff --git a/AgCloud/services/db_api_service/app/tables/generic/router.py b/AgCloud/services/db_api_service/app/tables/generic/router.py new file mode 100644 index 000000000..21696fdc1 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/generic/router.py @@ -0,0 +1,144 @@ +from __future__ import annotations +from typing import Any, Dict, List, Optional, Literal +from fastapi import APIRouter, HTTPException, Path, Query, Depends, Request, Body + +from app.auth import require_auth +from . import repo + + +# ----- Request models ----- + +def build_generic_router(contract_store) -> APIRouter: + """ + Returns a composed router that includes: + - /tables/... endpoints (schema, list, insert, batch) + The contract_store is injected from app.main to avoid circular imports. + """ + + tables_router = APIRouter( + prefix="/tables", + tags=["generic"], + dependencies=[Depends(require_auth)], + ) + + # Map repo exceptions to HTTP responses (DRY) + def handle_repo_exceptions(e: Exception): + if isinstance(e, repo.NotAllowed): + raise HTTPException(status_code=404, detail=str(e)) + if isinstance(e, repo.ValidationFailed): + raise HTTPException(status_code=400, detail=e.payload or str(e)) + if isinstance(e, repo.DbConstraintError): + raise HTTPException(status_code=400, detail={"db_error": "integrity_error", **(e.payload or {})}) + if isinstance(e, repo.DbSqlError): + raise HTTPException(status_code=400, detail={"db_error": "sql_error", **(e.payload or {})}) + # unknown exception -> re-raise + raise e + + + # Returns the JSON contract/schema for the resource from contract_store. + @tables_router.get("/{resource}/schema") + def get_schema(resource: str = Path(..., regex=r"^[a-zA-Z_][a-zA-Z0-9_]*$")): + try: + schema = contract_store.get(resource) + if not schema: + raise repo.NotAllowed(f"Table '{resource}' not found") + return schema + except Exception as e: + handle_repo_exceptions(e) + + # List rows from the DB for the specified resource. Supports filters, ordering, pagination. + @tables_router.get("/{resource}") + def list_rows( + resource: str, + request: Request, + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0), + order_by: Optional[str] = Query(None), + order_dir: str = Query("desc", regex="^(?i:asc|desc)$") + ): + try: + # Extract user filters from query parameters (exclude pagination/order params). + filters = { + k: v for k, v in request.query_params.items() + if k not in {"limit", "offset", "order_by", "order_dir"} + } + return repo.list_rows( + resource=resource, + limit=limit, + offset=offset, + order_by=order_by, + order_dir=order_dir, + filters=filters or None, + ) + except Exception as e: + handle_repo_exceptions(e) + + # Insert a single row into the resource after validation. + @tables_router.post("/{resource}", status_code=201) + def create_row( + resource: str = Path(..., regex=r"^[a-zA-Z_][a-zA-Z0-9_]*$"), + payload: Dict[str, Any] = Body(...), + returning: Literal["keys","full"] = Query("keys") + ): + try: + return repo.insert_row(resource, payload, returning) + except Exception as e: + handle_repo_exceptions(e) + + # Insert multiple rows (batch) into the resource, validating each entry. + @tables_router.post("/{resource}/rows:batch") + def create_rows_batch( + resource: str = Path(..., regex=r"^[a-zA-Z_][a-zA-Z0-9_]*$"), + body: List[Dict[str, Any]] = Body(...), + ): + try: + return repo.insert_batch(resource, body) + except Exception as e: + handle_repo_exceptions(e) + + # Partial update (PATCH): body must include {"keys": {...}, "data": {...}} + @tables_router.patch("/{resource}") + def patch_row( + resource: str = Path(..., regex=r"^[a-zA-Z_][a-zA-Z0-9_]*$"), + body: Dict[str, Any] = Body(...), + ): + try: + keys = body.get("keys") + data = body.get("data") + if not isinstance(keys, dict) or not isinstance(data, dict): + raise HTTPException(status_code=400, detail="body must include 'keys' and 'data' objects") + return repo.update_row(resource, keys, data, replace=False) + except Exception as e: + handle_repo_exceptions(e) + + # Full replace (PUT): body must include {"keys": {...}, "data": {...}} and does full validation + @tables_router.put("/{resource}") + def put_row( + resource: str = Path(..., regex=r"^[a-zA-Z_][a-zA-Z0-9_]*$"), + body: Dict[str, Any] = Body(...), + ): + try: + keys = body.get("keys") + data = body.get("data") + if not isinstance(keys, dict) or not isinstance(data, dict): + raise HTTPException(status_code=400, detail="body must include 'keys' and 'data' objects") + return repo.update_row(resource, keys, data, replace=True) + except Exception as e: + handle_repo_exceptions(e) + + # Delete row + @tables_router.delete("/{resource}") + def delete_row( + resource: str = Path(..., regex=r"^[a-zA-Z_][a-zA-Z0-9_]*$"), + body: Dict[str, Any] = Body(...), + ): + try: + keys = body.get("keys") + if not isinstance(keys, dict): + raise HTTPException(status_code=400, detail="body must include 'keys' object") + return repo.delete_row(resource, keys) + except Exception as e: + handle_repo_exceptions(e) + + + return tables_router diff --git a/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/__init__.py b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/repo.py b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/repo.py new file mode 100644 index 000000000..4060d1d26 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/repo.py @@ -0,0 +1,40 @@ +from typing import Optional, Dict, Any, List +from sqlalchemy import text +from app.db import session_scope +from datetime import datetime + + +def list_rollups(from_ts: str | None = None, to_ts: str | None = None) -> List[Dict[str, Any]]: + q = """ + SELECT * FROM ripeness_weekly_rollups_ts + WHERE 1=1 + """ + params: Dict[str, Any] = {} + + if from_ts: + q += " AND ts >= :from_ts" + params["from_ts"] = parse_ts(from_ts) + if to_ts: + q += " AND ts <= :to_ts" + params["to_ts"] = parse_ts(to_ts) + + q += " ORDER BY ts DESC" + + with session_scope() as s: + rows = s.execute(text(q), params).mappings().all() + return [dict(r) for r in rows] + + +def get_rollup(id: int) -> Optional[Dict[str, Any]]: + """ + Retrieve a single rollup entry by ID. + """ + sql = text(""" + SELECT id, ts, window_start, window_end, fruit_type, device_id, + run_id, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, pct_ripe + FROM public.ripeness_weekly_rollups_ts + WHERE id = :id + """) + with session_scope() as s: + row = s.execute(sql, {"id": id}).mappings().first() + return dict(row) if row else None diff --git a/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/router.py b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/router.py new file mode 100644 index 000000000..c0a11db18 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/router.py @@ -0,0 +1,33 @@ + +from typing import Optional, List +from fastapi import APIRouter, HTTPException, Query +from . import schemas, repo + +router = APIRouter(prefix="/ripeness_weekly_rollups_ts", tags=["ripeness_weekly_rollups_ts"]) + +@router.get("", response_model=List[schemas.RipenessWeeklyRollupRead]) +def list_rollups( + from_ts: Optional[str] = Query(None, description="Filter from timestamp (ISO8601)"), + to_ts: Optional[str] = Query(None, description="Filter to timestamp (ISO8601)"), +): + """ + Retrieve weekly ripeness rollups by time range. + """ + try: + rows = repo.list_rollups(from_ts=from_ts, to_ts=to_ts) + return rows + except Exception as e: + print(f"[ERROR][router] list_rollups failed: {e}") + raise HTTPException(status_code=400, detail=str(e)) + + +@router.get("/{id}", response_model=schemas.RipenessWeeklyRollupOut) +def get_rollup(id: int): + """ + Retrieve a specific rollup entry by ID. + """ + row = repo.get_rollup(id) + if not row: + raise HTTPException(status_code=404, detail="Rollup not found") + return row + diff --git a/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/schemas.py b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/schemas.py new file mode 100644 index 000000000..5fbbbfce4 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/schemas.py @@ -0,0 +1,36 @@ +from datetime import datetime +from typing import Optional +from uuid import UUID +from pydantic import BaseModel, Field, conint, confloat + + +class RipenessWeeklyRollupBase(BaseModel): + """Base schema for weekly ripeness rollups table.""" + ts: Optional[datetime] = Field(None, description="Insertion timestamp") + window_start: datetime = Field(..., description="Start of weekly window") + window_end: datetime = Field(..., description="End of weekly window") + fruit_type: str = Field(..., description="Type of fruit analyzed") + device_id: Optional[str] = Field(None, description="Source device ID") + run_id: UUID = Field(..., description="Unique identifier for the run") # ? UUID instead of str + cnt_total: conint(ge=0) = Field(..., description="Total fruit count in window") + cnt_ripe: conint(ge=0) = Field(..., description="Ripe fruit count") + cnt_unripe: conint(ge=0) = Field(..., description="Unripe fruit count") + cnt_overripe: conint(ge=0) = Field(..., description="Overripe fruit count") + pct_ripe: confloat(ge=0, le=1) = Field(..., description="Ripe ratio (0-1)") + + +class RipenessWeeklyRollupCreate(RipenessWeeklyRollupBase): + """Schema used for POST inserts (single or batch).""" + pass + + +class RipenessWeeklyRollupRead(RipenessWeeklyRollupBase): + """Schema used for GET responses (includes DB ID).""" + id: int = Field(..., description="Primary key ID") + + class Config: + orm_mode = True + +class RipenessWeeklyRollupOut(RipenessWeeklyRollupBase): + id: int + diff --git a/AgCloud/services/db_api_service/app/tables/task_thresholds/_init_.py b/AgCloud/services/db_api_service/app/tables/task_thresholds/_init_.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/tables/task_thresholds/repo.py b/AgCloud/services/db_api_service/app/tables/task_thresholds/repo.py new file mode 100644 index 000000000..1fe10821f --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/task_thresholds/repo.py @@ -0,0 +1,90 @@ +from __future__ import annotations +from typing import Any, Dict, List, Tuple,Optional +from sqlalchemy import text +from app.db import session_scope + +def list_all() -> list[dict]: + q = text(""" + SELECT + task::text AS task, + label, + threshold, + updated_by, + updated_at + FROM task_thresholds + ORDER BY task, label; + """) + with session_scope() as s: + rows = s.execute(q).mappings().all() + return [dict(r) for r in rows] + +def get_one(task: str, label: Optional[str] = "") -> Optional[dict]: + # If you maintain a unique (task, label) pair – keep label; otherwise label can be ignored + q = text(""" + SELECT + task::text AS task, + label, + threshold, + updated_by, + updated_at + FROM task_thresholds + WHERE task = CAST(:task AS task_type_enum) + AND label = :label + LIMIT 1; + """) + with session_scope() as s: + row = s.execute(q, {"task": task, "label": label or ""}).mappings().first() + return dict(row) if row else None + + +def upsert_one(task: str, label: str, threshold: float, updated_by: str | None) -> None: + q = text(""" + INSERT INTO task_thresholds (task, label, threshold, updated_by) + VALUES (CAST(:task AS task_type_enum), :label, :threshold, :updated_by) + ON CONFLICT (task, label) + DO UPDATE SET + threshold = EXCLUDED.threshold, + updated_by = EXCLUDED.updated_by, + updated_at = NOW(); + """) + with session_scope() as s: + s.execute(q, { + "task": task, "label": label or "", "threshold": float(threshold), + "updated_by": updated_by, + }) + +def upsert_batch(items: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + items: [{"task": "...", "label": "", "threshold": 0.8, "updated_by": "gui"}, ...] + מחזיר {"ok": [[task,label], ...], "fail": [[[task,label], "reason"], ...]} + """ + ok: List[List[str]] = [] + fail: List[List[Any]] = [] + if not items: + return {"ok": ok, "fail": fail} + + q = text(""" + INSERT INTO task_thresholds (task, label, threshold, updated_by) + VALUES (CAST(:task AS task_type_enum), :label, :threshold, :updated_by) + ON CONFLICT (task, label) + DO UPDATE SET + threshold = EXCLUDED.threshold, + updated_by = EXCLUDED.updated_by, + updated_at = NOW(); + """) + + with session_scope() as s: + for it in items: + task = str(it.get("task", "")) + label = str(it.get("label") or "") + try: + s.execute(q, { + "task": task, + "label": label, + "threshold": float(it["threshold"]), + "updated_by": it.get("updated_by"), + }) + ok.append([task, label]) + except Exception as e: + fail.append([[task, label], str(e)]) + return {"ok": ok, "fail": fail} diff --git a/AgCloud/services/db_api_service/app/tables/task_thresholds/router.py b/AgCloud/services/db_api_service/app/tables/task_thresholds/router.py new file mode 100644 index 000000000..da842e66b --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/task_thresholds/router.py @@ -0,0 +1,43 @@ +from typing import List, Optional +from fastapi import APIRouter, HTTPException, Body, Query +from . import repo + +router = APIRouter(prefix="/task_thresholds", tags=["task_thresholds"]) + +@router.get("", response_model=List[dict]) +def list_thresholds(): + try: + return repo.list_all() # returns [{task, label, threshold, updated_by, ...}, ...] + except Exception as e: + print("[ERROR] list_thresholds failed:", e, flush=True) + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/{task}", response_model=dict) +def get_threshold(task: str): + row = repo.get_one(task) + if not row: + raise HTTPException(status_code=404, detail="task not found") + return row + +@router.post("", status_code=201) +def upsert_threshold( + task: str, + label: Optional[str] = "", + threshold: float = Body(..., embed=True), + updated_by: Optional[str] = Body(None, embed=True), +): + try: + repo.upsert_one(task, label or "", threshold, updated_by) + return {"status": "ok"} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/batch", status_code=201) +def upsert_thresholds_batch(items: List[dict] = Body(...)): + """ + items: [{"task":"ripeness","label":"","threshold":0.8,"updated_by":"gui"}, ...] + """ + try: + return repo.upsert_batch(items) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/AgCloud/services/db_api_service/app/tables/task_thresholds/schemas.py b/AgCloud/services/db_api_service/app/tables/task_thresholds/schemas.py new file mode 100644 index 000000000..71e15edc6 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tables/task_thresholds/schemas.py @@ -0,0 +1,14 @@ +from typing import Optional +from pydantic import BaseModel, Field, confloat # type: ignore + +class TaskThresholdCreate(BaseModel): + task: str # e.g. "ripeness", "disease" + label: str = Field(default="") # optional sub-type; empty = default bucket + threshold: confloat(ge=0, le=1) + updated_by: Optional[str] = None + +class TaskThresholdUpdate(BaseModel): + task: Optional[str] = None # allow renaming if needed (rare) + label: Optional[str] = None + threshold: Optional[confloat(ge=0, le=1)] = None + updated_by: Optional[str] = None diff --git a/AgCloud/services/db_api_service/app/tools/__init__.py b/AgCloud/services/db_api_service/app/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/db_api_service/app/tools/generate_contracts.py b/AgCloud/services/db_api_service/app/tools/generate_contracts.py new file mode 100644 index 000000000..343b3c6d0 --- /dev/null +++ b/AgCloud/services/db_api_service/app/tools/generate_contracts.py @@ -0,0 +1,188 @@ +# tools/generate_contracts.py +import json +import os +import sys +from pathlib import Path +from typing import Any, Dict, List, Tuple + +from sqlalchemy import create_engine, inspect +from sqlalchemy.engine.reflection import Inspector + +from app.config import settings + + +CONTRACTS_DIR: Path = Path(settings.CONTRACTS_DIR) +ALLOWED: List[str] = list(settings.ALLOWED_TABLES) +DB_URL = os.environ.get("DATABASE_URL") + +if not ALLOWED: + raise RuntimeError("[contracts-gen] ERROR: No allowed tables defined in config.py") +if not DB_URL: + raise RuntimeError("[contracts-gen] ERROR: DATABASE_URL not found in environment") + + +# Normalize a SQLAlchemy column type (or its string representation) to a canonical lowercase string +# used by subsequent type-mapping logic. +def _normalize_type_name(col_type: Any) -> str: + """ + Convert SQLAlchemy/type string to a lowercase descriptor we can pattern-match on. + """ + return str(col_type).lower() + + +# Map a database column type to a JSON Schema `type` and optional `format`. +# Returns a tuple (json_type, json_format_or_None). +def map_json_type_and_format(col_type: Any) -> Tuple[str, str | None]: + """ + Map a DB column type to (json_type, json_format?). + - integer-like -> ("integer", None) + - numeric/decimal/float -> ("number", None) + - boolean -> ("boolean", None) + - date -> ("string", "date") + - time/timestamp/datetime -> ("string", "date-time") + - default -> ("string", None) + """ + t = _normalize_type_name(col_type) + + if "json" in t: + return "object", None + + if any(k in t for k in ("int", "serial", "bigint", "smallint")): + return "integer", None + if any(k in t for k in ("float", "double", "numeric", "decimal", "real")): + return "number", None + if "bool" in t: + return "boolean", None + + # Date/Time family + if "timestamp" in t or "datetime" in t: + return "string", "date-time" + if "time" in t and "without time zone" in t: + # DBs differ—treat bare "time" conservatively + return "string", None + if "date" in t: + return "string", "date" + + # Fallback + return "string", None + + +# Inspect the database for primary key constraint and return primary key column names. +# Returns an empty list if no PK could be determined. +def _infer_key_fields(ins: Inspector, table: str, schema: str | None = None) -> List[str]: + """ + Return primary key column names if available; empty list otherwise. + """ + try: + pk = ins.get_pk_constraint(table, schema=schema) + cols = pk.get("constrained_columns") or [] + return [c for c in cols if isinstance(c, str)] + except Exception: + return [] + + +# Build a JSON Schema (Draft 2020-12) for the given DB table by introspecting its columns. +# Adds helpful extensions (x-keyFields, x-sortable, x-queryable) and a legacy readOnly list. +def build_schema_for_table(ins: Inspector, table: str, schema: str | None = None) -> Dict[str, Any]: + """ + Build a JSON Schema (Draft 2020-12) for a DB table, enriched with: + - "x-keyFields": primary key column names (if found) + - "x-sortable": true (per property, defaults; can be edited later) + - "x-queryable": true (per property, defaults; can be edited later) + - "format": "date" / "date-time" where applicable + Also exposes legacy root-level "readOnly" (list of column names) for compatibility. + """ + cols = ins.get_columns(table, schema=schema) # List[dict] + props: Dict[str, Any] = {} + required: List[str] = [] + read_only: List[str] = [] + + # Per-column properties + for c in cols: + name = c["name"] + json_type, json_format = map_json_type_and_format(c["type"]) + prop: Dict[str, Any] = {"type": json_type} + + if json_format: + prop["format"] = json_format + + # schema-first hints (can be tightened manually later) + prop["x-sortable"] = True + prop["x-queryable"] = True + + props[name] = prop + + # required if DB says non-nullable (good initial hint) + is_required = not c.get("nullable", True) + autoinc = bool(c.get("autoincrement")) + has_default = c.get("default") is not None + server_default = c.get("server_default") is not None + identity = bool(c.get("identity")) + computed = bool(c.get("computed")) + generated = bool(c.get("generated")) + + is_read_only = autoinc or has_default or server_default or identity or computed or generated + if is_read_only: + read_only.append(name) + + if is_required and not is_read_only: + required.append(name) + + # mark read-only if auto-increment or server/default exists + if c.get("autoincrement") or c.get("default") is not None or c.get("server_default") is not None: + read_only.append(name) + + # Key fields (primary key) for returning="keys" + key_fields = _infer_key_fields(ins, table, schema=schema) + + schema_json: Dict[str, Any] = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": table, + "type": "object", + "properties": props, + "required": required or [], + # compatibility with your existing usage: + "readOnly": read_only or [], + } + + if key_fields: + schema_json["x-keyFields"] = key_fields + + return schema_json + + +# Main entrypoint: generate JSON contract files for allowed tables by introspecting the DB. +# Writes one {table}.json file per allowed table into CONTRACTS_DIR. +def main() -> None: + outdir = CONTRACTS_DIR + outdir.mkdir(parents=True, exist_ok=True) + + eng = create_engine(DB_URL) + ins = inspect(eng) + + generated_files: List[str] = [] + skipped: List[str] = [] + + + for table in ALLOWED: + try: + schema_json = build_schema_for_table(ins, table) + except Exception as e: + print(f"[contracts-gen] ⚠️ Skipped table '{table}': {e}") + skipped.append(table) + continue + + out_path = outdir / f"{table}.json" + out_path.write_text( + json.dumps(schema_json, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + generated_files.append(out_path.name) + + print(f"[contracts-gen] ✅ generated {len(generated_files)} files: {generated_files}") + if skipped: + print(f"[contracts-gen] ⚠️ skipped {len(skipped)} tables: {skipped}") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/db_api_service/docker-compose.yml b/AgCloud/services/db_api_service/docker-compose.yml new file mode 100644 index 000000000..4fbff34eb --- /dev/null +++ b/AgCloud/services/db_api_service/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.9" + +volumes: + contracts: {} +services: + contracts-gen: + build: + context: . + dockerfile: app/contracts/Dockerfile + env_file: + - .env + environment: + DATABASE_URL: postgresql+psycopg://missions_user:pg123@host.docker.internal:5432/missions_db + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - contracts:/app/app/contracts + networks: [airnet] + restart: "no" + + api: + build: + context: . + env_file: + - .env + environment: + DB_DSN: postgresql+psycopg://missions_user:pg123@host.docker.internal:5432/missions_db + volumes: + - contracts:/app/app/contracts + ports: + - "8001:8001" + depends_on: + contracts-gen: + condition: service_completed_successfully + networks: [airnet] + restart: unless-stopped + +networks: + airnet: + name: airnet + driver: bridge + + diff --git a/AgCloud/services/db_api_service/requirements.txt b/AgCloud/services/db_api_service/requirements.txt new file mode 100644 index 000000000..091ea771c --- /dev/null +++ b/AgCloud/services/db_api_service/requirements.txt @@ -0,0 +1,13 @@ +python-multipart==0.0.9 +python-dotenv>=1.0.0 +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +SQLAlchemy==2.0.36 +psycopg[binary]==3.2.1 +python-jose==3.3.0 +passlib==1.7.4 +bcrypt==4.0.1 +pydantic>=2.7 +pydantic-settings>=2.0 +pathlib +jsonschema>=4.18,<5 diff --git a/AgCloud/services/db_api_service/tests/Dockerfile b/AgCloud/services/db_api_service/tests/Dockerfile new file mode 100644 index 000000000..308decd88 --- /dev/null +++ b/AgCloud/services/db_api_service/tests/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates build-essential gcc curl && \ + rm -rf /var/lib/apt/lists/* + +COPY *.crt /usr/local/share/ca-certificates/ +RUN chmod 644 /usr/local/share/ca-certificates/ && update-ca-certificates + +RUN python -m pip install --no-cache-dir --upgrade pip certifi +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DEFAULT_TIMEOUT=120 + +COPY . /app +ENV PYTHONPATH=/app + +RUN pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir pytest httpx + +ENV ENV=dev +ENV DB_DRY_RUN=1 +ENV DRY_RUN_SPOOL=/tmp/api_spool +ENV DB_DSN=sqlite+pysqlite:///:memory: +ENV PYTHONDONTWRITEBYTECODE=1 + +CMD ["pytest", "-q", "tests"] diff --git a/AgCloud/services/db_api_service/tests/README.md b/AgCloud/services/db_api_service/tests/README.md new file mode 100644 index 000000000..a96619b32 --- /dev/null +++ b/AgCloud/services/db_api_service/tests/README.md @@ -0,0 +1,24 @@ + + +## Docker (one command) + +```bash +docker build -f tests/Dockerfile -t db-api-tests . +docker run --rm -e PYTHONDONTWRITEBYTECODE=1 db-api-tests +``` + +## Locally (optional) + +```bash +python -m venv .venv && . .venv/bin/activate +pip install -r requirements.txt +pip install pytest httpx +export ENV=dev +export DB_DRY_RUN=1 +export DB_DSN="sqlite+pysqlite:///:memory:" +pytest -q tests +``` + +Notes: +- Files tests use `client_overridden_auth` to bypass auth (router dependency) and validate DRY-RUN spooling. +- Auth tests use real auth (no override) with `bootstrap_tokens` fixture. diff --git a/AgCloud/services/db_api_service/tests/conftest.py b/AgCloud/services/db_api_service/tests/conftest.py new file mode 100644 index 000000000..1ebb58510 --- /dev/null +++ b/AgCloud/services/db_api_service/tests/conftest.py @@ -0,0 +1,64 @@ +import os +import shutil +import tempfile +import pytest +from fastapi.testclient import TestClient + + +os.environ.setdefault("ENV", "dev") +os.environ.setdefault("DB_DRY_RUN", "1") +os.environ.setdefault("DB_DSN", "sqlite+pysqlite:///:memory:") + +SPOOL_BASE = tempfile.mkdtemp(prefix="api_spool_combined_") +os.environ["DRY_RUN_SPOOL"] = SPOOL_BASE + +from app.main import app +import app.auth as auth_mod + + +@pytest.fixture(scope="session") +def client_raw(): + with TestClient(app) as c: + r0 = c.get("/ready") + assert r0.status_code == 200 and r0.json().get("ready") is True + yield c + + +@pytest.fixture() +def client_overridden_auth(): + + from types import SimpleNamespace + app.dependency_overrides[auth_mod.require_auth] = lambda: ("service", SimpleNamespace(name="tests")) + try: + with TestClient(app) as c: + # גם כאן נוודא מוכנות (ליתר בטחון) + r0 = c.get("/ready") + assert r0.status_code == 200 and r0.json().get("ready") is True + yield c + finally: + app.dependency_overrides.pop(auth_mod.require_auth, None) + + +@pytest.fixture(scope="session") +def bootstrap_tokens(client_raw): + + r0 = client_raw.get("/ready") + assert r0.status_code == 200 and r0.json().get("ready") is True + + body = {"service_name": "svc-tests-unified", "rotate_if_exists": True} + r = client_raw.post("/auth/_dev_bootstrap", json=body) + assert r.status_code == 201, f"bootstrap failed: {r.status_code} {r.text}" + data = r.json() + return { + "access_token": data["tokens"]["access_token"], + "refresh_token": data["tokens"]["refresh_token"], + "service_raw": data["service_account"]["raw_token"] or data["service_account"]["token"], + "service_name": data["service_account"]["name"], + } + + +def teardown_module(module=None): + try: + shutil.rmtree(SPOOL_BASE, ignore_errors=True) + except Exception: + pass diff --git a/AgCloud/services/db_api_service/tests/test_auth_tokens.py b/AgCloud/services/db_api_service/tests/test_auth_tokens.py new file mode 100644 index 000000000..ce0aa8534 --- /dev/null +++ b/AgCloud/services/db_api_service/tests/test_auth_tokens.py @@ -0,0 +1,27 @@ +def test_jwt_access_token_allows_me(client_raw, bootstrap_tokens): + access = bootstrap_tokens["access_token"] + r = client_raw.get("/api/me", headers={"Authorization": f"Bearer {access}"}) + assert r.status_code == 200, r.text + j = r.json() + assert j.get("type") == "user" + assert "username" in j + +def test_invalid_jwt_rejected(client_raw): + r = client_raw.get("/api/me", headers={"Authorization": "Bearer not.a.valid.token"}) + assert r.status_code in (401, 403) + +def test_service_token_allows_me(client_raw, bootstrap_tokens): + raw = bootstrap_tokens["service_raw"] + r = client_raw.get("/api/me", headers={"X-Service-Token": raw}) + assert r.status_code == 200, r.text + j = r.json() + assert j.get("type") == "service" + assert isinstance(j.get("name"), str) + +def test_wrong_service_token_rejected(client_raw): + r = client_raw.get("/api/me", headers={"X-Service-Token": "totally-wrong"}) + assert r.status_code in (401, 403) + +def test_no_auth_rejected(client_raw): + r = client_raw.get("/api/me") + assert r.status_code in (401, 403) diff --git a/AgCloud/services/db_api_service/tests/test_files_endpoints.py b/AgCloud/services/db_api_service/tests/test_files_endpoints.py new file mode 100644 index 000000000..db650172c --- /dev/null +++ b/AgCloud/services/db_api_service/tests/test_files_endpoints.py @@ -0,0 +1,85 @@ +import json +import os +from pathlib import Path +from urllib.parse import quote + + +def _read_spooled(kind: str): + spool = Path(os.environ["DRY_RUN_SPOOL"]) + return sorted(spool.glob(f"*{kind}.json")) + + +def _updates_map(body: dict) -> dict: + + for key in ("fields", "updates", "set", "payload", "values", "data"): + val = body.get(key) + if isinstance(val, dict): + return val + + return {k: v for k, v in body.items() + if k not in {"bucket", "object_key", "action", "event", "timestamp", "ts", "op", "type", "table"}} + +def test_files_upsert_spools(client_overridden_auth): + payload = { + "bucket": "imagery", + "object_key": "cam-01/2025-09-14 16-00-56.png", + "content_type": "image/png", + "size_bytes": 12345, + "device_id": "cam-01", + "metadata": {"hello": "world"}, + } + r = client_overridden_auth.post("/api/files", json=payload) + assert r.status_code == 201, r.text + assert r.json().get("status") == "ok" + + files = _read_spooled("files_upsert") + assert files, "Expected spooled 'files_upsert'" + body = json.loads(files[-1].read_text(encoding="utf-8")) + + assert body["bucket"] == payload["bucket"] + assert body["object_key"] == payload["object_key"] + assert body["size_bytes"] == payload["size_bytes"] + + +def test_files_update_spools(client_overridden_auth): + bucket = "imagery" + object_key = "spa ces/obj 1.png" + url_key = quote(object_key, safe="") + + r = client_overridden_auth.put( + f"/api/files/{bucket}/{url_key}", + json={"device_id": "dev-9", "size_bytes": 77}, + ) + assert r.status_code == 200, r.text + assert r.json().get("status") == "ok" + + files = _read_spooled("files_update") + assert files, "Expected spooled 'files_update'" + body = json.loads(files[-1].read_text(encoding="utf-8")) + + assert body["bucket"] == bucket + assert body["object_key"] == object_key + + updates = _updates_map(body) + assert updates.get("device_id") == "dev-9" + assert updates.get("size_bytes") == 77 + + +def test_files_delete_spools_and_decodes_url(client_overridden_auth): + bucket = "imagery" + object_key = "with spaces/obj 2.png" + url_key = quote(object_key, safe="") + + r = client_overridden_auth.delete(f"/api/files/{bucket}/{url_key}") + assert r.status_code == 200, r.text + assert r.json().get("status") == "deleted" + + files = _read_spooled("files_delete") + assert files, "Expected spooled 'files_delete'" + body = json.loads(files[-1].read_text(encoding="utf-8")) + + assert body["bucket"] == bucket + assert body["object_key"] == object_key + + + diff --git a/AgCloud/services/db_api_service/tests/test_files_list_router_unit.py b/AgCloud/services/db_api_service/tests/test_files_list_router_unit.py new file mode 100644 index 000000000..253d751f1 --- /dev/null +++ b/AgCloud/services/db_api_service/tests/test_files_list_router_unit.py @@ -0,0 +1,19 @@ +def test_files_list_router_unit(client_overridden_auth, monkeypatch): + # Fake list_files to avoid DB + captured = {} + def fake_list_files(bucket, device_id, limit): + captured.update(bucket=bucket, device_id=device_id, limit=limit) + return [{"file_id":1,"bucket":bucket or "imagery","object_key":"a/b.png","device_id":device_id or "dev-1"}] + import app.tables.files.repo as repo_mod + monkeypatch.setattr(repo_mod, "list_files", fake_list_files, raising=True) + + from urllib.parse import quote + bucket = "with spaces" + device_id = "cam-99" + resp = client_overridden_auth.get(f"/api/files?bucket={quote(bucket)}&device_id={device_id}&limit=2") + assert resp.status_code == 200 + data = resp.json() + assert isinstance(data, list) and len(data) == 1 + assert captured["bucket"] == bucket + assert captured["device_id"] == device_id + assert captured["limit"] == 2 diff --git a/AgCloud/services/db_api_service/tests/test_health_ready.py b/AgCloud/services/db_api_service/tests/test_health_ready.py new file mode 100644 index 000000000..ab7f7ab6f --- /dev/null +++ b/AgCloud/services/db_api_service/tests/test_health_ready.py @@ -0,0 +1,9 @@ +def test_healthz(client_raw): + r = client_raw.get("/healthz") + assert r.status_code == 200 + assert r.json() == {"status": "ok"} + +def test_ready(client_raw): + r = client_raw.get("/ready") + assert r.status_code == 200 + assert r.json().get("ready") is True diff --git a/AgCloud/services/fence_hole_detector/.env.example b/AgCloud/services/fence_hole_detector/.env.example new file mode 100644 index 000000000..da2441f8b --- /dev/null +++ b/AgCloud/services/fence_hole_detector/.env.example @@ -0,0 +1,20 @@ +# Model +FENCE_ONNX_PATH=runs_fence/y8n_cpu_v1/weights/best.onnx +FENCE_CONF=0.35 +FENCE_ROI=none +FENCE_MIN_OVERLAP=0.20 +FENCE_VOTE_N=1 +FENCE_VOTE_M=1 +FENCE_VOTE_COOLDOWN=0 +FENCE_IMG_SIZE=640 + +# MinIO +MINIO_ENDPOINT=127.0.0.1:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin +MINIO_BUCKET=rover-images +MINIO_SECURE=false + +# Alert Manager (אופציונלי) +ALERT_MANAGER_URL= +ALERT_ENABLE_AUTO_POST=false diff --git a/AgCloud/services/fence_hole_detector/Dockerfile b/AgCloud/services/fence_hole_detector/Dockerfile new file mode 100644 index 000000000..86e65a336 --- /dev/null +++ b/AgCloud/services/fence_hole_detector/Dockerfile @@ -0,0 +1,41 @@ +# Base image +FROM python:3.11-slim + +# System deps for OpenCV wheels + SSL + curl +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 libglib2.0-0 ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* + +# Trust store (optional org CAs) +WORKDIR /app +COPY certs/ /app/certs/ +RUN if ls /app/certs/*.crt >/dev/null 2>&1; then \ + cp /app/certs/*.crt /usr/local/share/ca-certificates/ && update-ca-certificates; \ + else \ + echo "No custom CA certs found, skipping."; \ + fi +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +# Install Python deps early for better layer caching +COPY requirements.txt /tmp/requirements.txt +RUN python -m pip install --upgrade pip && \ + pip install --no-cache-dir -r /tmp/requirements.txt + +# Copy service sources into a package path: /app/services/fence_hole_detector +# This preserves the import path 'services.fence_hole_detector.*' +COPY . /app/services/fence_hole_detector + +# Ensure these are packages (PEP420 usually ok, but we make it explicit) +RUN mkdir -p /app/services && \ + touch /app/services/__init__.py && \ + touch /app/services/fence_hole_detector/__init__.py + +# Make sure Python can import from /app +ENV PYTHONPATH=/app + +EXPOSE 8088 + +# Run FastAPI app (module path) +CMD ["uvicorn", "services.fence_hole_detector.app:app", \ + "--host", "0.0.0.0", "--port", "8088", "--log-level", "info"] diff --git a/AgCloud/services/fence_hole_detector/__init__.py b/AgCloud/services/fence_hole_detector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/fence_hole_detector/alert_client.py b/AgCloud/services/fence_hole_detector/alert_client.py new file mode 100644 index 000000000..eb9e9d905 --- /dev/null +++ b/AgCloud/services/fence_hole_detector/alert_client.py @@ -0,0 +1,129 @@ +import json +import os +import uuid +from datetime import datetime, timezone + +from kafka import KafkaProducer +from kafka.admin import KafkaAdminClient, NewTopic +from kafka.errors import TopicAlreadyExistsError, NoBrokersAvailable + +# --- Configuration (env-first) --- +_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:9092") +_TOPIC = os.getenv("ALERTS_TOPIC", "alerts") +_ALERT_TYPE = os.getenv("ALERT_TYPE", "fence_hole") + +# You can tune these if you want faster failures during dev. +_PRODUCER_KW = dict( + bootstrap_servers=_BOOTSTRAP, + value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode("utf-8"), + linger_ms=50, + retries=5, + request_timeout_ms=10_000, + metadata_max_age_ms=15_000, +) + +_admin = None +_producer = None + + +def _iso(ts: datetime | None) -> str: + """Return RFC3339/ISO-8601 UTC string without microseconds (…Z).""" + return ( + (ts or datetime.now(timezone.utc)) + .astimezone(timezone.utc) + .replace(microsecond=0) + .isoformat() + .replace("+00:00", "Z") + ) + + +def _get_admin() -> KafkaAdminClient: + """Create a singleton admin client.""" + global _admin + if _admin is None: + _admin = KafkaAdminClient(bootstrap_servers=_BOOTSTRAP, request_timeout_ms=10_000) + return _admin + + +def _ensure_topic(topic: str, num_partitions: int = 1, replication_factor: int = 1) -> None: + """ + Idempotently create the topic if it doesn't exist. + This is required because auto-create is disabled on the broker. + """ + try: + admin = _get_admin() + new_topic = NewTopic(name=topic, num_partitions=num_partitions, + replication_factor=replication_factor) + admin.create_topics([new_topic], validate_only=False) + except TopicAlreadyExistsError: + # OK: topic already present. + return + except NoBrokersAvailable as e: + # Bubble up; the caller will log and continue the request flow. + raise e + except Exception: + # For repeated concurrent creates we may see unknown errors from the broker; + # treat them as non-fatal if the topic exists by the time we send. + pass + + +def _get_producer() -> KafkaProducer: + """Create a singleton producer.""" + global _producer + if _producer is None: + _producer = KafkaProducer(**_PRODUCER_KW) + return _producer + + +def post_alert( + *, + device_id: str, + started_at: datetime, + confidence: float, + image_url: str | None, + area: str | None = None, + lat: float | None = None, + lon: float | None = None, + severity: int | None = None, + extra: dict | None = None, +) -> str: + """ + Build and send an alert message to Kafka in the agreed schema. + Returns the generated alert_id (UUID string). + Raises if the broker is not reachable. + """ + # Ensure topic exists (safe to call on every send). + _ensure_topic(_TOPIC) + + alert_id = str(uuid.uuid4()) + payload = { + # Required + "alert_id": alert_id, + "alert_type": _ALERT_TYPE, + "device_id": device_id, + "started_at": _iso(started_at), + + # Optional/metadata + "confidence": round(float(confidence), 3), + } + if severity is not None: + payload["severity"] = int(severity) + if area: + payload["area"] = area + if lat is not None: + payload["lat"] = float(lat) + if lon is not None: + payload["lon"] = float(lon) + if image_url: + payload["image_url"] = image_url + if extra: + payload["meta"] = extra + + # Send + producer = _get_producer() + fut = producer.send(_TOPIC, payload) + # Block until acknowledged (fail fast in dev) + fut.get(timeout=10.0) + producer.flush(timeout=5.0) + + return alert_id diff --git a/AgCloud/services/fence_hole_detector/app.py b/AgCloud/services/fence_hole_detector/app.py new file mode 100644 index 000000000..5aeec7a2d --- /dev/null +++ b/AgCloud/services/fence_hole_detector/app.py @@ -0,0 +1,142 @@ +# services/fence_hole_detector/app.py +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field +from typing import Optional +import os, io, cv2, numpy as np +from minio import Minio +from datetime import datetime, timezone +from dotenv import load_dotenv + +from .infer import OnnxDetector +from .alert_client import post_alert # used only if alerts are enabled + +# Load .env next to this file if present +load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), ".env")) + +# --- Config (env-first) --- +ONNX_PATH = os.getenv("FENCE_ONNX_PATH", "best.onnx") +CONF = float(os.getenv("FENCE_CONF", "0.35")) + +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "") +MINIO_BUCKET = os.getenv("MINIO_BUCKET", "rover-images") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" +PUBLIC_BASE_URL = os.getenv("MINIO_PUBLIC_BASE_URL", "").rstrip("/") + +DEFAULT_DEVICE_ID = os.getenv("DEFAULT_DEVICE_ID", "camera-01") +DEFAULT_AREA = os.getenv("DEFAULT_AREA", "north_field") +DEFAULT_SEVERITY = int(os.getenv("DEFAULT_SEVERITY", "3")) + +# Critical: alerts flag (0/1). When 0, service will never talk to Kafka. +ALERT_ENABLED = os.getenv("ALERT_ENABLED", "0") == "1" + +# --- App objects --- +app = FastAPI(title="fence_hole_detector") + +detector = OnnxDetector( + onnx_path=ONNX_PATH, + conf=CONF, + roi=os.getenv("FENCE_ROI", "none"), + vote_n=int(os.getenv("FENCE_VOTE_N", "1")), + vote_m=int(os.getenv("FENCE_VOTE_M", "1")), + cooldown=int(os.getenv("FENCE_VOTE_COOLDOWN", "0")), +) + +minio_client = Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=MINIO_SECURE, +) + +# --- Schemas --- +class ProcessReq(BaseModel): + bucket: str = Field(default_factory=lambda: MINIO_BUCKET) + key: str + captured_at: Optional[datetime] = None # ISO, e.g. "2025-10-29T14:32:12Z" + device_id: Optional[str] = None + area: Optional[str] = None + lat: Optional[float] = None + lon: Optional[float] = None + +class ProcessResp(BaseModel): + detected_boxes: int + fired_alert: int + max_conf: float + alert_id: Optional[str] = None + image_url: Optional[str] = None + +# --- Helpers --- +def read_minio_image(bucket: str, key: str): + try: + resp = minio_client.get_object(bucket, key) + data = resp.read() + resp.close(); resp.release_conn() + except Exception as e: + raise HTTPException(status_code=404, detail=f"MinIO get_object failed: {e}") + img_array = np.frombuffer(data, np.uint8) + img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + if img is None: + raise HTTPException(status_code=415, detail="Failed to decode image") + return img + +def build_image_url(bucket: str, key: str) -> str: + # Prefer a public base URL if provided, else use presigned URL for 24h + if PUBLIC_BASE_URL: + return f"{PUBLIC_BASE_URL}/{key}" + try: + return minio_client.get_presigned_url("GET", bucket, key, expires=24*60*60) + except Exception: + scheme = "https" if MINIO_SECURE else "http" + return f"{scheme}://{MINIO_ENDPOINT}/{bucket}/{key}" + +# --- Health --- +@app.get("/health") +def health(): + return {"status": "ok"} + +# --- Main endpoint: Flink -> HTTP --- +@app.post("/process_minio", response_model=ProcessResp) +def process_minio(req: ProcessReq): + bucket = req.bucket or MINIO_BUCKET + key = req.key + if not key: + raise HTTPException(400, "key is required") + + # 1) Read image from MinIO + img = read_minio_image(bucket, key) + + # 2) Inference + det, detected, fired, max_conf = detector.infer_image(img) + + # 3) Optional alert + alert_id = None + image_url = build_image_url(bucket, key) if detected else None + + if detected and ALERT_ENABLED: + try: + started_at = req.captured_at or datetime.now(timezone.utc) + alert_id = post_alert( + device_id=req.device_id or DEFAULT_DEVICE_ID, + started_at=started_at, + confidence=max_conf, + image_url=image_url, + area=req.area or DEFAULT_AREA, + lat=req.lat, lon=req.lon, + severity=DEFAULT_SEVERITY, + extra={"bucket": bucket, "key": key}, + ) + except Exception as e: + # Never fail the request because of the alert pipeline + # (log prints will appear in the Uvicorn console) + print(f"[WARN] alert send failed: {e}") + + # Note: fired_alert == 1 means "detected now" (no voting when camera batches) + return ProcessResp( + detected_boxes=int(len(det)), + fired_alert=int(1 if detected else 0), + max_conf=float(max_conf), + alert_id=alert_id, + image_url=image_url, + ) diff --git a/AgCloud/services/fence_hole_detector/config.py b/AgCloud/services/fence_hole_detector/config.py new file mode 100644 index 000000000..6ed24dbc1 --- /dev/null +++ b/AgCloud/services/fence_hole_detector/config.py @@ -0,0 +1,26 @@ +import os + +# --- Model / Inference --- +ONNX_PATH = os.getenv("FENCE_ONNX_PATH", "runs_fence/y8n_cpu_v1/weights/best.onnx") +CONF = float(os.getenv("FENCE_CONF", "0.35")) + +# ROI: "none" or "ymin-ymax" ("0.20-0.85") +ROI = os.getenv("FENCE_ROI", "none") +MIN_OVERLAP = float(os.getenv("FENCE_MIN_OVERLAP", "0.20")) + +VOTE_N = int(os.getenv("FENCE_VOTE_N", "1")) +VOTE_M = int(os.getenv("FENCE_VOTE_M", "1")) +VOTE_COOLDOWN = int(os.getenv("FENCE_VOTE_COOLDOWN", "0")) + +IMG_SIZE = int(os.getenv("FENCE_IMG_SIZE", "640")) + +# --- MinIO --- +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "") +MINIO_BUCKET = os.getenv("MINIO_BUCKET", "") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" + +# --- Alert Manager --- +ALERT_MANAGER_URL = os.getenv("ALERT_MANAGER_URL", "") # http://localhost:9093/api/v2/alerts +ALERT_ENABLE_AUTO_POST = os.getenv("ALERT_ENABLE_AUTO_POST", "false").lower() == "true" diff --git a/AgCloud/services/fence_hole_detector/infer.py b/AgCloud/services/fence_hole_detector/infer.py new file mode 100644 index 000000000..cfc4873ee --- /dev/null +++ b/AgCloud/services/fence_hole_detector/infer.py @@ -0,0 +1,253 @@ +import cv2 +import numpy as np +import onnxruntime as ort +from collections import deque +from typing import List, Tuple +from .config import ( + CONF, ROI, MIN_OVERLAP, VOTE_N, VOTE_M, VOTE_COOLDOWN, IMG_SIZE, ONNX_PATH +) + +# ----------------------- image utilities ----------------------- # + +def letterbox(im: np.ndarray, new_shape: int = 640, color=(114, 114, 114)) -> np.ndarray: + """ + Resize with unchanged aspect ratio (scale) and pad to a square canvas. + Padding is placed on the right/bottom only (top-left anchored). + Returns a (new_shape, new_shape, 3) uint8 image. + """ + h, w = im.shape[:2] + r = min(new_shape / h, new_shape / w) + nh, nw = int(round(h * r)), int(round(w * r)) + im_resized = cv2.resize(im, (nw, nh), interpolation=cv2.INTER_LINEAR) + canvas = np.full((new_shape, new_shape, 3), color, dtype=np.uint8) + canvas[:nh, :nw] = im_resized + return canvas + +def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray: + """Convert [x, y, w, h] to [x1, y1, x2, y2].""" + x, y, w, h = np.split(xywh, 4, axis=1) + x1 = x - w / 2.0 + y1 = y - h / 2.0 + x2 = x + w / 2.0 + y2 = y + h / 2.0 + return np.concatenate([x1, y1, x2, y2], axis=1) + +# ----------------------- ONNX output normalization ----------------------- # + +def _to_xywh_conf_cls(raw: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """ + Normalize raw ONNX output to (A, 5 or more) and return: + - xywh_conf: (A, 5) float32 = [x, y, w, h, conf] + - cls_idx: (A, 1) float32 = best class id (0 when single-class) + Supports typical Ultralytics shapes: + (1, 5, 8400), (1, 6, 8400), (1, 84, 8400), (A, 5/6/84) and post-NMS (N, 6). + """ + arr = raw + + # If it is already post-NMS of shape (N, 6): [x1,y1,x2,y2,conf,cls] -> convert back to xywh for uniformity + if arr.ndim == 2 and arr.shape[1] == 6 and (arr.dtype == np.float32 or arr.dtype == np.float64): + # We will treat it specially in the caller (no further NMS required). + # Return a marker by sending empty xywh; caller detects via None. + return None, None # type: ignore[return-value] + + # Squeeze batch dim if present + if arr.ndim == 3 and arr.shape[0] == 1: + arr = np.squeeze(arr, axis=0) # (C, A) + elif arr.ndim == 4 and arr.shape[0] == 1: + arr = np.squeeze(arr, axis=0) + + # If layout is (C, A), transpose to (A, C) + if arr.ndim == 2 and arr.shape[0] in (5, 6, 84): + arr = arr.T # (A, C) + + if arr.ndim != 2: + raise ValueError(f"Unexpected ONNX output shape {raw.shape}") + + A, C = arr.shape + arr = arr.astype(np.float32) + + if C == 5: + # [x,y,w,h,conf] single-class + xywh_conf = arr[:, :5] + cls_idx = np.zeros((A, 1), dtype=np.float32) + elif C == 6: + # Either [x,y,w,h,obj,cls] for single-class or already has 2 confidences. + obj = arr[:, 4:5] + cls_score = arr[:, 5:6] + conf = obj * cls_score + xywh_conf = np.concatenate([arr[:, :4], conf], axis=1) + cls_idx = np.zeros((A, 1), dtype=np.float32) # single-class + elif C > 6: + # [x,y,w,h,obj, cls0..clsN] + obj = arr[:, 4:5] + cls_scores = arr[:, 5:] + cls_idx = np.argmax(cls_scores, axis=1, keepdims=True).astype(np.float32) + max_cls = np.max(cls_scores, axis=1, keepdims=True) + conf = obj * max_cls + xywh_conf = np.concatenate([arr[:, :4], conf], axis=1) + else: + raise ValueError(f"Unsupported channel count C={C} in ONNX output") + + return xywh_conf, cls_idx + +# ----------------------- NMS utilities ----------------------- # + +def _iou_xyxy(box: np.ndarray, boxes: np.ndarray) -> np.ndarray: + """Compute IoU between one box [x1,y1,x2,y2] and many boxes.""" + x1 = np.maximum(box[0], boxes[:, 0]) + y1 = np.maximum(box[1], boxes[:, 1]) + x2 = np.minimum(box[2], boxes[:, 2]) + y2 = np.minimum(box[3], boxes[:, 3]) + + inter = np.maximum(0.0, x2 - x1) * np.maximum(0.0, y2 - y1) + area1 = (box[2] - box[0]) * (box[3] - box[1]) + area2 = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) + union = np.maximum(area1 + area2 - inter, 1e-9) + return inter / union + +def nms_xyxy(boxes: np.ndarray, scores: np.ndarray, iou_th: float = 0.45, topk: int = 300) -> List[int]: + """ + Greedy NMS. boxes: (N,4) in xyxy, scores: (N,) + Returns list of kept indices. + """ + idxs = scores.argsort()[::-1] + keep: List[int] = [] + while idxs.size > 0: + i = int(idxs[0]) + keep.append(i) + if len(keep) >= topk or idxs.size == 1: + break + ious = _iou_xyxy(boxes[i], boxes[idxs[1:]]) + idxs = idxs[1:][ious < iou_th] + return keep + +# ----------------------- ROI helpers ----------------------- # + +def overlap_ratio_with_roi(box: np.ndarray, H: int, ymin_frac: float, ymax_frac: float) -> float: + x1, y1, x2, y2 = box + box_h = max(0.0, y2 - y1) + if box_h <= 0: + return 0.0 + roi_y1 = ymin_frac * H + roi_y2 = ymax_frac * H + inter_h = max(0.0, min(y2, roi_y2) - max(y1, roi_y1)) + return inter_h / (box_h + 1e-9) + +def filter_boxes_by_roi(boxes: np.ndarray, H: int, ymin_frac: float, ymax_frac: float, + min_overlap: float = 0.20) -> np.ndarray: + # boxes: ndarray [N,6] = x1,y1,x2,y2,conf,cls + keep = [] + for b in boxes: + if overlap_ratio_with_roi(b[:4], H, ymin_frac, ymax_frac) >= min_overlap: + keep.append(b) + return np.array(keep, dtype=np.float32) if keep else np.empty((0, 6), dtype=np.float32) + +# ----------------------- vote logic ----------------------- # + +class VoteNM: + def __init__(self, N=2, M=3, cooldown_frames=10): + self.N = N + self.M = M + self.buf = deque(maxlen=M) + self.curr = 0 + self.cooldown = cooldown_frames + + def update(self, detected: bool) -> bool: + if self.curr > 0: + self.curr -= 1 + self.buf.append(1 if detected else 0) + if len(self.buf) == self.M and sum(self.buf) >= self.N and self.curr == 0: + self.curr = self.cooldown + return True + return False + +# ----------------------- detector ----------------------- # + +class OnnxDetector: + def __init__( + self, + onnx_path=ONNX_PATH, + img_size=IMG_SIZE, + conf=CONF, + roi=ROI, + min_overlap=MIN_OVERLAP, + vote_n=VOTE_N, + vote_m=VOTE_M, + cooldown=VOTE_COOLDOWN, + ): + self.session = ort.InferenceSession(onnx_path, providers=["CPUExecutionProvider"]) + self.img_size = int(img_size) + self.conf = float(conf) + self.voter = VoteNM(vote_n, vote_m, cooldown) + self.use_roi = False + self.ymin = self.ymax = 0.0 + if isinstance(roi, str) and roi.lower() != "none": + y0, y1 = map(float, roi.split("-")) + self.use_roi = True + self.ymin = y0 + self.ymax = y1 + + self.input_name = self.session.get_inputs()[0].name + self.out_name = self.session.get_outputs()[0].name # usually "output0" + + def infer_image(self, img_bgr: np.ndarray) -> Tuple[np.ndarray, bool, bool, float]: + H, W = img_bgr.shape[:2] + canvas = letterbox(img_bgr, self.img_size) + + # Build input blob (NCHW, normalized to [0,1]) + x = canvas.transpose(2, 0, 1)[None].astype(np.float32) / 255.0 + + # ONNX forward + raw = self.session.run([self.out_name], {self.input_name: x})[0] + + # Detect whether output is already post-NMS (N,6). If yes, use it directly. + det: np.ndarray + if raw.ndim == 2 and raw.shape[1] == 6: + det = raw.astype(np.float32) + # det currently in canvas coordinates; we will rescale below. + else: + # Normalize to (A,5) and best class id + xywh_conf, cls_idx = _to_xywh_conf_cls(raw) + # Confidence filter + mask = xywh_conf[:, 4] >= self.conf + xywh_conf = xywh_conf[mask] + cls_idx = cls_idx[mask] + if xywh_conf.size == 0: + det = np.empty((0, 6), dtype=np.float32) + else: + # Convert to xyxy + xyxy = xywh_to_xyxy(xywh_conf[:, :4]) + scores = xywh_conf[:, 4] + + # NMS + keep = nms_xyxy(xyxy, scores, iou_th=0.45, topk=300) + xyxy = xyxy[keep] + scores = scores[keep] + cls_idx = cls_idx[keep] + + det = np.concatenate( + [xyxy.astype(np.float32), + scores.reshape(-1, 1).astype(np.float32), + cls_idx.astype(np.float32)], + axis=1 + ) # (N, 6) + + # Map boxes from canvas (img_size) back to original (H, W) + if det.size > 0: + r = min(self.img_size / H, self.img_size / W) # same ratio used in letterbox() + det[:, [0, 2]] = det[:, [0, 2]] / r + det[:, [1, 3]] = det[:, [1, 3]] / r + # Clip to image bounds + det[:, 0::2] = np.clip(det[:, 0::2], 0, W - 1e-3) + det[:, 1::2] = np.clip(det[:, 1::2], 0, H - 1e-3) + + # Optional ROI filtering on original image scale + if self.use_roi and det.size > 0: + det = filter_boxes_by_roi(det, H, self.ymin, self.ymax, min_overlap=MIN_OVERLAP) + + detected = det.size > 0 + fired = self.voter.update(detected) + max_conf = float(det[:, 4].max()) if detected else 0.0 + + # Return: boxes [x1,y1,x2,y2,conf,cls], detected flag, fired flag, max confidence + return det.astype(np.float32), detected, fired, max_conf diff --git a/AgCloud/services/fence_hole_detector/minio_io.py b/AgCloud/services/fence_hole_detector/minio_io.py new file mode 100644 index 000000000..456e79860 --- /dev/null +++ b/AgCloud/services/fence_hole_detector/minio_io.py @@ -0,0 +1,25 @@ +from minio import Minio +from io import BytesIO +import cv2 +import numpy as np +from config import MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_SECURE + +def get_minio_client(): + return Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=MINIO_SECURE + ) + +def load_image_from_minio(bucket: str, key: str) -> np.ndarray: + client = get_minio_client() + resp = client.get_object(bucket, key) + data = resp.read() + resp.close() + resp.release_conn() + arr = np.frombuffer(data, np.uint8) + img = cv2.imdecode(arr, cv2.IMREAD_COLOR) + if img is None: + raise RuntimeError("Failed to decode image from MinIO object") + return img diff --git a/AgCloud/services/fence_hole_detector/requirements.txt b/AgCloud/services/fence_hole_detector/requirements.txt new file mode 100644 index 000000000..5a0ccb8cb --- /dev/null +++ b/AgCloud/services/fence_hole_detector/requirements.txt @@ -0,0 +1,9 @@ +fastapi>=0.110,<0.116 +uvicorn[standard]>=0.23,<0.32 +kafka-python>=2.0.2,<3 +minio>=7.1.17,<8 +python-dotenv>=1.0.1,<2 +pydantic>=2.6,<3 +numpy>=1.26,<2 +opencv-python-headless>=4.8.0.76,<4.10 +onnxruntime>=1.17,<1.20 diff --git a/AgCloud/services/fence_hole_detector/weights/best.onnx b/AgCloud/services/fence_hole_detector/weights/best.onnx new file mode 100644 index 000000000..fddf21ac1 Binary files /dev/null and b/AgCloud/services/fence_hole_detector/weights/best.onnx differ diff --git a/AgCloud/services/flink_writer_db/Dockerfile.flink b/AgCloud/services/flink_writer_db/Dockerfile.flink new file mode 100644 index 000000000..bd043ed8e --- /dev/null +++ b/AgCloud/services/flink_writer_db/Dockerfile.flink @@ -0,0 +1,46 @@ +FROM flink:1.20.0-scala_2.12-java11 +USER root + +# Copy certs dir (may be empty) and trust *.crt if present +COPY certs/ /tmp/certs/ + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* && \ + if [ -d /tmp/certs ] && ls /tmp/certs/*.crt >/dev/null 2>&1; then \ + cp /tmp/certs/*.crt /usr/local/share/ca-certificates/ && \ + chmod 644 /usr/local/share/ca-certificates/*.crt && \ + update-ca-certificates; \ + else \ + echo "No extra CA certs found. Skipping CA update."; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Python & tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +RUN pip install --upgrade pip certifi && \ + pip install --no-cache-dir --prefer-binary apache-flink==2.1.0 requests urllib3 + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar && \ + curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +RUN mkdir -p /opt/app/secrets && chmod -R 777 /opt/app +WORKDIR /opt/app +COPY app.py /opt/app/app.py + +ENV PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python \ + PYTHONPATH=/opt/app + +CMD ["bash", "-lc", "python app.py"] diff --git a/AgCloud/services/flink_writer_db/README.txt b/AgCloud/services/flink_writer_db/README.txt new file mode 100644 index 000000000..6eb24d039 --- /dev/null +++ b/AgCloud/services/flink_writer_db/README.txt @@ -0,0 +1,61 @@ + +# flink_writer_db + +Flink job that listens to Kafka topics and forwards each message to your DB API at: + POST /api/ +with the original message body (JSON). + +## Quick start + +1) Put your `*.crt` files next to `Dockerfile.flink` (required for HTTPS trust). +2) Ensure you have an external Docker network named `ag_cloud` and that your Kafka (`kafka:9092`) and DB API service (`db_api_service:8001`) are reachable on it. +3) Build & run: + ```bash + docker compose up -d --build + ``` + +## Environment + +- `KAFKA_BROKERS` (default `kafka:9092`) +- `TOPICS` (comma-separated; default `files`) +- `DB_API_BASE` (default `http://db_api_service:8001`) +- `DB_API_AUTH_MODE` (default `service` → uses `X-Service-Token` header) +- `DB_API_SERVICE_NAME` (default `flink-writer-db` for token bootstrap) +- `DB_API_TOKEN_FILE` (path to persist service token; default `/app/secrets/db_api_token`) +- `DUMMY_DB` (set `1` to log-only without calling the API) + +## Logs & Debug + +Follow logs: +```bash +docker logs -f flink_writer_db +``` + +Enter container shell: +```bash +docker exec -it flink_writer_db bash +``` + +Expected log lines when things work: +``` +[FLINK] Listening on topic: files +[DB] wrote to files ✅ +``` + +If a message is not valid JSON, you will see: +``` +[WARN] skip invalid JSON on topic=files: '...' +``` + +If API is not reachable or times out, you'll see warnings or errors like: +``` +[DB][WARN] API not reachable (http://db_api_service:8001): ... +[DB][WARN] API timeout (http://db_api_service:8001): ... +[DB][ERROR] ... +``` + +## Notes + +- Token bootstrap uses `POST {DB_API_BASE}/auth/_dev_bootstrap` with `{"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": true}` + and stores the token at `DB_API_TOKEN_FILE`. Header is `X-Service-Token` when `DB_API_AUTH_MODE=service`. +- To listen to multiple topics, set e.g. `TOPICS=files,alerts,devices`. diff --git a/AgCloud/services/flink_writer_db/app.py b/AgCloud/services/flink_writer_db/app.py new file mode 100644 index 000000000..30c4834ab --- /dev/null +++ b/AgCloud/services/flink_writer_db/app.py @@ -0,0 +1,170 @@ +import os, json, pathlib, requests +from pyflink.common import Types +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import KafkaSource +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy +from pyflink.common import Configuration +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +# ---------- ENV ---------- +DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001") +DB_API_AUTH_MODE = os.getenv("DB_API_AUTH_MODE", "service") +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/opt/app/secrets/db_api_token") +DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "flink-writer-db") +DUMMY_DB = int(os.getenv("DUMMY_DB", "0")) == 1 + +KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") +TOPICS = [t.strip() for t in os.getenv("TOPICS", "sensor_anomalies,alerts,image_new_aerial_connections,sound_new_sounds_connections,sound_new_plants_connections,sounds_metadata,sounds_ultra_metadata").split(",") if t.strip()] + +# ---------- Token Bootstrap ---------- +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + + +def _read_token_from_file(path: str) -> str | None: + try: + p = pathlib.Path(path) + if p.exists(): + t = p.read_text(encoding="utf-8").strip() + return t or None + except Exception: + pass + return None + + +def _write_token_to_file(path: str, token: str) -> None: + p = pathlib.Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + + +def _fetch_token_via_dev_bootstrap(base: str, retries: int = 3, backoff: float = 0.8) -> str | None: + url = _safe_join_url(base, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": True} + for attempt in range(1, retries + 1): + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code not in (200, 201): + import time + time.sleep(backoff * attempt) + continue + data = r.json() + raw = (data.get("service_account", {}) or {}).get("raw_token") \ + or (data.get("service_account", {}) or {}).get("token") + if raw and isinstance(raw, str) and raw.strip() and "***" not in raw: + return raw.strip() + except Exception: + import time + time.sleep(backoff * attempt) + return None + + +def get_or_bootstrap_token() -> str | None: + token = _read_token_from_file(DB_API_TOKEN_FILE) + if token: + return token + if not DB_API_BASE: + print("[BOOTSTRAP][WARN] DB_API_BASE not set; cannot bootstrap token.", flush=True) + return None + token = _fetch_token_via_dev_bootstrap(DB_API_BASE) + if token: + _write_token_to_file(DB_API_TOKEN_FILE, token) + print(f"[BOOTSTRAP] wrote service token to {DB_API_TOKEN_FILE}", flush=True) + return token + print("[BOOTSTRAP][ERROR] Failed to obtain service token (dev bootstrap).", flush=True) + return None + + +# ---------- HTTP client ---------- +_http = requests.Session() +svc_token = get_or_bootstrap_token() +if svc_token: + if DB_API_AUTH_MODE == "service": + _http.headers.update({"X-Service-Token": svc_token}) + else: + _http.headers.update({"Authorization": f"Bearer {svc_token}"}) +_http.headers.update({"Content-Type": "application/json"}) +_http.mount("http://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) +_http.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) + + +# ---------- DB Writer ---------- +def write_to_db_api(table: str, payload: dict) -> bool: + if DUMMY_DB: + print(f"[DB-DUMMY] would POST to {DB_API_BASE or 'N/A'}/api/{table}: {json.dumps(payload, ensure_ascii=False)[:250]}", flush=True) + return True + + if not DB_API_BASE: + print("[DB][WARN] DB_API_BASE not set; skipping DB write.", flush=True) + return False + + base = DB_API_BASE.rstrip("/") + try: + r = _http.post(f"{base}/api/tables/{table}", json=payload, timeout=10) + if 200 <= r.status_code < 300: + print(f"[DB] wrote to {table} ", flush=True) + return True + print(f"[DB] POST failed ({table}): {r.status_code} {r.text[:200]}", flush=True) + return False + except requests.ConnectionError as e: + print(f"[DB][WARN] API not reachable ({base}): {e}", flush=True) + except requests.Timeout as e: + print(f"[DB][WARN] API timeout ({base}): {e}", flush=True) + except requests.RequestException as e: + print(f"[DB][ERROR] {e}", flush=True) + return False + + +# ---------- Flink Job ---------- +def handle_message(topic: str, raw: str) -> bool: + try: + data = json.loads(raw) + except json.JSONDecodeError: + print(f"[WARN] skip invalid JSON on topic={topic}: {raw[:150]!r}", flush=True) + return False + ok = write_to_db_api(topic, data) + return ok + + +def main(): + # env = StreamExecutionEnvironment.get_execution_environment() + # env.set_parallelism(int(os.getenv("FLINK_PARALLELISM", "1"))) + + # cfg = env.get_configuration() + # cfg.set_string("restart-strategy", "fixed-delay") + # cfg.set_string("restart-strategy.fixed-delay.attempts", "9999") + # cfg.set_string("restart-strategy.fixed-delay.delay", "5 s") + + conf = Configuration() + conf.set_string("restart-strategy", "fixed-delay") + conf.set_string("restart-strategy.fixed-delay.attempts", "9999") + conf.set_string("restart-strategy.fixed-delay.delay", "5 s") + + env = StreamExecutionEnvironment.get_execution_environment(conf) + env.set_parallelism(int(os.getenv("FLINK_PARALLELISM", "1"))) + + for topic in TOPICS: + print(f"[FLINK] Listening on topic: {topic}", flush=True) + + source = ( + KafkaSource.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_topics(topic) + .set_group_id(f"flink-writer-db-{topic}") + .set_value_only_deserializer(SimpleStringSchema()) + + .set_property("allow.auto.create.topics", "true") + .set_property("metadata.max.age.ms", "10000") + .build() + ) + + ds = env.from_source(source, WatermarkStrategy.no_watermarks(), f"kafka-{topic}") + ds.map(lambda raw, t=topic: handle_message(t, raw), output_type=Types.BOOLEAN()).print() + + env.execute("flink-writer-db") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/AgCloud/services/flink_writer_db/docker-compose.yml b/AgCloud/services/flink_writer_db/docker-compose.yml new file mode 100644 index 000000000..376c39b88 --- /dev/null +++ b/AgCloud/services/flink_writer_db/docker-compose.yml @@ -0,0 +1,28 @@ + +version: "3.9" + +services: + flink_writer_db: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink_writer_db + environment: + - KAFKA_BROKERS=kafka:9092 + - TOPICS=files,image_new_aerial_connections,sound_new_sounds_connections,sound_new_plants_connections,sounds_metadata,sounds_ultra_metadata,alerts + - DB_API_BASE=http://db_api_service:8001 + - DB_API_AUTH_MODE=service + - DB_API_SERVICE_NAME=flink-writer-db + - DB_API_TOKEN_FILE=/app/secrets/db_api_token + - FLINK_PARALLELISM=1 + # depends_on: + # - kafka + # - db_api_service + networks: + - ag_cloud + restart: unless-stopped + + +networks: + ag_cloud: + external: true diff --git a/AgCloud/services/fruit-orchestration/Dockerfile.airflow b/AgCloud/services/fruit-orchestration/Dockerfile.airflow new file mode 100644 index 000000000..7f5e87016 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/Dockerfile.airflow @@ -0,0 +1,29 @@ +# Dockerfile.airflow (Option A: bake scripts into the image) +FROM apache/airflow:2.9.3-python3.11 + +USER root + +# 1) Base setup: certificates + dos2unix (useful for Windows) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates dos2unix \ + && rm -rf /var/lib/apt/lists/* + +# 2) Netfree certificate (optional) — if file is missing, skip without failing +COPY certs/netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN test -s /usr/local/share/ca-certificates/netfree-ca.crt \ + && update-ca-certificates || echo "No Netfree CA - skipping" + +# 3) Create directories and set permissions +RUN mkdir -p /opt/airflow/dags /opt/airflow/scripts /opt/airflow/plugins /opt/airflow/logs \ + && chown -R airflow:0 /opt/airflow \ + && chmod -R g=u /opt/airflow + +# 4) Copy project scripts into the image +# Note: source path is ./scripts/ (not airflow/scripts) +COPY --chown=airflow:0 scripts/ /opt/airflow/scripts/ + +# 5) Convert CRLF to LF and make scripts executable +RUN dos2unix /opt/airflow/scripts/*.sh || true \ + && chmod +x /opt/airflow/scripts/*.sh || true + +USER airflow diff --git a/AgCloud/services/fruit-orchestration/dags/ag_compose_scheduler.py b/AgCloud/services/fruit-orchestration/dags/ag_compose_scheduler.py new file mode 100644 index 000000000..643eda5d5 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/dags/ag_compose_scheduler.py @@ -0,0 +1,191 @@ +# dags/run_external_composes.py + +from airflow import DAG +from airflow.operators.bash import BashOperator +from airflow.operators.empty import EmptyOperator +from airflow.operators.python import BranchPythonOperator, get_current_context +from airflow.models import Variable +from datetime import datetime +import pendulum + +# Use Israel local timezone for correct weekday calculation +TZ = pendulum.timezone("Asia/Jerusalem") + +default_args = {"owner": "airflow"} + +with DAG( + dag_id="run_external_composes", + start_date=datetime(2024, 1, 1, tzinfo=TZ), + schedule="@daily", + catchup=False, + default_args=default_args, +) as dag: + + # ---------- Helpers to run docker compose (down / up-detached / curl / batch) ---------- + + def compose_down_task(task_id, service_dir, project, compose_file="docker-compose.yml"): + # Runs "docker compose down" via docker/compose image; also removes default network if left over + cmd = f""" + set -euo pipefail + cd {service_dir} + COMPOSE_IMAGE="docker/compose:latest" + + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --volumes-from "$HOSTNAME" \ + -w "{service_dir}" \ + $COMPOSE_IMAGE \ + -f {compose_file} -p {project} down -v --remove-orphans || true + + docker network rm {project}_default || true + """ + return BashOperator(task_id=task_id, bash_command=cmd) + + def compose_up_detached_task(task_id, service_dir, project, env_file=None, compose_file="docker-compose.yml"): + # Brings services up in detached mode after cleaning previous resources + env_part = f"--env-file {env_file}" if env_file else "" + cmd = f""" + set -euo pipefail + cd {service_dir} + export DOCKER_BUILDKIT=1 + COMPOSE_IMAGE="docker/compose:latest" + + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --volumes-from "$HOSTNAME" -w "{service_dir}" $COMPOSE_IMAGE -f {compose_file} -p {project} down -v --remove-orphans || true + docker network rm {project}_default || true + + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --volumes-from "$HOSTNAME" \ + -w "{service_dir}" \ + $COMPOSE_IMAGE \ + -f {compose_file} -p {project} {env_part} up -d --build + """ + return BashOperator(task_id=task_id, bash_command=cmd) + + def compose_run_batch_task(task_id, service_dir, project, service_name, env_file=None, compose_file="docker-compose.yml"): + # Runs a single service as a batch job; waits for completion and propagates its exit code + env_part = f"--env-file {env_file}" if env_file else "" + cmd = f""" + set -euo pipefail + cd {service_dir} + export DOCKER_BUILDKIT=1 + COMPOSE_IMAGE="docker/compose:latest" + + # Clean before run + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --volumes-from "$HOSTNAME" -w "{service_dir}" $COMPOSE_IMAGE -f {compose_file} -p {project} down -v --remove-orphans || true + docker network rm {project}_default || true + + # Run batch (non-detached) and return the service exit code to Airflow + docker run --rm \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --volumes-from "$HOSTNAME" \ + -w "{service_dir}" \ + $COMPOSE_IMAGE \ + -f {compose_file} -p {project} {env_part} up --build --abort-on-container-exit --exit-code-from {service_name} + rc=$? + + # Cleanup after batch + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --volumes-from "$HOSTNAME" -w "{service_dir}" $COMPOSE_IMAGE -f {compose_file} -p {project} down -v --remove-orphans + exit $rc + """ + return BashOperator(task_id=task_id, bash_command=cmd) + + def curl_inside_network_task(task_id, url, docker_network): + # Waits for a known HTTP endpoint inside a Docker network, then performs the POST + cmd = f""" + set -euo pipefail + + # Wait for service health check (max ~120 seconds) + for i in $(seq 1 60); do + if docker run --rm --network {docker_network} curlimages/curl:8.10.1 -fsS http://ripeness-api:8088/healthz >/dev/null; then + echo "ripeness-api is healthy" + break + fi + echo "waiting for ripeness-api..." + sleep 2 + done + + # Actual POST request + docker run --rm --network {docker_network} curlimages/curl:8.10.1 \ + --retry 5 --retry-delay 2 --fail-with-body \ + -X POST {url} + echo + """ + return BashOperator(task_id=task_id, bash_command=cmd) + + # ---------- Daily classifier job (runs normally every day) ---------- + run_classifier = BashOperator( + task_id="run_classifier_compose", + bash_command=""" + set -euo pipefail + cd /opt/services/classifier + export DOCKER_BUILDKIT=1 + COMPOSE_IMAGE="docker/compose:latest" + + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --volumes-from "$HOSTNAME" -w "/opt/services/classifier" $COMPOSE_IMAGE -f docker-compose.yml -p classifier_job down -v --remove-orphans || true + docker network rm classifier_job_default || true + + # Run batch (not detached, wait for completion) + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --volumes-from "$HOSTNAME" -w "/opt/services/classifier" $COMPOSE_IMAGE -f docker-compose.yml -p classifier_job --env-file .env up --build --abort-on-container-exit --exit-code-from batch + rc=$? + + # Cleanup after batch + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --volumes-from "$HOSTNAME" -w "/opt/services/classifier" $COMPOSE_IMAGE -f docker-compose.yml -p classifier_job down -v --remove-orphans + exit $rc + """ + ) + + # ---------- Weekly branch: only continue on specific weekday ---------- + def should_post(): + """Run the weekly chain only on the selected weekday; otherwise skip.""" + ctx = get_current_context() + now_local = ctx["logical_date"].in_timezone(TZ) + + # Variable allows custom weekday (1=Mon .. 7=Sun), default=2 (Tuesday) + target_dow = int(Variable.get("ripeness_weekly_dow", default_var="3")) + print(f"[weekly_gate] logical_date={now_local}, isoweekday={now_local.isoweekday()}, target={target_dow}") + return "ripeness_up" if now_local.isoweekday() == target_dow else "skip_weekly" + + weekly_gate = BranchPythonOperator( + task_id="weekly_gate", + python_callable=should_post, + ) + + skip_weekly = EmptyOperator(task_id="skip_weekly") + + # ---------- Ripeness chain (runs only on the weekly day) ---------- + ripeness_up = compose_up_detached_task( + task_id="ripeness_up", + service_dir="/opt/services/ripeness", + project="ripeness_job", + env_file=".env", + compose_file="docker-compose.yml" + ) + + ripeness_call = curl_inside_network_task( + task_id="ripeness_call_predict_last_week", + url="http://ripeness-api:8088/predict-last-week", + docker_network="ag_cloud" # Must match the external Docker network name + ) + + ripeness_down = compose_down_task( + task_id="ripeness_down", + service_dir="/opt/services/ripeness", + project="ripeness_job", + compose_file="docker-compose.yml" + ) + + # ---------- Fruit ripeness alert (weekly batch run, runs AFTER ripeness chain) ---------- + alerts_run = compose_run_batch_task( + task_id="alerts_run", + service_dir="/opt/services/fruit_ripeness_alert", + project="fruit_ripeness_alert_job", + service_name="fruit_ripeness_alert", # Service name from the compose file + env_file=None, # Set ".env" if you store secrets there + compose_file="docker-compose.yml" + ) + + # ---------- Dependencies ---------- + run_classifier >> weekly_gate + weekly_gate >> ripeness_up >> ripeness_call >> ripeness_down >> alerts_run + weekly_gate >> skip_weekly diff --git a/AgCloud/services/fruit-orchestration/docker-compose.yml b/AgCloud/services/fruit-orchestration/docker-compose.yml new file mode 100644 index 000000000..7c1786778 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/docker-compose.yml @@ -0,0 +1,104 @@ +version: "3.8" + +services: + postgres: + image: postgres:15 + container_name: fruit-postgres + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - postgres-db-volume:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 20 + networks: + - default + - agcloud_net + + airflow-init: + build: + context: . + dockerfile: Dockerfile.airflow + args: + ADD_NETFREE_CA: ${ADD_NETFREE_CA} + depends_on: + postgres: + condition: service_healthy + environment: + AIRFLOW__CORE__EXECUTOR: LocalExecutor + AIRFLOW__CORE__LOAD_EXAMPLES: "false" + AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: ${AIRFLOW__DATABASE__SQL_ALCHEMY_CONN} + AIRFLOW__WEBSERVER__SECRET_KEY: ${AIRFLOW__WEBSERVER__SECRET_KEY} + AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW__CORE__FERNET_KEY} + _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME} + _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD} + volumes: + - ./dags:/opt/airflow/dags + - ./services:/opt/services:ro + - ./scripts:/opt/airflow/scripts:ro + - ./plugins:/opt/airflow/plugins + - ./logs:/opt/airflow/logs + - /var/run/docker.sock:/var/run/docker.sock + command: > + bash -lc "airflow db migrate && airflow users create --username ${_AIRFLOW_WWW_USER_USERNAME} --firstname Admin --lastname User --role Admin --email admin@example.org --password ${_AIRFLOW_WWW_USER_PASSWORD} || true" + + airflow-webserver: + build: + context: . + dockerfile: Dockerfile.airflow + args: + ADD_NETFREE_CA: ${ADD_NETFREE_CA} + depends_on: + postgres: + condition: service_healthy + environment: + AIRFLOW__CORE__EXECUTOR: LocalExecutor + AIRFLOW__CORE__LOAD_EXAMPLES: "false" + AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: ${AIRFLOW__DATABASE__SQL_ALCHEMY_CONN} + AIRFLOW__WEBSERVER__SECRET_KEY: ${AIRFLOW__WEBSERVER__SECRET_KEY} + AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW__CORE__FERNET_KEY} + ports: + - "8085:8080" + volumes: + - ./dags:/opt/airflow/dags + - ./services:/opt/services + - ./scripts:/opt/airflow/scripts + - ./plugins:/opt/airflow/plugins + - ./logs:/opt/airflow/logs + - /var/run/docker.sock:/var/run/docker.sock + command: webserver + + airflow-scheduler: + build: + context: . + dockerfile: Dockerfile.airflow + args: + ADD_NETFREE_CA: ${ADD_NETFREE_CA} + depends_on: + postgres: + condition: service_healthy + environment: + AIRFLOW__CORE__EXECUTOR: LocalExecutor + AIRFLOW__CORE__LOAD_EXAMPLES: "false" + AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: ${AIRFLOW__DATABASE__SQL_ALCHEMY_CONN} + AIRFLOW__CORE__FERNET_KEY: ${AIRFLOW__CORE__FERNET_KEY} + volumes: + - ./dags:/opt/airflow/dags + - ./scripts:/opt/airflow/scripts + - /var/run/docker.sock:/var/run/docker.sock + - ./services:/opt/services + - ./plugins:/opt/airflow/plugins + - ./logs:/opt/airflow/logs + command: scheduler + +volumes: + postgres-db-volume: + +networks: + agcloud_net: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/services/fruit-orchestration/scripts/run_classifier.sh b/AgCloud/services/fruit-orchestration/scripts/run_classifier.sh new file mode 100644 index 000000000..52ecd6475 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/scripts/run_classifier.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# נעביר משתנים דרך env מה־BashOperator +HOST_DATA_DIR="${HOST_DATA_DIR:-/data}" +IMAGES_DIR="${IMAGES_DIR:-/data/images}" +RUN_ID="${RUN_ID:-manual}" + +echo "[run_classifier] start | RUN_ID=${RUN_ID} | IMAGES_DIR=${IMAGES_DIR}" + +# הדגמה: כאן מריצים את הפרויקט שלך (python, poetry, וכו') +# לדוגמה: +# python -m fruit_classifier.batch --images "${IMAGES_DIR}" --run-id "${RUN_ID}" + +# כרגע רק נכתוב פלט לדוגמה ללוגים: +echo "[run_classifier] processing images under ${IMAGES_DIR} ..." +sleep 1 +echo "[run_classifier] done." + +exit 0 diff --git a/AgCloud/services/fruit-orchestration/scripts/run_ripeness.sh b/AgCloud/services/fruit-orchestration/scripts/run_ripeness.sh new file mode 100644 index 000000000..ae098fd5b --- /dev/null +++ b/AgCloud/services/fruit-orchestration/scripts/run_ripeness.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "${AIRFLOW_HOME}/services/ripeness" +docker compose up --build --abort-on-container-exit --exit-code-from ripeness diff --git a/AgCloud/services/fruit-orchestration/services/classifier/.dockerignore b/AgCloud/services/fruit-orchestration/services/classifier/.dockerignore new file mode 100644 index 000000000..77b0c25d3 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/.dockerignore @@ -0,0 +1,12 @@ +.git +__pycache__ +*.pyc +*.pyo +*.log +*.DS_Store +venv +.venv +node_modules +dist +build +samples diff --git a/AgCloud/services/fruit-orchestration/services/classifier/.gitignore b/AgCloud/services/fruit-orchestration/services/classifier/.gitignore new file mode 100644 index 000000000..09568e037 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/.gitignore @@ -0,0 +1,2 @@ +.env +.netfree-ca.crt \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/classifier/Dockerfile b/AgCloud/services/fruit-orchestration/services/classifier/Dockerfile new file mode 100644 index 000000000..48234e0cc --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/Dockerfile @@ -0,0 +1,76 @@ +# ========================= +# Stage 1: Build Python wheels +# ========================= +FROM python:3.11-slim AS builder + +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG NO_PROXY +ARG TORCH_INDEX_URL="https://download.pytorch.org/whl/cpu" + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + TZ=UTC + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* + +# <<< Add: Organization CA certificate >>> +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN update-ca-certificates + +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +COPY requirements.txt . + +RUN pip install --upgrade pip +RUN pip install --no-cache-dir --index-url ${TORCH_INDEX_URL} \ + torch==2.3.1 torchvision==0.18.1 + +# Exclude torch and torchvision from the second requirements installation +RUN grep -viE '^(torch|torchvision)=' requirements.txt > req-no-torch.txt && \ + pip wheel --no-cache-dir --wheel-dir=/wheels -r req-no-torch.txt + + +# ========================= +# Stage 2: Runtime environment +# ========================= +FROM python:3.11-slim + +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG NO_PROXY + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + TZ=UTC \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# <<< Add: Same CA certificate in runtime stage >>> +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN update-ca-certificates + +# Install dependencies from Stage 1 (including torch) + wheels +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels + + +# Copy project files +COPY . /app + +# Create non-root user +RUN useradd -m appuser +USER appuser + +# Default command: run the batch inference +CMD ["python", "-m", "inference.infer_minio_batch"] diff --git a/AgCloud/services/fruit-orchestration/services/classifier/README.md b/AgCloud/services/fruit-orchestration/services/classifier/README.md new file mode 100644 index 000000000..bc4853e6a --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/README.md @@ -0,0 +1,85 @@ +🥭 Fruit Classification – Inference Service + +This service performs batch inference for fruit classification using a trained PyTorch model. +It connects to MinIO for input images and logs results into PostgreSQL. + +⚙️ 1. Environment Configuration + +Create a .env file in the project root with the following variables: + +# --- MinIO Connection --- +S3_ENDPOINT=http://host.docker.internal:9000 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_SECURE=false +S3_BUCKET=classification +S3_PREFIX=samples + +# --- Model & Config Paths --- +WEIGHTS_PATH=models/best.pt +LABELS_PATH=models/labels.json +CFG_PATH=configs/fruit_cls.yaml + +# --- Database Connection --- +DATABASE_URL=postgresql://missions_user:pg123@host.docker.internal:5432/missions_db?sslmode=disable + + +🧠 Tip: +If your PostgreSQL or MinIO services run on external servers, update host.docker.internal to the relevant hostname or IP. + +🔒 2. Certificates (Optional) + +If your environment requires SSL interception (e.g., behind filtered networks like NetFree), +add the certificate file (e.g. netfree-ca.crt) to the project root — +it will be installed into the Docker image automatically. + +🐳 3. Build the Docker Image +docker compose build + +▶️ 4. Run the Service +docker compose up + + +This will: + +Load the model and configuration. + +Fetch images from MinIO (s3://classification/samples/...). + +Perform inference. + +Write classification results to the PostgreSQL inference_logs table. + +✅ 5. Prerequisites Checklist + +Before running the service, make sure you have: + +Component Requirement +PostgreSQL A database named missions_db containing a table inference_logs. +MinIO Bucket: classification, prefix: samples/. +Data Folder Structure samples//// containing image files. + +Example MinIO path: + +classification/ + └── samples/ + └── 2025/ + └── 10/ + └── 15/ + ├── apple1.png + ├── freshGrape (7).jpg + └── ... + +🧩 Example Output +[INFO] s3://classification/samples/2025/10/15/ | secure=False +{"object": "samples/2025/10/15/apple1.png", "fruit_type": "Apple", "score": 0.9258} +{"object": "samples/2025/10/15/freshOrange.png", "fruit_type": "Orange", "score": 0.9123} +[DONE] processed=25 | date=2025-10-15 + +🛠️ Notes + +The service automatically retries MinIO connections. + +Database inserts are skipped if the connection fails (with a warning). + +To rebuild dependencies or configuration, use docker compose up --build. \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/classifier/configs/fruit_cls.yaml b/AgCloud/services/fruit-orchestration/services/classifier/configs/fruit_cls.yaml new file mode 100644 index 000000000..ef0495fc2 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/configs/fruit_cls.yaml @@ -0,0 +1,46 @@ +seed: 42 +device: cpu # 'cpu' or 'cuda' if available +num_workers: 4 + +# Data +image_size: 224 +batch_size: 64 +val_batch_size: 128 +train_split: 0.9 # if building split from MinIO set + +# Model +backbone: mobilenet_v3_small # options: mobilenet_v3_small, efficientnet_b0, clip_mini +pretrained: true +freeze_backbone: false +num_epochs: 15 +learning_rate: 0.001 +weight_decay: 0.0001 +label_smoothing: 0.1 + +# Augmentations (baseline: torchvision) +augment: + brightness: 0.3 + contrast: 0.3 + saturation: 0.2 + hue: 0.05 + random_resized_crop_scale: [0.7, 1.0] + random_erasing_p: 0.25 + horizontal_flip_p: 0.5 + color_jitter_p: 0.8 + +# MinIO / dataset +s3: + endpoint: ${S3_ENDPOINT} + access_key: ${S3_ACCESS_KEY} + secret_key: ${S3_SECRET_KEY} + secure: ${S3_SECURE} + bucket: ${S3_BUCKET} + prefix: ${S3_PREFIX} + artifacts_bucket: ${ARTIFACTS_BUCKET} + artifacts_prefix: ${ARTIFACTS_PREFIX} + data_cache_dir: ${DATA_CACHE_DIR} + +# Few-shot add-class (optional, classification via nearest-centroid in embedding space) +few_shot: + enabled: false + shots_per_class: 5 diff --git a/AgCloud/services/fruit-orchestration/services/classifier/docker-compose.yml b/AgCloud/services/fruit-orchestration/services/classifier/docker-compose.yml new file mode 100644 index 000000000..43ae27229 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/docker-compose.yml @@ -0,0 +1,16 @@ +version: "3.8" +services: + batch: + build: + context: . + dockerfile: Dockerfile + args: + TORCH_INDEX_URL: "https://download.pytorch.org/whl/cpu" + container_name: fruit-batch + env_file: .env + # volumes: + # - .:/app + restart: "no" + command: ["python", "-m", "inference.infer_minio_batch", "--limit", "100"] + + diff --git a/AgCloud/services/fruit-orchestration/services/classifier/inference/infer_minio_batch.py b/AgCloud/services/fruit-orchestration/services/classifier/inference/infer_minio_batch.py new file mode 100644 index 000000000..8941ad39b --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/inference/infer_minio_batch.py @@ -0,0 +1,202 @@ +import argparse +import io +import json +import os +from datetime import date, datetime +from pathlib import Path +from typing import Iterable, Tuple + +import torch +import torch.nn.functional as F +from PIL import Image +from dotenv import load_dotenv +from minio import Minio +from minio.deleteobjects import DeleteObject +from time import perf_counter + + +from inference.utils_infer import build_infer_transforms, load_model + +# Optional: DB logging (graceful if missing) +try: + from metrics_db.db import insert_inference_log + DB_AVAILABLE = True +except Exception: + DB_AVAILABLE = False + + +def make_minio_client(endpoint: str, access_key: str, secret_key: str, + secure: bool) -> Minio: + ep = endpoint.replace("http://", "").replace("https://", "") + return Minio(ep, access_key=access_key, secret_key=secret_key, secure=secure) + + +def list_images(client: Minio, bucket: str, prefix: str) -> Iterable[str]: + exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} + for obj in client.list_objects(bucket, prefix=prefix, recursive=True): + name = obj.object_name + if Path(name).suffix.lower() in exts: + yield name + + +def load_labels(labels_path: str) -> Tuple[dict, dict]: + with open(labels_path, "r", encoding="utf-8") as f: + labels_map = json.load(f) + if all(isinstance(k, str) for k in labels_map.keys()): + idx_to_class = {int(v): str(k) for k, v in labels_map.items()} + else: + idx_to_class = {int(k): str(v) for k, v in labels_map.items()} + return labels_map, idx_to_class + + +def build_cfg(cfg_path: str) -> dict: + import yaml + with open(cfg_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + + +def classify_bytes(model, tfms, idx_to_class: dict, file_bytes: bytes): + + img = Image.open(io.BytesIO(file_bytes)).convert("RGB") + x = tfms(img).unsqueeze(0) + model.eval() + with torch.no_grad(): + logits = model(x) + probs = F.softmax(logits, dim=1) + score, pred = probs.max(dim=1) + return idx_to_class[int(pred.item())], float(score.item()) + + +def build_prefix(base_prefix: str, dt: date) -> str: + yyyy = f"{dt.year:04d}" + mm = f"{dt.month:02d}" + dd = f"{dt.day:02d}" + return f"{base_prefix.strip('/')}/{yyyy}/{mm}/{dd}/" + + +def build_minio_url(endpoint: str, bucket: str, object_name: str) -> str: + ep = endpoint if endpoint.startswith("http") else f"http://{endpoint}" + return f"{ep.rstrip('/')}/{bucket}/{object_name}" + + +def main(): + load_dotenv() + + p = argparse.ArgumentParser( + description="Batch inference from MinIO by YYYY/MM/DD folder" + ) + p.add_argument("--date", type=str, default=date.today().isoformat(), + help="YYYY-MM-DD (default: today)") + p.add_argument("--year", type=int, help="Alternative to --date: year") + p.add_argument("--month", type=int, help="Alternative to --date: month") + p.add_argument("--day", type=int, help="Alternative to --date: day") + p.add_argument("--dry-run", action="store_true", + help="Print only, do not write DB") + p.add_argument("--limit", type=int, default=0, + help="Limit number of images (0 = no limit)") + p.add_argument("--delete-ok", action="store_true", + help="Delete images from MinIO after success") + p.add_argument("--weights", type=str, + default=os.getenv("WEIGHTS_PATH", "models/best.pt")) + p.add_argument("--labels", type=str, + default=os.getenv("LABELS_PATH", "models/labels.json")) + p.add_argument("--cfg", type=str, + default=os.getenv("CFG_PATH", "configs/fruit_cls.yaml")) + args = p.parse_args() + + # Load cfg/model + cfg = build_cfg(args.cfg) + res = load_model(args.weights, args.labels, + backbone=cfg.get("backbone", "mobilenet_v3_small")) + model = res[0] if isinstance(res, tuple) else res + _, idx_to_class = load_labels(args.labels) + tfms = build_infer_transforms(cfg.get("image_size", 224)) + + # MinIO settings (ENV > YAML) + endpoint = os.getenv("S3_ENDPOINT", cfg["s3"]["endpoint"]) + access_key = os.getenv("S3_ACCESS_KEY", cfg["s3"]["access_key"]) + secret_key = os.getenv("S3_SECRET_KEY", cfg["s3"]["secret_key"]) + secure = str(os.getenv("S3_SECURE", cfg["s3"].get("secure", False)) + ).lower() == "true" + bucket = os.getenv("S3_BUCKET", cfg["s3"]["bucket"]) + base_prefix = os.getenv("S3_PREFIX", cfg["s3"].get("base_prefix", "images")) + + # Pick date + if args.year and args.month and args.day: + run_date = date(args.year, args.month, args.day) + else: + run_date = datetime.strptime(args.date, "%Y-%m-%d").date() + + day_prefix = build_prefix(base_prefix, run_date) + client = make_minio_client(endpoint, access_key, secret_key, secure) + + print(f"[INFO] s3://{bucket}/{day_prefix} | secure={secure}") + count, to_delete = 0, [] + + for object_name in list_images(client, bucket, day_prefix): + if args.limit and count >= args.limit: + break + + # Download + try: + resp = client.get_object(bucket, object_name) + data = resp.read() + resp.close() + resp.release_conn() + except Exception as e: + print(f"[WARN] download failed: {object_name} | {e}") + continue + + + try: + # Classify + + t0 = perf_counter() + cls, score = classify_bytes(model, tfms, idx_to_class, data) + t_ms = (perf_counter() - t0) * 1000.0 + + cls, score = classify_bytes(model, tfms, idx_to_class, data) + print(json.dumps({ + "object": object_name, + "fruit_type": cls, + "score": round(score, 4), + })) + + # DB log + if DB_AVAILABLE and not args.dry_run and os.getenv("DATABASE_URL"): + try: + image_url = build_minio_url(endpoint, bucket, object_name) + insert_inference_log( + model_backbone=cfg.get("backbone", "mobilenet_v3_small"), + image_size=cfg.get("image_size", 224), + fruit_type=str(cls), + score=float(score), + latency_ms=float(t_ms), # <<< לא None + client_ip=f"minio-batch:{run_date.isoformat()}", + error=None, + image_url=image_url, + ) + + except Exception as db_e: + print(f"[WARN] DB insert failed: {db_e}") + + # Delete after success if requested + if args.delete_ok: + to_delete.append(DeleteObject(object_name)) + + except Exception as e: + print(f"[ERR] classify failed: {object_name} | {e}") + + count += 1 + + # Bulk delete + if to_delete: + errors = client.remove_objects(bucket, to_delete) + for err in errors: + print(f"[WARN] delete failed: {err.object_name}: {err}") + + print(f"[DONE] processed={count} | date={run_date.isoformat()}") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit-orchestration/services/classifier/inference/service.py b/AgCloud/services/fruit-orchestration/services/classifier/inference/service.py new file mode 100644 index 000000000..b2b993279 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/inference/service.py @@ -0,0 +1,138 @@ +import os +import io +import json +import time +from typing import Tuple + +import torch +import torch.nn.functional as F +from PIL import Image + +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import Response +from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST + +from dotenv import load_dotenv +load_dotenv() + +# --- מדדים --- +REQS = Counter("inference_requests_total", "Total inference requests") +ERRS = Counter("inference_errors_total", "Total inference errors") +LATENCY = Histogram("inference_latency_seconds", "Inference latency per image (seconds)") +LOADED = Gauge("model_loaded", "Model loaded (1=yes)") + +from inference.utils_infer import build_infer_transforms, load_model +from metrics_db.db import insert_inference_log + +app = FastAPI() + +MODEL = None +LABELS = None +TFMS = None +CFG = None + +def _load_labels(labels_path: str): + with open(labels_path, "r", encoding="utf-8") as f: + d = json.load(f) + idx_to_class = {int(v): k for k, v in d.items()} + return d, idx_to_class + +def preprocess_image(file_bytes: bytes) -> torch.Tensor: + img = Image.open(io.BytesIO(file_bytes)).convert("RGB") + x = TFMS(img) # [C,H,W] + x = x.unsqueeze(0) # [1,C,H,W] + return x + +def run_inference(model: torch.nn.Module, x: torch.Tensor, idx_to_class: dict) -> Tuple[str, float]: + model.eval() + with torch.no_grad(): + logits = model(x) + probs = F.softmax(logits, dim=1) + score, pred = probs.max(dim=1) # [1] + cls_idx = int(pred.item()) + cls_name = idx_to_class[cls_idx] + return cls_name, float(score.item()) + +@app.on_event("startup") +def on_startup(): + """ + Load config, model, labels, and transforms once at startup. + Supports load_model(...) returning either: + - model + - (model, labels_map) where labels_map is {class_name: idx} or {idx: class_name} + """ + global MODEL, LABELS, TFMS, CFG + + weights_path = os.environ["WEIGHTS_PATH"] + labels_path = os.environ["LABELS_PATH"] + cfg_path = os.environ["CFG_PATH"] + + import yaml + with open(cfg_path, "r", encoding="utf-8") as f: + CFG = yaml.safe_load(f) + + # --- load model (support both signatures) --- + res = load_model(weights_path, labels_path, backbone=CFG.get("backbone", "mobilenet_v3_small")) + if isinstance(res, tuple): + MODEL, labels_map = res + else: + MODEL = res + # fallback: load labels_map from file + with open(labels_path, "r", encoding="utf-8") as lf: + labels_map = json.load(lf) + + # labels_map can be {class_name: idx} OR {idx: class_name}. + # Normalize to idx_to_class: {int(idx): class_name} + if all(isinstance(k, str) for k in labels_map.keys()): + # assume {class_name: idx} + idx_to_class = {int(v): str(k) for k, v in labels_map.items()} + else: + # assume {idx: class_name} + idx_to_class = {int(k): str(v) for k, v in labels_map.items()} + + LABELS = labels_map + app.state.idx_to_class = idx_to_class + + # --- transforms --- + TFMS = build_infer_transforms(image_size=CFG.get("image_size", 224)) + + # ready + LOADED.set(1) + print("Startup OK | model loaded:", type(MODEL).__name__, + "| classes:", len(app.state.idx_to_class)) + + +@app.post("/infer") +async def infer(file: UploadFile = File(...)): + start = time.time() + try: + raw = await file.read() + x = preprocess_image(raw) + pred, score = run_inference(MODEL, x, app.state.idx_to_class) + REQS.inc() + latency_ms = (time.time() - start) * 1000.0 + LATENCY.observe(latency_ms / 1000.0) + + + try: + insert_inference_log( + model_backbone=CFG.get("backbone", "mobilenet_v3_small"), + image_size=CFG.get("image_size", 224), + fruit_type=str(pred), + score=float(score), + latency_ms=float(latency_ms), + client_ip="127.0.0.1", + error=None, + ) + except Exception as db_e: + print(f"[WARN] DB insert failed: {db_e}") + + return {"fruit_type": pred, "score": score, "latency_ms": round(latency_ms, 2)} + + except Exception as e: + ERRS.inc() + raise HTTPException(status_code=500, detail=f"Inference failed: {e}") + +@app.get("/metrics") +def metrics(): + return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST) diff --git a/AgCloud/services/fruit-orchestration/services/classifier/inference/utils_infer.py b/AgCloud/services/fruit-orchestration/services/classifier/inference/utils_infer.py new file mode 100644 index 000000000..ef39c5bef --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/inference/utils_infer.py @@ -0,0 +1,46 @@ +import io +import json +from pathlib import Path +from typing import Dict, Tuple + +import torch +from PIL import Image +from torchvision import models, transforms + +def build_infer_transforms(image_size: int): + return transforms.Compose([ + transforms.Resize(int(image_size * 1.14)), + transforms.CenterCrop(image_size), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]), + ]) + +def load_model(weights_path: Path, labels_json: Path, backbone: str, device: str = "cpu"): + with open(labels_json, "r", encoding="utf-8") as f: + class_to_idx = json.load(f) + idx_to_class = {v: k for k, v in class_to_idx.items()} + + if backbone == "mobilenet_v3_small": + m = models.mobilenet_v3_small() + m.classifier[-1] = torch.nn.Linear(m.classifier[-1].in_features, len(idx_to_class)) + elif backbone == "efficientnet_b0": + m = models.efficientnet_b0() + m.classifier[-1] = torch.nn.Linear(m.classifier[-1].in_features, len(idx_to_class)) + else: + raise ValueError(f"Unsupported backbone: {backbone}") + + state = torch.load(weights_path, map_location=device) + m.load_state_dict(state["model"]) + m.eval().to(device) + return m, idx_to_class + +def infer_image_bytes(model, idx_to_class: Dict[int, str], img_bytes: bytes, + tfms, device: str = "cpu") -> Tuple[str, float]: + img = Image.open(io.BytesIO(img_bytes)).convert("RGB") + x = tfms(img).unsqueeze(0).to(device) + with torch.no_grad(): + logits = model(x) + probs = torch.softmax(logits, dim=1).cpu().numpy()[0] + idx = int(probs.argmax()) + return idx_to_class[idx], float(probs[idx]) diff --git a/AgCloud/services/fruit-orchestration/services/classifier/metrics_db/__init__.py b/AgCloud/services/fruit-orchestration/services/classifier/metrics_db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/fruit-orchestration/services/classifier/metrics_db/db.py b/AgCloud/services/fruit-orchestration/services/classifier/metrics_db/db.py new file mode 100644 index 000000000..5170bb6d0 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/metrics_db/db.py @@ -0,0 +1,110 @@ +# metrics_db/db.py +import os +from typing import Any, Dict, Optional +import psycopg2 +from psycopg2.extensions import connection as PGConnection +from dotenv import load_dotenv + + +def _get_conn() -> PGConnection: + """ + Open a PostgreSQL connection. No DDL here. + Priority: + 1) DATABASE_URL + 2) discrete env vars (DB_HOST, DB_PORT, ...) + """ + load_dotenv() + + dsn = (os.environ.get("DATABASE_URL") or "").strip() + if dsn: + return psycopg2.connect(dsn) + + host = os.environ.get("DB_HOST", "localhost") + port = int(os.environ.get("DB_PORT", "5432")) + name = os.environ.get("DB_NAME", "fruitdb") + user = os.environ.get("DB_USER", "fruituser") + password = os.environ.get("DB_PASSWORD", "fruitpass") + sslmode = os.environ.get("DB_SSLMODE", "disable") # prod: require/verify-full + + return psycopg2.connect( + host=host, + port=port, + dbname=name, + user=user, + password=password, + sslmode=sslmode, + application_name=os.environ.get("DB_APP_NAME", "metrics_service"), + connect_timeout=int(os.environ.get("DB_CONNECT_TIMEOUT", "10")), + ) + + +def insert_training_run(rec: Dict[str, Any]) -> None: + """ + Expects keys: + backbone, image_size, num_epochs, train_split, top1_acc, best_top1_acc, + artifacts_bucket, artifacts_prefix, labels_object, best_ckpt_object, + metrics_object, cm_object, seed + All columns must already exist in table training_runs. + """ + sql = """ + INSERT INTO training_runs ( + backbone, image_size, num_epochs, train_split, top1_acc, best_top1_acc, + artifacts_bucket, artifacts_prefix, labels_object, best_ckpt_object, + metrics_object, cm_object, seed + ) + VALUES (%(backbone)s, %(image_size)s, %(num_epochs)s, %(train_split)s, + %(top1_acc)s, %(best_top1_acc)s, %(artifacts_bucket)s, + %(artifacts_prefix)s, %(labels_object)s, %(best_ckpt_object)s, + %(metrics_object)s, %(cm_object)s, %(seed)s) + """ + with _get_conn() as conn, conn.cursor() as cur: + cur.execute(sql, rec) + conn.commit() + + +def insert_inference_log( + *, + model_backbone: str, + image_size: int, + fruit_type: str, + score: float, + latency_ms: Optional[float] = None, + client_ip: Optional[str] = None, + error: Optional[str] = None, + image_url: Optional[str] = None, +) -> None: + """ + Inserts a single inference log record. Table 'inference_logs' must exist. + """ + sql = """ + INSERT INTO inference_logs ( + fruit_type, score, latency_ms, model_backbone, image_size, + client_ip, error, image_url + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """ + with _get_conn() as conn, conn.cursor() as cur: + cur.execute( + sql, + ( + fruit_type, + score, + latency_ms, + model_backbone, + image_size, + client_ip, + error, + image_url, + ), + ) + conn.commit() + + +def db_healthcheck() -> bool: + """Simple reachability check.""" + try: + with _get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT 1;") + return cur.fetchone() == (1,) + except Exception: + return False diff --git a/AgCloud/services/fruit-orchestration/services/classifier/models/best.pt b/AgCloud/services/fruit-orchestration/services/classifier/models/best.pt new file mode 100644 index 000000000..81a91795b Binary files /dev/null and b/AgCloud/services/fruit-orchestration/services/classifier/models/best.pt differ diff --git a/AgCloud/services/fruit-orchestration/services/classifier/models/labels.json b/AgCloud/services/fruit-orchestration/services/classifier/models/labels.json new file mode 100644 index 000000000..83b09546f --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/models/labels.json @@ -0,0 +1,15 @@ +{ + "Apple": 0, + "Banana": 1, + "Grape": 2, + "Guava": 3, + "Jujube": 4, + "Mango": 5, + "Orange": 6, + "Pomegranate": 7, + "Strawberry": 8, + "lemon": 9, + "peach": 10, + "pear": 11, + "plum": 12 +} \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/classifier/requirements.txt b/AgCloud/services/fruit-orchestration/services/classifier/requirements.txt new file mode 100644 index 000000000..13d19cd80 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/classifier/requirements.txt @@ -0,0 +1,19 @@ +torch==2.3.1 +torchvision==0.18.1 +numpy==1.26.4 +pyyaml==6.0.2 +pillow==10.4.0 +tqdm==4.66.4 +scikit-learn==1.5.1 +matplotlib==3.9.0 +minio==7.2.10 +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +prometheus-client==0.20.0 +onnx==1.16.0 +psycopg2-binary +python-multipart==0.0.9 + +python-dotenv==1.0.1 + +onnxruntime==1.18.1 diff --git a/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/Dockerfile b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/Dockerfile new file mode 100644 index 000000000..05824590d --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +# מתקין תעודות מערכת +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# מוסיף את תעודת נטפרי/הפרוקסי +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN update-ca-certificates + +# אומר ל־pip ול-Python להשתמש בתעודה הזו +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +CMD ["python", "app.py"] diff --git a/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/app.py b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/app.py new file mode 100644 index 000000000..4a3d07217 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/app.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +import os, json, uuid, requests +from datetime import datetime, timedelta, timezone +from kafka import KafkaProducer +from token_bootstrap import get_service_token + +# === Environment === +DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001") +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/app/secret/db_api_token") +KAFKA_BROKER = os.getenv("KAFKA_BROKER", "kafka:9092") +ALERT_TOPIC = os.getenv("ALERT_TOPIC", "alerts") +WINDOW_HOURS = int(os.getenv("WINDOW_HOURS", "168")) # default = 7 days + + +def now_utc() -> datetime: + return datetime.now(timezone.utc) + +def iso(ts: datetime) -> str: + return ts.replace(tzinfo=timezone.utc).isoformat() + +def get_threshold(task_name="ripeness", headers=None): + url = f"{DB_API_BASE}/api/task_thresholds" + try: + r = requests.get(url, headers=headers, timeout=15) + if r.status_code >= 500: + print(f"[WARN] thresholds API {r.status_code}, using default 0.8") + return 0.8 + r.raise_for_status() + try: + data = r.json() + except ValueError as je: + print(f"[WARN] thresholds response is not JSON: {je}; text={r.text[:200]!r}") + return 0.8 + except Exception as e: + print(f"[WARN] thresholds fetch failed: {e}; using default 0.8") + return 0.8 + + # Supports both list and dict responses (rows/items/data) + if isinstance(data, list): + rows = data + elif isinstance(data, dict): + rows = data.get("rows") or data.get("items") or data.get("data") or [] + if not isinstance(rows, list): + rows = [] + else: + rows = [] + + if not rows: + print(f"[WARN] No thresholds found at all, using default 0.8") + return 0.8 + + match = next((row for row in rows if row.get("task") == task_name), None) + if not match: + print(f"[WARN] No threshold for task={task_name}, using default 0.8") + return 0.8 + + try: + return float(match.get("threshold", 0.8)) + except Exception: + return 0.8 + + +from datetime import datetime, timezone + +def get_rollups(window_start, window_end, headers=None): + """ + Fetch data from ripeness_weekly_rollups_ts table and filter by time window. + Works with both list response and dict response containing {"rows": [...]}. + """ + url = f"{DB_API_BASE}/api/ripeness_weekly_rollups_ts" + print(f"[DEBUG] Fetching from {url}", flush=True) + + try: + r = requests.get(url, headers=headers, timeout=60) + r.raise_for_status() + try: + data = r.json() + except ValueError as je: + print(f"[ERROR] response is not JSON: {je}; text={r.text[:300]}", flush=True) + return [] + except requests.exceptions.HTTPError as e: + print(f"[ERROR] HTTP {r.status_code}: {r.text}", flush=True) + return [] + except Exception as e: + print(f"[ERROR] failed to fetch rollups: {e}", flush=True) + return [] + + # --- Normalize response structure --- + if isinstance(data, dict): + rows = data.get("rows") or data.get("items") or data.get("data") or [] + if not isinstance(rows, list): + print(f"[WARN] unexpected dict shape, using empty list; keys={list(data.keys())}", flush=True) + rows = [] + elif isinstance(data, list): + rows = data + else: + print(f"[WARN] unexpected JSON type: {type(data).__name__}", flush=True) + rows = [] + + # --- Parse timestamps and filter by time window --- + def parse_ts(ts_str: str) -> datetime: + try: + return datetime.fromisoformat(ts_str.replace("Z", "+00:00")) + except Exception: + return datetime.min.replace(tzinfo=timezone.utc) + + filtered = [] + for row in rows: + ts = parse_ts(str(row.get("ts", ""))) + if window_start <= ts <= window_end: + filtered.append(row) + + print(f"[INFO] Retrieved {len(filtered)} rollups after filtering (out of {len(rows)} total)") + return filtered + + +def send_kafka_alert(producer, device_id, ratio, threshold, run_id, rollup_id): + alert = { + "alert_id": str(uuid.uuid4()), + "alert_type": "fruit_ripeness_high", + "device_id": "device_id", + "started_at": iso(now_utc()), + "confidence": float(ratio), + "severity": 3, + "meta": { # 👈 Additional metadata is included here + "run_id": str(run_id), + "threshold": threshold, # Save the threshold value as well + "rollup_id": str(rollup_id), + "description": f"{ratio*100:.1f}% ripe/overripe fruits" + } + } + + producer.send(ALERT_TOPIC, json.dumps(alert).encode("utf-8")) + producer.flush() + print(f"[ALERT] sent for {device_id}: {ratio*100:.1f}%") + + +def main(): + print("ello!") + token = get_service_token() + headers = {"Content-Type": "application/json"} + if token: + headers["X-Service-Token"] = token + + window_end = now_utc() + window_start = window_end - timedelta(hours=WINDOW_HOURS) + print(f"[INFO] Checking rollups {window_start} → {window_end}") + + threshold = get_threshold("ripeness", headers) + print(f"[INFO] Using ripeness threshold: {threshold:.2f}") + rows = get_rollups(window_start, window_end, headers) + print(f"[INFO] Fetched {rows} rollup records") + if not rows: + print("[INFO] No data found.") + return + + producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) + + # Iterate through each device + for row in rows: + rollup_id = row.get("id") + device_id = row.get("device_id") + run_id = row.get("run_id") + pct = row.get("pct_ripe", 0.0) + if pct >= threshold: + send_kafka_alert(producer, device_id, pct, threshold, run_id, rollup_id) + else: + print(f"[INFO] {device_id}: below threshold {pct:.2f} < {threshold:.2f}") + + producer.close() + print("[DONE] process complete.") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/docker-compose.yml b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/docker-compose.yml new file mode 100644 index 000000000..9fa01c2f4 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3.8" # docker-compose v1 parser requires explicit version + +services: + fruit_ripeness_alert: + build: . + working_dir: /app + command: ["python", "app.py"] # run the built-in app from the image + environment: + - DB_API_BASE=http://db_api_service:8001 + - DB_API_SERVICE_NAME=fruit_ripeness_alert + - DB_ADMIN_USER=admin + - DB_ADMIN_PASS=admin123 + - DB_API_TOKEN_FILE=/app/secret/db_api_token + - KAFKA_BROKER=kafka:9092 + - ALERT_TOPIC=alerts + - WINDOW_HOURS=168 + volumes: + + - /opt/services/fruit_ripeness_alert/secret:/app/secret + networks: + + - ag_cloud + +networks: + + ag_cloud: + external: true + name: ag_cloud diff --git a/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/requirements.txt b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/requirements.txt new file mode 100644 index 000000000..d9e23d1df --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/requirements.txt @@ -0,0 +1,2 @@ +requests +kafka-python diff --git a/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/secret/db_api_token b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/secret/db_api_token new file mode 100644 index 000000000..f5d3b3f0d --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/secret/db_api_token @@ -0,0 +1 @@ +cf7bba69-678a-4708-829b-cb6e01c9b454 \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/token_bootstrap.py b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/token_bootstrap.py new file mode 100644 index 000000000..cf3af7531 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/fruit_ripeness_alert/token_bootstrap.py @@ -0,0 +1,62 @@ +import os, pathlib, time, requests + +DB_API_BASE = os.getenv("DB_API_BASE", "").strip() +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/app/secret/db_api_token") +DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "fruit_ripeness_alert").strip() or "fruit_ripeness_alert" + +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + +def _read_token(path: str) -> str | None: + p = pathlib.Path(path) + if p.exists(): + t = p.read_text(encoding="utf-8").strip() + if t and "***" not in t: + return t + return None + +def _write_token(path: str, token: str) -> None: + p = pathlib.Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + +def _try_dev_bootstrap(): + """Try to get token using /auth/_dev_bootstrap (new API).""" + url = _safe_join_url(DB_API_BASE, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": True} + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code in (200, 201): + data = r.json() + sa = data.get("service_account") or {} + token = sa.get("raw_token") or sa.get("token") + if token and "***" not in token: + print("[BOOTSTRAP] obtained token via /auth/_dev_bootstrap") + return token.strip() + print(f"[BOOTSTRAP][WARN] _dev_bootstrap returned {r.status_code}: {r.text[:100]}") + except Exception as e: + print(f"[BOOTSTRAP][ERROR] {e}") + return None + +def get_service_token() -> str | None: + """Get or create a service token automatically.""" + if not DB_API_BASE: + print("[BOOTSTRAP][WARN] DB_API_BASE not set") + return None + + # Try existing file + token = _read_token(DB_API_TOKEN_FILE) + if token: + print(f"[BOOTSTRAP] using existing token from {DB_API_TOKEN_FILE}") + return token + + # Try bootstrap (new unified API) + print(f"[BOOTSTRAP] fetching new service token from {DB_API_BASE}") + token = _try_dev_bootstrap() + if token: + _write_token(DB_API_TOKEN_FILE, token) + print(f"[BOOTSTRAP] wrote token to {DB_API_TOKEN_FILE}") + return token + + print("[BOOTSTRAP][ERROR] Could not obtain service token.") + return None \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/.gitignore b/AgCloud/services/fruit-orchestration/services/ripeness/.gitignore new file mode 100644 index 000000000..17751b717 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.onnx +datasets/ +*.png +.venv/ +venv/ +ENV/ \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/Dockerfile b/AgCloud/services/fruit-orchestration/services/ripeness/Dockerfile new file mode 100644 index 000000000..d700be237 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/Dockerfile @@ -0,0 +1,59 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates openssl libpq-dev build-essential gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY certs/ /usr/local/share/ca-certificates/ +RUN set -eux; \ + for f in /usr/local/share/ca-certificates/*.cer; do \ + [ -f "$f" ] && openssl x509 -inform der -in "$f" -out "${f%.cer}.crt" && rm -f "$f" || true; \ + done; \ + update-ca-certificates + +RUN printf "[global]\n\ +cert = /etc/ssl/certs/ca-certificates.crt\n\ +index-url = https://pypi.org/simple\n\ +trusted-host =\n\ + pypi.org\n\ + files.pythonhosted.org\n\ + download.pytorch.org\n" > /etc/pip.conf + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +COPY requirements.txt /app/ +RUN pip install --no-cache-dir --timeout 120 --index-url https://download.pytorch.org/whl/cpu \ + --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org \ + torch==2.3.1 torchvision==0.18.1 \ + && pip install --no-cache-dir -r /app/requirements.txt \ + && pip install --no-cache-dir fastapi "uvicorn[standard]" +COPY api/ /app/api/ +COPY model/ /app/model +COPY jobs/ /app/jobs/ +COPY configs/ /app/configs/ +# Create models directory and copy model file +RUN mkdir -p /app/models +COPY checkpoints/mobilenet_v3_large/best_conditional.pt /app/models/best_conditional.pt + +# Create __init__.py files for Python modules +RUN touch /app/model/__init__.py \ + && touch /app/model/architecture/__init__.py \ + && touch /app/model/data/__init__.py \ + && touch /app/jobs/__init__.py \ + && touch /app/api/__init__.py + +ENV PYTHONPATH=/app + +EXPOSE 8088 +ENV MODEL_PATH=/app/models/best_conditional.pt \ + MODEL_NAME=best_conditional \ + BATCH_LIMIT=500 + +CMD ["uvicorn", "api.ripeness_api:app", "--host", "0.0.0.0", "--port", "8088", "--reload"] diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/README.md b/AgCloud/services/fruit-orchestration/services/ripeness/README.md new file mode 100644 index 000000000..19de71518 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/README.md @@ -0,0 +1,226 @@ +# Ripeness ML – API & Weekly Job + +A small **FastAPI** service that: +- Predicts fruit ripeness (**ripe / unripe / overripe**) for new images from **MinIO** based on the trained conditional model, and writes results to **Postgres**. +- Creates a **weekly rollup snapshot** (with TS window) per fruit. + +--- + +## 🧩 Repo layout (service) + +``` +services/ripeness-ml/ +├─ api/ +│ └─ ripeness_api.py # FastAPI endpoints (predict + rollup) +├─ jobs/ +│ └─ weekly_ripeness_job.py # model/minio/db helpers reused by the API +├─ model/ +│ ├─ architecture/ +│ │ └─ mobilenet_v3_large_head.py # Model architecture definition +│ └─ data/ +│ └─ data_multitask.py # Data loading and preprocessing +├─ checkpoints/ +│ └─ mobilenet_v3_large/ +│ └─ best_conditional.pt # trained model weights +├─ deploy/ +│ ├─ Dockerfile +│ └─ docker-compose.ripeness.yml +├─ configs/ +│ └─ config.yaml # Model and training configuration +├─ requirements.txt +└─ .env (optional) +``` + +--- + +## ⚙️ Requirements + +- Docker Desktop +- External Docker network: **agcloud_ag_cloud** (same as your existing stack) + +**Running services on that network:** +- Postgres (`postgres:5432`, DB: `missions_db`, user: `missions_user`) +- MinIO (`minio-hot:9000`) + +--- + +## 🌍 Environment variables + +Set via `docker-compose.ripeness.yml` or `.env`: + +| Name | Default | Notes | +|------|----------|-------| +| `PGHOST` | postgres | DB host (inside Docker network) | +| `PGPORT` | 5432 | | +| `PGDATABASE` | missions_db | | +| `PGUSER` | missions_user | | +| `PGPASSWORD` | pg123 | | +| `MINIO_ENDPOINT` | minio-hot:9000 | S3 API port is 9000 inside Docker | +| `MINIO_SECURE` | false | set true if TLS to MinIO | +| `MINIO_ACCESS_KEY` | minioadmin | | +| `MINIO_SECRET_KEY` | minioadmin | | +| `MODEL_PATH` | /models/best_conditional.pt | mounted from host | +| `MODEL_NAME` | best_conditional | stored in DB | +| `BATCH_LIMIT` | 500 | safety cap per run | +| `FRUITS` (optional) | Apple,Orange,Grape,Strawberry | if enabled in code | + +If you’re behind **NetFree/proxy**, copy your CA file to `deploy/certs/` and use the Dockerfile section that installs CA + `update-ca-certificates`. + +--- + +## 🐳 Build & Run (Docker) + +From `services/ripeness-ml/`: + +```bash +docker compose -f docker-compose.ripeness.yml build ripeness-api +docker compose -f docker-compose.ripeness.yml up -d ripeness-api +``` + +**Health check:** + +```bash +curl http://localhost:8088/healthz +``` + +# **logs** +```bash +docker logs -n 200 ripeness-api +``` + +--- + +## 🔌 API + +**Base URL:** `http://localhost:8088` + +### POST `/predict-last-week` +Runs prediction for images from the last 7 days that don’t have a record yet in `ripeness_predictions`. + +```bash +curl -X POST http://localhost:8088/predict-last-week +# -> {"processed": 17} +``` + +### POST `/predict-batch` +Run for a custom time window and limit. + +**Request body (JSON):** +```json +{ + "since_ts": "2025-10-01T00:00:00", + "limit": 1000 +} +``` + +**Example:** +```bash +curl -X POST http://localhost:8088/predict-batch -H "Content-Type: application/json" -d '{"since_ts":"2025-10-01T00:00:00","limit":1000}' +``` + +### POST `/rollup/weekly` +Creates a weekly snapshot into `ripeness_weekly_rollups_ts` for the last 7 days (creates the table if missing). + +```bash +curl -X POST http://localhost:8088/rollup/weekly +# -> {"ok": true} +``` + +--- + +## 🧮 Database schema + +### Predictions table +```sql +CREATE TABLE IF NOT EXISTS ripeness_predictions ( + id BIGSERIAL PRIMARY KEY, + inference_log_id BIGINT NOT NULL REFERENCES inference_logs(id) ON DELETE CASCADE, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), + ripeness_label TEXT NOT NULL CHECK (ripeness_label IN ('ripe','unripe','overripe')), + ripeness_score DOUBLE PRECISION NOT NULL, + model_name TEXT NOT NULL, + UNIQUE (inference_log_id) +); +``` + +### Weekly rollups +```sql +CREATE TABLE IF NOT EXISTS ripeness_weekly_rollups_ts ( + id BIGSERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), -- snapshot time + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + fruit_type TEXT NOT NULL, + cnt_total INTEGER NOT NULL, + cnt_ripe INTEGER NOT NULL, + cnt_unripe INTEGER NOT NULL, + cnt_overripe INTEGER NOT NULL, + pct_ripe DOUBLE PRECISION NOT NULL +); +``` + +--- + +## 🔍 Useful queries + +**Show latest predictions joined with inference logs:** +```sql +SELECT il.id, il.fruit_type, il.image_url, rp.ripeness_label, rp.ripeness_score, rp.model_name, rp.ts +FROM inference_logs il +JOIN ripeness_predictions rp ON rp.inference_log_id = il.id +ORDER BY rp.ts DESC +LIMIT 20; +``` + +**Show rollup snapshots:** +```sql +SELECT ts::date AS snapshot_day, fruit_type, cnt_total, +cnt_ripe, cnt_unripe, cnt_overripe, +ROUND(pct_ripe*100,2) AS pct_ripe_pct +FROM ripeness_weekly_rollups_ts +ORDER BY ts DESC, fruit_type; +``` + +**From Docker (network agcloud_ag_cloud):** +```bash +docker run --rm --network agcloud_ag_cloud -e PGPASSWORD=pg123 postgres:16-alpine psql -h postgres -U missions_user -d missions_db -c "SELECT ts::date AS snapshot_day, fruit_type, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, ROUND(pct_ripe*100,2) AS pct_ripe_pct + FROM ripeness_weekly_rollups_ts + ORDER BY ts DESC, fruit_type;" +``` + +--- + +## 🕒 Scheduling (Windows Task Scheduler) + +Create a weekly job that first predicts, then rolls up. + +**run_weekly.ps1:** +```powershell +Invoke-RestMethod -Method Post -Uri "http://localhost:8088/predict-last-week" +# note: /predict-last-week now triggers the weekly rollup automatically, +# so a single call is sufficient (no duplicate predictions are inserted). +``` + +**Register task:** +```bash +schtasks /Create /TN "RipenessWeekly" /TR "powershell.exe -ExecutionPolicy Bypass -File C:\path\run_weekly.ps1" /SC WEEKLY /D MON /ST 03:00 +``` + +--- + +## 🧰 Troubleshooting + +- **MinIO errors / 9000 vs 9001:** inside Docker network always use `minio-hot:9000` (S3 API). + Ports 9001/9002 are host-exposed console/proxy. +- **SignatureDoesNotMatch:** wrong `MINIO_ACCESS_KEY`/`SECRET_KEY` or endpoint (should be the S3 API). +- **Model FRUITS mismatch:** ensure the FRUITS list in code matches the model checkpoint (e.g. include Grape if trained). +- **SSL to PyPI (NetFree/proxy):** add your CA to the image and run `update-ca-certificates`. +- **No rows processed:** endpoint processes only inference logs without an existing prediction; expand window with `/predict-batch`. + +--- + +## 👩‍💻 Maintainer + +**Name:** Ayala +**Service name:** ripeness-api +**Ports:** 8088/tcp diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/api/ripeness_api.py b/AgCloud/services/fruit-orchestration/services/ripeness/api/ripeness_api.py new file mode 100644 index 000000000..12fe8f5fb --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/api/ripeness_api.py @@ -0,0 +1,175 @@ +# scripts/ripeness_api.py +from fastapi import FastAPI +from pydantic import BaseModel +from datetime import datetime, timedelta +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), "")) + +from jobs.weekly_ripeness_job import ( + get_conn, + fetch_from_minio, + load_image_for_model, + predict_ripeness, +) + +app = FastAPI(title="Ripeness Service") + + +class BatchRequest(BaseModel): + since_ts: datetime | None = None + limit: int = 500 + + +def run_batch(since_ts: datetime | None, limit: int) -> int: + if since_ts is None: + since_ts = datetime.utcnow() - timedelta(days=7) + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT il.id, il.ts, il.fruit_type, il.image_url + FROM inference_logs il + LEFT JOIN ripeness_predictions rp ON rp.inference_log_id = il.id + WHERE il.ts >= %s + AND rp.id IS NULL + ORDER BY il.id ASC + LIMIT %s; + """, (since_ts, limit)) + rows = cur.fetchall() + + processed = 0 + # Generate a new run_id for this batch (once per batch) + with get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT gen_random_uuid()") + run_id = cur.fetchone()[0] + + for inflog_id, ts, fruit_type, image_url in rows: + try: + img_bytes = fetch_from_minio(image_url) + tensor = load_image_for_model(img_bytes) + label, score = predict_ripeness(tensor, fruit_type) + + # Parse bucket and object_key from image_url (expects format minio://bucket/object_key) + device_id = None + if image_url.startswith("minio://"): + path = image_url[len("minio://"):] + if "/" in path: + bucket, object_key = path.split("/", 1) + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT device_id FROM files + WHERE bucket = %s AND object_key = %s + """, (bucket, object_key)) + res = cur.fetchone() + device_id = res[0] if res else None + + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + INSERT INTO ripeness_predictions + (inference_log_id, ts, ripeness_label, ripeness_score, model_name, run_id, device_id) + VALUES (%s, now(), %s, %s, %s, %s, %s) + ON CONFLICT (inference_log_id) DO NOTHING; + """, (inflog_id, label, score, os.getenv("MODEL_NAME", "best_conditional"), run_id, device_id)) + processed += 1 + except Exception as e: + print(f"[ERR] inflog_id={inflog_id} :: {e}") + return processed + + +@app.get("/healthz") +def healthz(): + return {"ok": True} + + +@app.post("/predict-batch") +def predict_batch(req: BatchRequest): + n = run_batch(req.since_ts, req.limit) + return {"processed": n} + + +@app.post("/predict-last-week") +def predict_last_week(): + n = run_batch(None, int(os.getenv("BATCH_LIMIT", "500"))) + # After predicting new images, immediately create the weekly rollup + # This keeps the workflow to a single endpoint call (no duplicates because + # predictions use ON CONFLICT DO NOTHING) + try: + insert_weekly_rollup() + return {"processed": n, "rollup": True} + except Exception as e: + # Log the error but still return the number of processed items + print(f"[ERR] rollup: {e}") + return {"processed": n, "rollup": False, "error": str(e)} + + +def insert_weekly_rollup(): + ddl = """ + CREATE TABLE IF NOT EXISTS ripeness_weekly_rollups_ts ( + id BIGSERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + fruit_type TEXT NOT NULL, + device_id TEXT, + run_id UUID, + cnt_total INTEGER NOT NULL, + cnt_ripe INTEGER NOT NULL, + cnt_unripe INTEGER NOT NULL, + cnt_overripe INTEGER NOT NULL, + pct_ripe DOUBLE PRECISION NOT NULL + ); + CREATE INDEX IF NOT EXISTS ix_rwrt_ts ON ripeness_weekly_rollups_ts(ts); + CREATE INDEX IF NOT EXISTS ix_rwrt_fruit_ts ON ripeness_weekly_rollups_ts(fruit_type, ts); + CREATE INDEX IF NOT EXISTS ix_rwrt_device ON ripeness_weekly_rollups_ts(device_id); + CREATE INDEX IF NOT EXISTS ix_rwrt_run ON ripeness_weekly_rollups_ts(run_id); + """ + + # optional filter by fruits from environment (comma-separated) + fruits_env = os.getenv("FRUITS") + fruits = None + fruit_where = "" + if fruits_env: + fruits = [f.strip() for f in fruits_env.split(",") if f.strip()] + # use = ANY(%s) with a TEXT[] parameter + fruit_where = "WHERE il.fruit_type = ANY(%s)" + + sql = """ + WITH w AS ( + SELECT now() - interval '7 days' AS ws, now() AS we + ), + agg AS ( + SELECT + il.fruit_type, + rp.device_id, + rp.run_id, + COUNT(*) AS cnt_total, + SUM(CASE WHEN rp.ripeness_label='ripe' THEN 1 ELSE 0 END) AS cnt_ripe, + SUM(CASE WHEN rp.ripeness_label='unripe' THEN 1 ELSE 0 END) AS cnt_unripe, + SUM(CASE WHEN rp.ripeness_label='overripe' THEN 1 ELSE 0 END) AS cnt_overripe + FROM ripeness_predictions rp + JOIN inference_logs il ON il.id = rp.inference_log_id + JOIN w ON rp.ts >= w.ws AND rp.ts < w.we + """ + ("\n " + fruit_where if fruit_where else "") + """ + GROUP BY il.fruit_type, rp.device_id, rp.run_id + ) + INSERT INTO ripeness_weekly_rollups_ts + (ts, window_start, window_end, fruit_type, device_id, run_id, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, pct_ripe) + SELECT + now(), (SELECT ws FROM w), (SELECT we FROM w), + fruit_type, device_id, run_id, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, + CASE WHEN cnt_total>0 THEN (cnt_ripe+cnt_overripe)::double precision/cnt_total ELSE 0 END + FROM agg; + """ + + with get_conn() as conn, conn.cursor() as cur: + cur.execute(ddl) + if fruits: + # psycopg2 adapts Python list to SQL array + cur.execute(sql, (fruits,)) + else: + cur.execute(sql) + return True + + +@app.post("/rollup/weekly") +def rollup_weekly(): + insert_weekly_rollup() + return {"ok": True} diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/eval/classification_report.txt b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/eval/classification_report.txt new file mode 100644 index 000000000..f2e2f5a54 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/eval/classification_report.txt @@ -0,0 +1,13 @@ + precision recall f1-score support + + unripe 1.0000 0.9981 0.9990 1041 + ripe 0.9983 1.0000 0.9991 1164 + overripe 1.0000 1.0000 1.0000 1534 + + accuracy 0.9995 3739 + macro avg 0.9994 0.9994 0.9994 3739 +weighted avg 0.9995 0.9995 0.9995 3739 + + +Accuracy: 0.9995 +Macro-F1: 0.9994 diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/eval/metrics.json b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/eval/metrics.json new file mode 100644 index 000000000..00bdd6890 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/eval/metrics.json @@ -0,0 +1,9 @@ +{ + "accuracy": 0.9994650976196844, + "macro_f1": 0.9993933641465831, + "per_class_f1": { + "unripe": 0.9990384615384615, + "ripe": 0.9991416309012876, + "overripe": 1.0 + } +} \ No newline at end of file diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional.pt b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional.pt new file mode 100644 index 000000000..0637d2fd0 Binary files /dev/null and b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional.pt differ diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt new file mode 100644 index 000000000..29b9cfb1c Binary files /dev/null and b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt differ diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt new file mode 100644 index 000000000..c9ef651e1 Binary files /dev/null and b/AgCloud/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt differ diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/configs/config.yaml b/AgCloud/services/fruit-orchestration/services/ripeness/configs/config.yaml new file mode 100644 index 000000000..bfd6c7863 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/configs/config.yaml @@ -0,0 +1,25 @@ +seed: 42 +classes: ["unripe", "ripe", "overripe"] +img_size: 224 +batch_size: 32 +num_workers: 0 +epochs_frozen: 5 +epochs_unfrozen: 10 +lr: 0.0003 +weight_decay: 0.0001 +label_smoothing: 0.05 +use_class_weights: true +train_dir: "data/train" +val_dir: "data/val" +test_dir: "data/test" +checkpoint_dir: "checkpoints/mobilenet_v3_large" +best_metric: "f1_macro" + +fruits: ["apple","banana","orange"] +ripeness: ["unripe","ripe","overripe"] + +csv: + train: "data_mt_train/train.csv" + val: "data_mt_train/val.csv" + test: "data_mt_test/test.csv" + diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/docker-compose.yml b/AgCloud/services/fruit-orchestration/services/ripeness/docker-compose.yml new file mode 100644 index 000000000..6ac2eca11 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/docker-compose.yml @@ -0,0 +1,48 @@ +version: "3.8" + +services: + ripeness-api: + build: + context: . + dockerfile: Dockerfile + image: ripeness-api:latest + container_name: ripeness-api + + environment: + PGHOST: postgres + PGPORT: "5432" + PGDATABASE: missions_db + PGUSER: missions_user + PGPASSWORD: pg123 + + MINIO_ENDPOINT: minio-hot:9000 + MINIO_SECURE: "false" + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + + MODEL_NAME: best_conditional + BATCH_LIMIT: "500" + FRUITS: "Apple,Banana,Orange" + + + volumes: + - ./checkpoints:/app/checkpoints + - ./configs:/app/configs + # - ./model:/app/model + + networks: + - agcloud_net + - ag_cloud + + ports: + - "8091:8088" + restart: unless-stopped + +networks: + agcloud_net: + external: true + name: agcloud_ag_cloud + + ag_cloud: + external: true + name: ag_cloud diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/jobs/weekly_ripeness_job.py b/AgCloud/services/fruit-orchestration/services/ripeness/jobs/weekly_ripeness_job.py new file mode 100644 index 000000000..387b7c034 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/jobs/weekly_ripeness_job.py @@ -0,0 +1,167 @@ +# file: services/weekly_ripeness_job.py +import io +import time +import torch +import psycopg2 +import datetime as dt +from urllib.parse import urlparse +from minio import Minio +from PIL import Image +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) # so "models" is importable +from model.architecture.mobilenet_v3_large_head import build_conditional +from tqdm.auto import tqdm + + +from pathlib import Path +try: + from dotenv import load_dotenv + env_path = Path(__file__).resolve().parents[1] / ".env" + if env_path.exists(): + load_dotenv(env_path.as_posix()) +except Exception: + pass + +# ---- ENV ---- +PGHOST = os.getenv("PGHOST", "db") +PGPORT = int(os.getenv("PGPORT", "5432")) +PGDATABASE = os.getenv("PGDATABASE", "missions_db") +PGUSER = os.getenv("PGUSER", "missions_user") +PGPASSWORD = os.getenv("PGPASSWORD", "pg123") + +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "127.0.0.1:9000") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin") + +MODEL_PATH = os.getenv("MODEL_PATH", "/models/best_conditional.pt") +MODEL_NAME = os.getenv("MODEL_NAME", "best_conditional") +BATCH_LIMIT = int(os.getenv("BATCH_LIMIT", "200")) + +# ----- labels & fruits mapping ----- +LABELS = ["unripe", "ripe", "overripe"] +FRUITS = ["Apple", "Banana", "Orange", "."] +FRUIT2IDX = {name.lower(): i for i, name in enumerate(FRUITS)} + +# ----- build model & load weights ----- +device = "cuda" if torch.cuda.is_available() else "cpu" +num_ripeness = len(LABELS) +num_fruits = len(FRUITS) + +model = build_conditional(num_ripeness=num_ripeness, num_fruits=num_fruits, embed_dim=16).to(device) + +ckpt = torch.load(MODEL_PATH, map_location=device) +state = ckpt["state_dict"] if (isinstance(ckpt, dict) and "state_dict" in ckpt) else ckpt + +assert state["fruit_embed.weight"].shape[0] == num_fruits, \ + f"Checkpoint expects {state['fruit_embed.weight'].shape[0]} fruits, but FRUITS has {num_fruits}" + +model.load_state_dict(state, strict=True) +model.eval() + +def load_image_for_model(img_bytes): + im = Image.open(io.BytesIO(img_bytes)).convert("RGB") + from torchvision import transforms + preprocess = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]) + ]) + return preprocess(im).unsqueeze(0).to(device) + +@torch.no_grad() +def predict_ripeness(img_tensor, fruit_type: str): + idx = FRUIT2IDX.get(fruit_type.lower()) + if idx is None: + raise KeyError(f"skip: fruit '{fruit_type}' not in trained set {FRUITS}") + fruit_idx_tensor = torch.tensor([idx], dtype=torch.long, device=device) + logits = model(img_tensor, fruit_idx_tensor) + probs = torch.softmax(logits, dim=1).squeeze(0).cpu().numpy() + j = int(probs.argmax()) + return LABELS[j], float(probs[j]) + +# ---- MINIO ---- +minio_client = Minio(MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, secure=MINIO_SECURE) + +def fetch_from_minio(image_url: str) -> bytes: + p = urlparse(image_url) + path = p.path.lstrip("/") + bucket, *rest = path.split("/", 1) + if not rest: + raise ValueError(f"Invalid URL path for MinIO: {image_url}") + obj = rest[0] + resp = minio_client.get_object(bucket, obj) + data = resp.read() + resp.close() + resp.release_conn() + return data + +# ---- DB ---- +def get_conn(): + return psycopg2.connect( + host=PGHOST, port=PGPORT, dbname=PGDATABASE, user=PGUSER, password=PGPASSWORD + ) + +def main(): + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT il.id, il.ts, il.fruit_type, il.image_url + FROM inference_logs il + LEFT JOIN ripeness_predictions rp ON rp.inference_log_id = il.id + WHERE il.ts >= now() - interval '7 days' + AND rp.id IS NULL + ORDER BY il.id ASC + LIMIT %s; + """, (BATCH_LIMIT,)) + rows = cur.fetchall() + + processed = 0 + + # generate a single run_id for this batch + with get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT gen_random_uuid()") + run_id = cur.fetchone()[0] + + for inflog_id, ts, fruit_type, image_url in tqdm(rows, desc="Predicting ripeness"): + try: + if processed % 20 == 0: + print(f"...processed {processed} so far") + img_bytes = fetch_from_minio(image_url) + tensor = load_image_for_model(img_bytes) + try: + label, score = predict_ripeness(tensor, fruit_type) + except KeyError as skip: + print(f"[SKIP] inflog_id={inflog_id} :: {skip}") + continue + + # derive bucket/object_key and lookup device_id + device_id = None + try: + p = urlparse(image_url) + path = p.path.lstrip('/') + if '/' in path: + bucket, object_key = path.split('/', 1) + with get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT device_id FROM files WHERE bucket = %s AND object_key = %s", (bucket, object_key)) + res = cur.fetchone() + device_id = res[0] if res else None + except Exception: + # keep device_id as None if parsing/lookup fails + device_id = None + + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + INSERT INTO ripeness_predictions + (inference_log_id, ts, ripeness_label, ripeness_score, model_name, run_id, device_id) + VALUES (%s, now(), %s, %s, %s, %s, %s) + ON CONFLICT (inference_log_id) DO NOTHING; + """, (inflog_id, label, score, MODEL_NAME, run_id, device_id)) + processed += 1 + print(f"[OK] inflog_id={inflog_id} -> {label} ({score:.4f})") + except Exception as e: + print(f"[ERR] inflog_id={inflog_id} url={image_url} :: {e}") + + print(f"Done. processed={processed}") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/model/architecture/mobilenet_v3_large_head.py b/AgCloud/services/fruit-orchestration/services/ripeness/model/architecture/mobilenet_v3_large_head.py new file mode 100644 index 000000000..3457d6953 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/model/architecture/mobilenet_v3_large_head.py @@ -0,0 +1,34 @@ +import torch.nn as nn +import torch +from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights + +class RipenessModelConditional(nn.Module): + """ + Image -> MobileNetV3 backbone + Fruit type (idx) -> Embedding + Concatenate -> Linear -> ripeness logits + """ + def __init__(self, num_ripeness: int, num_fruits: int, embed_dim: int = 16): + super().__init__() + weights = MobileNet_V3_Large_Weights.IMAGENET1K_V2 + self.backbone = mobilenet_v3_large(weights=weights) + in_feats = self.backbone.classifier[-1].in_features + self.backbone.classifier[-1] = nn.Identity() + self.fruit_embed = nn.Embedding(num_fruits, embed_dim) + self.head = nn.Linear(in_feats + embed_dim, num_ripeness) + + def forward(self, x, fruit_idx): + feats = self.backbone(x) # [B, in_feats] + fvec = self.fruit_embed(fruit_idx) # [B, embed_dim] + out = torch.cat([feats, fvec], dim=1) # [B, in_feats+embed_dim] + return self.head(out) # [B, num_ripeness] + +def build_conditional(num_ripeness: int, num_fruits: int, embed_dim: int = 16) -> nn.Module: + return RipenessModelConditional(num_ripeness, num_fruits, embed_dim) + +def build_model(num_classes: int) -> nn.Module: + weights = MobileNet_V3_Large_Weights.IMAGENET1K_V2 + model = mobilenet_v3_large(weights=weights) + in_feats = model.classifier[-1].in_features + model.classifier[-1] = nn.Linear(in_feats, num_classes) + return model diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/model/data/data_multitask.py b/AgCloud/services/fruit-orchestration/services/ripeness/model/data/data_multitask.py new file mode 100644 index 000000000..9b01959a3 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/model/data/data_multitask.py @@ -0,0 +1,48 @@ +from torch.utils.data import Dataset, DataLoader +from PIL import Image +from torchvision import transforms +import pandas as pd + +IMAGENET_MEAN=(0.485,0.456,0.406); IMAGENET_STD=(0.229,0.224,0.225) + +def build_transforms(img_size=224): + from torchvision import transforms as T + t_train = T.Compose([ + T.RandomResizedCrop(img_size, scale=(0.7,1.0)), + T.RandomHorizontalFlip(), + T.ColorJitter(0.2,0.2,0.2,0.05), + T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + t_val = T.Compose([ + T.Resize(int(img_size*1.15)), T.CenterCrop(img_size), + T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + return t_train, t_val + +class CSVConditional(Dataset): + def __init__(self, csv_path, fruit_to_idx, ripeness_to_idx, transform=None): + self.df = pd.read_csv(csv_path) + self.fruit_to_idx = fruit_to_idx + self.ripeness_to_idx = ripeness_to_idx + self.transform = transform + + def __len__(self): return len(self.df) + + def __getitem__(self, i): + row = self.df.iloc[i] + img = Image.open(row["path"]).convert("RGB") + if self.transform: img = self.transform(img) + fruit_idx = self.fruit_to_idx[row["fruit"]] + ripeness_idx = self.ripeness_to_idx[row["ripeness"]] + return img, fruit_idx, ripeness_idx + +def make_loaders(csv_train, csv_val, img_size, batch_size, num_workers, fruits, ripeness): + t_train, t_val = build_transforms(img_size) + f2i = {f:i for i,f in enumerate(fruits)} + r2i = {r:i for i,r in enumerate(ripeness)} + dtr = CSVConditional(csv_train, f2i, r2i, t_train) + dva = CSVConditional(csv_val, f2i, r2i, t_val) + from torch.utils.data import DataLoader + ltr = DataLoader(dtr, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True) + lva = DataLoader(dva, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True) + return ltr, lva, f2i, r2i diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/model/data/transforms.py b/AgCloud/services/fruit-orchestration/services/ripeness/model/data/transforms.py new file mode 100644 index 000000000..a8f2c4b5b --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/model/data/transforms.py @@ -0,0 +1,18 @@ +from torchvision import transforms +IMAGENET_MEAN=(0.485,0.456,0.406); IMAGENET_STD=(0.229,0.224,0.225) + +def build_transforms(img_size=224): + t_train = transforms.Compose([ + transforms.RandomResizedCrop(img_size, scale=(0.7,1.0)), + transforms.RandomHorizontalFlip(), + transforms.ColorJitter(0.2,0.2,0.2,0.05), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + t_val = transforms.Compose([ + transforms.Resize(int(img_size*1.15)), + transforms.CenterCrop(img_size), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + return t_train, t_val diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/model/training/evaluate_conditional.py b/AgCloud/services/fruit-orchestration/services/ripeness/model/training/evaluate_conditional.py new file mode 100644 index 000000000..e10977e01 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/model/training/evaluate_conditional.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Evaluate the conditional ripeness model on test/val CSVs. +# Outputs: +# - metrics.json (accuracy, macro_f1, per-class F1) +# - classification_report.txt +# - confusion_matrix.png + +import os, sys, json, yaml +from pathlib import Path +import numpy as np +import torch +from sklearn.metrics import accuracy_score, f1_score, classification_report, confusion_matrix +import matplotlib.pyplot as plt + +# --- make 'models' & 'training' importable when running as a script --- +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + +from model.architecture.mobilenet_v3_large_head import build_conditional +from data.data_multitask import CSVConditional, build_transforms + +IMAGENET_MEAN=(0.485,0.456,0.406) +IMAGENET_STD=(0.229,0.224,0.225) + +def softmax(x): + x = x - x.max(axis=1, keepdims=True) + e = np.exp(x) + return e / e.sum(axis=1, keepdims=True) + +def load_cfg(): + return yaml.safe_load(open(os.path.join(PROJECT_ROOT, "configs/config.yaml"), "r", encoding="utf-8")) + +def make_loader(csv_path, fruits, ripeness, img_size=224, batch_size=64, num_workers=0): + _, t_val = build_transforms(img_size) + f2i = {f:i for i,f in enumerate(fruits)} + r2i = {r:i for i,r in enumerate(ripeness)} + ds = CSVConditional(csv_path, f2i, r2i, transform=t_val) + from torch.utils.data import DataLoader + return DataLoader(ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True) + +def plot_confusion_matrix(cm, classes, out_png): + fig = plt.figure(figsize=(5.5, 4.5)) + ax = fig.add_subplot(111) + im = ax.imshow(cm, interpolation='nearest') + ax.set_title('Confusion Matrix') + fig.colorbar(im) + tick_marks = np.arange(len(classes)) + ax.set_xticks(tick_marks); ax.set_xticklabels(classes, rotation=45, ha="right") + ax.set_yticks(tick_marks); ax.set_yticklabels(classes) + ax.set_ylabel('True'); ax.set_xlabel('Predicted') + # write counts + thresh = cm.max() / 2.0 if cm.size else 0.5 + for i in range(cm.shape[0]): + for j in range(cm.shape[1]): + ax.text(j, i, format(cm[i, j], 'd'), + ha="center", va="center", + color="white" if cm[i, j] > thresh else "black") + fig.tight_layout() + fig.savefig(out_png, dpi=160) + plt.close(fig) + +if __name__ == "__main__": + cfg = load_cfg() + device = "cuda" if torch.cuda.is_available() else "cpu" + + fruits = cfg["fruits"] + ripeness = cfg["ripeness"] + + # choose CSV: prefer test.csv; if missing/empty -> use val.csv + csv_test = Path(cfg["csv"].get("test", "data_mt/test.csv")) + csv_val = Path(cfg["csv"].get("val", "data_mt/val.csv")) + csv_path = csv_test if csv_test.exists() and csv_test.stat().st_size > 50 else csv_val + if not csv_path.exists(): + raise SystemExit(f"CSV not found: {csv_path}. Run ingest to create it.") + + # dataloader + loader = make_loader( + str(csv_path), fruits, ripeness, + img_size=cfg.get("img_size", 224), + batch_size=cfg.get("batch_size", 32), + num_workers=cfg.get("num_workers", 0) + ) + + # model + ckpt_dir = cfg["checkpoint_dir"] + ckpt = os.path.join(ckpt_dir, "best_conditional.pt") + if not os.path.exists(ckpt): + raise SystemExit(f"Checkpoint not found: {ckpt}") + + model = build_conditional(num_ripeness=len(ripeness), num_fruits=len(fruits)) + model.load_state_dict(torch.load(ckpt, map_location="cpu")) + model.eval().to(device) + + # predict + y_true, y_pred = [], [] + probs_all = [] + with torch.no_grad(): + for x, fidx, ridx in loader: + x = x.to(device) + fidx = torch.as_tensor(fidx, device=device) + logits = model(x, fidx).cpu().numpy() + prob = softmax(logits) + preds = prob.argmax(1) + y_pred.extend(preds.tolist()) + y_true.extend(ridx.numpy().tolist()) + probs_all.append(prob) + + y_true = np.array(y_true) + y_pred = np.array(y_pred) + probs = np.concatenate(probs_all, axis=0) if probs_all else np.empty((0,len(ripeness))) + + # metrics + acc = float(accuracy_score(y_true, y_pred)) + macro_f1 = float(f1_score(y_true, y_pred, average="macro")) + per_class_f1 = f1_score(y_true, y_pred, average=None) + per_class = {ripeness[i]: float(per_class_f1[i]) for i in range(len(ripeness))} + report = classification_report(y_true, y_pred, target_names=ripeness, digits=4) + cm = confusion_matrix(y_true, y_pred) + + # outputs + out_dir = os.path.join(PROJECT_ROOT, "checkpoints", "eval") + os.makedirs(out_dir, exist_ok=True) + # confusion matrix PNG + cm_png = os.path.join(out_dir, "confusion_matrix.png") + plot_confusion_matrix(cm, ripeness, cm_png) + # classification report + with open(os.path.join(out_dir, "classification_report.txt"), "w", encoding="utf-8") as f: + f.write(report + "\n") + f.write(f"\nAccuracy: {acc:.4f}\nMacro-F1: {macro_f1:.4f}\n") + # json metrics + with open(os.path.join(out_dir, "metrics.json"), "w", encoding="utf-8") as f: + json.dump({"accuracy": acc, "macro_f1": macro_f1, "per_class_f1": per_class}, f, indent=2) + + print(f"Evaluated on: {csv_path}") + print(f"Accuracy: {acc:.4f} | Macro-F1: {macro_f1:.4f}") + print("Per-class F1:", per_class) + print(f"Saved: {cm_png} and classification_report.txt, metrics.json") diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/model/training/train_conditional.py b/AgCloud/services/fruit-orchestration/services/ripeness/model/training/train_conditional.py new file mode 100644 index 000000000..c2bf56357 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/model/training/train_conditional.py @@ -0,0 +1,114 @@ +import os, yaml, torch +from torch import nn +from sklearn.metrics import accuracy_score, f1_score + +from model.architecture.mobilenet_v3_large_head import build_conditional +from data.data_multitask import make_loaders + + +def evaluate(model, loader, device): + model.eval() + y_true, y_pred = [], [] + with torch.no_grad(): + for x, fidx, ridx in loader: + x = x.to(device) + fidx = torch.as_tensor(fidx, device=device) + logits = model(x, fidx) + y_pred.extend(logits.argmax(1).cpu().numpy()) + y_true.extend(ridx.numpy()) + acc = accuracy_score(y_true, y_pred) + f1 = f1_score(y_true, y_pred, average="macro") + return acc, f1 + + +def train_phase(model, ltr, lva, device, epochs, lr, wd, ckpt_dir, tag, ce, patience=2): + from torch.optim import AdamW + from torch.optim.lr_scheduler import CosineAnnealingLR + + os.makedirs(ckpt_dir, exist_ok=True) + opt = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=lr, weight_decay=wd) + sch = CosineAnnealingLR(opt, T_max=epochs) + + best_f1 = -1.0 + best_state = None + no_improve = 0 + + try: + for ep in range(1, epochs + 1): + model.train() + for x, fidx, ridx in ltr: + x = x.to(device) + fidx = torch.as_tensor(fidx, device=device) + ridx = torch.as_tensor(ridx, device=device) + + logits = model(x, fidx) + loss = ce(logits, ridx) + + opt.zero_grad() + loss.backward() + opt.step() + + acc, f1 = evaluate(model, lva, device) + sch.step() + print(f"[{tag} Epoch {ep}] val_acc={acc:.3f} val_f1={f1:.3f}") + + if f1 > best_f1 + 1e-4: + best_f1 = f1 + best_state = {k: v.cpu() for k, v in model.state_dict().items()} + torch.save(best_state, os.path.join(ckpt_dir, f"best_conditional_{tag}.pt")) + no_improve = 0 + else: + no_improve += 1 + if no_improve >= patience: + print(f"Early stopping ({tag}) — no improvement for {patience} epochs") + break + + except KeyboardInterrupt: + print("KeyboardInterrupt — saving best checkpoint so far...") + + finally: + if best_state is not None: + model.load_state_dict(best_state) + return model, best_f1 + + +if __name__ == "__main__": + cfg = yaml.safe_load(open("configs/config.yaml", "r", encoding="utf-8")) + device = "cuda" if torch.cuda.is_available() else "cpu" + + train_csv = cfg["csv"]["train"] + val_csv = cfg["csv"]["val"] + fruits = cfg["fruits"] + ripeness = cfg["ripeness"] + + ltr, lva, f2i, r2i = make_loaders( + train_csv, val_csv, + cfg["img_size"], cfg["batch_size"], cfg["num_workers"], + fruits, ripeness + ) + + model = build_conditional(num_ripeness=len(ripeness), num_fruits=len(fruits)).to(device) + ce = nn.CrossEntropyLoss() + + for p in model.backbone.features.parameters(): + p.requires_grad = False + + model, _ = train_phase( + model, ltr, lva, device, + cfg["epochs_frozen"], cfg["lr"], cfg["weight_decay"], + cfg["checkpoint_dir"], tag="frozen", ce=ce, patience=2 + + ) + + for p in model.parameters(): + p.requires_grad = True + + model, best_f1 = train_phase( + model, ltr, lva, device, + cfg["epochs_unfrozen"], cfg["lr"]/3, cfg["weight_decay"], + cfg["checkpoint_dir"], tag="unfrozen", ce=ce, patience=2 + ) + + os.makedirs(cfg["checkpoint_dir"], exist_ok=True) + torch.save(model.state_dict(), os.path.join(cfg["checkpoint_dir"], "best_conditional.pt")) + print("Saved:", os.path.join(cfg["checkpoint_dir"], "best_conditional.pt"), "| best F1:", best_f1) diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/model/training/utils.py b/AgCloud/services/fruit-orchestration/services/ripeness/model/training/utils.py new file mode 100644 index 000000000..23e47edc7 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/model/training/utils.py @@ -0,0 +1,14 @@ +# import torch, random, numpy as np +# from collections import Counter + +# def set_seed(s=42): +# random.seed(s); np.random.seed(s); torch.manual_seed(s); torch.cuda.manual_seed_all(s) + +# def load_class_weights(trainloader, use=True): +# if not use: return None +# counts = Counter() +# for _,y in trainloader: +# for i in y.numpy(): counts[int(i)]+=1 +# total = sum(counts.values()) +# weights = [total/counts[i] for i in range(len(counts))] +# return torch.tensor(weights, dtype=torch.float32) diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/requirements.txt b/AgCloud/services/fruit-orchestration/services/ripeness/requirements.txt new file mode 100644 index 000000000..0aa2be591 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/requirements.txt @@ -0,0 +1,16 @@ +torch==2.3.1 +torchvision==0.18.1 +# timm==1.0.9 +# scikit-learn==1.5.1 +matplotlib==3.9.0 +pillow==10.4.0 +pyyaml==6.0.2 +tqdm==4.66.4 +pandas==2.2.2 +onnx==1.16.0 +onnxruntime==1.18.1 +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +minio==7.2.10 +python-dotenv==1.0.1 +psycopg2-binary diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/tools/data_prep/ingest_kaggle_multitask.py b/AgCloud/services/fruit-orchestration/services/ripeness/tools/data_prep/ingest_kaggle_multitask.py new file mode 100644 index 000000000..46daeed76 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/tools/data_prep/ingest_kaggle_multitask.py @@ -0,0 +1,79 @@ + +import argparse, csv, random +from pathlib import Path + +IMG_EXT = {".jpg",".jpeg",".png",".bmp",".tif",".tiff",".webp"} + +RIPENESS_MAP = { + "unripe": "unripe", + "fresh": "ripe", + "ripe": "ripe", + "rotten": "overripe", +} + +FRUIT_KEYS = ["apple", "banana", "orange", "pineapple"] + +def detect_from_path(p: Path): + names = [pp.name.lower().replace(" ", "").replace("_","") for pp in [p] + list(p.parents)] + fruit = None + ripeness = None + + for n in names: + for fk in FRUIT_KEYS: + if fk in n: + fruit = fk + break + for key, mapped in RIPENESS_MAP.items(): + if key in n: + ripeness = mapped + break + if fruit and ripeness: + return fruit, ripeness + return fruit, ripeness + +def gather(root: Path): + rows = [] # (path, fruit, ripeness) + for fp in root.rglob("*"): + if fp.is_file() and fp.suffix.lower() in IMG_EXT: + fruit, ripeness = detect_from_path(fp) + if fruit and ripeness: + rows.append((fp.resolve().as_posix(), fruit, ripeness)) + return rows + +def write_csv(path: Path, rows): + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(["path","fruit","ripeness"]) + w.writerows(rows) + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Create CSVs (train/val/test) with path,fruit,ripeness from Kaggle folders") + ap.add_argument("--src", required=True, help="path to .../dataset (the folder that contains train/ and test/)") + ap.add_argument("--outdir", default="data_mt", help="output folder for CSVs") + ap.add_argument("--split", default="0.8,0.2,0.0", help="train,val,test ratios") + ap.add_argument("--seed", type=int, default=42) + args = ap.parse_args() + + root = Path(args.src).resolve() + all_rows = gather(root) + if not all_rows: + raise SystemExit(f"No images found under: {root}. Check --src path.") + + random.seed(args.seed) + random.shuffle(all_rows) + + tr, va, te = [float(x) for x in args.split.split(",")] + assert abs(tr+va+te - 1.0) < 1e-6, "--split must sum to 1.0" + n = len(all_rows); ntr = int(tr*n); nv = int(va*n) + rows_tr = all_rows[:ntr]; rows_va = all_rows[ntr:ntr+nv]; rows_te = all_rows[ntr+nv:] + + out = Path(args.outdir) + write_csv(out/"train.csv", rows_tr) + write_csv(out/"val.csv", rows_va) + write_csv(out/"test.csv", rows_te) + + print(f"Saved CSVs in {out.resolve()}") + print(f" train.csv: {len(rows_tr)}") + print(f" val.csv: {len(rows_va)}") + print(f" test.csv: {len(rows_te)}") diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/tools/data_prep/prepare_from_minio.py b/AgCloud/services/fruit-orchestration/services/ripeness/tools/data_prep/prepare_from_minio.py new file mode 100644 index 000000000..0afb84c54 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/tools/data_prep/prepare_from_minio.py @@ -0,0 +1,161 @@ +# AGCLOUD/services/ripeness-ml/scripts/prepare_from_minio.py +import os, io, csv, argparse, sys, re, datetime as dt +from pathlib import Path +from typing import Dict, List, Tuple, Optional +from minio import Minio +from minio.error import S3Error +from tqdm import tqdm +import random + +def parse_args(): + p = argparse.ArgumentParser(description="Sync labeled images from MinIO into local data/train|val|test/") + p.add_argument("--minio-url", required=True, help="e.g. http://127.0.0.1:9000") + p.add_argument("--access-key", required=False, default=os.getenv("MINIO_ACCESS_KEY","minioadmin")) + p.add_argument("--secret-key", required=False, default=os.getenv("MINIO_SECRET_KEY","minioadmin")) + p.add_argument("--secure", action="store_true", help="use HTTPS") + p.add_argument("--bucket", required=True, help="e.g. classification") + p.add_argument("--prefix", required=True, help="e.g. samples/2025/ or samples/") + p.add_argument("--outdir", default="data", help="local output root") + p.add_argument("--split", default="0.7,0.15,0.15", help="train,val,test ratios") + p.add_argument("--labels-csv", help="path to labels.csv (local file) OR object path in bucket (starts without leading /)") + p.add_argument("--infer-label-from-folder", action="store_true", help="take class name from folder under prefix") + p.add_argument("--from-date", help="YYYY-MM-DD (inclusive)") + p.add_argument("--to-date", help="YYYY-MM-DD (inclusive)") + p.add_argument("--last-days", type=int, help="Use only last N days under prefix (overrides from/to)") + p.add_argument("--dry-run", action="store_true") + return p.parse_args() + +def list_objects(client: Minio, bucket: str, prefix: str): + return client.list_objects(bucket, prefix=prefix, recursive=True) + +DATE_RE = re.compile(r"/(\d{4})/(\d{2})/(\d{2})(?:/|$)") + +def object_date(obj_name: str) -> Optional[dt.date]: + m = DATE_RE.search("/"+obj_name.strip("/")) + if not m: return None + y, mth, d = map(int, m.groups()) + return dt.date(y, mth, d) + +def load_labels_from_csv_local(csv_path: str) -> Dict[str, str]: + mapping = {} + with open(csv_path, "r", newline="", encoding="utf-8") as f: + r = csv.DictReader(f) + for row in r: + mapping[row["object"].strip()] = row["label"].strip() + return mapping + +def load_labels_from_csv_minio(client: Minio, bucket: str, obj_path: str) -> Dict[str, str]: + resp = client.get_object(bucket, obj_path) + data = resp.read().decode("utf-8") + mapping = {} + for row in csv.DictReader(io.StringIO(data)): + mapping[row["object"].strip()] = row["label"].strip() + return mapping + +def ensure_dirs(root: Path, classes: List[str]): + for split in ["train","val","test"]: + for c in classes: + (root/split/c).mkdir(parents=True, exist_ok=True) + +def main(): + args = parse_args() + tr, va, te = [float(x) for x in args.split.split(",")] + assert abs(tr+va+te - 1.0) < 1e-6, "--split must sum to 1.0" + + secure = args.secure or args.minio_url.startswith("https://") + endpoint = args.minio_url.replace("http://","").replace("https://","") + client = Minio(endpoint, access_key=args.access_key, secret_key=args.secret_key, secure=secure) + + # python arg names can't contain hyphen; fallback + access = getattr(args, "access_key", getattr(args, "access-key", None)) + secret = getattr(args, "secret_key", getattr(args, "secret-key", None)) + client = Minio(endpoint, access_key=access, secret_key=secret, secure=secure) + + # gather all candidate objects under prefix + objs = list(list_objects(client, args.bucket, args.prefix)) + if len(objs)==0: + print("No objects under prefix:", args.prefix); sys.exit(1) + + # filter by date + if args.last_days: + cutoff = dt.date.today() - dt.timedelta(days=args.last_days) + objs = [o for o in objs if (object_date(o.object_name) or dt.date.min) >= cutoff] + else: + dfrom = dt.date.fromisoformat(args.from_date) if args.from_date else None + dto = dt.date.fromisoformat(args.to_date) if args.to_date else None + if dfrom or dto: + def inrange(o): + od = object_date(o.object_name) + if not od: return False + if dfrom and od < dfrom: return False + if dto and od > dto: return False + return True + objs = [o for o in objs if inrange(o)] + + # Build label mapping + label_map: Dict[str,str] = {} + classes: set = set() + + if args.labels_csv: + if os.path.exists(args.labels_csv): + label_map = load_labels_from_csv_local(args.labels_csv) + else: + label_map = load_labels_from_csv_minio(client, args.bucket, args.labels_csv) + classes = set(label_map.values()) + candidates = [(o.object_name, label_map.get(o.object_name)) for o in objs if o.object_name in label_map] + elif args.infer_label_from_folder: + # Expect ...//... somewhere AFTER prefix + pref = args.prefix.strip("/") + candidates = [] + for o in objs: + rel = o.object_name[len(pref):].strip("/") + parts = rel.split("/") + if len(parts)>=2: + cls = parts[0] + candidates.append((o.object_name, cls)) + classes.add(cls) + if not classes: + print("Could not infer classes from folders; provide --labels-csv", file=sys.stderr) + sys.exit(2) + else: + print("Provide either --labels-csv or --infer-label-from-folder", file=sys.stderr) + sys.exit(2) + + classes = sorted(list(classes)) + print("Classes:", classes, "| samples:", len(candidates)) + root = Path(args.outdir) + ensure_dirs(root, classes) + + # stratified split by class + by_cls: Dict[str, List[str]] = {c: [] for c in classes} + for obj, lab in candidates: + if lab in by_cls: + by_cls[lab].append(obj) + for c in classes: random.shuffle(by_cls[c]) + + plan: List[Tuple[str, str]] = [] # (object_name, target_path) + for c in classes: + items = by_cls[c] + n = len(items); ntr = int(tr*n); nv = int(va*n) + tr_items = items[:ntr]; va_items = items[ntr:ntr+nv]; te_items = items[ntr+nv:] + for src in tr_items: + plan.append((src, str(root/ "train"/c/ Path(src).name))) + for src in va_items: + plan.append((src, str(root/ "val"/c/ Path(src).name))) + for src in te_items: + plan.append((src, str(root/ "test"/c/ Path(src).name))) + + if args.dry_run: + print(f"DRY-RUN: would download {len(plan)} files.") + return + + # download + for src, dst in tqdm(plan, desc="Downloading"): + dpath = Path(dst) + if dpath.exists(): continue + dpath.parent.mkdir(parents=True, exist_ok=True) + client.fget_object(args.bucket, src, dst) + print("Done. Data prepared under:", root.resolve()) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/tools/export/export_onnx_conditional.py b/AgCloud/services/fruit-orchestration/services/ripeness/tools/export/export_onnx_conditional.py new file mode 100644 index 000000000..1431ea0e6 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/tools/export/export_onnx_conditional.py @@ -0,0 +1,25 @@ +import torch, yaml, os +from model.architecture.mobilenet_v3_large_head import build_conditional + +if __name__ == "__main__": + cfg = yaml.safe_load(open("configs/config.yaml")) + fruits = cfg["fruits"] + ripeness = cfg["ripeness"] + + model = build_conditional(num_ripeness=len(ripeness), num_fruits=len(fruits)) + ckpt_path = os.path.join(cfg["checkpoint_dir"], "best_conditional.pt") + model.load_state_dict(torch.load(ckpt_path, map_location="cpu")) + model.eval() + + dummy_x = torch.randn(1, 3, cfg["img_size"], cfg["img_size"]) + dummy_f = torch.zeros(1, dtype=torch.long) # example fruit index + torch.onnx.export( + model, (dummy_x, dummy_f), + "ripeness_conditional.onnx", + input_names=["image", "fruit_idx"], + output_names=["ripeness_logits"], + dynamic_axes={"image": {0: "batch"}, "ripeness_logits": {0: "batch"}}, + opset_version=13 + ) + + print("✅ Exported: ripeness_conditional.onnx") diff --git a/AgCloud/services/fruit-orchestration/services/ripeness/tools/inference/infer_minio_batch.py b/AgCloud/services/fruit-orchestration/services/ripeness/tools/inference/infer_minio_batch.py new file mode 100644 index 000000000..0206a3791 --- /dev/null +++ b/AgCloud/services/fruit-orchestration/services/ripeness/tools/inference/infer_minio_batch.py @@ -0,0 +1,193 @@ +# AGCLOUD/services/ripeness-ml/scripts/infer_minio_batch.py +import argparse, os, sys, csv, json +from io import BytesIO +from pathlib import Path + +import numpy as np +from PIL import Image +from minio import Minio +from tqdm import tqdm +import onnxruntime as ort +from torchvision import transforms + +# ---- Configurable defaults ---- +IMAGENET_MEAN = (0.485, 0.456, 0.406) +IMAGENET_STD = (0.229, 0.224, 0.225) +IMG_TFM = transforms.Compose([ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), +]) + +DEFAULT_FRUITS = ["apple", "banana", "orange", "pineapple"] # order matters! +RIPENESS = ["unripe", "ripe", "overripe"] + +IMG_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp") + + +def parse_args(): + p = argparse.ArgumentParser( + description="Batch inference from MinIO prefix with conditional ONNX model (image + fruit_idx)." + ) + p.add_argument("--minio-url", required=True, help="http://127.0.0.1:9001") + p.add_argument("--access-key", default=os.getenv("MINIO_ACCESS_KEY", "minioadmin")) + p.add_argument("--secret-key", default=os.getenv("MINIO_SECRET_KEY", "minioadmin")) + p.add_argument("--secure", action="store_true", help="Use HTTPS") + + p.add_argument("--bucket", required=True, help="MinIO bucket name") + p.add_argument("--prefix", help="Prefix to scan, e.g. samples/2025/10/15 (ignored if --pairs-csv is used)") + + p.add_argument("--onnx", default="ripeness_conditional.onnx", help="Path to conditional ONNX model") + p.add_argument("--providers", nargs="*", default=None, help="ONNX Runtime providers list (default: CPU)") + + # Fruit specification + p.add_argument("--fruit", help="Fruit for ALL objects (apple|banana|orange|pineapple)") + p.add_argument("--pairs-csv", help="CSV file with columns: object,fruit (mapping per object)") + + # Fruits list order (so fruit_idx matches training) + p.add_argument("--fruits", default=None, + help='Fruits list in order, e.g. \'["apple","banana","orange","pineapple"]\' or "apple,banana,orange,pineapple"') + + # Output + p.add_argument("--out-csv", help="Optional: write results to CSV (object,fruit,label,prob_unripe,prob_ripe,prob_overripe)") + p.add_argument("--quiet", action="store_true", help="Do not print JSON lines to stdout") + + args = p.parse_args() + + if not args.pairs_csv and not (args.prefix and args.fruit): + p.error("Provide either --pairs-csv OR both --prefix and --fruit.") + + return args + + +def parse_fruits_list(fruits_arg): + if not fruits_arg: + return DEFAULT_FRUITS + s = fruits_arg.strip() + if s.startswith("["): + # JSON-ish + try: + import json as _json + lst = _json.loads(s) + return [x.strip().lower() for x in lst] + except Exception: + pass + # comma separated + return [x.strip().lower() for x in s.split(",") if x.strip()] + + +def softmax(x): + x = x - x.max(axis=1, keepdims=True) + e = np.exp(x) + return e / e.sum(axis=1, keepdims=True) + + +def is_image(name: str) -> bool: + return name.lower().endswith(IMG_EXTS) + + +def load_pairs_csv(path: str): + mapping = {} + with open(path, "r", newline="", encoding="utf-8") as f: + r = csv.DictReader(f) + if "object" not in r.fieldnames or "fruit" not in r.fieldnames: + raise SystemExit("pairs CSV must have columns: object,fruit") + for row in r: + obj = row["object"].strip() + fruit = row["fruit"].strip().lower() + mapping[obj] = fruit + return mapping + + +def open_minio(args): + secure = args.secure or args.minio_url.startswith("https://") + endpoint = args.minio_url.replace("http://", "").replace("https://", "") + return Minio(endpoint, access_key=args.access_key, secret_key=args.secret_key, secure=secure) + + +def main(): + args = parse_args() + fruits = parse_fruits_list(args.fruits) + + # Validate fruit names + fruit_set = set(fruits) + + # Prepare ONNX Runtime session + providers = args.providers or ["CPUExecutionProvider"] + sess = ort.InferenceSession(args.onnx, providers=providers) + + client = open_minio(args) + + # Prepare iterator over (object_name, fruit) + if args.pairs_csv: + mapping = load_pairs_csv(args.pairs_csv) + # Only iterate the keys present in the CSV (no MinIO list needed) + iterator = [(obj, mapping[obj]) for obj in mapping] + else: + fixed_fruit = args.fruit.lower() + if fixed_fruit not in fruit_set: + raise SystemExit(f"--fruit must be one of {fruits}; got {fixed_fruit}") + iterator = [] + for obj in client.list_objects(args.bucket, prefix=args.prefix, recursive=True): + if is_image(obj.object_name): + iterator.append((obj.object_name, fixed_fruit)) + + # Output CSV writer (optional) + csv_writer = None + if args.out_csv: + Path(args.out_csv).parent.mkdir(parents=True, exist_ok=True) + fcsv = open(args.out_csv, "w", newline="", encoding="utf-8") + csv_writer = csv.writer(fcsv) + csv_writer.writerow(["object", "fruit", "label", "prob_unripe", "prob_ripe", "prob_overripe"]) + + # Run predictions + for obj_name, fruit in tqdm(iterator, desc="Predicting"): + if fruit not in fruit_set: + # Unknown fruit -> skip + if not args.quiet: + print(json.dumps({"object": obj_name, "error": f"unknown fruit '{fruit}' (allowed {fruits})"}, ensure_ascii=False)) + continue + + # Fetch image bytes + if args.pairs_csv: + # object names in CSV must be full paths in bucket + resp = client.get_object(args.bucket, obj_name) + else: + resp = client.get_object(args.bucket, obj_name) + + try: + img = Image.open(BytesIO(resp.read())).convert("RGB") + finally: + resp.close(); resp.release_conn() + + x = IMG_TFM(img).unsqueeze(0).numpy() + fidx = np.array([fruits.index(fruit)], dtype=np.int64) + + logits = sess.run(["ripeness_logits"], {"images": x, "fruit_idx": fidx})[0] + prob = softmax(logits)[0] + idx = int(prob.argmax()) + label = RIPENESS[idx] + + record = { + "object": obj_name, + "fruit": fruit, + "label": label, + "probs": {RIPENESS[i]: float(prob[i]) for i in range(len(RIPENESS))} + } + + if not args.quiet: + print(json.dumps(record, ensure_ascii=False)) + + if csv_writer: + csv_writer.writerow([ + obj_name, fruit, label, + f"{prob[0]:.6f}", f"{prob[1]:.6f}", f"{prob[2]:.6f}" + ]) + + if csv_writer: + fcsv.close() + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit_classifier/.dockerignore b/AgCloud/services/fruit_classifier/.dockerignore new file mode 100644 index 000000000..77b0c25d3 --- /dev/null +++ b/AgCloud/services/fruit_classifier/.dockerignore @@ -0,0 +1,12 @@ +.git +__pycache__ +*.pyc +*.pyo +*.log +*.DS_Store +venv +.venv +node_modules +dist +build +samples diff --git a/AgCloud/services/fruit_classifier/.gitignore b/AgCloud/services/fruit_classifier/.gitignore new file mode 100644 index 000000000..b70a32d67 --- /dev/null +++ b/AgCloud/services/fruit_classifier/.gitignore @@ -0,0 +1,2 @@ +.env +.*.crt \ No newline at end of file diff --git a/AgCloud/services/fruit_classifier/README.md b/AgCloud/services/fruit_classifier/README.md new file mode 100644 index 000000000..bc4853e6a --- /dev/null +++ b/AgCloud/services/fruit_classifier/README.md @@ -0,0 +1,85 @@ +🥭 Fruit Classification – Inference Service + +This service performs batch inference for fruit classification using a trained PyTorch model. +It connects to MinIO for input images and logs results into PostgreSQL. + +⚙️ 1. Environment Configuration + +Create a .env file in the project root with the following variables: + +# --- MinIO Connection --- +S3_ENDPOINT=http://host.docker.internal:9000 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin +S3_SECURE=false +S3_BUCKET=classification +S3_PREFIX=samples + +# --- Model & Config Paths --- +WEIGHTS_PATH=models/best.pt +LABELS_PATH=models/labels.json +CFG_PATH=configs/fruit_cls.yaml + +# --- Database Connection --- +DATABASE_URL=postgresql://missions_user:pg123@host.docker.internal:5432/missions_db?sslmode=disable + + +🧠 Tip: +If your PostgreSQL or MinIO services run on external servers, update host.docker.internal to the relevant hostname or IP. + +🔒 2. Certificates (Optional) + +If your environment requires SSL interception (e.g., behind filtered networks like NetFree), +add the certificate file (e.g. netfree-ca.crt) to the project root — +it will be installed into the Docker image automatically. + +🐳 3. Build the Docker Image +docker compose build + +▶️ 4. Run the Service +docker compose up + + +This will: + +Load the model and configuration. + +Fetch images from MinIO (s3://classification/samples/...). + +Perform inference. + +Write classification results to the PostgreSQL inference_logs table. + +✅ 5. Prerequisites Checklist + +Before running the service, make sure you have: + +Component Requirement +PostgreSQL A database named missions_db containing a table inference_logs. +MinIO Bucket: classification, prefix: samples/. +Data Folder Structure samples//// containing image files. + +Example MinIO path: + +classification/ + └── samples/ + └── 2025/ + └── 10/ + └── 15/ + ├── apple1.png + ├── freshGrape (7).jpg + └── ... + +🧩 Example Output +[INFO] s3://classification/samples/2025/10/15/ | secure=False +{"object": "samples/2025/10/15/apple1.png", "fruit_type": "Apple", "score": 0.9258} +{"object": "samples/2025/10/15/freshOrange.png", "fruit_type": "Orange", "score": 0.9123} +[DONE] processed=25 | date=2025-10-15 + +🛠️ Notes + +The service automatically retries MinIO connections. + +Database inserts are skipped if the connection fails (with a warning). + +To rebuild dependencies or configuration, use docker compose up --build. \ No newline at end of file diff --git a/AgCloud/services/fruit_classifier/configs/fruit_cls.yaml b/AgCloud/services/fruit_classifier/configs/fruit_cls.yaml new file mode 100644 index 000000000..ef0495fc2 --- /dev/null +++ b/AgCloud/services/fruit_classifier/configs/fruit_cls.yaml @@ -0,0 +1,46 @@ +seed: 42 +device: cpu # 'cpu' or 'cuda' if available +num_workers: 4 + +# Data +image_size: 224 +batch_size: 64 +val_batch_size: 128 +train_split: 0.9 # if building split from MinIO set + +# Model +backbone: mobilenet_v3_small # options: mobilenet_v3_small, efficientnet_b0, clip_mini +pretrained: true +freeze_backbone: false +num_epochs: 15 +learning_rate: 0.001 +weight_decay: 0.0001 +label_smoothing: 0.1 + +# Augmentations (baseline: torchvision) +augment: + brightness: 0.3 + contrast: 0.3 + saturation: 0.2 + hue: 0.05 + random_resized_crop_scale: [0.7, 1.0] + random_erasing_p: 0.25 + horizontal_flip_p: 0.5 + color_jitter_p: 0.8 + +# MinIO / dataset +s3: + endpoint: ${S3_ENDPOINT} + access_key: ${S3_ACCESS_KEY} + secret_key: ${S3_SECRET_KEY} + secure: ${S3_SECURE} + bucket: ${S3_BUCKET} + prefix: ${S3_PREFIX} + artifacts_bucket: ${ARTIFACTS_BUCKET} + artifacts_prefix: ${ARTIFACTS_PREFIX} + data_cache_dir: ${DATA_CACHE_DIR} + +# Few-shot add-class (optional, classification via nearest-centroid in embedding space) +few_shot: + enabled: false + shots_per_class: 5 diff --git a/AgCloud/services/fruit_classifier/docker-compose.yaml b/AgCloud/services/fruit_classifier/docker-compose.yaml new file mode 100644 index 000000000..e9d18c903 --- /dev/null +++ b/AgCloud/services/fruit_classifier/docker-compose.yaml @@ -0,0 +1,12 @@ +services: + batch: + build: + context: . + args: + TORCH_INDEX_URL: https://download.pytorch.org/whl/cpu + container_name: fruit-batch + env_file: .env + volumes: + - .:/app + restart: "no" + command: ["python", "-m", "inference.infer_minio_batch", "--limit", "100"] diff --git a/AgCloud/services/fruit_classifier/dockerfile b/AgCloud/services/fruit_classifier/dockerfile new file mode 100644 index 000000000..b523afba3 --- /dev/null +++ b/AgCloud/services/fruit_classifier/dockerfile @@ -0,0 +1,75 @@ +# ========================= +# Stage 1: Build Python wheels +# ========================= +FROM python:3.11-slim AS builder + +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG NO_PROXY +ARG TORCH_INDEX_URL="https://download.pytorch.org/whl/cpu" + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + TZ=UTC + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* + +# <<< Add: Organization CA certificate >>> +COPY *.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates + +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +COPY requirements.txt . + +RUN pip install --upgrade pip +RUN pip install --no-cache-dir --index-url ${TORCH_INDEX_URL} \ + torch==2.3.1 torchvision==0.18.1 + +# Exclude torch and torchvision from the second requirements installation +RUN grep -viE '^(torch|torchvision)=' requirements.txt > req-no-torch.txt && \ + pip wheel --no-cache-dir --wheel-dir=/wheels -r req-no-torch.txt + + +# ========================= +# Stage 2: Runtime environment +# ========================= +FROM python:3.11-slim + +ARG HTTP_PROXY +ARG HTTPS_PROXY +ARG NO_PROXY + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + TZ=UTC \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# <<< Add: Same CA certificate in runtime stage >>> +COPY *.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates + +# Install dependencies from Stage 1 (including torch) + wheels +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels + +# Copy project files +COPY . /app + +# Create non-root user +RUN useradd -m appuser +USER appuser + +# Default command: run the batch inference +CMD ["python", "-m", "inference.infer_minio_batch"] diff --git a/AgCloud/services/fruit_classifier/inference/infer_minio_batch.py b/AgCloud/services/fruit_classifier/inference/infer_minio_batch.py new file mode 100644 index 000000000..e67f2e988 --- /dev/null +++ b/AgCloud/services/fruit_classifier/inference/infer_minio_batch.py @@ -0,0 +1,202 @@ +import argparse +import io +import json +import os +from datetime import date, datetime +from pathlib import Path +from typing import Iterable, Tuple + +import torch +import torch.nn.functional as F +from PIL import Image +from dotenv import load_dotenv +from minio import Minio +from minio.deleteobjects import DeleteObject +from time import perf_counter + + +from inference.utils_infer import build_infer_transforms, load_model + +# Optional: DB logging (graceful if missing) +try: + from metrics_db.db import insert_inference_log + DB_AVAILABLE = True +except Exception: + DB_AVAILABLE = False + + +def make_minio_client(endpoint: str, access_key: str, secret_key: str, + secure: bool) -> Minio: + ep = endpoint.replace("http://", "").replace("https://", "") + return Minio(ep, access_key=access_key, secret_key=secret_key, secure=secure) + + +def list_images(client: Minio, bucket: str, prefix: str) -> Iterable[str]: + exts = {".jpg", ".jpeg", ".png", ".bmp", ".webp"} + for obj in client.list_objects(bucket, prefix=prefix, recursive=True): + name = obj.object_name + if Path(name).suffix.lower() in exts: + yield name + + +def load_labels(labels_path: str) -> Tuple[dict, dict]: + with open(labels_path, "r", encoding="utf-8") as f: + labels_map = json.load(f) + if all(isinstance(k, str) for k in labels_map.keys()): + idx_to_class = {int(v): str(k) for k, v in labels_map.items()} + else: + idx_to_class = {int(k): str(v) for k, v in labels_map.items()} + return labels_map, idx_to_class + + +def build_cfg(cfg_path: str) -> dict: + import yaml + with open(cfg_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + + +def classify_bytes(model, tfms, idx_to_class: dict, file_bytes: bytes): + + img = Image.open(io.BytesIO(file_bytes)).convert("RGB") + x = tfms(img).unsqueeze(0) + model.eval() + with torch.no_grad(): + logits = model(x) + probs = F.softmax(logits, dim=1) + score, pred = probs.max(dim=1) + return idx_to_class[int(pred.item())], float(score.item()) + + +def build_prefix(base_prefix: str, dt: date) -> str: + yyyy = f"{dt.year:04d}" + mm = f"{dt.month:02d}" + dd = f"{dt.day:02d}" + return f"{base_prefix.strip('/')}/{yyyy}/{mm}/{dd}/" + + +def build_minio_url(endpoint: str, bucket: str, object_name: str) -> str: + ep = endpoint if endpoint.startswith("http") else f"http://{endpoint}" + return f"{ep.rstrip('/')}/{bucket}/{object_name}" + + +def main(): + load_dotenv() + + p = argparse.ArgumentParser( + description="Batch inference from MinIO by YYYY/MM/DD folder" + ) + p.add_argument("--date", type=str, default=date.today().isoformat(), + help="YYYY-MM-DD (default: today)") + p.add_argument("--year", type=int, help="Alternative to --date: year") + p.add_argument("--month", type=int, help="Alternative to --date: month") + p.add_argument("--day", type=int, help="Alternative to --date: day") + p.add_argument("--dry-run", action="store_true", + help="Print only, do not write DB") + p.add_argument("--limit", type=int, default=0, + help="Limit number of images (0 = no limit)") + p.add_argument("--delete-ok", action="store_true", + help="Delete images from MinIO after success") + p.add_argument("--weights", type=str, + default=os.getenv("WEIGHTS_PATH", "models/best.pt")) + p.add_argument("--labels", type=str, + default=os.getenv("LABELS_PATH", "models/labels.json")) + p.add_argument("--cfg", type=str, + default=os.getenv("CFG_PATH", "configs/fruit_cls.yaml")) + args = p.parse_args() + + # Load cfg/model + cfg = build_cfg(args.cfg) + res = load_model(args.weights, args.labels, + backbone=cfg.get("backbone", "mobilenet_v3_small")) + model = res[0] if isinstance(res, tuple) else res + _, idx_to_class = load_labels(args.labels) + tfms = build_infer_transforms(cfg.get("image_size", 224)) + + # MinIO settings (ENV > YAML) + endpoint = os.getenv("S3_ENDPOINT", cfg["s3"]["endpoint"]) + access_key = os.getenv("S3_ACCESS_KEY", cfg["s3"]["access_key"]) + secret_key = os.getenv("S3_SECRET_KEY", cfg["s3"]["secret_key"]) + secure = str(os.getenv("S3_SECURE", cfg["s3"].get("secure", False)) + ).lower() == "true" + bucket = os.getenv("S3_BUCKET", cfg["s3"]["bucket"]) + base_prefix = os.getenv("S3_PREFIX", cfg["s3"].get("base_prefix", "images")) + + # Pick date + if args.year and args.month and args.day: + run_date = date(args.year, args.month, args.day) + else: + run_date = datetime.strptime(args.date, "%Y-%m-%d").date() + + day_prefix = build_prefix(base_prefix, run_date) + client = make_minio_client(endpoint, access_key, secret_key, secure) + + print(f"[INFO] s3://{bucket}/{day_prefix} | secure={secure}") + count, to_delete = 0, [] + + for object_name in list_images(client, bucket, day_prefix): + if args.limit and count >= args.limit: + break + + # Download + try: + resp = client.get_object(bucket, object_name) + data = resp.read() + resp.close() + resp.release_conn() + except Exception as e: + print(f"[WARN] download failed: {object_name} | {e}") + continue + + + try: + # Classify + + t0 = perf_counter() + cls, score = classify_bytes(model, tfms, idx_to_class, data) + t_ms = (perf_counter() - t0) * 1000.0 + + cls, score = classify_bytes(model, tfms, idx_to_class, data) + print(json.dumps({ + "object": object_name, + "fruit_type": cls, + "score": round(score, 4), + })) + + # DB log + if DB_AVAILABLE and not args.dry_run and os.getenv("DATABASE_URL"): + try: + image_url = build_minio_url(endpoint, bucket, object_name) + insert_inference_log( + model_backbone=cfg.get("backbone", "mobilenet_v3_small"), + image_size=cfg.get("image_size", 224), + fruit_type=str(cls), + score=float(score), + latency_ms=float(t_ms), + client_ip=f"minio-batch:{run_date.isoformat()}", + error=None, + image_url=image_url, + ) + + except Exception as db_e: + print(f"[WARN] DB insert failed: {db_e}") + + # Delete after success if requested + if args.delete_ok: + to_delete.append(DeleteObject(object_name)) + + except Exception as e: + print(f"[ERR] classify failed: {object_name} | {e}") + + count += 1 + + # Bulk delete + if to_delete: + errors = client.remove_objects(bucket, to_delete) + for err in errors: + print(f"[WARN] delete failed: {err.object_name}: {err}") + + print(f"[DONE] processed={count} | date={run_date.isoformat()}") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit_classifier/inference/service.py b/AgCloud/services/fruit_classifier/inference/service.py new file mode 100644 index 000000000..9c3a0f123 --- /dev/null +++ b/AgCloud/services/fruit_classifier/inference/service.py @@ -0,0 +1,139 @@ +import os +import io +import json +import time +from typing import Tuple + +import torch +import torch.nn.functional as F +from PIL import Image + +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import Response +from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST + +from dotenv import load_dotenv +load_dotenv() + +# --- Metrics --- +REQS = Counter("inference_requests_total", "Total inference requests") +ERRS = Counter("inference_errors_total", "Total inference errors") +LATENCY = Histogram("inference_latency_seconds", "Inference latency per image (seconds)") +LOADED = Gauge("model_loaded", "Model loaded (1=yes)") + +# --- Config/Model/Transforms are loaded once --- +from inference.utils_infer import build_infer_transforms, load_model +from metrics_db.db import insert_inference_log + +app = FastAPI() + +MODEL = None +LABELS = None +TFMS = None +CFG = None # will be loaded from YAML config + +def _load_labels(labels_path: str): + with open(labels_path, "r", encoding="utf-8") as f: + d = json.load(f) + idx_to_class = {int(v): k for k, v in d.items()} + return d, idx_to_class + +def preprocess_image(file_bytes: bytes) -> torch.Tensor: + img = Image.open(io.BytesIO(file_bytes)).convert("RGB") + x = TFMS(img) # [C,H,W] + x = x.unsqueeze(0) # [1,C,H,W] + return x + +def run_inference(model: torch.nn.Module, x: torch.Tensor, idx_to_class: dict) -> Tuple[str, float]: + model.eval() + with torch.no_grad(): + logits = model(x) + probs = F.softmax(logits, dim=1) + score, pred = probs.max(dim=1) # [1] + cls_idx = int(pred.item()) + cls_name = idx_to_class[cls_idx] + return cls_name, float(score.item()) + +@app.on_event("startup") +def on_startup(): + """ + Load config, model, labels, and transforms once at startup. + Supports load_model(...) returning either: + - model + - (model, labels_map) where labels_map is {class_name: idx} or {idx: class_name} + """ + global MODEL, LABELS, TFMS, CFG + + weights_path = os.environ["WEIGHTS_PATH"] + labels_path = os.environ["LABELS_PATH"] + cfg_path = os.environ["CFG_PATH"] + + import yaml + with open(cfg_path, "r", encoding="utf-8") as f: + CFG = yaml.safe_load(f) + + # --- load model (support both signatures) --- + res = load_model(weights_path, labels_path, backbone=CFG.get("backbone", "mobilenet_v3_small")) + if isinstance(res, tuple): + MODEL, labels_map = res + else: + MODEL = res + # fallback: load labels_map from file + with open(labels_path, "r", encoding="utf-8") as lf: + labels_map = json.load(lf) + + # labels_map can be {class_name: idx} OR {idx: class_name}. + # Normalize to idx_to_class: {int(idx): class_name} + if all(isinstance(k, str) for k in labels_map.keys()): + # assume {class_name: idx} + idx_to_class = {int(v): str(k) for k, v in labels_map.items()} + else: + # assume {idx: class_name} + idx_to_class = {int(k): str(v) for k, v in labels_map.items()} + + LABELS = labels_map + app.state.idx_to_class = idx_to_class + + # --- transforms --- + TFMS = build_infer_transforms(image_size=CFG.get("image_size", 224)) + + # ready + LOADED.set(1) + print("Startup OK | model loaded:", type(MODEL).__name__, + "| classes:", len(app.state.idx_to_class)) + + +@app.post("/infer") +async def infer(file: UploadFile = File(...)): + start = time.time() + try: + raw = await file.read() + x = preprocess_image(raw) + pred, score = run_inference(MODEL, x, app.state.idx_to_class) + REQS.inc() + latency_ms = (time.time() - start) * 1000.0 + LATENCY.observe(latency_ms / 1000.0) + + # Try to write to DB, but don’t crash the API if it fails + try: + insert_inference_log( + model_backbone=CFG.get("backbone", "mobilenet_v3_small"), + image_size=CFG.get("image_size", 224), + fruit_type=str(pred), + score=float(score), + latency_ms=float(latency_ms), + client_ip="127.0.0.1", + error=None, + ) + except Exception as db_e: + print(f"[WARN] DB insert failed: {db_e}") + + return {"fruit_type": pred, "score": score, "latency_ms": round(latency_ms, 2)} + + except Exception as e: + ERRS.inc() + raise HTTPException(status_code=500, detail=f"Inference failed: {e}") + +@app.get("/metrics") +def metrics(): + return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST) diff --git a/AgCloud/services/fruit_classifier/inference/utils_infer.py b/AgCloud/services/fruit_classifier/inference/utils_infer.py new file mode 100644 index 000000000..ef39c5bef --- /dev/null +++ b/AgCloud/services/fruit_classifier/inference/utils_infer.py @@ -0,0 +1,46 @@ +import io +import json +from pathlib import Path +from typing import Dict, Tuple + +import torch +from PIL import Image +from torchvision import models, transforms + +def build_infer_transforms(image_size: int): + return transforms.Compose([ + transforms.Resize(int(image_size * 1.14)), + transforms.CenterCrop(image_size), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], + std=[0.229, 0.224, 0.225]), + ]) + +def load_model(weights_path: Path, labels_json: Path, backbone: str, device: str = "cpu"): + with open(labels_json, "r", encoding="utf-8") as f: + class_to_idx = json.load(f) + idx_to_class = {v: k for k, v in class_to_idx.items()} + + if backbone == "mobilenet_v3_small": + m = models.mobilenet_v3_small() + m.classifier[-1] = torch.nn.Linear(m.classifier[-1].in_features, len(idx_to_class)) + elif backbone == "efficientnet_b0": + m = models.efficientnet_b0() + m.classifier[-1] = torch.nn.Linear(m.classifier[-1].in_features, len(idx_to_class)) + else: + raise ValueError(f"Unsupported backbone: {backbone}") + + state = torch.load(weights_path, map_location=device) + m.load_state_dict(state["model"]) + m.eval().to(device) + return m, idx_to_class + +def infer_image_bytes(model, idx_to_class: Dict[int, str], img_bytes: bytes, + tfms, device: str = "cpu") -> Tuple[str, float]: + img = Image.open(io.BytesIO(img_bytes)).convert("RGB") + x = tfms(img).unsqueeze(0).to(device) + with torch.no_grad(): + logits = model(x) + probs = torch.softmax(logits, dim=1).cpu().numpy()[0] + idx = int(probs.argmax()) + return idx_to_class[idx], float(probs[idx]) diff --git a/AgCloud/services/fruit_classifier/metrics_db/__init__.py b/AgCloud/services/fruit_classifier/metrics_db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/fruit_classifier/metrics_db/db.py b/AgCloud/services/fruit_classifier/metrics_db/db.py new file mode 100644 index 000000000..5170bb6d0 --- /dev/null +++ b/AgCloud/services/fruit_classifier/metrics_db/db.py @@ -0,0 +1,110 @@ +# metrics_db/db.py +import os +from typing import Any, Dict, Optional +import psycopg2 +from psycopg2.extensions import connection as PGConnection +from dotenv import load_dotenv + + +def _get_conn() -> PGConnection: + """ + Open a PostgreSQL connection. No DDL here. + Priority: + 1) DATABASE_URL + 2) discrete env vars (DB_HOST, DB_PORT, ...) + """ + load_dotenv() + + dsn = (os.environ.get("DATABASE_URL") or "").strip() + if dsn: + return psycopg2.connect(dsn) + + host = os.environ.get("DB_HOST", "localhost") + port = int(os.environ.get("DB_PORT", "5432")) + name = os.environ.get("DB_NAME", "fruitdb") + user = os.environ.get("DB_USER", "fruituser") + password = os.environ.get("DB_PASSWORD", "fruitpass") + sslmode = os.environ.get("DB_SSLMODE", "disable") # prod: require/verify-full + + return psycopg2.connect( + host=host, + port=port, + dbname=name, + user=user, + password=password, + sslmode=sslmode, + application_name=os.environ.get("DB_APP_NAME", "metrics_service"), + connect_timeout=int(os.environ.get("DB_CONNECT_TIMEOUT", "10")), + ) + + +def insert_training_run(rec: Dict[str, Any]) -> None: + """ + Expects keys: + backbone, image_size, num_epochs, train_split, top1_acc, best_top1_acc, + artifacts_bucket, artifacts_prefix, labels_object, best_ckpt_object, + metrics_object, cm_object, seed + All columns must already exist in table training_runs. + """ + sql = """ + INSERT INTO training_runs ( + backbone, image_size, num_epochs, train_split, top1_acc, best_top1_acc, + artifacts_bucket, artifacts_prefix, labels_object, best_ckpt_object, + metrics_object, cm_object, seed + ) + VALUES (%(backbone)s, %(image_size)s, %(num_epochs)s, %(train_split)s, + %(top1_acc)s, %(best_top1_acc)s, %(artifacts_bucket)s, + %(artifacts_prefix)s, %(labels_object)s, %(best_ckpt_object)s, + %(metrics_object)s, %(cm_object)s, %(seed)s) + """ + with _get_conn() as conn, conn.cursor() as cur: + cur.execute(sql, rec) + conn.commit() + + +def insert_inference_log( + *, + model_backbone: str, + image_size: int, + fruit_type: str, + score: float, + latency_ms: Optional[float] = None, + client_ip: Optional[str] = None, + error: Optional[str] = None, + image_url: Optional[str] = None, +) -> None: + """ + Inserts a single inference log record. Table 'inference_logs' must exist. + """ + sql = """ + INSERT INTO inference_logs ( + fruit_type, score, latency_ms, model_backbone, image_size, + client_ip, error, image_url + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + """ + with _get_conn() as conn, conn.cursor() as cur: + cur.execute( + sql, + ( + fruit_type, + score, + latency_ms, + model_backbone, + image_size, + client_ip, + error, + image_url, + ), + ) + conn.commit() + + +def db_healthcheck() -> bool: + """Simple reachability check.""" + try: + with _get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT 1;") + return cur.fetchone() == (1,) + except Exception: + return False diff --git a/AgCloud/services/fruit_classifier/models/best.pt b/AgCloud/services/fruit_classifier/models/best.pt new file mode 100644 index 000000000..81a91795b Binary files /dev/null and b/AgCloud/services/fruit_classifier/models/best.pt differ diff --git a/AgCloud/services/fruit_classifier/models/labels.json b/AgCloud/services/fruit_classifier/models/labels.json new file mode 100644 index 000000000..83b09546f --- /dev/null +++ b/AgCloud/services/fruit_classifier/models/labels.json @@ -0,0 +1,15 @@ +{ + "Apple": 0, + "Banana": 1, + "Grape": 2, + "Guava": 3, + "Jujube": 4, + "Mango": 5, + "Orange": 6, + "Pomegranate": 7, + "Strawberry": 8, + "lemon": 9, + "peach": 10, + "pear": 11, + "plum": 12 +} \ No newline at end of file diff --git a/AgCloud/services/fruit_classifier/requirements.txt b/AgCloud/services/fruit_classifier/requirements.txt new file mode 100644 index 000000000..13d19cd80 --- /dev/null +++ b/AgCloud/services/fruit_classifier/requirements.txt @@ -0,0 +1,19 @@ +torch==2.3.1 +torchvision==0.18.1 +numpy==1.26.4 +pyyaml==6.0.2 +pillow==10.4.0 +tqdm==4.66.4 +scikit-learn==1.5.1 +matplotlib==3.9.0 +minio==7.2.10 +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +prometheus-client==0.20.0 +onnx==1.16.0 +psycopg2-binary +python-multipart==0.0.9 + +python-dotenv==1.0.1 + +onnxruntime==1.18.1 diff --git a/AgCloud/services/fruit_ripeness_alert/Dockerfile b/AgCloud/services/fruit_ripeness_alert/Dockerfile new file mode 100644 index 000000000..0603b1cba --- /dev/null +++ b/AgCloud/services/fruit_ripeness_alert/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +# --- System setup --- +WORKDIR /app +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + TZ=UTC + +# --- Dependencies --- +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# --- App --- +COPY . . + + +# --- Default command --- +CMD ["python", "-u", "app.py"] diff --git a/AgCloud/services/fruit_ripeness_alert/app.py b/AgCloud/services/fruit_ripeness_alert/app.py new file mode 100644 index 000000000..9d0a724fe --- /dev/null +++ b/AgCloud/services/fruit_ripeness_alert/app.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +import os, json, uuid, requests +from datetime import datetime, timedelta, timezone +from kafka import KafkaProducer +from token_bootstrap import get_service_token + +# === Environment === +DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001") +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/app/secret/db_api_token") +KAFKA_BROKER = os.getenv("KAFKA_BROKER", "kafka:9092") +ALERT_TOPIC = os.getenv("ALERT_TOPIC", "alerts") +WINDOW_HOURS = int(os.getenv("WINDOW_HOURS", "168")) + + +def now_utc() -> datetime: + return datetime.now(timezone.utc) + +def iso(ts: datetime) -> str: + return ts.replace(tzinfo=timezone.utc).isoformat() + +def get_threshold(task_name="ripeness", headers=None): + """שולף את אחוז הסף מהטבלה task_thresholds לפי שם המשימה.""" + url = f"{DB_API_BASE}/api/tables/task_thresholds" + r = requests.get(url, headers=headers, timeout=15) + r.raise_for_status() + rows = r.json().get("rows", []) + if not rows: + print(f"[WARN] No thresholds found at all, using default 0.8") + return 0.8 + + match = next((row for row in rows if row.get("task") == task_name), None) + if not match: + print(f"[WARN] No threshold found for task={task_name}, using default 0.8") + return 0.8 + + threshold = float(match.get("threshold", 0.8)) + print(f"[INFO] Task '{task_name}' threshold: {threshold*100:.1f}%") + return threshold +from datetime import datetime, timezone + +def get_rollups(window_start, window_end, headers=None): + """ + שולפת את כל הרשומות מהטבלה ripeness_weekly_rollups_ts + ואז מסננת לפי טווח התאריכים (window_start → window_end) בפייתון. + """ + url = f"{DB_API_BASE}/api/tables/ripeness_weekly_rollups_ts" + print(f"[DEBUG] Fetching full table from {url}", flush=True) + + try: + # שולף את כל הנתונים (בלי פילטרים) + r = requests.get(url, headers=headers, timeout=60) + r.raise_for_status() + except requests.exceptions.HTTPError as e: + print(f"[ERROR] HTTP {r.status_code}: {r.text}", flush=True) + return [] + except Exception as e: + print(f"[ERROR] failed to fetch rollups: {e}", flush=True) + return [] + + data = r.json() + rows = data.get("rows", data) + + + def parse_ts(ts_str: str) -> datetime: + try: + return datetime.fromisoformat(ts_str.replace("Z", "+00:00")) + except Exception: + return datetime.min.replace(tzinfo=timezone.utc) + + filtered = [] + for row in rows: + ts = parse_ts(row.get("ts", "")) + if window_start <= ts <= window_end: + filtered.append(row) + + print(f"[INFO] Retrieved {len(filtered)} rollups after filtering (out of {len(rows)} total)") + return filtered + +def send_kafka_alert(producer, device_id, ratio, threshold): + alert = { + "alert_id": str(uuid.uuid4()), + "alert_type": "fruit_ripeness_high", + "device_id": device_id, + "started_at": iso(now_utc()), + "confidence": float(ratio), + "severity": 3, + "threshold": threshold, + "description": f"{ratio*100:.1f}% ripe/overripe fruits", # <── וגם את זה + } + + producer.send(ALERT_TOPIC, json.dumps(alert).encode("utf-8")) + producer.flush() + print(f"[ALERT] sent for {device_id}: {ratio*100:.1f}%") + +def main(): + token = get_service_token() + headers = {"Content-Type": "application/json"} + if token: + headers["X-Service-Token"] = token + + window_end = now_utc() + window_start = window_end - timedelta(hours=WINDOW_HOURS) + print(f"[INFO] Checking rollups {window_start} → {window_end}") + + threshold = get_threshold("ripeness", headers) + rows = get_rollups(window_start, window_end, headers) + if not rows: + print("[INFO] No data found.") + return + + producer = KafkaProducer(bootstrap_servers=[KAFKA_BROKER]) + + # iterate each device + for row in rows: + device_id = row.get("device_id") + pct = row.get("pct_ripe", 0.0) + if pct >= threshold: + send_kafka_alert(producer, device_id, pct, threshold) + else: + print(f"[INFO] {device_id}: below threshold {pct:.2f} < {threshold:.2f}") + + producer.close() + print("[DONE] process complete.") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/fruit_ripeness_alert/docker-compose.yml b/AgCloud/services/fruit_ripeness_alert/docker-compose.yml new file mode 100644 index 000000000..7a6966ee9 --- /dev/null +++ b/AgCloud/services/fruit_ripeness_alert/docker-compose.yml @@ -0,0 +1,23 @@ +services: + fruit_ripeness_alert: + build: . + container_name: fruit_ripeness_alert + environment: + - DB_API_BASE=http://db_api_service:8001 + - DB_API_SERVICE_NAME=fruit_ripeness_alert + - DB_ADMIN_USER=admin + - DB_ADMIN_PASS=admin123 + - DB_API_TOKEN_FILE=/app/secret/db_api_token + - KAFKA_BROKER=kafka:9092 + - ALERT_TOPIC=alerts + - WINDOW_HOURS=168 + volumes: + - .:/app + - ./secret:/app/secret + command: ["sleep", "infinity"] + networks: + - ag_cloud + +networks: + ag_cloud: + external: true diff --git a/AgCloud/services/fruit_ripeness_alert/requirements.txt b/AgCloud/services/fruit_ripeness_alert/requirements.txt new file mode 100644 index 000000000..d9e23d1df --- /dev/null +++ b/AgCloud/services/fruit_ripeness_alert/requirements.txt @@ -0,0 +1,2 @@ +requests +kafka-python diff --git a/AgCloud/services/fruit_ripeness_alert/token_bootstrap.py b/AgCloud/services/fruit_ripeness_alert/token_bootstrap.py new file mode 100644 index 000000000..a1dee593b --- /dev/null +++ b/AgCloud/services/fruit_ripeness_alert/token_bootstrap.py @@ -0,0 +1,62 @@ +import os, pathlib, time, requests + +DB_API_BASE = os.getenv("DB_API_BASE", "").strip() +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/app/secret/db_api_token") +DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "fruit_ripeness_alert").strip() or "fruit_ripeness_alert" + +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + +def _read_token(path: str) -> str | None: + p = pathlib.Path(path) + if p.exists(): + t = p.read_text(encoding="utf-8").strip() + if t and "***" not in t: + return t + return None + +def _write_token(path: str, token: str) -> None: + p = pathlib.Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + +def _try_dev_bootstrap(): + """Try to get token using /auth/_dev_bootstrap (new API).""" + url = _safe_join_url(DB_API_BASE, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": True} + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code in (200, 201): + data = r.json() + sa = data.get("service_account") or {} + token = sa.get("raw_token") or sa.get("token") + if token and "***" not in token: + print("[BOOTSTRAP] obtained token via /auth/_dev_bootstrap") + return token.strip() + print(f"[BOOTSTRAP][WARN] _dev_bootstrap returned {r.status_code}: {r.text[:100]}") + except Exception as e: + print(f"[BOOTSTRAP][ERROR] {e}") + return None + +def get_service_token() -> str | None: + """Get or create a service token automatically.""" + if not DB_API_BASE: + print("[BOOTSTRAP][WARN] DB_API_BASE not set") + return None + + # Try existing file + token = _read_token(DB_API_TOKEN_FILE) + if token: + print(f"[BOOTSTRAP] using existing token from {DB_API_TOKEN_FILE}") + return token + + # Try bootstrap (new unified API) + print(f"[BOOTSTRAP] fetching new service token from {DB_API_BASE}") + token = _try_dev_bootstrap() + if token: + _write_token(DB_API_TOKEN_FILE, token) + print(f"[BOOTSTRAP] wrote token to {DB_API_TOKEN_FILE}") + return token + + print("[BOOTSTRAP][ERROR] Could not obtain service token.") + return None diff --git a/AgCloud/services/image-linker/Dockerfile.flink b/AgCloud/services/image-linker/Dockerfile.flink new file mode 100644 index 000000000..dd05fc18d --- /dev/null +++ b/AgCloud/services/image-linker/Dockerfile.flink @@ -0,0 +1,44 @@ +# syntax=docker/dockerfile:1 +# Base Flink image +FROM flink:1.19.3-scala_2.12-java11 + +USER root + +# Add local CA (place netfree-ca.crt next to this Dockerfile before building) +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +# --- Install Python and tools --- +RUN apt-get update && \ + apt-get install -y python3 python3-pip python3-venv wget && \ + rm -rf /var/lib/apt/lists/* + +# Working directory +WORKDIR /opt/app + +# --- Add Kafka connector JAR --- + RUN mkdir -p /opt/flink/lib && \ + wget -v https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar -P /opt/flink/lib/ && \ + wget -v https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar -P /opt/flink/lib/ + +# --- Virtual environment --- +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# --- Install Python dependencies --- +RUN pip install --no-cache-dir apache-flink==1.19.3 pyyaml requests minio + +# --- Copy project files --- +COPY job_linker.py /opt/app/job_linker.py +COPY config /opt/app/config + +# --- Default environment variables --- +ENV KAFKA_BROKERS=kafka:9092 \ + CONFIG_PATH=/opt/app/config/topics.yaml + +# --- Default command: stay idle (job submitted separately) --- +CMD ["bash", "-c", "echo 'Flink Python environment ready.' && tail -f /dev/null"] diff --git a/AgCloud/services/image-linker/config/topics.yaml b/AgCloud/services/image-linker/config/topics.yaml new file mode 100644 index 000000000..bbdd3588e --- /dev/null +++ b/AgCloud/services/image-linker/config/topics.yaml @@ -0,0 +1,26 @@ +teams: + air: + metadata_topic: aerial_images_metadata + minio_topic: image.new.aerial + output_topic: image_new_aerial_connections + security: + metadata_topic: dev-security-images-keys + minio_topic: image.new.security + output_topic: image_new_security_connections + + sounds: + metadata_topic: sounds_metadata + minio_topic: sound.new.sounds + output_topic: sound_new_sounds_connections + + plants: + metadata_topic: sounds_ultra_metadata + minio_topic: sound.new.plants + output_topic: sound_new_plants_connections + + # fruits: + # metadata: dev-fruits-images-keys + # minio: image.new.fruits + # output: image.new.fruits.connections + + diff --git a/AgCloud/services/image-linker/docker-compose.yml b/AgCloud/services/image-linker/docker-compose.yml new file mode 100644 index 000000000..007e1ed7f --- /dev/null +++ b/AgCloud/services/image-linker/docker-compose.yml @@ -0,0 +1,56 @@ +services: + jobmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-jobmanager + command: jobmanager + ports: + - "8081:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - CONFIG_PATH=/opt/app/config/topics.yaml + networks: + - flink-net + - agcloud_ag_cloud + + taskmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-taskmanager + command: taskmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - CONFIG_PATH=/opt/app/config/topics.yaml + depends_on: + jobmanager: + condition: service_started + networks: + - flink-net + - agcloud_ag_cloud + + submitter: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-submit + depends_on: + jobmanager: + condition: service_started + command: > + bash -lc "sleep 10 && + flink run -m jobmanager:8081 -py /opt/app/job_linker.py && + echo 'Job submitted successfully' && + sleep 1" + networks: + - flink-net + - agcloud_ag_cloud + +networks: + flink-net: + driver: bridge + agcloud_ag_cloud: + external: true diff --git a/AgCloud/services/image-linker/job_linker.py b/AgCloud/services/image-linker/job_linker.py new file mode 100644 index 000000000..e1f66dd2c --- /dev/null +++ b/AgCloud/services/image-linker/job_linker.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Flink job: real-time matcher between metadata and MinIO events +-------------------------------------------------------------- +- Matches file_name between two Kafka topics +- Order-independent (works even if MinIO arrives before metadata) +- Clears state immediately after a successful match +- 5-minute TTL cleanup for unmatched (orphan) entries +- Exactly-once Kafka delivery +""" + +import os +import json +import time +import pathlib +import yaml +from pyflink.common import Types, WatermarkStrategy +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import ( + KafkaSource, KafkaSink, KafkaRecordSerializationSchema, DeliveryGuarantee +) +from pyflink.datastream.functions import CoProcessFunction, RuntimeContext +from pyflink.datastream.state import ValueStateDescriptor +from pyflink.datastream.checkpointing_mode import CheckpointingMode +from pyflink.common.serialization import SimpleStringSchema + + +# ---------- Configuration ---------- +CONFIG_PATH = os.getenv("CONFIG_PATH", "/opt/app/config/topics.yaml") +KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") +TTL_MS = 5 * 60 * 1000 # 5 minutes TTL for orphan cleanup + + +# ---------- Helper Functions ---------- +def normalize_name(name: str) -> str: + """Normalize file name for consistent matching.""" + if not name: + return "" + return pathlib.Path(name.strip().replace('"', "")).name.lower() + + +def try_parse_json(raw: str): + """Safely parse a JSON string.""" + try: + return json.loads(raw) + except Exception: + return None + + +def extract_file_name(raw: str) -> str: + """Extract normalized file_name from metadata or MinIO message.""" + data = try_parse_json(raw) + if not data: + return "" + if "file_name" in data: + return normalize_name(data["file_name"]) + if "Key" in data: + return normalize_name(pathlib.Path(data["Key"]).name) + records = data.get("Records") or [] + if records and "s3" in records[0]: + key = records[0]["s3"]["object"]["key"] + return normalize_name(pathlib.Path(key).name) + return "" + + +# ---------- Matcher Class ---------- +class Matcher(CoProcessFunction): + """CoProcessFunction that joins two Kafka streams by file_name.""" + + def __init__(self): + self.meta_state = None + self.minio_state = None + self.cleanup_ts_state = None + + def open(self, ctx: RuntimeContext): + self.meta_state = ctx.get_state(ValueStateDescriptor("meta_state", Types.STRING())) + self.minio_state = ctx.get_state(ValueStateDescriptor("minio_state", Types.STRING())) + self.cleanup_ts_state = ctx.get_state(ValueStateDescriptor("cleanup_ts_state", Types.LONG())) + + def process_element1(self, value, ctx): + """Handle metadata messages.""" + img = extract_file_name(value) + self._register_cleanup_timer(ctx) + self.meta_state.update(value) + minio_val = self.minio_state.value() + if minio_val: + yield self._emit_and_clear(img, value, minio_val) + + def process_element2(self, value, ctx): + """Handle MinIO messages.""" + img = extract_file_name(value) + self._register_cleanup_timer(ctx) + self.minio_state.update(value) + meta_val = self.meta_state.value() + if meta_val: + yield self._emit_and_clear(img, meta_val, value) + + def _emit_and_clear(self, file_name, meta_raw, minio_raw): + """Emit a short match result and clear both states immediately.""" + minio_data = try_parse_json(minio_raw) or {} + key = minio_data.get("Key") or minio_data.get("key") + + print(f"[MATCH] {file_name} -> {key}") + + # Clear both states after successful match + self.meta_state.clear() + self.minio_state.clear() + self.cleanup_ts_state.clear() + + # Emit minimal message + result = { + "file_name": file_name, + "key": key, + "linked_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) + } + return json.dumps(result) + + def _register_cleanup_timer(self, ctx): + """Register cleanup timer for orphan entries.""" + now = ctx.timer_service().current_processing_time() + scheduled = self.cleanup_ts_state.value() + if scheduled is None or now + TTL_MS > scheduled: + ts = now + TTL_MS + ctx.timer_service().register_processing_time_timer(ts) + self.cleanup_ts_state.update(ts) + + def on_timer(self, timestamp, ctx): + """Automatically clear unmatched states after TTL.""" + print(f"[CLEANUP] Timer fired at {time.strftime('%H:%M:%S', time.gmtime(timestamp / 1000))}") + self.meta_state.clear() + self.minio_state.clear() + self.cleanup_ts_state.clear() + print("[CLEANUP] Cleared stale state after 5 minutes") + yield from [] # fix: must return iterable (even empty) + + +# ---------- Main Function ---------- +def main(): + # Load configuration file + with open(CONFIG_PATH, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) + teams = cfg.get("teams", {}) + if not teams: + raise RuntimeError(f"No teams found in {CONFIG_PATH}") + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(1) + env.enable_checkpointing(10000, CheckpointingMode.EXACTLY_ONCE) + + for team, info in teams.items(): + meta_t = info["metadata_topic"] + minio_t = info["minio_topic"] + out_t = info["output_topic"] + + print(f"[FLINK] {meta_t} + {minio_t} -> {out_t}") + + # Create Kafka sources + meta_src = ( + KafkaSource.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_topics(meta_t) + .set_group_id(f"flink-{team}-meta") + .set_property("auto.offset.reset", "latest") + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + minio_src = ( + KafkaSource.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_topics(minio_t) + .set_group_id(f"flink-{team}-minio") + .set_property("auto.offset.reset", "latest") + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + meta_stream = env.from_source(meta_src, WatermarkStrategy.no_watermarks(), f"{team}-meta") + minio_stream = env.from_source(minio_src, WatermarkStrategy.no_watermarks(), f"{team}-minio") + + # Key both streams by file_name + keyed_meta = meta_stream.key_by(extract_file_name, key_type=Types.STRING()) + keyed_minio = minio_stream.key_by(extract_file_name, key_type=Types.STRING()) + + # Connect and process both streams + matched_stream = keyed_meta.connect(keyed_minio).process(Matcher(), output_type=Types.STRING()) + + # Define Kafka sink + sink = ( + KafkaSink.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(out_t) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .set_delivery_guarantee(DeliveryGuarantee.EXACTLY_ONCE) + .set_transactional_id_prefix(f"{team}-linker-") + .set_property("transaction.timeout.ms", "600000") + .build() + ) + + # Write results to sink + matched_stream.sink_to(sink) + + env.execute("flink-image-linker-realtime") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/image-linker/readme.md b/AgCloud/services/image-linker/readme.md new file mode 100644 index 000000000..d5ec07880 --- /dev/null +++ b/AgCloud/services/image-linker/readme.md @@ -0,0 +1,46 @@ +# Image Linker — Run Instructions + +## 1. Run the entire project +```bash +docker compose up -d --build +``` + +## 2. Start the simulator device +Navigate to the simulator directory: +```bash +cd AgCloud/simulator/drone +docker compose up -d --build +``` + +## 3. Start the Flink linking service +Navigate to the service directory: +```bash +cd AgCloud/services/image-linker +docker compose up -d --build +``` + +## 4. View metadata messages produced by the simulator +```bash +docker exec -it kafka /opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic dev-aerial-images-keys --from-beginning +``` + +## 5. Listen for messages after successful linking +```bash +docker exec -it kafka /opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic image.new.aerial.connections +``` + +## 6. Simulate MinIO bucket notifications +From PowerShell, run: +```powershell +.\send-matching-minio.ps1 -Count 5 +``` + +## 7. Example of expected linked message +After successful matching, a message like this should appear: +```json +{ + "file_name": "drone-01_20251029t093413z.jpg", + "key": "imagery/air/2025-10-29/123/drone-01_20251029T093413Z.jpg", + "linked_time": "2025-10-29T09:34:23Z" +} +``` diff --git a/AgCloud/services/image-linker/send-matching-minio.ps1 b/AgCloud/services/image-linker/send-matching-minio.ps1 new file mode 100644 index 000000000..729186d39 --- /dev/null +++ b/AgCloud/services/image-linker/send-matching-minio.ps1 @@ -0,0 +1,37 @@ +# --- Auto send MinIO messages to match metadata --- +param ( + [int]$Count = 10 # how many latest metadata messages to match +) + +Write-Host "`n[INFO] Fetching last $Count metadata messages from Kafka..." -ForegroundColor Cyan + +# 1️⃣ Get last metadata messages (inside the Kafka container) +$cmd = "/opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic dev-aerial-images-keys --from-beginning --timeout-ms 5000 2>/dev/null" +$metaMessages = docker exec kafka bash -c $cmd | + Select-String -Pattern '"file_name"' | + ForEach-Object { ($_ -split '\"file_name\"\s*:\s*\"')[1] -split '\"' | Select-Object -First 1 } | + Where-Object { $_ -match '^drone-01_' } | + Select-Object -Last $Count + +if (-not $metaMessages) { + Write-Host "[WARN] No metadata messages found." -ForegroundColor Yellow + exit +} + +Write-Host "`n[INFO] Found $($metaMessages.Count) image names:" -ForegroundColor Green +$metaMessages | ForEach-Object { Write-Host " - $_" } + +# 2️⃣ Build MinIO JSON messages for each file_name +$today = (Get-Date).ToString('yyyy-MM-dd') +$minioMessages = $metaMessages | ForEach-Object { + $keyName = $_ + return '{"EventName":"s3:ObjectCreated:Put","Key":"imagery/air/' + $today + '/123/' + $keyName + '"}' +} + +Write-Host "`n[INFO] Sending MinIO messages to Kafka topic 'image.new.aerial'..." -ForegroundColor Cyan + +# 3️⃣ Send them to Kafka (inside container) +$producerCmd = "/opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka:9092 --topic image.new.aerial" +$minioMessages | docker exec -i kafka bash -c $producerCmd + +Write-Host "`n[DONE] Sent $($minioMessages.Count) messages successfully.`n" -ForegroundColor Green diff --git a/AgCloud/services/inference_http/Dockerfile b/AgCloud/services/inference_http/Dockerfile new file mode 100644 index 000000000..d5167d1ea --- /dev/null +++ b/AgCloud/services/inference_http/Dockerfile @@ -0,0 +1,84 @@ +# ============================================================ +# Unified Inference HTTP Dockerfile (Fruit + Camera + YOLO) +# ============================================================ +FROM python:3.11-slim + + + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 \ + PIP_DEFAULT_TIMEOUT=1200 + + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 \ + libglib2.0-dev \ + libsm6 \ + libxrender1 \ + libxext6 \ + libgl1 \ + libopenblas-dev \ + liblapack-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN python -m pip install --upgrade pip setuptools wheel --only-binary=:all: && \ + pip install --no-cache-dir numpy==1.26.4 --only-binary=:all: && \ + pip install --no-cache-dir --extra-index-url https://download.pytorch.org/whl/cpu \ + torch==2.1.2 torchvision==0.16.2 --only-binary=:all: --upgrade-strategy only-if-needed + + +ENV PIP_NO_CACHE_DIR=1 \ + PIP_DEFAULT_TIMEOUT=1200 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app + +# Copy certs dir (may be empty) and trust *.crt if present +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* && \ + if [ -d /tmp/certs ] && ls /tmp/certs/*.crt >/dev/null 2>&1; then \ + cp /tmp/certs/*.crt /usr/local/share/ca-certificates/ && \ + chmod 644 /usr/local/share/ca-certificates/*.crt && \ + update-ca-certificates; \ + else \ + echo "No extra CA certs found. Skipping CA update."; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt +RUN printf "[global]\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +# Python deps +RUN python -m pip install --upgrade pip setuptools wheel certifi && \ + pip install --no-cache-dir numpy==1.26.4 --only-binary=:all: + +RUN pip install --no-cache-dir --extra-index-url https://download.pytorch.org/whl/cpu \ + torch==2.1.2 torchvision==0.16.2 --only-binary=:all: --upgrade-strategy only-if-needed + +COPY requirements.txt /app/requirements.txt + + +RUN pip install --no-cache-dir -r /app/requirements.txt && \ + pip install --no-cache-dir \ + opencv-python-headless \ + ultralytics==8.2.34 \ + boto3 \ + pillow \ + requests \ + "numpy<2" \ + && rm -rf /root/.cache/pip + + +COPY app.py model_registry.py /app/ +COPY adapters /app/adapters +COPY models /app/models +COPY weights /app/weights +COPY models/soil_moisture/artifacts /app/artifacts + +EXPOSE 8004 + +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8004"] diff --git a/AgCloud/services/inference_http/adapters/__init__.py b/AgCloud/services/inference_http/adapters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/inference_http/adapters/fruit_defect_runner.py b/AgCloud/services/inference_http/adapters/fruit_defect_runner.py new file mode 100644 index 000000000..5a1cd00eb --- /dev/null +++ b/AgCloud/services/inference_http/adapters/fruit_defect_runner.py @@ -0,0 +1,39 @@ +import os, io +from pathlib import Path +from typing import Any, Dict, Optional + +from PIL import Image +import torch + +# Core code imported from your fruit-defect module +from models.fruit_defect.inference.infer_fruit_defect import ( + load_model, get_preprocess, infer_single +) + +# Local weights only +WEIGHTS_PATH = Path(os.getenv("WEIGHTS_PATH", "/app/weights/fruit_cls_best.ts")) + +def _ensure_local_weights(p: Path) -> Path: + if not p.exists(): + raise FileNotFoundError(f"Local weights not found at: {p}") + return p + +class FruitDefectRunner: + def __init__(self, model_tag: Optional[str] = None): + # Allows selecting a different weights file in future via extra/model_tag + weights_path = _ensure_local_weights(WEIGHTS_PATH) + self.model = load_model(weights_path) + self.preprocess = get_preprocess() + self.device = "cuda" if torch.cuda.is_available() else "cpu" + self.model = self.model.to(self.device).eval() + + def run(self, image_bytes: bytes, model_tag=None, extra=None) -> Dict[str, Any]: + img = Image.open(io.BytesIO(image_bytes)).convert("RGB") + result = infer_single(self.model, img, self.preprocess, device=self.device) + # Normalize to standard HTTP response structure + return { + "label": result.get("status"), + "score": result.get("prob_defect"), + "confidence": result.get("confidence"), + "latency_ms_model": result.get("latency_ms_model"), + } diff --git a/AgCloud/services/inference_http/adapters/fruit_segmentation_runner.py b/AgCloud/services/inference_http/adapters/fruit_segmentation_runner.py new file mode 100644 index 000000000..7df623a8d --- /dev/null +++ b/AgCloud/services/inference_http/adapters/fruit_segmentation_runner.py @@ -0,0 +1,92 @@ +import os, io, tempfile, hashlib, cv2, numpy as np, boto3, torch + +def allow_unrestricted_torch_load(): + _original_load = torch.load + def patched_load(*args, **kwargs): + kwargs["weights_only"] = False + return _original_load(*args, **kwargs) + torch.load = patched_load + +allow_unrestricted_torch_load() +# === End Patch === + +import time +from typing import Any, Dict, Optional +from datetime import datetime, timezone +from ultralytics import YOLO + +def sha256_hex(path: str) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + +class FruitSegmentationRunner: + def __init__(self, weights_path: Optional[str] = None, model_tag: Optional[str] = None): + self.weights_path = weights_path or os.getenv("WEIGHTS_PATH", "/app/weights/yolov8-fruits.pt") + self.model = YOLO(self.weights_path) + raw_endpoint = os.getenv("MINIO_ENDPOINT", "minio-hot:9000").strip() + if not raw_endpoint.startswith(("http://", "https://")): + endpoint = f"http://{raw_endpoint}" + else: + endpoint = raw_endpoint + self.s3 = boto3.client( + "s3", + endpoint_url=endpoint, + aws_access_key_id=os.getenv("MINIO_ACCESS_KEY", "minioadmin"), + aws_secret_access_key=os.getenv("MINIO_SECRET_KEY", "minioadmin123") + ) + def run(self, image_bytes: bytes | None = None, model_tag=None, extra=None) -> Dict[str, Any]: + """Main inference entrypoint for HTTP""" + bucket_in = extra.get("bucket") if extra else "imagery" + key = extra.get("key") if extra else None + if not key: + return {"error": "missing key"} + + + if image_bytes: + img_array = np.frombuffer(image_bytes, np.uint8) + img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + if img is None: + return {"error": "failed to decode image from bytes"} + else: + with tempfile.TemporaryDirectory() as tmpdir: + local_path = os.path.join(tmpdir, os.path.basename(key)) + self.s3.download_file(bucket_in, key, local_path) + img = cv2.imread(local_path) + if img is None: + return {"error": "failed to read image"} + + t0 = time.time() + results = self.model.predict(img, conf=0.3, iou=0.45, verbose=False) + latency_ms = int((time.time() - t0) * 1000) + boxes = results[0].boxes + count = 0 + + if boxes: + with tempfile.TemporaryDirectory() as tmpdir: + for i, box in enumerate(boxes): + label = results[0].names[int(box.cls[0])] + if label.lower() not in [ + "apple", "banana", "orange", "pear", "peach", "plum", + "mango", "grape", "cherry", "pomegranate" + ]: + continue + x1, y1, x2, y2 = map(int, box.xyxy[0]) + crop = img[y1:y2, x1:x2] + if crop.size == 0: + continue + out_name = f"{os.path.splitext(os.path.basename(key))[0]}_fruit_{i+1}.jpg" + out_key = f"segments/{out_name}" + out_path = os.path.join(tmpdir, out_name) + cv2.imwrite(out_path, crop) + self.s3.upload_file(out_path, bucket_in, out_key) + count += 1 + + return { + "label": "fruit", + "count": count, + "latency_ms_model": latency_ms, + "bucket_out": bucket_in + } diff --git a/AgCloud/services/inference_http/adapters/soil_moisture_runner.py b/AgCloud/services/inference_http/adapters/soil_moisture_runner.py new file mode 100644 index 000000000..7c1624c70 --- /dev/null +++ b/AgCloud/services/inference_http/adapters/soil_moisture_runner.py @@ -0,0 +1,158 @@ + +""" +Adapter for soil moisture inference in the generic HTTP inference flow. +Uses the shared inference logic from the soil-moisture service. +""" + +import os +import base64 +import logging +import sys +from typing import Any, Dict, Optional +from PIL import Image +from io import BytesIO +import numpy as np +import cv2 +import time +import re + +logger = logging.getLogger(__name__) + + +class SoilMoistureRunner: + """ + Adapter that wraps the soil moisture inference logic. + """ + + def __init__(self, weights_path: Optional[str] = None, model_tag: Optional[str] = None): + self.model_tag = model_tag + self.weights_path = weights_path + + try: + # Add models directory to path + models_dir = os.path.join(os.path.dirname(__file__), '..', 'models') + if models_dir not in sys.path: + sys.path.insert(0, models_dir) + + # Import soil moisture components + from soil_moisture.src.app.config import Settings, load_zones + from soil_moisture.src.app.inference import Inferencer + from soil_moisture.src.app.db import DB + from soil_moisture.src.app.inference_logic import SoilMoistureInferenceLogic + + logger.info("Initializing SoilMoistureRunner...") + + # Initialize components + self.settings = Settings() + + # Load zones config if available + if hasattr(self.settings, 'zones_file') and self.settings.zones_file: + if os.path.exists(self.settings.zones_file): + self.zones_cfg = load_zones(self.settings.zones_file) + else: + logger.warning(f"zones_file not found: {self.settings.zones_file}") + self.zones_cfg = {} + else: + self.zones_cfg = {} + + self.db = DB(self.settings.pg_dsn) + self.inferencer = Inferencer(self.settings, self.db) + + # Initialize Kafka producer (optional) + producer = None + try: + from soil_moisture.src.app.kafka_producer import ControlProducer + producer = ControlProducer( + self.settings.kafka_brokers, + self.settings.kafka_topic, + self.settings.kafka_dlt + ) + except Exception as e: + logger.warning(f"Kafka producer init failed: {e}") + + # Initialize shared inference logic + self.inference_logic = SoilMoistureInferenceLogic( + settings=self.settings, + db=self.db, + inferencer=self.inferencer, + producer=producer + ) + + logger.info("SoilMoistureRunner initialized successfully!") + + except Exception as e: + logger.error(f"Failed to initialize SoilMoistureRunner: {e}", exc_info=True) + raise + + def run(self, image_bytes: Any, model_tag: Optional[str] = None, + extra: Optional[Dict] = None) -> Dict: + """ + Run soil moisture inference using the shared inference logic. + """ + start_time = time.time() + + try: + bucket_in = extra.get("bucket") if extra else "imagery" + key = extra.get("key") if extra else None + if not key: + return {"error": "missing key"} + + # --- Extract device_id from the key (pattern: path/to/image/dev-id_ts.jpg) --- + def extract_device_id_from_key(key: str) -> str: + filename = key.split("/")[-1] # get "dev-id_ts.jpg" + match = re.match(r"([^_]+)_", filename) # capture part before "_" + if match: + return match.group(1) + return "unknown" + + # --- Decode image --- + img_array = np.frombuffer(image_bytes, np.uint8) + img = cv2.imdecode(img_array, cv2.IMREAD_COLOR) + if img is None: + return {"error": "failed to decode image from bytes"} + + # --- Determine device_id --- + device_id = "unknown" + if extra: + if "device_id" in extra: + device_id = extra["device_id"] + elif "filename" in extra: + device_id = self.inference_logic.extract_device_id(extra["filename"]) + elif "key" in extra: + device_id = extract_device_id_from_key(extra["key"]) + + # --- Convert input to PIL Image --- + if isinstance(image_bytes, bytes): + img = Image.open(BytesIO(image_bytes)) + elif isinstance(image_bytes, str): + img_bytes = base64.b64decode(image_bytes) + img = Image.open(BytesIO(img_bytes)) + elif isinstance(image_bytes, Image.Image): + img = image_bytes + else: + raise ValueError(f"Unsupported input type: {type(image_bytes)}") + + # --- Run inference --- + result = self.inference_logic.infer_from_image(img, device_id) + + return { + "device_id": result["device_id"], + "dry_ratio": result["dry_ratio"], + "decision": result["decision"], + "confidence": result["confidence"], + "patch_count": result["patch_count"], + "duration_min": result.get("duration_min", 0), + "latency_ms_model": result.get("latency_ms", 0), + "ts": result.get("ts"), + "idempotency_key": result.get("idempotency_key"), + "debug": result.get("debug") + } + + except Exception as e: + logger.error(f"Inference failed: {e}", exc_info=True) + latency_ms = int((time.time() - start_time) * 1000) + return { + "error": str(e), + "device_id": locals().get("device_id", "unknown"), + "latency_ms_model": latency_ms + } \ No newline at end of file diff --git a/AgCloud/services/inference_http/app.py b/AgCloud/services/inference_http/app.py new file mode 100644 index 000000000..ec6b984e9 --- /dev/null +++ b/AgCloud/services/inference_http/app.py @@ -0,0 +1,84 @@ + +import os, time +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel, ConfigDict +from minio import Minio +from model_registry import get_model_runner + +TEAM = os.getenv("TEAM") +if not TEAM: + raise RuntimeError("Missing TEAM environment variable – please set TEAM=") + +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio-hot:9000") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin123") +MINIO_SECURE = os.getenv("MINIO_SECURE", "0") == "1" + +app = FastAPI(title="Fruit Inference HTTP") + +class InferRequest(BaseModel): + # Accept only bucket+key; any other fields are rejected (422) + model_config = ConfigDict(extra="forbid") + bucket: str + key: str + + +@app.on_event("startup") +def _startup(): + app.state.mc = Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=MINIO_SECURE, + ) + # The runner already knows how to read image_uri from S3 + app.state.runner = get_model_runner(TEAM) + +@app.get("/healthz") +def healthz(): + return {"ok": True, "team": TEAM} + +@app.post("/infer_json") +def infer_json( + req: InferRequest, + idem_key: str | None = Header(default=None, alias="Idempotency-Key"), + corr_id: str | None = Header(default=None, alias="X-Correlation-ID"), +): + started = time.perf_counter() + + try: + runner = app.state.runner + + # Always build the image URI from bucket and key + s3_uri = f"s3://{req.bucket}/{req.key}" + + # Try to read the image bytes from MinIO + obj = app.state.mc.get_object(req.bucket, req.key) + try: + image_bytes = obj.read() + finally: + obj.close() + obj.release_conn() + + # Attempt to run the model with bytes input first + # Attempt to run the model with bytes input first + try: + result = runner.run(image_bytes, extra={"bucket": req.bucket, "key": req.key}) + except TypeError: + # If the function does not accept bytes, try with URI instead + result = runner.run(s3_uri, extra={"bucket": req.bucket, "key": req.key}) + + + latency_ms = int((time.perf_counter() - started) * 1000) + return { + "ok": True, + "team": TEAM, + "result": result, + "image_uri": s3_uri, + "latency_ms": latency_ms, + "idempotency_key": idem_key, + "correlation_id": corr_id, + } + + except Exception as e: + raise HTTPException(status_code=500, detail=f"inference failed: {e}") diff --git a/AgCloud/services/inference_http/model_registry.py b/AgCloud/services/inference_http/model_registry.py new file mode 100644 index 000000000..ccf92373f --- /dev/null +++ b/AgCloud/services/inference_http/model_registry.py @@ -0,0 +1,13 @@ +from adapters.fruit_defect_runner import FruitDefectRunner +from adapters.fruit_segmentation_runner import FruitSegmentationRunner +from adapters.soil_moisture_runner import SoilMoistureRunner + +def get_model_runner(team: str): + t = (team or "").lower() + if t == "fruit_defect": + return FruitDefectRunner() + if t == "camera": + return FruitSegmentationRunner() + if t == "soil_moisture": + return SoilMoistureRunner() + raise ValueError(f"unknown TEAM {t}") diff --git a/AgCloud/services/inference_http/models/__init__.py b/AgCloud/services/inference_http/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/inference_http/models/fruit_defect/Docs/DATASET_SPEC.md b/AgCloud/services/inference_http/models/fruit_defect/Docs/DATASET_SPEC.md new file mode 100644 index 000000000..ac1e7375c --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/Docs/DATASET_SPEC.md @@ -0,0 +1,50 @@ +# Dataset Spec — Fruit Defect Classification + +## Source +- **Name**: Fruit and Vegetable Disease - Healthy vs Rotten (Kaggle-based) +- **Structure**: Two conditions (`Healthy`, `Rotten`) across multiple fruits and vegetables. +- **Usage in this project**: collapsed into **binary labels**: + - `ok` (healthy/fresh) + - `defect` (rotten/decayed/moldy) + +## Local Path (outside git) +``` +C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten +``` + +Inside this folder: +``` +train/ +val/ +test/ +``` + +> **Note**: Dataset folders remain **outside git**. The model accesses them via config paths. + +## Split +- Method: stratified random split by class (script `split_dataset.py`). +- Ratios: `train=0.8`, `val=0.1`, `test=0.1`. +- Seed fixed for reproducibility. + +## Preprocessing +- Resize images to `192×192` (chosen to improve CPU latency while preserving accuracy). +- Normalize using ImageNet mean/std. +- Light augmentations (random flip, small color jitter) during training. + +## Labels +- `ok` ← all `Healthy` folders. +- `defect` ← all `Rotten` folders. + +## Metrics & Targets +- **Classification**: + - Accuracy: **98.91%** + - Precision: **99.22%** + - Recall: **98.71%** + - F1: **98.96%** +- **Latency (CPU)**: + - TorchScript + 192 px → **p95 = 15.72 ms** + +- **Segmentation**: *not implemented* + - The dataset provides only **binary health labels**, without **lesion masks**. + - Alternative datasets checked (Strawberry, PlantSeg) either provided masks of whole leaves or leaf diseases, not fruit defects. + - As segmentation was optional in the acceptance criteria, we focused on classification. \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/fruit_defect/Docs/USAGE.md b/AgCloud/services/inference_http/models/fruit_defect/Docs/USAGE.md new file mode 100644 index 000000000..e5cb8eec6 --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/Docs/USAGE.md @@ -0,0 +1,84 @@ +# Usage — Fruit Defect Classifier + +## Project Path +``` +C:/Users/משתמש/Desktop/vectordb_efrat/AgCloud/fruit-defect +``` + +## Install +Install required Python packages manually if no `requirements.txt` is provided. +Typical dependencies include: +```bash +pip install torch torchvision torchaudio +pip install pillow numpy pyyaml tqdm scikit-learn tensorboard minio +``` + +## Config +Edit `configs/fruit_defect.yaml` with your dataset paths: + +```yaml +data: + train_dir: "C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten/train" + val_dir: "C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten/val" + test_dir: "C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten/test" + +model: + backbone: "mobilenet_v3_small" + img_size: 192 + +train: + epochs: 8 + batch_size: 32 + lr: 3e-4 + weight_decay: 1e-4 + +paths: + best_weights: "outputs/fruit_cls_best.pt" + +inference: + img_size: 192 +``` + +## Train +```bash +python training/train_fruit_defect_cls.py +``` +- Best weights saved to `outputs/fruit_cls_best.pt`. +- TensorBoard logs in `outputs/runs`: +```bash +python -m tensorboard.main --logdir outputs/runs +``` + +## Evaluate +```bash +python scripts/eval_cls.py +``` +Example: +```json +{"accuracy": 0.9891, "precision": 0.9922, "recall": 0.9871, "f1": 0.9896} +``` + +## Inference (Local Folder) +```bash +python inference/infer_fruit_defect.py +``` +- Reads from `data.test_dir` and writes `outputs/infer_results.json`. +- Console prints latency stats (p50/p90/p95). + +## TorchScript (Optional) +```bash +python scripts/export_torchscript.py +``` +Produces `outputs/fruit_cls_best.ts`. Inference auto-uses it if present. + +## (Optional) MinIO Integration +- `scripts/infer_from_minio.py`: download images from MinIO prefix → run inference → upload JSON back. +- `scripts/upload_weights.py`: upload trained weights (`outputs/fruit_cls_best.pt`) to MinIO. + +## .gitignore (recommended) +``` +/outputs/ +/**/train/ +/**/val/ +/**/test/ +``` \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/fruit_defect/__init__.py b/AgCloud/services/inference_http/models/fruit_defect/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/inference_http/models/fruit_defect/configs/fruit_defect.yaml b/AgCloud/services/inference_http/models/fruit_defect/configs/fruit_defect.yaml new file mode 100644 index 000000000..b415ebaa1 --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/configs/fruit_defect.yaml @@ -0,0 +1,20 @@ +data: + train_dir: "C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten/train" + val_dir: "C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten/val" + test_dir: "C:/Users/משתמש/Desktop/vectordb_efrat/Datasets/Fruit and Vegetable Disease - Healthy vs Rotten/test" + +model: + backbone: "mobilenet_v3_small" + img_size: 192 + +train: + epochs: 8 + batch_size: 32 + lr: 3e-4 + weight_decay: 1e-4 + +inference: + img_size: 192 + +paths: + best_weights: "outputs/fruit_cls_best.pt" diff --git a/AgCloud/services/inference_http/models/fruit_defect/inference/__init__.py b/AgCloud/services/inference_http/models/fruit_defect/inference/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/inference_http/models/fruit_defect/inference/infer_fruit_defect.py b/AgCloud/services/inference_http/models/fruit_defect/inference/infer_fruit_defect.py new file mode 100644 index 000000000..530146c78 --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/inference/infer_fruit_defect.py @@ -0,0 +1,367 @@ +# inference/infer_fruit_defect_minio.py +import os +import io +import json +import time +import sys +import logging +from pathlib import Path +from typing import List, Dict +from concurrent.futures import ThreadPoolExecutor, as_completed + +from minio import Minio +from PIL import Image +import numpy as np + +# PyTorch +import torch +from torchvision import transforms + +# Optional tqdm +try: + from tqdm import tqdm + TQDM_AVAILABLE = True +except Exception: + TQDM_AVAILABLE = False + +# --- קונפיג דרך ENV (ברירות מחדל מותאמות לסטאק שלך) --- +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9001") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin123") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" + +BUCKET_INPUT = os.getenv("MINIO_BUCKET_INPUT", "imagery") +BUCKET_OUTPUT = os.getenv("MINIO_BUCKET_OUTPUT", "telemetry") +INPUT_PREFIX = os.getenv("MINIO_INPUT_PREFIX", "inputs/batch1/") # מאיפה לקרוא תמונות +OUTPUT_PREFIX = os.getenv("MINIO_OUTPUT_PREFIX", "results/batch1/") # לאן להעלות תוצאות + +WEIGHTS_BUCKET = os.getenv("MINIO_BUCKET_WEIGHTS","imagery") +WEIGHTS_PREFIX = os.getenv("MINIO_WEIGHTS_PREFIX","models/") +LOCAL_WEIGHTS_TS = os.getenv("MODEL_TS_LOCAL", "./outputs/fruit_cls_best.ts") +LOCAL_WEIGHTS_PT = os.getenv("MODEL_PT_LOCAL", "./outputs/fruit_cls_best.pt") + +IMG_SIZE = int(os.getenv("IMG_SIZE", "192")) +THRESHOLD = float(os.getenv("CLS_THRESHOLD", "0.5")) # סף בינארי defect/ok + +DL_WORKERS = int(os.getenv("DL_WORKERS", "8")) +BATCH_SIZE = int(os.getenv("BATCH_SIZE", "8")) +HEARTBEAT_PERIOD = int(os.getenv("HEARTBEAT_PERIOD", "30")) # שניות + +# --- logging config --- +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-7s %(message)s", + handlers=[logging.StreamHandler(sys.stdout)] +) +log = logging.getLogger("infer_minio") + +# --- MinIO client --- +mc = Minio(MINIO_ENDPOINT, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=MINIO_SECURE) + + +# ---------------------- +# utility / IO helpers +# ---------------------- +def ensure_local_dir(p: Path): + p.mkdir(parents=True, exist_ok=True) + +def list_images(bucket: str, prefix: str) -> List[str]: + allowed = (".jpg",".jpeg",".png",".bmp",".webp",".tif",".tiff") + keys = [] + for obj in mc.list_objects(bucket, prefix=prefix, recursive=True): + if getattr(obj, "is_dir", False): + continue + name = obj.object_name + if name.lower().endswith(allowed): + keys.append(name) + return keys + +def download_object(bucket: str, object_name: str, local_path: Path): + ensure_local_dir(local_path.parent) + mc.fget_object(bucket, object_name, str(local_path)) + +def download_images_parallel(bucket: str, keys: List[str], out_dir: Path, workers: int = 8) -> List[Path]: + """Download keys to out_dir using threads; returns list of local Paths.""" + ensure_local_dir(out_dir) + local_paths = [] + + log.info(f"Starting download of {len(keys)} images (workers={workers})") + pbar = tqdm(total=len(keys), desc="downloading", unit="img") if TQDM_AVAILABLE else None + + def _dl(key): + local = out_dir / Path(key).name + try: + mc.fget_object(bucket, key, str(local)) + return local, None + except Exception as e: + return local, e + + with ThreadPoolExecutor(max_workers=workers) as ex: + futures = [ex.submit(_dl, k) for k in keys] + for fut in as_completed(futures): + local, err = fut.result() + if err: + log.warning(f"download failed: {local} -> {err}") + else: + local_paths.append(local) + if pbar: + pbar.update(1) + + if pbar: + pbar.close() + log.info(f"Downloaded {len(local_paths)}/{len(keys)} images") + local_paths.sort(key=lambda p: p.name.lower()) + return local_paths + +def write_heartbeat(work_dir: Path, status: str, extra: dict = None): + try: + ensure_local_dir(work_dir) + hb = {"ts": time.time(), "status": status} + if extra: + hb.update(extra) + (work_dir / "status.json").write_text(json.dumps(hb)) + except Exception as e: + log.debug(f"heartbeat write failed: {e}") + +# ---------------------- +# model / preprocess +# ---------------------- +def fetch_weights_if_missing() -> Path: + ts_obj = WEIGHTS_PREFIX + "fruit_cls_best.ts" + pt_obj = WEIGHTS_PREFIX + "fruit_cls_best.pt" + ts_local = Path(LOCAL_WEIGHTS_TS) + pt_local = Path(LOCAL_WEIGHTS_PT) + + if ts_local.exists(): + log.info(f"Found local TorchScript weights: {ts_local}") + return ts_local + if pt_local.exists(): + log.info(f"Found local PT weights: {pt_local}") + return pt_local + + # נעדיף TorchScript + log.info(f"Attempting to download weights from MinIO: {WEIGHTS_BUCKET}/{ts_obj} or .pt") + try: + download_object(WEIGHTS_BUCKET, ts_obj, ts_local) + log.info(f"Downloaded weights: {ts_local}") + return ts_local + except Exception as e: + log.info(f"TorchScript not found in MinIO ({e}), trying PT fallback...") + # fallback ל-pt + download_object(WEIGHTS_BUCKET, pt_obj, pt_local) + log.info(f"Downloaded PT weights: {pt_local}") + return pt_local + +def load_model(weights_path: Path): + log.info(f"Loading model from: {weights_path}") + if weights_path.suffix == ".ts": + model = torch.jit.load(str(weights_path), map_location="cpu") + else: + obj = torch.load(str(weights_path), map_location="cpu") + if hasattr(obj, "state_dict"): + model = obj + elif isinstance(obj, dict): + raise RuntimeError("Loaded a state_dict dict but no model class is defined here. Please export TorchScript (.ts).") + else: + model = obj + model.eval() + log.info("Model loaded and set to eval()") + return model + +def get_preprocess(): + return transforms.Compose([ + transforms.Resize((IMG_SIZE, IMG_SIZE)), + transforms.ToTensor(), + transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]), + ]) + +def infer_single(model, img: Image.Image, preprocess, device="cpu") -> Dict: + x = preprocess(img.convert("RGB")).unsqueeze(0).to(device) + t0 = time.perf_counter() + with torch.no_grad(): + y = model(x) + dt = (time.perf_counter() - t0) * 1000.0 # ms + + if isinstance(y, (list, tuple)): + y = y[0] + if isinstance(y, torch.Tensor): + y = y.squeeze().detach().cpu() + prob_defect = torch.sigmoid(y).item() if y.numel()==1 else float(torch.softmax(y, dim=0)[1]) + else: + prob_defect = float(y) + + status = "defect" if prob_defect >= THRESHOLD else "ok" + confidence = prob_defect if status=="defect" else 1.0 - prob_defect + return {"status": status, "prob_defect": prob_defect, "confidence": confidence, "latency_ms_model": dt} + + +# ---------------------- +# main runner +# ---------------------- +def run_inference_from_minio() -> Dict: + work = Path("./work_minio") + imgs_dir = work / "images" + ensure_local_dir(imgs_dir) + + # heartbeat: starting + write_heartbeat(work, "starting") + + # 1) Weights + log.info("Checking for local weights...") + write_heartbeat(work, "checking_weights") + weights_path = fetch_weights_if_missing() + write_heartbeat(work, "weights_ready", {"weights": str(weights_path)}) + log.info(f"Using weights: {weights_path}") + sys.stdout.flush() + + # 2) Download images to temp workdir + log.info("Fetching list of images from MinIO...") + keys = list_images(BUCKET_INPUT, INPUT_PREFIX) + log.info(f"Found {len(keys)} image keys under s3://{BUCKET_INPUT}/{INPUT_PREFIX}") + if not keys: + write_heartbeat(work, "no_images") + return {"error": f"no images under s3://{BUCKET_INPUT}/{INPUT_PREFIX}"} + + write_heartbeat(work, "downloading_images", {"count": len(keys)}) + downloaded = download_images_parallel(BUCKET_INPUT, keys, imgs_dir, workers=DL_WORKERS) + write_heartbeat(work, "downloaded_images", {"downloaded": len(downloaded)}) + sys.stdout.flush() + + # 3) Inference loop (batched, tqdm) + device = "cuda" if torch.cuda.is_available() else "cpu" + model = load_model(weights_path) + model = model.to(device) + preprocess = get_preprocess() + + n_images = len(downloaded) + if n_images == 0: + write_heartbeat(work, "no_downloaded_images") + return {"error": f"no images downloaded from s3://{BUCKET_INPUT}/{INPUT_PREFIX}"} + + log.info(f"Starting inference: {n_images} images | batch_size={BATCH_SIZE} | device={device}") + write_heartbeat(work, "inferring", {"count": n_images, "batch_size": BATCH_SIZE}) + sys.stdout.flush() + + indices = range(0, n_images, BATCH_SIZE) + pbar = tqdm(total=n_images, desc="Batches" if TQDM_AVAILABLE else "images", unit="img") if TQDM_AVAILABLE else None + + latencies = [] + results = [] + + for start in indices: + batch_paths = downloaded[start:start + BATCH_SIZE] + names = [] + tensors = [] + for p in batch_paths: + img = Image.open(p).convert("RGB") + x = preprocess(img) + tensors.append(x) + names.append(p.name) + + batch = torch.stack(tensors, dim=0).to(device) + + t0 = time.perf_counter() + with torch.no_grad(): + out_batch = model(batch) + dt_batch_ms = (time.perf_counter() - t0) * 1000.0 + per_image_ms = dt_batch_ms / float(len(batch_paths)) + + if isinstance(out_batch, (list, tuple)): + out_batch = out_batch[0] + if isinstance(out_batch, torch.Tensor): + out_cpu = out_batch.detach().cpu() + for j in range(out_cpu.shape[0]): + y = out_cpu[j] + if y.numel() == 1: + prob_defect = torch.sigmoid(y).item() + else: + probs = torch.softmax(y, dim=0) + prob_defect = float(probs[1]) if probs.numel() > 1 else float(probs[0]) + status = "defect" if prob_defect >= THRESHOLD else "ok" + confidence = prob_defect if status == "defect" else 1.0 - prob_defect + latencies.append(per_image_ms) + results.append({ + "image": names[j], + "status": status, + "prob_defect": prob_defect, + "confidence": confidence, + "latency_ms_model": round(per_image_ms, 3), + }) + else: + for j, nm in enumerate(names): + prob_defect = float(out_batch) if isinstance(out_batch, (float, int)) else 0.0 + status = "defect" if prob_defect >= THRESHOLD else "ok" + confidence = prob_defect if status == "defect" else 1.0 - prob_defect + latencies.append(per_image_ms) + results.append({ + "image": nm, + "status": status, + "prob_defect": prob_defect, + "confidence": confidence, + "latency_ms_model": round(per_image_ms, 3), + }) + + if pbar: + pbar.update(len(batch_paths)) + + # heartbeat update per batch + if (start // BATCH_SIZE) % 10 == 0: + write_heartbeat(work, "inferring_in_progress", {"processed": min(start + BATCH_SIZE, n_images), "total": n_images}) + + if pbar: + pbar.close() + + # compute stats + if latencies: + p50 = float(np.percentile(latencies, 50)) + p90 = float(np.percentile(latencies, 90)) + p95 = float(np.percentile(latencies, 95)) + else: + p50 = p90 = p95 = 0.0 + + summary = { + "count": len(results), + "p50_ms": round(p50,2), + "p90_ms": round(p90,2), + "p95_ms": round(p95,2), + "weights_path": str(weights_path), + "input_bucket": BUCKET_INPUT, + "input_prefix": INPUT_PREFIX, + "output_bucket": BUCKET_OUTPUT, + "output_prefix": OUTPUT_PREFIX, + "threshold": THRESHOLD, + "device": device, + "batch_size": BATCH_SIZE, + } + + payload = {"summary": summary, "results": results} + + # 4) Upload results.json back to MinIO (telemetry) + out_json = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8") + obj_name = (OUTPUT_PREFIX.rstrip("/") + "/results.json") + mc.put_object(BUCKET_OUTPUT, obj_name, io.BytesIO(out_json), length=len(out_json), content_type="application/json") + log.info(f"Uploaded results to {BUCKET_OUTPUT}/{obj_name}") + + try: + from db_writer import write_results_to_db + write_results_to_db(payload) + log.info("Results written to Postgres successfully") + except Exception as e: + log.warning(f"Failed to write results to DB: {e}") + + write_heartbeat(work, "done", {"summary": summary}) + return summary + + +if __name__ == "__main__": + try: + s = run_inference_from_minio() + print(json.dumps(s, indent=2, ensure_ascii=False)) + except Exception as e: + log.exception("Fatal error during inference") + # write heartbeat error + try: + write_heartbeat(Path("./work_minio"), "error", {"error": str(e)}) + except Exception: + pass + raise diff --git a/AgCloud/services/inference_http/models/fruit_defect/requirements.txt b/AgCloud/services/inference_http/models/fruit_defect/requirements.txt new file mode 100644 index 000000000..16a84df06 --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/requirements.txt @@ -0,0 +1,16 @@ +torchaudio +pillow +numpy +pyyaml +tqdm +scikit-learn +tensorboard +minio +fastapi +uvicorn[standard] +minio +pydantic +pillow +numpy==1.26.4 + + diff --git a/AgCloud/services/inference_http/models/fruit_defect/scripts/eval_cls.py b/AgCloud/services/inference_http/models/fruit_defect/scripts/eval_cls.py new file mode 100644 index 000000000..07aff87ec --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/scripts/eval_cls.py @@ -0,0 +1,53 @@ +import yaml +from pathlib import Path +import torch +from torchvision import transforms, models +from torch.utils.data import Dataset, DataLoader +from PIL import Image +from sklearn.metrics import accuracy_score, precision_recall_fscore_support + +IMG_EXTS = {".jpg",".jpeg",".png",".bmp",".webp",".tif",".tiff"} + +def label_from_path(p: Path) -> int: + name = " ".join([s.lower() for s in p.parts]) + if "healthy" in name or "fresh" in name: return 0 + return 1 + +class DS(Dataset): + def __init__(self, root, img_size): + self.items = [p for p in Path(root).rglob("*") if p.is_file() and p.suffix.lower() in IMG_EXTS] + self.tf = transforms.Compose([ + transforms.Resize((img_size,img_size)), + transforms.ToTensor(), + transforms.Normalize([0.485,0.456,0.406],[0.229,0.224,0.225]) + ]) + def __len__(self): return len(self.items) + def __getitem__(self,i): + p = self.items[i]; y = label_from_path(p) + x = self.tf(Image.open(p).convert("RGB")) + return x,y + +def load_model(weights_path, backbone): + if backbone=="mobilenet_v3_small": + m = models.mobilenet_v3_small() + m.classifier[-1] = torch.nn.Linear(m.classifier[-1].in_features, 2) + else: + m = models.resnet18() + m.fc = torch.nn.Linear(m.fc.in_features, 2) + m.load_state_dict(torch.load(weights_path, map_location="cpu")) + m.eval() + return m + +if __name__=="__main__": + cfg = yaml.safe_load(open("configs/fruit_defect.yaml","r",encoding="utf-8")) + ds = DS(cfg["data"]["test_dir"], cfg["model"]["img_size"]) + dl = DataLoader(ds, batch_size=32, shuffle=False) + m = load_model(cfg["paths"]["best_weights"], cfg["model"]["backbone"]) + ys, ps = [], [] + with torch.no_grad(): + for x,y in dl: + pred = m(x).argmax(1) + ys += y.tolist(); ps += pred.tolist() + acc = accuracy_score(ys,ps) + p,r,f1,_ = precision_recall_fscore_support(ys,ps,average="binary",zero_division=0) + print({"accuracy":round(acc,4), "precision":round(p,4),"recall":round(r,4),"f1":round(f1,4)}) diff --git a/AgCloud/services/inference_http/models/fruit_defect/scripts/export_torchscript.py b/AgCloud/services/inference_http/models/fruit_defect/scripts/export_torchscript.py new file mode 100644 index 000000000..52202e061 --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/scripts/export_torchscript.py @@ -0,0 +1,22 @@ +from pathlib import Path +import torch, yaml +from torchvision import models + +cfg = yaml.safe_load(open("configs/fruit_defect.yaml","r",encoding="utf-8")) +img_size = int(cfg["model"]["img_size"]) +weights = Path(cfg["paths"]["best_weights"]) +assert weights.exists(), f"Missing weights: {weights}" + +m = models.mobilenet_v3_small() +m.classifier[-1] = torch.nn.Linear(m.classifier[-1].in_features, 2) +state = torch.load(weights, map_location="cpu") +m.load_state_dict(state) +m.eval() + +example = torch.randn(1, 3, img_size, img_size) +with torch.inference_mode(): + ts = torch.jit.trace(m, example, strict=False) + +out = Path("outputs/fruit_cls_best.ts") +ts.save(str(out)) +print("Saved TorchScript (safe trace):", out) diff --git a/AgCloud/services/inference_http/models/fruit_defect/scripts/extract_mistakes.py b/AgCloud/services/inference_http/models/fruit_defect/scripts/extract_mistakes.py new file mode 100644 index 000000000..e43cec53d --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/scripts/extract_mistakes.py @@ -0,0 +1,73 @@ +# scripts/extract_mistakes.py +import json, csv, shutil +from pathlib import Path + +def gt_from_path(p: Path) -> str: + s = "/".join([part.lower() for part in p.parts]) + if ("healthy" in s) or ("fresh" in s): + return "ok" + for k in ["rotten","defect","mold","mould","bad","decay","damaged","spoiled"]: + if k in s: + return "defect" + return "ok" + +def main(): + infer_json = Path("outputs/infer_results.json") + if not infer_json.exists(): + raise SystemExit("לא נמצא outputs/infer_results.json. תריצי קודם את האינפרנס: python inference/infer_fruit_defect.py") + + with open(infer_json, "r", encoding="utf-8") as f: + results = json.load(f) + + mistakes_dir = Path("outputs/mistakes") + fp_dir = mistakes_dir / "fp" # False Positive: GT=ok, Pred=defect + fn_dir = mistakes_dir / "fn" # False Negative: GT=defect, Pred=ok + for d in [fp_dir, fn_dir]: + d.mkdir(parents=True, exist_ok=True) + + rows = [] + tp = tn = fp = fn = 0 + + for r in results: + p = Path(r["path"]) + pred = r["status"] # "ok" / "defect" + gt = gt_from_path(p) + + if gt == "defect" and pred == "defect": + tp += 1 + elif gt == "ok" and pred == "ok": + tn += 1 + elif gt == "ok" and pred == "defect": + fp += 1 + out_name = f"FP_pred-defect_gt-ok_{p.name}" + shutil.copy2(p, fp_dir / out_name) + rows.append([str(p), gt, pred, "FP"]) + elif gt == "defect" and pred == "ok": + fn += 1 + out_name = f"FN_pred-ok_gt-defect_{p.name}" + shutil.copy2(p, fn_dir / out_name) + rows.append([str(p), gt, pred, "FN"]) + + mistakes_dir.mkdir(parents=True, exist_ok=True) + csv_path = mistakes_dir / "mistakes.csv" + with open(csv_path, "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(["path","gt","pred","type"]) + w.writerows(rows) + + total = tp + tn + fp + fn + acc = (tp + tn) / total if total else 0.0 + prec = tp / (tp + fp) if (tp + fp) else 0.0 + rec = tp / (tp + fn) if (tp + fn) else 0.0 + f1 = 2*prec*rec/(prec+rec) if (prec+rec) else 0.0 + + print("\nConfusion Matrix (based on infer_results.json paths)") + print(f"TP={tp} FP={fp}") + print(f"FN={fn} TN={tn}") + print(f"\nacc={acc:.4f} precision={prec:.4f} recall={rec:.4f} f1={f1:.4f}") + print(f"\nWrote mistakes CSV: {csv_path}") + print(f"Copied FP -> {fp_dir}") + print(f"Copied FN -> {fn_dir}") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/inference_http/models/fruit_defect/training/train_fruit_defect_cls.py b/AgCloud/services/inference_http/models/fruit_defect/training/train_fruit_defect_cls.py new file mode 100644 index 000000000..12ac0de83 --- /dev/null +++ b/AgCloud/services/inference_http/models/fruit_defect/training/train_fruit_defect_cls.py @@ -0,0 +1,213 @@ +# training/train_fruit_defect_cls.py +import os, time, yaml, math, random +from pathlib import Path +from collections import deque +from typing import List + +import torch +import torch.nn as nn +from torch.utils.data import Dataset, DataLoader +from torchvision import transforms, models +from PIL import Image +from sklearn.metrics import accuracy_score, precision_recall_fscore_support +from tqdm import tqdm + +# ========= Utils ========= +IMG_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"} + +def set_seed(seed: int = 42): + random.seed(seed); torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed); torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + +def is_image(p: Path) -> bool: + return p.is_file() and p.suffix.lower() in IMG_EXTS + +def label_from_path(p: Path) -> int: + parts = [s.lower() for s in p.parts] + name = " ".join(parts) + if ("healthy" in name) or ("fresh" in name): + return 0 + if any(k in name for k in ["rotten", "defect", "bad", "decay", "mold", "mould", "damaged", "spoiled"]): + return 1 + return 1 if "rotten" in p.parent.name.lower() else 0 + +# ========= Dataset ========= +class BinaryFruitDataset(Dataset): + def __init__(self, root: str, img_size: int, augment: bool): + self.root = Path(root) + self.items: List[Path] = [p for p in self.root.rglob("*") if is_image(p)] + if not self.items: + raise RuntimeError(f"No images found under: {root}") + mean, std = [0.485,0.456,0.406], [0.229,0.224,0.225] + base = [transforms.Resize((img_size, img_size)), + transforms.ToTensor(), + transforms.Normalize(mean, std)] + if augment: + aug = [transforms.RandomHorizontalFlip(), + transforms.ColorJitter(0.1,0.1,0.1,0.05)] + self.tf = transforms.Compose(aug + base) + else: + self.tf = transforms.Compose(base) + + def __len__(self): return len(self.items) + + def __getitem__(self, idx): + p = self.items[idx] + y = label_from_path(p) + im = Image.open(p).convert("RGB") + x = self.tf(im) + return x, y + +# ========= Model ========= +def build_model(backbone: str): + backbone = (backbone or "mobilenet_v3_small").lower() + if "mobile" in backbone: + try: + m = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.IMAGENET1K_V1) + except Exception: + m = models.mobilenet_v3_small() + m.classifier[-1] = nn.Linear(m.classifier[-1].in_features, 2) + return m + else: + try: + m = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) + except Exception: + m = models.resnet18() + m.fc = nn.Linear(m.fc.in_features, 2) + return m + +@torch.no_grad() +def evaluate(model, dl, device): + model.eval() + all_y, all_p = [], [] + for x, y in dl: + x = x.to(device); y = torch.tensor(y).to(device) + logits = model(x) + pred = logits.argmax(1) + all_y.extend(y.cpu().tolist()) + all_p.extend(pred.cpu().tolist()) + acc = accuracy_score(all_y, all_p) + p, r, f1, _ = precision_recall_fscore_support(all_y, all_p, average="binary", zero_division=0) + return {"accuracy": acc, "precision": p, "recall": r, "f1": f1} + +# ========= Train ========= +def train_one_run(cfg): + set_seed(42) + + # --- data --- + img_size = int(cfg["model"]["img_size"]) + bs = int(cfg["train"]["batch_size"]) + train_ds = BinaryFruitDataset(cfg["data"]["train_dir"], img_size, augment=True) + val_ds = BinaryFruitDataset(cfg["data"]["val_dir"], img_size, augment=False) + + train_dl = DataLoader(train_ds, batch_size=bs, shuffle=True, num_workers=2, pin_memory=True) + val_dl = DataLoader(val_ds, batch_size=bs, shuffle=False, num_workers=2, pin_memory=True) + + # --- model --- + device = "cuda" if torch.cuda.is_available() else "cpu" + model = build_model(cfg["model"]["backbone"]).to(device) + + # --- optim --- + lr = float(cfg["train"]["lr"]) + wd = float(cfg["train"]["weight_decay"]) + opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=wd) + loss_fn = nn.CrossEntropyLoss() + + # --- tensorboard (optional) --- + tb = None + try: + from torch.utils.tensorboard import SummaryWriter + log_dir = Path("outputs/runs"); log_dir.mkdir(parents=True, exist_ok=True) + tb = SummaryWriter(log_dir=str(log_dir)) + except Exception: + tb = None + + # --- training loop with live metrics --- + best_acc, best_state = -1.0, None + epochs = int(cfg["train"]["epochs"]) + + for ep in range(1, epochs+1): + model.train() + loss_window, acc_window = deque(maxlen=100), deque(maxlen=100) + pbar = tqdm( + train_dl, + desc=f"epoch {ep}/{epochs}", + dynamic_ncols=True, + mininterval=1.0, + smoothing=0.2, + leave=False + ) + + + correct_total, seen_total = 0, 0 + + for step, (x, y) in enumerate(pbar, start=1): + x = x.to(device); y = torch.tensor(y).to(device) + + logits = model(x) + loss = loss_fn(logits, y) + + opt.zero_grad() + loss.backward() + opt.step() + + # running metrics (per mini-batch) + with torch.no_grad(): + preds = logits.argmax(1) + correct = (preds == y).sum().item() + batch_acc = correct / y.size(0) + + loss_window.append(float(loss)) + acc_window.append(batch_acc) + correct_total += correct + seen_total += y.size(0) + + avg_loss = sum(loss_window)/len(loss_window) + avg_acc = 100.0 * (sum(acc_window)/len(acc_window)) + pbar.set_postfix(loss=f"{avg_loss:.4f}", + train_acc=f"{avg_acc:.1f}%", + lr=f"{opt.param_groups[0]['lr']:.1e}") + + # tensorboard (every ~10 steps) + if tb and (step % 10 == 0): + global_step = (ep-1)*len(train_dl) + step + tb.add_scalar("train/loss", avg_loss, global_step) + tb.add_scalar("train/acc", avg_acc/100.0, global_step) + tb.add_scalar("train/lr", opt.param_groups[0]['lr'], global_step) + + # epoch-level metrics + epoch_train_acc = 100.0 * correct_total / max(1, seen_total) + metrics = evaluate(model, val_dl, device) + msg = { + "val_accuracy": round(metrics["accuracy"], 4), + "val_precision": round(metrics["precision"], 4), + "val_recall": round(metrics["recall"], 4), + "val_f1": round(metrics["f1"], 4), + "train_acc_epoch_%": round(epoch_train_acc, 2) + } + print("val metrics:", msg) + + if tb: + tb.add_scalar("val/accuracy", metrics["accuracy"], ep) + tb.add_scalar("val/precision", metrics["precision"], ep) + tb.add_scalar("val/recall", metrics["recall"], ep) + tb.add_scalar("val/f1", metrics["f1"], ep) + + # keep best by val accuracy + if metrics["accuracy"] > best_acc: + best_acc = metrics["accuracy"] + best_state = {k: v.detach().cpu() for k, v in model.state_dict().items()} + + # --- save best weights --- + out = Path(cfg["paths"]["best_weights"]) + out.parent.mkdir(parents=True, exist_ok=True) + torch.save(best_state, out) + print(f"saved best weights: {out} best_val_acc={round(best_acc,4)}") + +def main(): + cfg = yaml.safe_load(open("configs/fruit_defect.yaml", "r", encoding="utf-8")) + train_one_run(cfg) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/inference_http/models/soil_moisture/.gitignore b/AgCloud/services/inference_http/models/soil_moisture/.gitignore new file mode 100644 index 000000000..62d87daf0 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/.gitignore @@ -0,0 +1 @@ +samples/ \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/Dockerfile b/AgCloud/services/inference_http/models/soil_moisture/Dockerfile new file mode 100644 index 000000000..9b1bb644d --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/Dockerfile @@ -0,0 +1,44 @@ + +FROM python:3.10-slim + +WORKDIR /app + +# --- 1) installing ca-certificates --- +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# --- 2) copying NetFree certificate and adding it to the system --- +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && \ + update-ca-certificates + +# Setting to ensure the updated certificate is used +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# --- 3) System dependencies required for FastAPI etc. --- +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 libsm6 libxrender1 libxext6 \ + && rm -rf /var/lib/apt/lists/* + +# --- 4) Installing dependencies --- +COPY requirements-api.txt . +# RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org \ +# --trusted-host files.pythonhosted.org --no-cache-dir -r requirements-api.txt + +RUN pip config set global.require-hashes false && \ + pip install --trusted-host pypi.org --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org --no-cache-dir -r requirements-api.txt +# --- 5) Copying code --- +COPY src ./src +COPY configs ./configs +COPY artifacts ./artifacts +COPY src/sql/init_db.sql /initdb/init_db.sql + +ENV PYTHONPATH=/app +ENV SCHEDULE_UPDATE=1 +RUN pip install python-multipart + +CMD ["uvicorn", "src.app.service:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/README.md b/AgCloud/services/inference_http/models/soil_moisture/README.md new file mode 100644 index 000000000..0aacb3f61 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/README.md @@ -0,0 +1,119 @@ +# Soil Moisture DL Pipeline – Real-Time Irrigation Control (ONNX Inference) + +This repository delivers an end-to-end **deep learning** pipeline to detect soil moisture state +(**wet / dry**) from ground-level RGB images and trigger **real-time irrigation** actions. + +## Highlights +- **Training (PyTorch)**: MobileNetV3-small (transfer learning) + augmentations. +- **Export** to **ONNX** for light-weight **CPU/Jetson** inference. +- **Inference Service (FastAPI)**: + - Tiling into patches + - Per-patch ONNX inference + - Zone policy with hysteresis (dry_ratio_high / dry_ratio_low / min_patches) + - **Kafka** publish to `irrigation.control` (idempotent) + DLQ + - **Postgres** persistence in `soil_moisture_events` (+ optional schedule UPSERT + audit) + - **Prometheus** metrics + health/ready endpoints + +--- + +## Run + +```bash +docker compose up -d api +``` + +The API will be available at: [http://localhost:8000](http://localhost:8000) + +--- + +## Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/health` | Basic health check | +| `GET` | `/ready` | Checks DB connectivity | +| `GET` | `/metrics` | Prometheus metrics | +| `POST` | `/infer` | Run inference on uploaded image | + +### Example request + +```bash +curl -X POST "http://localhost:8000/infer" -F "zone_id=zone1" -F "image=@sample.jpg" +``` + +Response: +```json +{ + "device_id": "zone1", + "dry_ratio": 0.42, + "decision": "stop", + "confidence": 0.87, + "patch_count": 48, + "ts": "2025-10-29T09:41:00Z", + "idempotency_key": "zone1:345621" +} +``` + +--- + +## Environment Variables + +| Name | Description | Example | +|------|--------------|----------| +| `PG_DSN` | Postgres connection string | `postgresql://user:pass@host.docker.internal:5432/missions_db` | +| `KAFKA_BROKERS` | Kafka brokers | `kafka:9092` | +| `KAFKA_TOPIC` | Kafka topic for irrigation control | `irrigation.control` | +| `KAFKA_DLT` | Kafka DLQ topic | `irrigation.control.dlq` | +| `ZONES_FILE` | Path to zone configuration | `/app/configs/zones.yaml` | +| `SCHEDULE_UPDATE` | Enables schedule table update | `1` | +| `DECISION_WINDOW_SEC` | Time window for decision hysteresis | `3` | + +--- + +## Notes +- The service depends on Postgres and Kafka within the `ag_cloud` Docker network. +- If Kafka is unreachable, messages are logged but not published. +- Duplicate inferences are prevented using an idempotency key per decision window. +- Metrics exposed for Prometheus under `/metrics`. + +--- + +## Example Compose Context + +```yaml +services: + api: + build: + context: . + dockerfile: Dockerfile + environment: + PG_DSN: postgresql://missions_user:pg123@host.docker.internal:5432/missions_db + KAFKA_BROKERS: kafka:9092 + KAFKA_TOPIC: irrigation.control + KAFKA_DLT: irrigation.control.dlq + ZONES_FILE: /app/configs/zones.yaml + DECISION_WINDOW_SEC: 3 + PATCH_SIZE: 256 + PATCH_STRIDE: 256 + SCHEDULE_UPDATE: 1 + volumes: + - ./configs:/app/configs + - ./artifacts:/app/artifacts + ports: + - "8000:8000" + networks: + - ag_cloud +``` + +--- + +## Testing + +```bash +pytest -v +``` + +--- + +## License +Internal AgCloud component – for research and development use only. diff --git a/AgCloud/services/inference_http/models/soil_moisture/artifacts/best.pt b/AgCloud/services/inference_http/models/soil_moisture/artifacts/best.pt new file mode 100644 index 000000000..4fbed017d Binary files /dev/null and b/AgCloud/services/inference_http/models/soil_moisture/artifacts/best.pt differ diff --git a/AgCloud/services/inference_http/models/soil_moisture/artifacts/label_mapping.json b/AgCloud/services/inference_http/models/soil_moisture/artifacts/label_mapping.json new file mode 100644 index 000000000..7b688603a --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/artifacts/label_mapping.json @@ -0,0 +1,4 @@ +{ + "0": "dry", + "1": "wet" +} \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/artifacts/model.onnx b/AgCloud/services/inference_http/models/soil_moisture/artifacts/model.onnx new file mode 100644 index 000000000..6052e846e Binary files /dev/null and b/AgCloud/services/inference_http/models/soil_moisture/artifacts/model.onnx differ diff --git a/AgCloud/services/inference_http/models/soil_moisture/configs/zones.yaml b/AgCloud/services/inference_http/models/soil_moisture/configs/zones.yaml new file mode 100644 index 000000000..fa76afbb7 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/configs/zones.yaml @@ -0,0 +1,12 @@ +zones: + ZONE_A: + dry_ratio_high: 0.35 + dry_ratio_low: 0.25 + min_patches: 2 + duration_min: 10 + + ZONE_B: + dry_ratio_high: 0.40 + dry_ratio_low: 0.30 + min_patches: 2 + duration_min: 12 \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/docker-compose.yml b/AgCloud/services/inference_http/models/soil_moisture/docker-compose.yml new file mode 100644 index 000000000..c23e6b3e3 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/docker-compose.yml @@ -0,0 +1,30 @@ +networks: + worktree-main_ag_cloud: + external: true + +services: + api: + build: + context: . + dockerfile: Dockerfile + container_name: soil_api + environment: + PG_DSN: postgresql://missions_user:pg123@host.docker.internal:5432/missions_db + KAFKA_BROKERS: kafka:9092 # host.docker.internal:29092 + KAFKA_TOPIC: irrigation.control + KAFKA_DLT: irrigation.control.dlq + ZONES_FILE: /app/configs/zones.yaml + DECISION_WINDOW_SEC: 3 + PATCH_SIZE: 256 + PATCH_STRIDE: 256 + SCHEDULE_UPDATE: 1 + volumes: + - ./configs:/app/configs + - ./artifacts:/app/artifacts + ports: + - "8000:8000" + + networks: + - worktree-main_ag_cloud + + diff --git a/AgCloud/services/inference_http/models/soil_moisture/requirements-api.txt b/AgCloud/services/inference_http/models/soil_moisture/requirements-api.txt new file mode 100644 index 000000000..6e3ae713a --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/requirements-api.txt @@ -0,0 +1,14 @@ +fastapi==0.114.2 +uvicorn==0.30.6 +onnxruntime==1.20.0 +numpy==2.1.1 +Pillow==10.4.0 +opencv-python==4.10.0.84 +kafka-python==2.0.2 +psycopg2-binary==2.9.10 +prometheus_client==0.21.0 +PyYAML==6.0.2 +python-dotenv==1.0.1 +requests==2.32.3 +python-multipart==0.0.6 +confluent_kafka==2.12.0 \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/requirements-train.txt b/AgCloud/services/inference_http/models/soil_moisture/requirements-train.txt new file mode 100644 index 000000000..21a78fde4 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/requirements-train.txt @@ -0,0 +1,9 @@ +torch>=2.2.0 +torchvision==0.23.0 +numpy==2.1.1 +Pillow==10.4.0 +opencv-python==4.10.0.84 +scikit-learn==1.5.2 +tqdm==4.66.5 +PyYAML==6.0.2 +onnx==1.19.0 diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/.dockerignore b/AgCloud/services/inference_http/models/soil_moisture/src/.dockerignore new file mode 100644 index 000000000..f5bddaa38 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/.dockerignore @@ -0,0 +1,2 @@ +models/soil_moisture/samples/ +models/soil_moisture/tests/ \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/__init__.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/config.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/config.py new file mode 100644 index 000000000..719e03fa3 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/config.py @@ -0,0 +1,21 @@ +import os +import yaml +from dataclasses import dataclass +from typing import Dict, Any +from dotenv import load_dotenv +load_dotenv() + +@dataclass +class Settings: + kafka_brokers: str = os.getenv("KAFKA_BROKERS", "localhost:9092") + kafka_topic: str = os.getenv("KAFKA_TOPIC", "irrigation.control") + kafka_dlt: str = os.getenv("KAFKA_DLT", "irrigation.control.dlq") + pg_dsn: str = os.getenv("PG_DSN", "postgresql://postgres:postgres@localhost:5432/soil") + zones_file: str = os.getenv("ZONES_FILE", "configs/zones.yaml") + decision_window_sec: int = int(os.getenv("DECISION_WINDOW_SEC", "1")) + patch_size: int = int(os.getenv("PATCH_SIZE", "256")) + patch_stride: int = int(os.getenv("PATCH_STRIDE", "256")) + +def load_zones(path: str) -> Dict[str, Any]: + with open(path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/db.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/db.py new file mode 100644 index 000000000..356f523b2 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/db.py @@ -0,0 +1,134 @@ + +import json +from typing import Optional, Dict, Any +import psycopg2 +import psycopg2.extras +from contextlib import contextmanager + +class DB: + def __init__(self, dsn: str): + self.dsn = dsn + + @contextmanager + def conn(self): + conn = psycopg2.connect(self.dsn) + try: + yield conn + finally: + conn.close() + + def init_ok(self) -> bool: + try: + with self.conn() as c: + with c.cursor() as cur: + cur.execute("SELECT 1") + return True + except Exception: + return False + + def log_event(self, device_id: str, ts_iso: str, dry_ratio: float, + decision: str, confidence: float, patch_count: int, + idem_key: str, extra: Optional[Dict[str, Any]]=None) -> bool: + q = ''' + INSERT INTO soil_moisture_events + (device_id, ts, dry_ratio, decision, confidence, patch_count, idempotency_key, extra) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (idempotency_key) DO NOTHING + ''' + with self.conn() as c: + with c.cursor() as cur: + cur.execute(q, ( + device_id, + ts_iso, + dry_ratio, + decision, + confidence, + patch_count, + idem_key, + json.dumps(extra or {}) + )) + c.commit() + return cur.rowcount > 0 + + def load_device_policy(self, device_id: str) -> dict: + try: + with self.conn() as c: + with c.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute(""" + SELECT prev_state, dry_ratio_high, dry_ratio_low, + min_patches, duration_min + FROM irrigation_policies + WHERE device_id = %s + """, (device_id,)) + row = cur.fetchone() + if not row: + print(f"No row found for device_id={device_id}") + raise ValueError("not found") + print(f"Loaded from DB: {dict(row)}") + return dict(row) + except Exception as e: + print(f"Falling back to defaults because: {e}") + # fallback defaults + return { + "prev_state": "stop", + "dry_ratio_high": 0.35, + "dry_ratio_low": 0.25, + "min_patches": 2, + "duration_min": 10 + } + + + def upsert_schedule(self, device_id: str, next_run_at: str, duration_min: int, + updated_by: str, update_reason: str) -> None: + with self.conn() as c: + with c.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute("SELECT next_run_at, duration_min FROM irrigation_schedule WHERE device_id=%s", (device_id,)) + prev = cur.fetchone() + cur.execute(''' + INSERT INTO irrigation_schedule(device_id, next_run_at, duration_min, updated_by, update_reason) + VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (device_id) DO UPDATE SET + next_run_at=EXCLUDED.next_run_at, + duration_min=EXCLUDED.duration_min, + updated_by=EXCLUDED.updated_by, + update_reason=EXCLUDED.update_reason, + updated_at=NOW() + ''', (device_id, next_run_at, duration_min, updated_by, update_reason)) + cur.execute(''' + INSERT INTO irrigation_schedule_audit(device_id, prev_next_run_at, prev_duration_min, + next_run_at, duration_min, updated_by, update_reason) + VALUES (%s, %s, %s, %s, %s, %s, %s) + ''', (device_id, + prev["next_run_at"] if prev else None, + prev["duration_min"] if prev else None, + next_run_at, duration_min, updated_by, update_reason)) + c.commit() + + def update_prev_state(self, device_id: str, new_state: str) -> None: + # default values for other new fields + default_policy = { + "dry_ratio_high": 0.35, + "dry_ratio_low": 0.25, + "min_patches": 2, + "duration_min": 10 + } + + with self.conn() as c: + with c.cursor() as cur: + cur.execute(""" + INSERT INTO irrigation_policies + (device_id, prev_state, dry_ratio_high, dry_ratio_low, min_patches, duration_min) + VALUES (%s, %s, %s, %s, %s, %s) + ON CONFLICT (device_id) DO UPDATE + SET prev_state = EXCLUDED.prev_state, + updated_at = NOW() + """, ( + device_id, + new_state, + default_policy["dry_ratio_high"], + default_policy["dry_ratio_low"], + default_policy["min_patches"], + default_policy["duration_min"] + )) + c.commit() + diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/inference.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/inference.py new file mode 100644 index 000000000..95c4d948c --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/inference.py @@ -0,0 +1,106 @@ +from typing import Dict, Any, Tuple +from PIL import Image +import numpy as np +import os, time, logging +from .config import Settings +from .utils import normalize_lighting, tile_image, preprocess_onnx +from .metrics import METRICS +from .onnx_model import ONNXMoistureModel +from .db import DB + +logger = logging.getLogger("soil_api") + +DRY_LABEL = "dry" + +class Inferencer: + def __init__(self, settings: Settings, db: DB, + model_path: str = "artifacts/model.onnx", + label_map_path: str = "artifacts/label_mapping.json"): + self.settings = settings + self.db = db + self.model = ONNXMoistureModel(model_path, label_map_path) + self.classes = [self.model.label_map[str(i)] for i in range(len(self.model.label_map))] + + def decision_window_bucket(self, ts: float) -> int: + return int(ts // self.settings.decision_window_sec) * self.settings.decision_window_sec + + def infer_image(self, img: Image.Image, device_id: str) -> Tuple[Dict[str, Any], Dict[str, Any]]: + + t0 = time.time() + img_n = normalize_lighting(img) + patches = tile_image(img_n, self.settings.patch_size, self.settings.patch_stride) + + dry_votes = 0 + probs = [] + + logger.info("infer_image start device_id=%s patch_size=%d stride=%d total_patches=%d",device_id, self.settings.patch_size, self.settings.patch_stride, len(patches)) + + try: + dry_idx = self.classes.index(DRY_LABEL) + except ValueError: + logger.warning("DRY_LABEL '%s' not found in classes %s", DRY_LABEL, self.classes) + dry_idx = None + + for idx, p in enumerate(patches): + try: + proba = self.model.predict_proba_patch(p) + except Exception as e: + logger.exception("model.predict_proba_patch failed for patch idx=%d: %s", idx, e) + proba = np.zeros(len(self.classes), dtype=float) + + probs.append(proba) + arg = int(proba.argmax()) if proba.size else -1 + arg_label = self.classes[arg] if (arg >= 0 and arg < len(self.classes)) else "unknown" + maxp = float(proba.max()) if proba.size else 0.0 + logger.debug("patch idx=%d arg=%d label=%s maxp=%.4f", idx, arg, arg_label, maxp) + + if dry_idx is not None and arg == dry_idx: + dry_votes += 1 + + mean_confidence = float(np.mean([max(x) for x in probs])) if probs else 0.0 + + patch_count = len(patches) + dry_ratio = dry_votes / max(1, patch_count) + + # Policy / hysteresis + policy = self.db.load_device_policy(device_id) + prev_state = policy.get("prev_state") or "stop" + high = policy.get("dry_ratio_high") or 0.35 + low = policy.get("dry_ratio_low") or 0.25 + min_patches = policy.get("min_patches") or 2 + duration_min = policy.get("duration_min") or 10 + + logger.info("decision inputs prev_state=%s dry_votes=%d patch_count=%d dry_ratio=%.4f high=%.4f low=%.4f min_patches=%d", + prev_state, dry_votes, patch_count, dry_ratio, high, low, min_patches) + + decision = "noop" + if patch_count >= min_patches: + if prev_state != "run" and dry_ratio >= high: + decision = "run" + elif prev_state != "stop" and dry_ratio <= low: + decision = "stop" + else: + logger.debug("hysteresis conditions not met (prev_state=%s)", prev_state) + else: + logger.debug("not enough patches for decision: patch_count=%d min_patches=%d", patch_count, min_patches) + + new_state = decision if decision in ("run", "stop") else prev_state + logger.info("decision result=%s updated_state=%s duration_min=%d confidence=%.4f", + decision, new_state, duration_min, mean_confidence) + + METRICS["inference_latency_ms"].observe((time.time() - t0) * 1000.0) + + result = { + "dry_ratio": float(dry_ratio), + "decision": decision, + "confidence": float(mean_confidence), + "patch_count": int(patch_count), + "duration_min": duration_min + } + debug = { + "probs_shape": (len(probs), len(probs[0]) if probs else 0) + } + if new_state != prev_state: + self.db.update_prev_state(device_id, new_state) + + return result, debug diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/inference_logic.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/inference_logic.py new file mode 100644 index 000000000..8da8bae44 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/inference_logic.py @@ -0,0 +1,171 @@ +""" +Shared inference logic that can be used by both the API and adapters. +""" +import logging +import time +import datetime as dt +from typing import Dict, Any, Tuple, Optional +from PIL import Image + +logger = logging.getLogger(__name__) + + +class SoilMoistureInferenceLogic: + """ + Encapsulates the inference logic for soil moisture detection. + This can be used by both the FastAPI service and the Flink adapter. + """ + + def __init__(self, settings, db, inferencer, producer=None): + self.settings = settings + self.db = db + self.inferencer = inferencer + self.producer = producer + + def extract_device_id(self, filename: str) -> str: + """ + Extract device ID from filename in the format device_ts + (For example dev-f_20251106T1030.jpg) + """ + import os + base = os.path.basename(filename) + device_id = base.split("_")[0] + if not device_id: + raise ValueError(f"Invalid filename format: {filename}") + return device_id + + def build_idem_key(self, device_id: str, ts_unix: float) -> str: + """Build idempotency key""" + bucket = self.inferencer.decision_window_bucket(ts_unix) + return f"{device_id}:{int(bucket)}" + + def publish_and_persist( + self, + device_id: str, + decision: str, + duration_min: int, + confidence: float, + dry_ratio: float, + patch_count: int, + idem: str, + ts_iso: str + ) -> bool: + """ + Publish to Kafka and persist to database. + Returns True if saved successfully, False if duplicate. + """ + import json + import os + + payload = { + "device_id": device_id, + "command": decision if decision in ("run", "stop") else "noop", + "reason": "soil_dry", + "duration_min": duration_min if decision == "run" else None, + "confidence": confidence, + "ts": ts_iso, + "idempotency_key": idem + } + + saved = self.db.log_event( + device_id, ts_iso, dry_ratio, payload["command"], + confidence, patch_count, idem, + extra={"dry_ratio": dry_ratio} + ) + + if not saved: + logger.info(json.dumps({ + "msg": "duplicate_idempotency", + "device_id": device_id, + "idem": idem + })) + return False + + # Schedule update + schedule_update = os.getenv('SCHEDULE_UPDATE', '1') == '1' + if schedule_update and decision == 'run': + try: + self.db.upsert_schedule( + device_id, ts_iso, duration_min, + updated_by='soil_api', + update_reason='soil_dry' + ) + except Exception as e: + logger.warning('schedule update failed: %s', e) + + # Publish to Kafka + if self.producer: + self.producer.publish(payload) + else: + logger.warning( + "Kafka producer unavailable; skipping publish. payload=%s", + payload + ) + + return True + + def infer_from_image( + self, + img: Image.Image, + device_id: str, + ts_unix: Optional[float] = None + ) -> Dict[str, Any]: + """ + Run inference on an image and return results. + + Args: + img: PIL Image object + device_id: Device identifier + ts_unix: Unix timestamp (optional, defaults to current time) + + Returns: + Dictionary with inference results including: + - device_id + - dry_ratio + - decision + - confidence + - patch_count + - duration_min + - ts + - idempotency_key + - latency_ms + """ + if ts_unix is None: + ts_unix = time.time() + + start_time = time.time() + ts_iso = dt.datetime.utcfromtimestamp(ts_unix).isoformat() + "Z" + + # Run inference + result, debug = self.inferencer.infer_image(img, device_id) + + # Build idempotency key + idem = self.build_idem_key(device_id, ts_unix) + + # Publish and persist + saved = self.publish_and_persist( + device_id=device_id, + decision=result["decision"], + duration_min=result["duration_min"], + confidence=result["confidence"], + dry_ratio=result["dry_ratio"], + patch_count=result["patch_count"], + idem=idem, + ts_iso=ts_iso + ) + + latency_ms = int((time.time() - start_time) * 1000) + + return { + "device_id": device_id, + "dry_ratio": result["dry_ratio"], + "decision": result["decision"], + "confidence": result["confidence"], + "patch_count": result["patch_count"], + "duration_min": result.get("duration_min", 0), + "ts": ts_iso, + "idempotency_key": idem, + "latency_ms": latency_ms, + "saved": saved, + "debug": debug + } \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/kafka_producer.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/kafka_producer.py new file mode 100644 index 000000000..b6052accd --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/kafka_producer.py @@ -0,0 +1,39 @@ +import json +import logging +from confluent_kafka import Producer, KafkaError +from .metrics import METRICS + +logger = logging.getLogger("soil_api") + +class ControlProducer: + def __init__(self, brokers: str, topic: str, dlt: str): + self.topic = topic + self.dlt = dlt + self.producer = Producer({"bootstrap.servers": brokers}) + + def publish(self, payload: dict) -> None: + try: + self.producer.produce( + self.topic, + value=json.dumps(payload).encode("utf-8"), + on_delivery=self._delivery_report + ) + self.producer.flush(2) + METRICS["alerts_sent_total"].labels(decision=payload.get("command", "unknown")).inc() + except Exception as e: + logger.warning("Kafka publish failed: %s", e) + METRICS["kafka_publish_errors_total"].labels(reason=type(e).__name__).inc() + # try send to DLT + try: + dlt_payload = dict(payload) + dlt_payload["error"] = str(e) + self.producer.produce(self.dlt, value=json.dumps(dlt_payload).encode("utf-8")) + self.producer.flush(2) + except Exception as e2: + logger.error("DLT publish failed: %s", e2) + + def _delivery_report(self, err, msg): + if err is not None: + logger.warning("Delivery failed for record %s: %s", msg.key(), err) + else: + logger.debug("Record delivered to %s [%d] @ %d", msg.topic(), msg.partition(), msg.offset()) diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/metrics.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/metrics.py new file mode 100644 index 000000000..ce38f34de --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/metrics.py @@ -0,0 +1,11 @@ +from prometheus_client import Counter, Histogram + +METRICS = { + "alerts_sent_total": Counter("alerts_sent_total", "Total alerts sent", ["decision"]), + "kafka_publish_errors_total": Counter("kafka_publish_errors_total", "Kafka publish errors", ["reason"]), + "inference_latency_ms": Histogram( + "inference_latency_ms", + "Inference latency (ms)", + buckets=(5,10,20,50,100,200,500,1000,2000) + ), +} diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/onnx_model.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/onnx_model.py new file mode 100644 index 000000000..c78db3f26 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/onnx_model.py @@ -0,0 +1,22 @@ +import json +import numpy as np +import onnxruntime as ort +from typing import List +from PIL import Image +from .utils import preprocess_onnx + +class ONNXMoistureModel: + def __init__(self, model_path: str, label_map_path: str): + self.sess = ort.InferenceSession(model_path, providers=['CPUExecutionProvider']) + with open(label_map_path, "r", encoding="utf-8") as f: + self.label_map = json.load(f) # index -> label + self.input_name = self.sess.get_inputs()[0].name + self.output_name = self.sess.get_outputs()[0].name + + def predict_proba_patch(self, patch: Image.Image): + x = preprocess_onnx(patch, size=224) + logits = self.sess.run([self.output_name], {self.input_name: x})[0] + # softmax on logits + e = np.exp(logits - logits.max(axis=1, keepdims=True)) + proba = e / e.sum(axis=1, keepdims=True) + return proba[0] diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/schemas.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/schemas.py new file mode 100644 index 000000000..336e7c323 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/schemas.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + +class InferRequest(BaseModel): + device_id: str + image_b64: str # base64-encoded RGB image + +class InferResponse(BaseModel): + device_id: str + dry_ratio: float + decision: str + confidence: float + patch_count: int + ts: str + idempotency_key: str diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/service.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/service.py new file mode 100644 index 000000000..36e09cc20 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/service.py @@ -0,0 +1,103 @@ +""" +FastAPI service for soil moisture inference. +Delegates business logic to inference_logic.py +""" +import base64 +import logging +from fastapi import FastAPI, UploadFile, File, HTTPException +from fastapi.responses import PlainTextResponse +from prometheus_client import generate_latest, CONTENT_TYPE_LATEST + +from .config import Settings, load_zones +from .schemas import InferRequest, InferResponse +from .inference import Inferencer +from .kafka_producer import ControlProducer +from .db import DB +from .metrics import METRICS +from .utils import load_image_from_b64 +from .inference_logic import SoilMoistureInferenceLogic + +logging.basicConfig(level=logging.DEBUG, format='%(message)s') +logger = logging.getLogger("soil_api") + +# Initialize components +settings = Settings() +zones_cfg = load_zones(settings.zones_file) +db = DB(settings.pg_dsn) +inferencer = Inferencer(settings, db) + +# Initialize Kafka producer +producer = None +try: + from kafka import KafkaProducer + producer = ControlProducer( + settings.kafka_brokers, + settings.kafka_topic, + settings.kafka_dlt + ) +except Exception as e: + import traceback + logger.warning("Kafka init failed: %s\n%s", e, traceback.format_exc()) + +# Initialize shared inference logic +inference_logic = SoilMoistureInferenceLogic( + settings=settings, + db=db, + inferencer=inferencer, + producer=producer +) + +app = FastAPI(title="Soil Moisture DL API", version="1.0.0") + + +@app.get("/health", response_class=PlainTextResponse) +def health(): + return "ok" + + +@app.get("/ready", response_class=PlainTextResponse) +def ready(): + if not db.init_ok(): + raise HTTPException(status_code=503, detail="DB not ready") + return "ready" + + +@app.get("/metrics") +def metrics(): + return PlainTextResponse(generate_latest(), media_type=CONTENT_TYPE_LATEST) + + +@app.post("/infer", response_model=InferResponse) +async def infer(image: UploadFile = File(None), body: InferRequest | None = None): + """ + Run inference on a soil moisture image. + Accepts either multipart form data (file upload) or JSON with base64 image. + """ + # Parse input + if body is not None: + img = load_image_from_b64(body.image_b64) + device_id = inference_logic.extract_device_id(body.filename) + else: + if image is None: + raise HTTPException( + status_code=400, + detail="Provide multipart (file) or JSON (image_b64)" + ) + filename = image.filename + device_id = inference_logic.extract_device_id(filename) + content = await image.read() + img = load_image_from_b64(base64.b64encode(content).decode("utf-8")) + + # Run inference using shared logic + result = inference_logic.infer_from_image(img, device_id) + + # Return response + return InferResponse( + device_id=result["device_id"], + dry_ratio=result["dry_ratio"], + decision=result["decision"], + confidence=result["confidence"], + patch_count=result["patch_count"], + ts=result["ts"], + idempotency_key=result["idempotency_key"] + ) \ No newline at end of file diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/app/utils.py b/AgCloud/services/inference_http/models/soil_moisture/src/app/utils.py new file mode 100644 index 000000000..2853c5250 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/app/utils.py @@ -0,0 +1,29 @@ +import base64, io +from PIL import Image, ImageOps +import numpy as np +from typing import List + +def load_image_from_b64(b64: str) -> Image.Image: + data = base64.b64decode(b64) + return Image.open(io.BytesIO(data)).convert("RGB") + +def normalize_lighting(img: Image.Image) -> Image.Image: + r, g, b = img.split() + r, g, b = ImageOps.equalize(r), ImageOps.equalize(g), ImageOps.equalize(b) + return Image.merge("RGB", (r, g, b)) + +def tile_image(img: Image.Image, patch_size: int, stride: int) -> List[Image.Image]: + w, h = img.size + patches = [] + for y in range(0, h - patch_size + 1, stride): + for x in range(0, w - patch_size + 1, stride): + patches.append(img.crop((x, y, x + patch_size, y + patch_size))) + if not patches: + patches.append(img.resize((patch_size, patch_size))) + return patches + +def preprocess_onnx(pil_img: Image.Image, size: int = 224) -> np.ndarray: + img = pil_img.resize((size, size)) + arr = np.asarray(img).astype("float32") / 255.0 + arr = arr.transpose(2,0,1) # HWC -> CHW + return arr[None, :, :, :] # NCHW diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/scripts/consume_once.py b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/consume_once.py new file mode 100644 index 000000000..b035f4017 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/consume_once.py @@ -0,0 +1,40 @@ +import argparse +import json +from kafka import KafkaConsumer, errors + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--brokers", default="localhost:29092", help="Broker list, e.g. localhost:29092 or kafka:9092") + parser.add_argument("--topic", default="irrigation.control") + parser.add_argument("--group", default="debug-consumer") + parser.add_argument("--from-beginning", action="store_true", help="Read from earliest offset") + args = parser.parse_args() + + print(f"Connecting to Kafka brokers: {args.brokers}") + try: + consumer = KafkaConsumer( + args.topic, + bootstrap_servers=args.brokers.split(","), + group_id=args.group, + enable_auto_commit=False, + auto_offset_reset="earliest" if args.from_beginning else "latest", + value_deserializer=lambda v: json.loads(v.decode("utf-8")), + consumer_timeout_ms=0 + ) + print(f"Listening on topic '{args.topic}' (group={args.group})...") + except errors.NoBrokersAvailable: + print("❌ Cannot connect to Kafka. Check host/port and Docker networking.") + return + + try: + for message in consumer: + print("\n--- New message ---") + print(json.dumps(message.value, indent=2)) + except KeyboardInterrupt: + print("\nStopped by user.") + finally: + consumer.close() + print("Consumer closed.") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/scripts/demo_feed.py b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/demo_feed.py new file mode 100644 index 000000000..3be42924a --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/demo_feed.py @@ -0,0 +1,48 @@ +import argparse, os, base64, time, json, glob +import requests + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--images-dir", required=True) + ap.add_argument("--api", default="http://localhost:8000") + args = ap.parse_args() + + # Collect all images + imgs = [] + for ext in ("*.jpg", "*.jpeg", "*.png", "*.bmp"): + imgs += glob.glob(os.path.join(args.images_dir, '**', ext), recursive=True) + imgs = sorted(imgs) + + if not imgs: + print("No images found in", args.images_dir) + return + + for path in imgs: + filename = os.path.basename(path) + + # IMPORTANT: The device_id must be encoded inside the filename, + # e.g. device123_20250101T1030.jpg + print(f"Sending {filename} ...") + + try: + with open(path, "rb") as f: + files = {"image": (filename, f)} + r = requests.post( + args.api + "/infer", + files=files, + timeout=60 + ) + + if r.status_code != 200: + print("Error:", r.status_code, r.text) + else: + print(json.dumps(r.json(), indent=2)) + + except Exception as e: + print("Request failed:", e) + + time.sleep(0.4) + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/scripts/eval_test_set.py b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/eval_test_set.py new file mode 100644 index 000000000..875834dea --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/eval_test_set.py @@ -0,0 +1,42 @@ +# src/scripts/eval_test_onnx.py +import json, numpy as np, onnxruntime as ort +from pathlib import Path +from torchvision import transforms +from torchvision.datasets import ImageFolder +from torch.utils.data import DataLoader + +def load_label_mapping(path="artifacts/label_mapping.json"): + with open(path,"r") as f: + return json.load(f) + +def preprocess_pil(img): + tf = transforms.Compose([transforms.Resize((224,224)), transforms.ToTensor()]) + t = tf(img).numpy() + return np.expand_dims(t, axis=0).astype(np.float32) + +def run_onnx_eval(onnx_path="artifacts/model.onnx", test_dir="samples/test", batch_size=16): + label_map = load_label_mapping() + classes = [label_map[str(i)] for i in range(len(label_map))] + ds = ImageFolder(test_dir, transform=None) # we'll read PIL ourselves + loader = DataLoader(ds, batch_size=batch_size, shuffle=False) + + sess = ort.InferenceSession(onnx_path, providers=['CPUExecutionProvider']) + input_name = sess.get_inputs()[0].name + output_name = sess.get_outputs()[0].name + + y_true, y_pred = [], [] + from PIL import Image + for path, label in ds.samples: + img = Image.open(path).convert("RGB") + x = preprocess_pil(img) + logits = sess.run([output_name], {input_name: x})[0] + pred = int(np.argmax(logits, axis=1)[0]) + y_true.append(label) + y_pred.append(pred) + + from sklearn.metrics import classification_report, confusion_matrix + print(classification_report(y_true, y_pred, target_names=classes, digits=4)) + print(confusion_matrix(y_true, y_pred)) + +if __name__ == "__main__": + run_onnx_eval() diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/scripts/print_db_events.py b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/print_db_events.py new file mode 100644 index 000000000..07b41b875 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/scripts/print_db_events.py @@ -0,0 +1,24 @@ +import argparse, psycopg2, psycopg2.extras, json + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--dsn", default="postgresql://missions_user:pg123@127.0.0.1:5432/missions_db") + ap.add_argument("--limit", type=int, default=10) + args = ap.parse_args() + + conn = psycopg2.connect(args.dsn) + try: + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute(""" + SELECT id, device_id, ts, dry_ratio, decision, confidence, patch_count, idempotency_key + FROM soil_moisture_events + ORDER BY id DESC + LIMIT %s + """, (args.limit,)) + rows = cur.fetchall() + print(json.dumps(rows, indent=2, default=str)) + finally: + conn.close() + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/sql/init_db.sql b/AgCloud/services/inference_http/models/soil_moisture/src/sql/init_db.sql new file mode 100644 index 000000000..1476755fc --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/sql/init_db.sql @@ -0,0 +1,47 @@ +CREATE TABLE IF NOT EXISTS soil_moisture_events ( + id SERIAL PRIMARY KEY, + device_id TEXT NOT NULL, + ts TIMESTAMPTZ NOT NULL DEFAULT NOW(), + dry_ratio REAL NOT NULL, + decision TEXT NOT NULL, + confidence REAL NOT NULL, + patch_count INT NOT NULL, + idempotency_key TEXT NOT NULL, + extra JSONB DEFAULT '{}'::jsonb +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_events_idem ON soil_moisture_events (idempotency_key); + +CREATE TABLE IF NOT EXISTS irrigation_schedule ( + device_id TEXT PRIMARY KEY, + next_run_at TIMESTAMPTZ NOT NULL, + duration_min INT NOT NULL, + updated_by TEXT NOT NULL, + update_reason TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS irrigation_schedule_audit ( + id SERIAL PRIMARY KEY, + device_id TEXT NOT NULL, + prev_next_run_at TIMESTAMPTZ, + prev_duration_min INT, + next_run_at TIMESTAMPTZ NOT NULL, + duration_min INT NOT NULL, + updated_by TEXT NOT NULL, + update_reason TEXT NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE TABLE irrigation_policies ( + device_id TEXT NOT NULL, + prev_state TEXT, + dry_ratio_high REAL, + dry_ratio_low REAL, + min_patches INT, + duration_min INT, + updated_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (device_id), + CONSTRAINT fk_device + FOREIGN KEY (device_id) REFERENCES devices(device_id) + ON DELETE CASCADE +); diff --git a/AgCloud/services/inference_http/models/soil_moisture/src/train/train_torch.py b/AgCloud/services/inference_http/models/soil_moisture/src/train/train_torch.py new file mode 100644 index 000000000..db7c89d9a --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/src/train/train_torch.py @@ -0,0 +1,110 @@ +import argparse, os, json +from pathlib import Path +import numpy as np +from PIL import Image +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader +from torchvision import datasets, transforms, models +from tqdm import tqdm + +def build_dataloaders(train_dir, val_dir, batch_size): + aug = transforms.Compose([ + transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), + transforms.ToTensor() + ]) + val_tf = transforms.Compose([ + transforms.Resize((224,224)), + transforms.ToTensor() + ]) + train_ds = datasets.ImageFolder(train_dir, transform=aug) + val_ds = datasets.ImageFolder(val_dir, transform=val_tf) + train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=2, pin_memory=True) + val_loader = DataLoader(val_ds, batch_size=batch_size, shuffle=False, num_workers=2, pin_memory=True) + return train_loader, val_loader, train_ds.classes + +@torch.no_grad() +def evaluate(model, loader, device): + model.eval() + correct, total = 0, 0 + for x, y in loader: + x, y = x.to(device), y.to(device) + logits = model(x) + pred = logits.argmax(1) + correct += (pred == y).sum().item() + total += y.numel() + return correct / max(1,total) + +def export_onnx(model, out_path, device): + model.eval() + dummy = torch.randn(1,3,224,224, device=device) + out_dir = os.path.dirname(out_path) + os.makedirs(out_dir, exist_ok=True) + torch.onnx.export( + model, dummy, out_path, + input_names=["input"], output_names=["logits"], + opset_version=17, dynamic_axes=None + ) + print("Exported ONNX to", out_path) + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--train-dir", required=True) + ap.add_argument("--val-dir", required=True) + ap.add_argument("--epochs", type=int, default=15) + ap.add_argument("--batch-size", type=int, default=64) + ap.add_argument("--lr", type=float, default=3e-4) + ap.add_argument("--out", required=True) # ONNX output path + args = ap.parse_args() + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + train_loader, val_loader, classes = build_dataloaders(args.train_dir, args.val_dir, args.batch_size) + + # MobileNetV3-small transfer learning + model = models.mobilenet_v3_small(weights=models.MobileNet_V3_Small_Weights.DEFAULT) + in_features = model.classifier[3].in_features + model.classifier[3] = nn.Linear(in_features, len(classes)) + model.to(device) + + criterion = nn.CrossEntropyLoss() + optimizer = optim.AdamW(model.parameters(), lr=args.lr) + + best_acc = 0.0 + best_pt = "artifacts/best.pt" + os.makedirs("artifacts", exist_ok=True) + + for epoch in range(1, args.epochs+1): + model.train() + pbar = tqdm(train_loader, desc=f"Epoch {epoch}/{args.epochs}") + for x, y in pbar: + x, y = x.to(device), y.to(device) + logits = model(x) + loss = criterion(logits, y) + optimizer.zero_grad() + loss.backward() + optimizer.step() + pbar.set_postfix(loss=float(loss.item())) + + acc = evaluate(model, val_loader, device) + print(f"Val acc: {acc:.4f}") + if acc > best_acc: + best_acc = acc + torch.save(model.state_dict(), best_pt) + + # Export ONNX from best weights + model.load_state_dict(torch.load(best_pt, map_location=device)) + export_onnx(model, args.out, device=device) + + # Save label mapping + lbl_path = os.path.join(os.path.dirname(args.out), "label_mapping.json") + label_mapping = {str(i): cls for i, cls in enumerate(classes)} + with open(lbl_path, "w", encoding="utf-8") as f: + json.dump(label_mapping, f, indent=2) + print("Saved label mapping:", lbl_path) + print("Best val acc:", best_acc) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/inference_http/models/soil_moisture/tests/conftest.py b/AgCloud/services/inference_http/models/soil_moisture/tests/conftest.py new file mode 100644 index 000000000..5cfe605bc --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/tests/conftest.py @@ -0,0 +1,10 @@ +import os +import sys + + +# Ensure `app` package (under src/) is importable when running tests +TEST_DIR = os.path.dirname(__file__) +SRC_DIR = os.path.abspath(os.path.join(TEST_DIR, "..", "src")) +if SRC_DIR not in sys.path: + sys.path.insert(0, SRC_DIR) + diff --git a/AgCloud/services/inference_http/models/soil_moisture/tests/test_config_and_schemas.py b/AgCloud/services/inference_http/models/soil_moisture/tests/test_config_and_schemas.py new file mode 100644 index 000000000..0fb9abae9 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/tests/test_config_and_schemas.py @@ -0,0 +1,43 @@ +import os +from app.config import load_zones, Settings +from app.schemas import InferRequest, InferResponse + + +def test_load_zones_file_exists_and_parses(): + # Use the repo's zones.yaml + base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + zones_path = os.path.join(base_dir, "configs", "zones.yaml") + data = load_zones(zones_path) + assert isinstance(data, dict) + assert "zones" in data + assert isinstance(data["zones"], dict) + + +def test_settings_defaults_are_present(): + s = Settings() + # Ensure critical fields exist and are strings/ints + assert isinstance(s.kafka_brokers, str) + assert isinstance(s.kafka_topic, str) + assert isinstance(s.pg_dsn, str) + assert isinstance(s.decision_window_sec, int) + assert isinstance(s.patch_size, int) + assert isinstance(s.patch_stride, int) + + +def test_schemas_models_construction(): + req = InferRequest(device_id="zone-a", image_b64="abcd==") + assert req.device_id == "zone-a" + assert isinstance(req.image_b64, str) + + resp = InferResponse( + device_id="zone-a", + dry_ratio=0.5, + decision="run", + confidence=0.9, + patch_count=4, + ts="2024-01-01T00:00:00Z", + idempotency_key="zone-a:12345", + ) + assert resp.decision in {"run", "stop", "noop"} + assert resp.patch_count == 4 + diff --git a/AgCloud/services/inference_http/models/soil_moisture/tests/test_inference.py b/AgCloud/services/inference_http/models/soil_moisture/tests/test_inference.py new file mode 100644 index 000000000..5906bacfe --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/tests/test_inference.py @@ -0,0 +1,96 @@ +import sys +import types +import numpy as np +from PIL import Image + +# Pre-inject a lightweight stub for app.onnx_model to avoid importing onnxruntime +stub_mod = types.ModuleType("app.onnx_model") +class _StubONNX: + def __init__(self, *a, **k): + # minimal surface to satisfy Inferencer init + self.label_map = {"0": "dry", "1": "wet"} + def predict_proba_patch(self, patch): + return np.array([0.5, 0.5], dtype=float) +stub_mod.ONNXMoistureModel = _StubONNX +sys.modules.setdefault("app.onnx_model", stub_mod) + +from app.config import Settings +from app import inference as infmod + + +class _FakeDryModel: + def __init__(self, *args, **kwargs): + # emulates label_map: index->label + self.label_map = {"0": "dry", "1": "wet"} + + def predict_proba_patch(self, patch): + # Always predict class 0 (dry) with high confidence + return np.array([0.9, 0.1], dtype=float) + + +class _FakeWetModel: + def __init__(self, *args, **kwargs): + self.label_map = {"0": "dry", "1": "wet"} + + def predict_proba_patch(self, patch): + # Always predict class 1 (wet) + return np.array([0.1, 0.9], dtype=float) + + +def _make_inferencer(monkeypatch, model_cls): + # Replace ONNX model with a lightweight fake + monkeypatch.setattr(infmod, "ONNXMoistureModel", lambda *a, **k: model_cls()) + + s = Settings() + s.patch_size = 10 + s.patch_stride = 10 + s.decision_window_sec = 300 + return infmod.Inferencer(s) + + +def _make_image(w=20, h=10): + return Image.new("RGB", (w, h), color=(128, 128, 128)) + + +def test_decision_run_when_dry_ratio_high(monkeypatch): + inf = _make_inferencer(monkeypatch, _FakeDryModel) + # 20x10 with 10x10 patches & stride 10 => 2 patches + img = _make_image(20, 10) + zone_cfg = {"_state": "stop", "dry_ratio_high": 0.5, "dry_ratio_low": 0.3, "min_patches": 2, "duration_min": 7} + + result, debug = inf.infer_image(img, zone_cfg) + assert result["patch_count"] == 2 + assert result["dry_ratio"] == 1.0 + assert result["decision"] == "run" + assert zone_cfg["_state"] == "run" + + +def test_decision_stop_when_dry_ratio_low_and_prev_run(monkeypatch): + inf = _make_inferencer(monkeypatch, _FakeWetModel) + img = _make_image(20, 10) # 2 patches + zone_cfg = {"_state": "run", "dry_ratio_high": 0.6, "dry_ratio_low": 0.25, "min_patches": 2, "duration_min": 5} + + result, _ = inf.infer_image(img, zone_cfg) + assert result["patch_count"] == 2 + assert result["dry_ratio"] == 0.0 + assert result["decision"] == "stop" + assert zone_cfg["_state"] == "stop" + + +def test_noop_when_not_enough_patches(monkeypatch): + inf = _make_inferencer(monkeypatch, _FakeDryModel) + img = _make_image(20, 10) # 2 patches + zone_cfg = {"_state": "stop", "dry_ratio_high": 0.5, "dry_ratio_low": 0.3, "min_patches": 3, "duration_min": 7} + + result, _ = inf.infer_image(img, zone_cfg) + assert result["patch_count"] == 2 + assert result["decision"] == "noop" + # State remains unchanged + assert zone_cfg["_state"] == "stop" + + +def test_decision_window_bucket_rounds_down(monkeypatch): + inf = _make_inferencer(monkeypatch, _FakeDryModel) + # With window 300s, 1234 -> bucket start 1200 + bucket = inf.decision_window_bucket(1234.0) + assert bucket == 1200 diff --git a/AgCloud/services/inference_http/models/soil_moisture/tests/test_utils.py b/AgCloud/services/inference_http/models/soil_moisture/tests/test_utils.py new file mode 100644 index 000000000..a45ea10e4 --- /dev/null +++ b/AgCloud/services/inference_http/models/soil_moisture/tests/test_utils.py @@ -0,0 +1,58 @@ +import base64 +import io +from PIL import Image +import numpy as np + +from app.utils import ( + load_image_from_b64, + normalize_lighting, + tile_image, + preprocess_onnx, +) + + +def make_rgb_image(w=8, h=6, color=(120, 100, 80)): + return Image.new("RGB", (w, h), color=color) + + +def test_load_image_from_b64_roundtrip(): + img = make_rgb_image(5, 7, (10, 20, 30)) + buf = io.BytesIO() + img.save(buf, format="PNG") + b64 = base64.b64encode(buf.getvalue()).decode("utf-8") + + out = load_image_from_b64(b64) + assert out.mode == "RGB" + assert out.size == (5, 7) + + +def test_normalize_lighting_basic_properties(): + img = make_rgb_image(10, 10, (50, 100, 150)) + out = normalize_lighting(img) + assert out.mode == "RGB" + assert out.size == img.size + + +def test_tile_image_regular_grid(): + img = make_rgb_image(5, 5, (0, 0, 0)) + patches = tile_image(img, patch_size=3, stride=2) + # Positions: x in {0,2}, y in {0,2} => 4 patches + assert len(patches) == 4 + assert all(p.size == (3, 3) for p in patches) + + +def test_tile_image_small_image_resizes_to_single_patch(): + img = make_rgb_image(2, 2, (0, 0, 0)) + patches = tile_image(img, patch_size=4, stride=4) + assert len(patches) == 1 + assert patches[0].size == (4, 4) + + +def test_preprocess_onnx_output_shape_and_range(): + img = make_rgb_image(6, 6, (255, 128, 0)) + arr = preprocess_onnx(img, size=8) + assert arr.shape == (1, 3, 8, 8) + assert arr.dtype == np.float32 + assert np.isfinite(arr).all() + assert arr.min() >= 0.0 and arr.max() <= 1.0 + diff --git a/AgCloud/services/inference_http/requirements.txt b/AgCloud/services/inference_http/requirements.txt new file mode 100644 index 000000000..bdc33b3eb --- /dev/null +++ b/AgCloud/services/inference_http/requirements.txt @@ -0,0 +1,23 @@ + +fastapi +uvicorn +pydantic +minio +requests +torch +numpy<2 +opencv-python-headless +ultralytics==8.2.34 +boto3 +pillow +numpy==1.26.4 +pydantic +onnxruntime==1.20.0 +kafka-python==2.0.2 +psycopg2-binary==2.9.10 +prometheus_client==0.21.0 +PyYAML==6.0.2 +python-dotenv==1.0.1 +requests==2.32.3 +python-multipart==0.0.6 +confluent_kafka==2.12.0 \ No newline at end of file diff --git a/AgCloud/services/inference_http/weights/fruit_cls_best.ts b/AgCloud/services/inference_http/weights/fruit_cls_best.ts new file mode 100644 index 000000000..d190cf3d9 Binary files /dev/null and b/AgCloud/services/inference_http/weights/fruit_cls_best.ts differ diff --git a/AgCloud/services/inference_http/weights/yolov8-fruits.pt b/AgCloud/services/inference_http/weights/yolov8-fruits.pt new file mode 100644 index 000000000..d61ef50d3 Binary files /dev/null and b/AgCloud/services/inference_http/weights/yolov8-fruits.pt differ diff --git a/AgCloud/services/mqtt_gateway/Dockerfile b/AgCloud/services/mqtt_gateway/Dockerfile new file mode 100644 index 000000000..4a0d1c1ad --- /dev/null +++ b/AgCloud/services/mqtt_gateway/Dockerfile @@ -0,0 +1,42 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim + +ARG USE_NETFREE=true + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl libc6 libsasl2-2 libssl3 librdkafka1 \ + && rm -rf /var/lib/apt/lists/* + +# (Optional) import NetFree cert if present in build context +RUN if [ "$USE_NETFREE" = "true" ] && [ -f ./netfree-ca.crt ]; then \ + cp ./netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt && \ + chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && \ + update-ca-certificates; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app +# COPY services/mqtt_gateway/requirements.txt . +COPY requirements.txt . +RUN pip install --no-cache-dir --trusted-host pypi.org --trusted-host files.pythonhosted.org -r requirements.txt + +# COPY services/mqtt_gateway /app/services/mqtt_gateway +COPY . /app/services/mqtt_gateway + + +# Default envs (override in deployment) +ENV MINIO_ENDPOINT=http://minio:9000 \ + MINIO_ACCESS_KEY=minioadmin \ + MINIO_SECRET_KEY=minioadmin \ + MINIO_BUCKET=rover-images \ + KAFKA_BOOTSTRAP=kafka:9092 \ + KAFKA_TOPIC=rover.images.meta.v1 \ + MQTT_HOST=mosquitto \ + MQTT_PORT=1883 \ + MQTT_TOPIC=MQTT/imagery/# \ + MQTT_CLIENT_ID=mqtt-gateway + +CMD ["python", "-m", "services.mqtt_gateway.main"] diff --git a/AgCloud/services/mqtt_gateway/adapters/kafka_producer.py b/AgCloud/services/mqtt_gateway/adapters/kafka_producer.py new file mode 100644 index 000000000..4703085b3 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/adapters/kafka_producer.py @@ -0,0 +1,32 @@ +# services/mqtt_gateway/adapters/kafka_producer.py +# Confluent Kafka producer adapter with lazy imports for unit-test friendliness. + +from __future__ import annotations +import json, importlib +from typing import Optional + +from services.mqtt_gateway.io import EventBusProducer + + +class ConfluentEventBusProducer(EventBusProducer): + """ + Thin wrapper over confluent_kafka. Handles JSON serialization and delivery errors. + Lazy-imports confluent_kafka to keep unit tests independent of that binary dep. + """ + + def __init__(self, bootstrap_servers: str, client_id: str = "mqtt-gateway") -> None: + if not bootstrap_servers: + raise ValueError("bootstrap_servers required") + ck = importlib.import_module("confluent_kafka") + self._Producer = ck.Producer + self._p = self._Producer({"bootstrap.servers": bootstrap_servers, "client.id": client_id}) + + def _delivery(self, err, msg) -> None: + if err is not None: + raise RuntimeError(f"kafka delivery failed: {err.str()}") + + def send(self, topic: str, key: str, value: dict) -> None: + payload = json.dumps(value, ensure_ascii=False).encode("utf-8") + self._p.produce(topic=topic, key=key, value=payload, callback=self._delivery) + self._p.poll(0) + self._p.flush(5) diff --git a/AgCloud/services/mqtt_gateway/adapters/minio_store.py b/AgCloud/services/mqtt_gateway/adapters/minio_store.py new file mode 100644 index 000000000..ab9074e85 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/adapters/minio_store.py @@ -0,0 +1,77 @@ +# services/mqtt_gateway/adapters/minio_store.py +# Boto3-based ImageStore adapter with lazy imports for unit-test friendliness. + +from __future__ import annotations +from typing import Tuple +import io, hashlib, importlib + +from services.mqtt_gateway.io import ImageStore + + +def _sha256_hex(b: bytes) -> str: + h = hashlib.sha256(); h.update(b); return h.hexdigest() + + +class Boto3ImageStore(ImageStore): + """ + Uploads bytes to S3-compatible storage (MinIO). + Lazy-imports boto3/botocore to avoid hard dependency during unit tests. + """ + + def __init__( + self, + endpoint_url: str, + access_key: str, + secret_key: str, + *, + addressing_style: str = "path", + multipart_threshold: int = 5 * 1024 * 1024, + multipart_chunksize: int = 5 * 1024 * 1024, + max_concurrency: int = 16, + retries: int = 3, + ) -> None: + # Lazy imports (so importing this module won't require boto3 installed) + boto3 = importlib.import_module("boto3") + botocore_config = importlib.import_module("botocore.config") + s3_transfer = importlib.import_module("boto3.s3.transfer") + + BotoConfig = botocore_config.Config + TransferConfig = s3_transfer.TransferConfig + + self._s3 = boto3.client( + "s3", + endpoint_url=endpoint_url, + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + config=BotoConfig( + signature_version="s3v4", + s3={"addressing_style": addressing_style}, + max_pool_connections=max(64, max_concurrency * 2), + retries={"max_attempts": retries, "mode": "standard"}, + ), + ) + self._tx = TransferConfig( + multipart_threshold=multipart_threshold, + multipart_chunksize=multipart_chunksize, + max_concurrency=max_concurrency, + use_threads=True, + ) + + def put_object(self, bucket: str, key: str, data: bytes, content_type: str) -> Tuple[str, int]: + if not bucket or not key: + raise ValueError("bucket/key must be non-empty") + if data is None: + raise ValueError("data must not be None") + ctype = content_type or "application/octet-stream" + + sha = _sha256_hex(data) + size = len(data) + extra = {"ContentType": ctype, "Metadata": {"checksum-sha256": sha}} + + if size >= self._tx.multipart_threshold: + bio = io.BytesIO(data) + self._s3.upload_fileobj(bio, bucket, key, ExtraArgs=extra, Config=self._tx) + else: + self._s3.put_object(Bucket=bucket, Key=key, Body=data, **extra) + + return sha, size diff --git a/AgCloud/services/mqtt_gateway/config.py b/AgCloud/services/mqtt_gateway/config.py new file mode 100644 index 000000000..126c75ee8 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/config.py @@ -0,0 +1,45 @@ +# services/mqtt_gateway/config.py +# Purpose: centralize and validate service configuration (12-factor style). + +from __future__ import annotations +from pydantic import BaseModel, Field, AnyUrl, ValidationError +import os + +class GatewayConfig(BaseModel): + # Storage + minio_endpoint: str = Field(default="http://minio:9000") + minio_access_key: str = Field(default="minioadmin", min_length=1) + minio_secret_key: str = Field(default="minioadmin", min_length=1) + minio_bucket: str = Field(default="rover-images", min_length=1) + + # Kafka + kafka_bootstrap: str = Field(default="kafka:9092", min_length=1) + kafka_topic: str = Field(default="rover.images.meta.v1", min_length=1) + + # MQTT + mqtt_host: str = Field(default="mosquitto") + mqtt_port: int = Field(default=1883, ge=1) + mqtt_topic: str = Field(default="MQTT/imagery/#") + mqtt_client_id: str = Field(default="mqtt-gateway") + + # Server/metrics + metrics_port: int = Field(default=9110, ge=1024, le=65535) + +def load_config() -> GatewayConfig: + env = os.getenv + try: + return GatewayConfig( + minio_endpoint=env("MINIO_ENDPOINT", "http://minio:9000"), + minio_access_key=env("MINIO_ACCESS_KEY", "minioadmin"), + minio_secret_key=env("MINIO_SECRET_KEY", "minioadmin"), + minio_bucket=env("MINIO_BUCKET", "rover-images"), + kafka_bootstrap=env("KAFKA_BOOTSTRAP", "kafka:9092"), + kafka_topic=env("KAFKA_TOPIC", "rover.images.meta.v1"), + mqtt_host=env("MQTT_HOST", "mosquitto"), + mqtt_port=int(env("MQTT_PORT", "1883")), + mqtt_topic=env("MQTT_TOPIC", "MQTT/imagery/#"), + mqtt_client_id=env("MQTT_CLIENT_ID", "mqtt-gateway"), + metrics_port=int(env("METRICS_PORT", "9110")), + ) + except ValidationError as ve: + raise SystemExit(f"[config] invalid env: {ve}") from ve diff --git a/AgCloud/services/mqtt_gateway/docs/contracts.md b/AgCloud/services/mqtt_gateway/docs/contracts.md new file mode 100644 index 000000000..42dc6f280 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/docs/contracts.md @@ -0,0 +1,33 @@ +# MQTT → MinIO → Kafka Data Contract + +> Purpose: a stable, versioned contract for image ingestion events. +> Scope: mapping from publisher topic to MinIO object key and Kafka event. + +## Example publisher file name +`garage_cam_01_20251027_121530.jpg` + +## Kafka topic (value JSON published to this topic) +`rover.images.meta.v1` + +## Kafka message (value) +```json +{ + "version": 1, + "event_id": "8b0a6f2b-1f95-4b28-9870-5f7f1d4a6a11", + "sensor_id": "camera-01", + "captured_ts": 1761548130123, + "image": { + "bucket": "rover-images", + "key": "images/camera-01/2025/10/27/camera-01_1761548130123.jpg", + "size_bytes": 531245, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "content_type": "image/jpeg" + }, + "telemetry": { + "lat": 32.061, + "lon": 34.772, + "heading": 182.4, + "speed_mps": 2.1 + }, + "producer": "mqtt-gateway" +} diff --git a/AgCloud/services/mqtt_gateway/io.py b/AgCloud/services/mqtt_gateway/io.py new file mode 100644 index 000000000..d0b9f3c4d --- /dev/null +++ b/AgCloud/services/mqtt_gateway/io.py @@ -0,0 +1,40 @@ +# services/mqtt_gateway/io.py +# Purpose: IO interfaces (protocols) used by the service for testability. + +from __future__ import annotations +from typing import Protocol, Optional, runtime_checkable + + +@runtime_checkable +class ImageStore(Protocol): + def put_object( + self, + bucket: str, + key: str, + data: bytes, + content_type: str, + ) -> tuple[str, int]: + """ + Uploads bytes to object storage. + Returns: (sha256_hex, size_bytes) + Must raise an exception on failure. + """ + + +@runtime_checkable +class EventBusProducer(Protocol): + def send(self, topic: str, key: str, value: dict) -> None: + """ + Publishes a message to an event bus (e.g., Kafka). + Must raise an exception on failure. + """ + + +class Clock(Protocol): + def now_ms(self) -> int: + """Epoch milliseconds (used mostly for timestamps in logs/metrics).""" + + +class IdGenerator(Protocol): + def new_event_id(self) -> str: + """Return a new UUID string for event_id.""" diff --git a/AgCloud/services/mqtt_gateway/main.py b/AgCloud/services/mqtt_gateway/main.py new file mode 100644 index 000000000..612f8f8f3 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/main.py @@ -0,0 +1,189 @@ +# services/mqtt_gateway/main.py +# MQTT consumer → build ImageInfo → process via IngestService. +# Designed for Docker. All configuration by env vars. +from __future__ import annotations + +from prometheus_client import Counter, Histogram, start_http_server +import logging +from services.mqtt_gateway.config import load_config + +import os, signal, sys, time, pathlib +from typing import Tuple +from paho.mqtt.client import Client, MQTTv5, CallbackAPIVersion +import paho.mqtt.client as mqtt +import json, threading +from typing import Optional, Dict, Tuple as _Tuple + +from services.mqtt_gateway.models import ImageInfo +from services.mqtt_gateway.service import ( + ServiceConfig, Deps, IngestService, DefaultClock, DefaultIds +) +from services.mqtt_gateway.adapters.minio_store import Boto3ImageStore +from services.mqtt_gateway.adapters.kafka_producer import ConfluentEventBusProducer + +TELEMETRY_TOPIC = os.getenv("MQTT_TOPIC_TEL", "MQTT/telemetry/#") +TELEMETRY_TTL_SEC = int(os.getenv("TELEMETRY_TTL_SEC", "10")) # seconds + +# --------- Metrics & Logging --------- +INGEST_OK = Counter("gateway_ingest_success_total", "successful publishes") +INGEST_FAIL = Counter("gateway_ingest_fail_total", "failed publishes") +INGEST_LAT = Histogram("gateway_ingest_latency_ms", "ingest latency (ms)") +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") +log = logging.getLogger("mqtt-gateway") + + +# --------- Topic parser (mirrors your existing convention) --------- +def parse_topic(topic: str, filename_fallback: str = "image.jpg") -> Tuple[str, int, str, str]: + """ + Returns (sensor_id, ts_ms, content_type, filename) + Topic format: MQTT/imagery/{camera}/{ts_ms}/{ctype_safe}/{filename} + """ + parts = [p for p in topic.split("/") if p] + try: + i = parts.index("imagery") + except ValueError: + return ("unknown", int(time.time() * 1000), "application/octet-stream", filename_fallback) + + sensor = parts[i + 1] if len(parts) > i + 1 else "unknown" + ts_ms = int(parts[i + 2]) if len(parts) > i + 2 and parts[i + 2].isdigit() else int(time.time() * 1000) + ctype = (parts[i + 3] if len(parts) > i + 3 else "application_octet-stream").replace("_", "/") + fname = parts[i + 4] if len(parts) > i + 4 else filename_fallback + return (sensor, ts_ms, ctype, fname) + +# sensor_id -> (ts_ms, telemetry_dict) +_telemetry_cache: Dict[str, _Tuple[int, dict]] = {} +_telemetry_lock = threading.Lock() + +def parse_tel_topic(topic: str) -> _Tuple[str, int]: + """ + Parse telemetry topic: MQTT/telemetry/{sensor_id}/{ts_ms} + Returns (sensor_id, ts_ms). + """ + parts = [p for p in topic.split("/") if p] + try: + i = parts.index("telemetry") + except ValueError: + return ("unknown", int(time.time() * 1000)) + sensor = parts[i + 1] if len(parts) > i + 1 else "unknown" + ts_ms = int(parts[i + 2]) if len(parts) > i + 2 and parts[i + 2].isdigit() else int(time.time() * 1000) + return (sensor, ts_ms) + +def put_telemetry(sensor_id: str, ts_ms: int, tel: dict) -> None: + """Store latest telemetry per sensor (monotonic by ts_ms).""" + with _telemetry_lock: + prev = _telemetry_cache.get(sensor_id) + if (not prev) or ts_ms >= prev[0]: + _telemetry_cache[sensor_id] = (ts_ms, tel) + +def get_telemetry_for(sensor_id: str, captured_ts: int) -> Optional[dict]: + """ + Return telemetry if within TTL window relative to captured_ts. + """ + with _telemetry_lock: + row = _telemetry_cache.get(sensor_id) + if not row: + return None + ts_ms, tel = row + if abs(captured_ts - ts_ms) <= TELEMETRY_TTL_SEC * 1000: + return tel + return None + +def make_service_from_cfg(cfg) -> IngestService: + store = Boto3ImageStore( + endpoint_url=cfg.minio_endpoint, + access_key=cfg.minio_access_key, + secret_key=cfg.minio_secret_key, + ) + producer = ConfluentEventBusProducer(bootstrap_servers=cfg.kafka_bootstrap) + s_cfg = ServiceConfig(bucket=cfg.minio_bucket, kafka_topic=cfg.kafka_topic) + deps = Deps(image_store=store, producer=producer, clock=DefaultClock(), ids=DefaultIds()) + return IngestService(s_cfg, deps) + +def main() -> None: + cfg = load_config() + start_http_server(cfg.metrics_port, addr="0.0.0.0") + log.info("metrics server started on :%d", cfg.metrics_port) + service = make_service_from_cfg(cfg) + + client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=cfg.mqtt_client_id, protocol=MQTTv5) + stop = False + + def on_connect(c, u, flags, rc, props): + if rc == 0: + log.info("mqtt connected, subscribing %s", cfg.mqtt_topic) + c.subscribe(cfg.mqtt_topic, qos=1) + c.subscribe(TELEMETRY_TOPIC, qos=0) + else: + print(f"[mqtt] connect failed rc={rc}") + + def on_message(c, u, msg): + # if "/telemetry/" in msg.topic: + # try: + # sensor, ts_ms, ctype, fname = parse_topic(msg.topic) + # tel = json.loads(msg.payload.decode("utf-8")) + # put_telemetry(sensor, ts_ms, tel) + # log.debug("telemetry cached sensor=%s ts=%d", sensor, ts_ms) + # except Exception as e: + # log.warning("bad telemetry message: %s", e, exc_info=False) + # return + if "/telemetry/" in msg.topic: + try: + sensor, ts_ms = parse_tel_topic(msg.topic) + tel = json.loads(msg.payload.decode("utf-8")) + put_telemetry(sensor, ts_ms, tel) + log.debug("telemetry cached sensor=%s ts=%d", sensor, ts_ms) + except Exception as e: + log.warning("bad telemetry message: %s", e, exc_info=False) + return + + # ---- image branch ---- + with INGEST_LAT.time(): + try: + # ✅ parse the image topic to get required fields + sensor, ts_ms, ctype, fname = parse_topic(msg.topic) + + # build ImageInfo from topic + payload mime + info = ImageInfo( + sensor_id=sensor, + captured_ts=ts_ms, + filename=fname, + content_type=ctype, + ) + + # match last telemetry (if within TTL) + tel = get_telemetry_for(sensor, ts_ms) + + # pass telemetry down to the service + _m, _e = service.process_image(info, msg.payload, telemetry=tel) + + INGEST_OK.inc() + log.info( + "ingest ok sensor=%s ts=%s key=%s tel=%s", + sensor, ts_ms, _m.key, "yes" if tel else "no" + ) + except Exception as e: + INGEST_FAIL.inc() + log.error("ingest error: %s", e, exc_info=False) + + client.on_connect = on_connect + client.on_message = on_message + client.reconnect_delay_set(min_delay=1, max_delay=8) + client.connect(cfg.mqtt_host, cfg.mqtt_port, keepalive=60) + client.loop_start() + + def _stop(*_): + nonlocal stop + stop = True + client.loop_stop() + client.disconnect() + + signal.signal(signal.SIGINT, _stop) + signal.signal(signal.SIGTERM, _stop) + + log.info("gateway running…") + while not stop: + time.sleep(0.5) + log.info("gateway stopped.") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/mqtt_gateway/mapper.py b/AgCloud/services/mqtt_gateway/mapper.py new file mode 100644 index 000000000..c8d77b359 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/mapper.py @@ -0,0 +1,87 @@ +# services/mqtt_gateway/mapper.py +# Purpose: pure functions to compute MinIO object key & Kafka event from MQTT topic parts. +# No network. Easy to unit-test. + +from __future__ import annotations +from datetime import datetime, timezone +from typing import Tuple, Optional +from uuid import uuid4 +import os + +from .models import MinioObject, ImageInfo, KafkaEvent + + +def safe_ext(filename: str, default: str = ".jpg") -> str: + """Return a safe file extension with dot. Fallback to default if missing/invalid.""" + _, ext = os.path.splitext(filename) + ext = (ext or "").strip().lower() + if not ext or len(ext) > 8 or any(c in ext for c in " \t/\\"): + return default + return ext + + +def date_parts_from_epoch_ms(ts_ms: int) -> Tuple[str, str, str]: + dt = datetime.fromtimestamp(ts_ms / 1000.0, tz=timezone.utc) + return f"{dt.year:04d}", f"{dt.month:02d}", f"{dt.day:02d}" + + +# def build_minio_key(sensor_id: str, ts_ms: int, filename: str) -> str: +# """images/{sensor}/{YYYY}/{MM}/{DD}/{sensor}_{ts}{ext}""" +# y, m, d = date_parts_from_epoch_ms(ts_ms) +# ext = safe_ext(filename) +# base = f"{sensor_id}_{ts_ms}{ext}" +# return f"images/{sensor_id}/{y}/{m}/{d}/{base}" + +def build_minio_key( + sensor_id: str, + ts_ms: int, + filename: str, + *, + incident_id: str, + rover_type: str = "rover-car", + prefix: str = "security/incidents", +) -> str: + """ + Build canonical S3 key: + imagery/security/incidents///_ + + Notes: + - `incident_id` must be provided by the caller (stable folder per incident). + - `rover_type` has a sensible default ("rover-car") but can be overridden. + """ + ext = safe_ext(filename, default=".jpg") + base = f"{sensor_id}_{ts_ms}{ext}" + return f"{prefix}/{rover_type}/{incident_id}/{base}" + + +def map_to_objects( + bucket: str, + info: ImageInfo, + *, + sha256: Optional[str] = None, + size_bytes: Optional[int] = None, + event_id: Optional[str] = None, + telemetry: Optional[dict] = None +) -> Tuple[MinioObject, KafkaEvent]: + """ + Given input image info → compute MinIO object + Kafka event. + Pure function: no IO. Easy to unit test. + """ + key = build_minio_key(info.sensor_id, info.captured_ts, info.filename, incident_id=event_id or str(uuid4())) + mobj = MinioObject( + bucket=bucket, + key=key, + content_type=info.content_type, + size_bytes=size_bytes, + sha256=sha256, + ) + kev = KafkaEvent( + version=1, + event_id=(event_id or str(uuid4())), + sensor_id=info.sensor_id, + captured_ts=info.captured_ts, + image=mobj, + # telemetry=None, + telemetry=telemetry, + ) + return mobj, kev diff --git a/AgCloud/services/mqtt_gateway/models.py b/AgCloud/services/mqtt_gateway/models.py new file mode 100644 index 000000000..327ed9f2b --- /dev/null +++ b/AgCloud/services/mqtt_gateway/models.py @@ -0,0 +1,46 @@ +# services/mqtt_gateway/models.py +# Purpose: Typed, validated data contracts for MQTT→MinIO/Kafka mapping. +# Clean, testable models used across the service. + +from __future__ import annotations +from pydantic import BaseModel, Field, ConfigDict, field_validator +from typing import Optional + + +class MinioObject(BaseModel): + model_config = ConfigDict(extra="forbid") + bucket: str = Field(min_length=1) + key: str = Field(min_length=1) + content_type: str = Field(min_length=1) + size_bytes: Optional[int] = Field(default=None, ge=0) + sha256: Optional[str] = None + + +class ImageInfo(BaseModel): + model_config = ConfigDict(extra="forbid") + sensor_id: str = Field(min_length=1) + captured_ts: int = Field(ge=0, description="epoch milliseconds") + filename: str = Field(min_length=1) + content_type: str = Field(min_length=1) + + @field_validator("content_type") + @classmethod + def normalize_ctype(cls, v: str) -> str: + return v.strip().lower() + + +class KafkaEvent(BaseModel): + model_config = ConfigDict(extra="forbid") + version: int = 1 + event_id: str + sensor_id: str + captured_ts: int + image: MinioObject + telemetry: Optional[dict] = None + producer: str = "mqtt-gateway" + + @property + def key(self) -> str: + """Kafka message key (partitioning & idempotency hint).""" + return f"{self.sensor_id}:{self.captured_ts}" + diff --git a/AgCloud/services/mqtt_gateway/requirements.txt b/AgCloud/services/mqtt_gateway/requirements.txt new file mode 100644 index 000000000..3bdb4f438 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/requirements.txt @@ -0,0 +1,8 @@ +pydantic>=2.7,<3 +pytest>=8,<9 +boto3>=1.34 +confluent-kafka>=2.5 +paho-mqtt>=2.1 +prometheus-client>=0.20 +Pillow>=10 +piexif>=1.1 \ No newline at end of file diff --git a/AgCloud/services/mqtt_gateway/service.py b/AgCloud/services/mqtt_gateway/service.py new file mode 100644 index 000000000..a7e4a70f5 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/service.py @@ -0,0 +1,117 @@ +# services/mqtt_gateway/service.py +# Purpose: Orchestrates the ingestion of an image payload: +# parse → store in MinIO (via ImageStore) → build Kafka event → publish (EventBusProducer). +# Pure business logic, with IO abstracted behind interfaces for unit testing. + +from __future__ import annotations +from dataclasses import dataclass +from typing import Optional, Tuple +from uuid import uuid4 +import hashlib + +from .models import ImageInfo, MinioObject, KafkaEvent +from .mapper import build_minio_key, map_to_objects +from .io import ImageStore, EventBusProducer, Clock, IdGenerator +import logging +log = logging.getLogger("ingest-service") + +def _sha256_hex(b: bytes) -> str: + h = hashlib.sha256() + h.update(b) + return h.hexdigest() + + +@dataclass(frozen=True) +class ServiceConfig: + bucket: str + kafka_topic: str + + +@dataclass +class Deps: + image_store: ImageStore + producer: EventBusProducer + clock: Clock + ids: IdGenerator + + +class DefaultClock(Clock): + def now_ms(self) -> int: + import time + return int(time.time() * 1000) + + +class DefaultIds(IdGenerator): + def new_event_id(self) -> str: + return str(uuid4()) + + +class IngestService: + """ + Public API: + - process_image(info, payload) -> (MinioObject, KafkaEvent) + + Notes: + - No direct imports of storage/kafka SDKs here. Only interfaces. + - Idempotency is enforced downstream by keying on (sensor_id, captured_ts). + """ + + def __init__(self, cfg: ServiceConfig, deps: Deps) -> None: + self.cfg = cfg + self.deps = deps + + def _retry(self, fn, attempts=3): + last = None + for i in range(1, attempts + 1): + try: + return fn() + except Exception as e: + last = e + if i == attempts: + raise + raise last + + def process_image(self, info: ImageInfo, payload: bytes, telemetry: Optional[dict] = None) -> Tuple[MinioObject, KafkaEvent]: + + # 1) Compute MinIO object key (stable contract) + # key = build_minio_key(info.sensor_id, info.captured_ts, info.filename) + event_id = self.deps.ids.new_event_id() # single source for both event & incident + incident_id = event_id # reuse; replace if you have a real incident id + key = build_minio_key( + info.sensor_id, info.captured_ts, info.filename, + incident_id=incident_id, + rover_type="rover-car" # adjust if needed + ) + + # 2) Upload to object storage + sha, size = self._retry(lambda: self.deps.image_store.put_object( + bucket=self.cfg.bucket, key=key, data=payload, content_type=info.content_type + )) + + # 3) Build event (add sha/size and deterministic IDs) + mobj, kev = map_to_objects( + bucket=self.cfg.bucket, + info=info, + sha256=sha, + size_bytes=size, + event_id=event_id, + telemetry=telemetry, + ) + + if telemetry is not None: + kev.telemetry = telemetry + + # 4) Publish to event bus + + try: + self._retry(lambda: self.deps.producer.send( + topic=self.cfg.kafka_topic, key=kev.key, value=kev.model_dump() + )) + except Exception as e: + # Do not raise — MinIO write already succeeded. Log and continue. + log.warning( + "kafka publish failed (topic=%s, key=%s): %s", + self.cfg.kafka_topic, kev.key, e + ) + + return mobj, kev diff --git a/AgCloud/services/mqtt_gateway/tests/fixtures/sample_event.json b/AgCloud/services/mqtt_gateway/tests/fixtures/sample_event.json new file mode 100644 index 000000000..b291d7530 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/tests/fixtures/sample_event.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "event_id": "00000000-0000-0000-0000-000000000000", + "sensor_id": "camera-01", + "captured_ts": 1761548130123, + "image": { + "bucket": "rover-images", + "key": "images/camera-01/2025/10/27/camera-01_1761548130123.jpg", + "size_bytes": 531245, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "content_type": "image/jpeg" + }, + "telemetry": { "lat": 32.061, "lon": 34.772, "heading": 182.4, "speed_mps": 2.1 }, + "producer": "mqtt-gateway" +} diff --git a/AgCloud/services/mqtt_gateway/tests/test_adapters_unit.py b/AgCloud/services/mqtt_gateway/tests/test_adapters_unit.py new file mode 100644 index 000000000..19c9a4122 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/tests/test_adapters_unit.py @@ -0,0 +1,52 @@ +# services/mqtt_gateway/tests/test_adapters_unit.py +from __future__ import annotations +from unittest.mock import Mock, patch +import types + +def test_minio_store_put_object_calls_boto3_correctly(): + # Fake boto3 + botocore modules + fake_s3_client = Mock() + fake_s3_client.put_object = Mock() + fake_s3_client.upload_fileobj = Mock() + + def fake_import(name): + if name == "boto3": + # expose client() that returns our fake client + m = types.SimpleNamespace(client=lambda *a, **k: fake_s3_client) + return m + if name == "botocore.config": + class _Cfg: + def __init__(self, **kwargs): pass + return types.SimpleNamespace(Config=_Cfg) + if name == "boto3.s3.transfer": + class _Tx: + def __init__(self, multipart_threshold, multipart_chunksize, max_concurrency, use_threads): + self.multipart_threshold = multipart_threshold + return types.SimpleNamespace(TransferConfig=_Tx) + raise ImportError(name) + + with patch("services.mqtt_gateway.adapters.minio_store.importlib.import_module", side_effect=fake_import): + from services.mqtt_gateway.adapters.minio_store import Boto3ImageStore + store = Boto3ImageStore(endpoint_url="http://minio:9000", access_key="a", secret_key="s") + sha, size = store.put_object("bkt", "key", b"1234", "image/jpeg") + # For small payload (4 bytes) → put_object path + fake_s3_client.put_object.assert_called_once() + assert sha and size == 4 + +def test_kafka_producer_json_serialization(): + fake_prod = Mock() + + def fake_import(name): + if name == "confluent_kafka": + return types.SimpleNamespace(Producer=lambda conf: fake_prod) + raise ImportError(name) + + with patch("services.mqtt_gateway.adapters.kafka_producer.importlib.import_module", side_effect=fake_import): + from services.mqtt_gateway.adapters.kafka_producer import ConfluentEventBusProducer + k = ConfluentEventBusProducer("kafka:9092") + k.send("t", "k1", {"a": 1}) + fake_prod.produce.assert_called_once() + args, kwargs = fake_prod.produce.call_args + assert kwargs["topic"] == "t" + assert kwargs["key"] == "k1" + assert isinstance(kwargs["value"], (bytes, bytearray)) diff --git a/AgCloud/services/mqtt_gateway/tests/test_mapper.py b/AgCloud/services/mqtt_gateway/tests/test_mapper.py new file mode 100644 index 000000000..598e62c7a --- /dev/null +++ b/AgCloud/services/mqtt_gateway/tests/test_mapper.py @@ -0,0 +1,33 @@ +# services/mqtt_gateway/tests/test_mapper.py +# Purpose: Unit tests for pure mapping logic (no MinIO/Kafka). +# Run: pytest -q + +from services.mqtt_gateway.models import ImageInfo +from services.mqtt_gateway.mapper import build_minio_key, map_to_objects + + +def test_build_minio_key_happy(): + key = build_minio_key("camera-01", 1761548130123, "foo.jpg") + assert key.startswith("images/camera-01/2025/10/27/") + assert key.endswith("camera-01_1761548130123.jpg") + + +def test_build_minio_key_ext_fallback(): + key = build_minio_key("s1", 1000, "noext") + assert key.endswith("s1_1000.jpg") + + +def test_map_to_objects_event_and_key(): + info = ImageInfo( + sensor_id="camera-01", + captured_ts=1761548130123, + filename="garage_cam_01_20251027_121530.jpg", + content_type="image/jpeg", + ) + mobj, kev = map_to_objects("rover-images", info, sha256="abc", size_bytes=123) + assert mobj.bucket == "rover-images" + assert mobj.key.endswith("camera-01_1761548130123.jpg") + assert mobj.sha256 == "abc" + assert kev.image.key == mobj.key + assert kev.key == "camera-01:1761548130123" + assert kev.version == 1 diff --git a/AgCloud/services/mqtt_gateway/tests/test_service.py b/AgCloud/services/mqtt_gateway/tests/test_service.py new file mode 100644 index 000000000..1a7278b92 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/tests/test_service.py @@ -0,0 +1,105 @@ +# services/mqtt_gateway/tests/test_service.py +# Purpose: Unit tests for the orchestration service using mocks for IO. + +from __future__ import annotations +from unittest.mock import Mock +import pytest + +from services.mqtt_gateway.models import ImageInfo +from services.mqtt_gateway.service import ( + ServiceConfig, Deps, IngestService, DefaultClock, DefaultIds +) + + +def make_info(): + return ImageInfo( + sensor_id="camera-01", + captured_ts=1761548130123, + filename="garage_cam_01_20251027_121530.jpg", + content_type="image/jpeg", + ) + + +def test_process_image_happy_path(): + # Arrange + cfg = ServiceConfig(bucket="rover-images", kafka_topic="rover.images.meta.v1") + + image_store = Mock() + image_store.put_object.return_value = ("abc123", 531245) + + producer = Mock() + + clock = DefaultClock() + ids = Mock() + ids.new_event_id.return_value = "11111111-1111-1111-1111-111111111111" + + service = IngestService(cfg, Deps(image_store=image_store, producer=producer, clock=clock, ids=ids)) + info = make_info() + payload = b"\xff\xd8\xff\xdb\x00..." # fake jpeg bytes + + # Act + mobj, kev = service.process_image(info, payload) + + # Assert: storage call + image_store.put_object.assert_called_once() + args, kwargs = image_store.put_object.call_args + assert kwargs["bucket"] == "rover-images" + assert kwargs["content_type"] == "image/jpeg" + assert kwargs["data"] == payload + assert kwargs["key"].endswith("camera-01_1761548130123.jpg") + + # Assert: event produced + producer.send.assert_called_once() + p_args, p_kwargs = producer.send.call_args + assert p_kwargs["topic"] == "rover.images.meta.v1" + assert p_kwargs["key"] == "camera-01:1761548130123" + val = p_kwargs["value"] + assert val["event_id"] == "11111111-1111-1111-1111-111111111111" + assert val["image"]["bucket"] == "rover-images" + assert val["image"]["key"].endswith("camera-01_1761548130123.jpg") + assert val["image"]["sha256"] == "abc123" + assert val["image"]["size_bytes"] == 531245 + + +def test_process_image_store_failure(): + cfg = ServiceConfig(bucket="rover-images", kafka_topic="rover.images.meta.v1") + + image_store = Mock() + image_store.put_object.side_effect = RuntimeError("store down") + + producer = Mock() + clock = DefaultClock() + ids = DefaultIds() + + service = IngestService(cfg, Deps(image_store=image_store, producer=producer, clock=clock, ids=ids)) + info = make_info() + payload = b"data" + + with pytest.raises(RuntimeError): + service.process_image(info, payload) + + # Ensure producer not called when store fails + producer.send.assert_not_called() + + +def test_process_image_producer_failure(): + cfg = ServiceConfig(bucket="rover-images", kafka_topic="rover.images.meta.v1") + + image_store = Mock() + image_store.put_object.return_value = ("sha-ok", 10) + + producer = Mock() + producer.send.side_effect = RuntimeError("kafka down") + + clock = DefaultClock() + ids = DefaultIds() + + service = IngestService(cfg, Deps(image_store=image_store, producer=producer, clock=clock, ids=ids)) + info = make_info() + payload = b"data" + + with pytest.raises(RuntimeError): + service.process_image(info, payload) + + # Ensure we DID upload but failed on publish + image_store.put_object.assert_called_once() diff --git a/AgCloud/services/mqtt_gateway/tests/test_service_retry.py b/AgCloud/services/mqtt_gateway/tests/test_service_retry.py new file mode 100644 index 000000000..5571fed97 --- /dev/null +++ b/AgCloud/services/mqtt_gateway/tests/test_service_retry.py @@ -0,0 +1,36 @@ +from unittest.mock import Mock +import pytest + +from services.mqtt_gateway.models import ImageInfo +from services.mqtt_gateway.service import ServiceConfig, Deps, IngestService, DefaultClock, DefaultIds + +def make_info(): + return ImageInfo(sensor_id="s1", captured_ts=1000, filename="a.jpg", content_type="image/jpeg") + +def test_retry_on_store_once_then_success(): + cfg = ServiceConfig(bucket="b", kafka_topic="t") + store = Mock() + store.put_object.side_effect = [RuntimeError("flaky"), ("sha", 3)] + prod = Mock() + svc = IngestService(cfg, Deps(store, prod, DefaultClock(), DefaultIds())) + m, e = svc.process_image(make_info(), b"xxx") + assert prod.send.called + +def test_retry_on_producer_once_then_success(): + cfg = ServiceConfig(bucket="b", kafka_topic="t") + store = Mock() + store.put_object.return_value = ("sha", 3) + prod = Mock() + prod.send.side_effect = [RuntimeError("flaky"), None] + svc = IngestService(cfg, Deps(store, prod, DefaultClock(), DefaultIds())) + m, e = svc.process_image(make_info(), b"xxx") + assert prod.send.call_count == 2 + +def test_retry_exhaust_raises(): + cfg = ServiceConfig(bucket="b", kafka_topic="t") + store = Mock() + store.put_object.side_effect = RuntimeError("down") + prod = Mock() + svc = IngestService(cfg, Deps(store, prod, DefaultClock(), DefaultIds())) + with pytest.raises(RuntimeError): + svc.process_image(make_info(), b"xxx") diff --git a/AgCloud/services/plant_stress/Dockerfile b/AgCloud/services/plant_stress/Dockerfile new file mode 100644 index 000000000..d456611aa --- /dev/null +++ b/AgCloud/services/plant_stress/Dockerfile @@ -0,0 +1,45 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + TZ=Asia/Jerusalem + +RUN apt-get update && apt-get install -y --no-install-recommends \ + cron tzdata ca-certificates curl libsndfile1 ffmpeg \ + && rm -rf /var/lib/apt/lists/* + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +COPY certs/ /usr/local/share/ca-certificates/ + +RUN if find /usr/local/share/ca-certificates -maxdepth 1 -type f \ + \( -name '*.crt' -o -name '*.cer' -o -name '*.pem' \) \ + -print -quit | grep -q .; then \ + echo "Installing custom certificates from /usr/local/share/ca-certificates..."; \ + update-ca-certificates; \ + else \ + echo "No custom cert files found under /usr/local/share/ca-certificates, continuing without extra certificates."; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY src /app/src +COPY models /models +ENV MODEL_DIR=/models + +COPY cronjob /etc/cron.d/plant-stress-cron +COPY run_job.sh /app/run_job.sh +COPY entrypoint.sh /entrypoint.sh + +RUN sed -i 's/\r$//' /etc/cron.d/plant-stress-cron /entrypoint.sh /app/run_job.sh \ + && chmod 0644 /etc/cron.d/plant-stress-cron \ + && chmod +x /entrypoint.sh /app/run_job.sh \ + && touch /var/log/cron.log + +CMD ["/entrypoint.sh"] diff --git a/AgCloud/services/plant_stress/README.md b/AgCloud/services/plant_stress/README.md new file mode 100644 index 000000000..636042768 --- /dev/null +++ b/AgCloud/services/plant_stress/README.md @@ -0,0 +1,12 @@ +# Plant Stress Daily Job +This service runs **once per day**. +It loads a pre-trained ML model, reads audio files from MinIO, predicts plant stress, writes results to PostgreSQL, **and if stress is detected, it sends an alert message to Kafka**. + + +--- + + +## Download Model Files +Download the required model files from: +https://drive.google.com/drive/folders/17iXRnP-a5_6wt_tS4ieLKkUlUlU5IhDT?usp=sharing +Place all files in a folder named **`AgCloud/services/plant_stress/models/`** in the project root. \ No newline at end of file diff --git a/AgCloud/services/plant_stress/cronjob b/AgCloud/services/plant_stress/cronjob new file mode 100644 index 000000000..de3cd28ab --- /dev/null +++ b/AgCloud/services/plant_stress/cronjob @@ -0,0 +1,4 @@ +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +CRON_TZ=Asia/Jerusalem +39 14 * * * root /app/run_job.sh >> /var/log/cron.log 2>&1 diff --git a/AgCloud/services/plant_stress/entrypoint.sh b/AgCloud/services/plant_stress/entrypoint.sh new file mode 100644 index 000000000..41727b16f --- /dev/null +++ b/AgCloud/services/plant_stress/entrypoint.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cron +tail -F /var/log/cron.log \ No newline at end of file diff --git a/AgCloud/services/plant_stress/models/efficientnet_best.keras b/AgCloud/services/plant_stress/models/efficientnet_best.keras new file mode 100644 index 000000000..34875790a Binary files /dev/null and b/AgCloud/services/plant_stress/models/efficientnet_best.keras differ diff --git a/AgCloud/services/plant_stress/models/label_encoder.pkl b/AgCloud/services/plant_stress/models/label_encoder.pkl new file mode 100644 index 000000000..e39b9dbb4 Binary files /dev/null and b/AgCloud/services/plant_stress/models/label_encoder.pkl differ diff --git a/AgCloud/services/plant_stress/models/normalization_params.npz b/AgCloud/services/plant_stress/models/normalization_params.npz new file mode 100644 index 000000000..18b967c14 Binary files /dev/null and b/AgCloud/services/plant_stress/models/normalization_params.npz differ diff --git a/AgCloud/services/plant_stress/models/scaler_params.npz b/AgCloud/services/plant_stress/models/scaler_params.npz new file mode 100644 index 000000000..5a819e757 Binary files /dev/null and b/AgCloud/services/plant_stress/models/scaler_params.npz differ diff --git a/AgCloud/services/plant_stress/models/ultrasonic_plant_cnn.keras b/AgCloud/services/plant_stress/models/ultrasonic_plant_cnn.keras new file mode 100644 index 000000000..07bdb55eb Binary files /dev/null and b/AgCloud/services/plant_stress/models/ultrasonic_plant_cnn.keras differ diff --git a/AgCloud/services/plant_stress/requirements.txt b/AgCloud/services/plant_stress/requirements.txt new file mode 100644 index 000000000..869c5ea97 --- /dev/null +++ b/AgCloud/services/plant_stress/requirements.txt @@ -0,0 +1,11 @@ +numpy +librosa +soundfile +tensorflow +psycopg2-binary +requests +urllib3 +pytz +minio +keras +kafka-python \ No newline at end of file diff --git a/AgCloud/services/plant_stress/run_job.sh b/AgCloud/services/plant_stress/run_job.sh new file mode 100644 index 000000000..f6de17b98 --- /dev/null +++ b/AgCloud/services/plant_stress/run_job.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "[$(date -Iseconds)] [plant_stress_daily] starting job..." +python -u /app/src/predict_minio_daily.py +echo "[$(date -Iseconds)] [plant_stress_daily] finished job." diff --git a/AgCloud/services/plant_stress/run_plant_stress_daily.sh b/AgCloud/services/plant_stress/run_plant_stress_daily.sh new file mode 100644 index 000000000..3d449ad43 --- /dev/null +++ b/AgCloud/services/plant_stress/run_plant_stress_daily.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +PROJECT_DIR="/mnt/c/Users/This User/Desktop/project_05112025/AgCloud" +LOG_DIR="$PROJECT_DIR/services/plant_stress/logs" +STAMP="$(date +%F)" +LOG_FILE="$LOG_DIR/cron_${STAMP}.log" + +mkdir -p "$LOG_DIR" + +echo "[cron start $(date '+%Y-%m-%d %H:%M:%S')]" >> "$LOG_FILE" + +cd "$PROJECT_DIR" + +exec /usr/bin/docker compose run --rm plant_stress_daily >> "$LOG_FILE" 2>&1 diff --git a/AgCloud/services/plant_stress/src/predict_minio_daily.py b/AgCloud/services/plant_stress/src/predict_minio_daily.py new file mode 100644 index 000000000..abbd5145e --- /dev/null +++ b/AgCloud/services/plant_stress/src/predict_minio_daily.py @@ -0,0 +1,827 @@ +import os +import sys +import time +import pickle +import datetime as dt +from pathlib import Path +import re +import uuid +import json +from io import BytesIO + +import numpy as np +import librosa +import tensorflow as tf +import psycopg2 +import psycopg2.extras +import pytz +import soundfile as sf +from minio import Minio + +# ======== Environment ======== + +MODEL_DIR = os.getenv("MODEL_DIR", "/models") +POSTGRES_DSN = os.getenv( + "POSTGRES_DSN", + "postgresql://missions_user:pg123@postgres:5432/missions_db", +) + +# MinIO +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio-hot:9000") +MINIO_ACCESS = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET = os.getenv("MINIO_SECRET_KEY", "minioadmin123") +MINIO_BUCKET = os.getenv("MINIO_BUCKET", "sound") +MINIO_PREFIX = os.getenv("MINIO_PREFIX", "plants/") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" + +# Fallback defaults (משמשים רק אם אין נתונים ב־DB) +DEFAULT_AREA = os.getenv("DEFAULT_AREA", "unknown").strip() +DEFAULT_LAT = os.getenv("DEFAULT_LAT", "0.0").strip() +DEFAULT_LON = os.getenv("DEFAULT_LON", "0.0").strip() +DEFAULT_IMAGE_URL = os.getenv("DEFAULT_IMAGE_URL", "https://example.com/placeholder.jpg").strip() +DEFAULT_VOD = os.getenv("DEFAULT_VOD", "https://example.com/placeholder.mp4").strip() +DEFAULT_HLS = os.getenv("DEFAULT_HLS", "https://example.com/placeholder.m3u8").strip() + +# ======== Date / TZ ======== + +TIMEZONE = os.getenv("TIMEZONE", "Asia/Jerusalem") +PROCESS_DATE = os.getenv("PROCESS_DATE", "").strip() + +# ======== Confidence ======== + +CONFIDENCE_THRESHOLD = float(os.getenv("CONFIDENCE_THRESHOLD", "0.60")) + +# ======== Audio Params ======== + +SAMPLE_RATE = 500_000 +DURATION_MS = 2 +N_SAMPLES = SAMPLE_RATE * DURATION_MS // 1000 +N_FFT = 256 +HOP_LENGTH = 64 +N_MELS = 64 + +# ======== Status mapping ======== + +CLASS_TO_STATUS = { + "Drought_Tomato": "Watering required", + "Drought_Tobacco": "Watering required", + "Control_Empty": "Normal / Empty", + "Control_Greenhouse": "Greenhouse noise / Normal", +} + +# ======== Kafka ======== + +ENABLE_ALERTS = os.getenv("ENABLE_ALERTS", "true").lower() == "true" +ALERT_TOPIC = os.getenv("ALERT_TOPIC", "alerts") +ALERT_TYPE = os.getenv("ALERT_TYPE", "plant_drought_detected") + +ALERT_IMAGE_URL = os.getenv("ALERT_IMAGE_URL", "") +ALERT_VOD = os.getenv("ALERT_VOD", "") +ALERT_HLS = os.getenv("ALERT_HLS", "") + +KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP", "kafka:9092") +KAFKA_CLIENT_ID = os.getenv("KAFKA_CLIENT_ID", "plant-stress-producer") + +# ======== Load Model & Scaler ======== + +model_path = os.path.join(MODEL_DIR, "ultrasonic_plant_cnn.keras") +scaler_path = os.path.join(MODEL_DIR, "scaler_params.npz") +le_path = os.path.join(MODEL_DIR, "label_encoder.pkl") + +try: + import keras + MODEL = keras.saving.load_model(model_path, compile=False) +except Exception: + MODEL = tf.keras.models.load_model(model_path, compile=False) + +sc = np.load(scaler_path) +SCALER_MEAN = sc["mean"] +SCALER_SCALE = sc["scale"] + +with open(le_path, "rb") as f: + LABEL_ENCODER = pickle.load(f) + +# ======== Filename Regex ======== + +FILENAME_RE = re.compile( + r"(?P[^/_]+)_(?P\d{8})T(?P\d{2})(?P\d{2})(?P\d{2})Z\.wav$", + re.IGNORECASE, +) + +def _tz(): + return pytz.timezone(TIMEZONE) + +def parse_from_name(key: str): + m = FILENAME_RE.search(key) + if not m: + return None, None + + sensor = m.group("sensor") + y = int(m.group("date")[0:4]) + mth = int(m.group("date")[4:6]) + d = int(m.group("date")[6:8]) + hh = int(m.group("hour")) + mm = int(m.group("minute")) + ss = int(m.group("second")) + + dt_local = _tz().localize(dt.datetime(y, mth, d, hh, mm, ss)) + return sensor, dt_local + +# ======== NEW FUNCTION ======== +# שליפה מה־DB של area/lat/lon מתוך metadata או devices + +def get_meta_for_alert(conn, device_id: str, file_name: str): + area = None + lat = None + lon = None + + short = file_name.split("/")[-1] + + # שליפה מ־sounds_ultra_metadata + try: + with conn.cursor() as cur: + cur.execute(""" + SELECT + (gis_origin->>'area') AS area, + (gis_origin->>'latitude')::double precision AS lat, + (gis_origin->>'longitude')::double precision AS lon + FROM public.sounds_ultra_metadata + WHERE device_id = %s + AND file_name = %s + ORDER BY created_at DESC + LIMIT 1; + """, (device_id, short)) + row = cur.fetchone() + if row: + area, lat, lon = row + except Exception as e: + print(f"[meta] error from metadata: {e}") + + # אם חסר משהו → devices + if area is None or lat is None or lon is None: + try: + with conn.cursor() as cur: + cur.execute(""" + SELECT owner, location_lat, location_lon + FROM public.devices + WHERE device_id = %s + LIMIT 1; + """, (device_id,)) + row = cur.fetchone() + if row: + owner, dlat, dlon = row + if area is None: + area = owner + if lat is None: + lat = dlat + if lon is None: + lon = dlon + except Exception as e: + print(f"[meta] error from devices: {e}") + + # אם עדיין None → fallback + if area is None: + area = DEFAULT_AREA + if lat is None: + lat = float(DEFAULT_LAT) + if lon is None: + lon = float(DEFAULT_LON) + + return area, lat, lon +# ======== Kafka Producer (dual impl) ======== + +KAFKA_SECURITY_PROTOCOL = os.getenv("KAFKA_SECURITY_PROTOCOL", "").strip() +KAFKA_SASL_MECHANISM = os.getenv("KAFKA_SASL_MECHANISM", "").strip() +KAFKA_SASL_USERNAME = os.getenv("KAFKA_SASL_USERNAME", "").strip() +KAFKA_SASL_PASSWORD = os.getenv("KAFKA_SASL_PASSWORD", "").strip() +KAFKA_SSL_CA = os.getenv("KAFKA_SSL_CA", "").strip() +KAFKA_SSL_CERT = os.getenv("KAFKA_SSL_CERT", "").strip() +KAFKA_SSL_KEY = os.getenv("KAFKA_SSL_KEY", "").strip() + + +class _KafkaProducer: + def __init__(self): + self.impl = None + self.mode = None + self._init_producer() + + def _init_producer(self): + if not ENABLE_ALERTS: + return + + # Try confluent-kafka + try: + from confluent_kafka import Producer + + conf: dict[str, object] = { + "bootstrap.servers": KAFKA_BOOTSTRAP, + "client.id": KAFKA_CLIENT_ID, + } + if KAFKA_SECURITY_PROTOCOL: + conf["security.protocol"] = KAFKA_SECURITY_PROTOCOL + if KAFKA_SASL_MECHANISM: + conf["sasl.mechanisms"] = KAFKA_SASL_MECHANISM + if KAFKA_SASL_USERNAME: + conf["sasl.username"] = KAFKA_SASL_USERNAME + if KAFKA_SASL_PASSWORD: + conf["sasl.password"] = KAFKA_SASL_PASSWORD + if KAFKA_SSL_CA: + conf["ssl.ca.location"] = KAFKA_SSL_CA + if KAFKA_SSL_CERT: + conf["ssl.certificate.location"] = KAFKA_SSL_CERT + if KAFKA_SSL_KEY: + conf["ssl.key.location"] = KAFKA_SSL_KEY + + self.impl = Producer(conf) + self.mode = "confluent" + print("[Kafka] Using confluent-kafka Producer") + return + except Exception as e: + print(f"[Kafka] confluent-kafka unavailable: {e}") + + # Fallback: kafka-python + try: + from kafka import KafkaProducer + + kwargs: dict[str, object] = { + "bootstrap_servers": KAFKA_BOOTSTRAP, + "client_id": KAFKA_CLIENT_ID, + "value_serializer": lambda v: json.dumps(v).encode("utf-8"), + "linger_ms": 10, + "acks": "all", + } + if KAFKA_SECURITY_PROTOCOL: + kwargs["security_protocol"] = KAFKA_SECURITY_PROTOCOL + if KAFKA_SASL_MECHANISM: + kwargs["sasl_mechanism"] = KAFKA_SASL_MECHANISM + if KAFKA_SASL_USERNAME and KAFKA_SASL_PASSWORD: + kwargs["sasl_plain_username"] = KAFKA_SASL_USERNAME + kwargs["sasl_plain_password"] = KAFKA_SASL_PASSWORD + if KAFKA_SSL_CA: + kwargs["ssl_cafile"] = KAFKA_SSL_CA + if KAFKA_SSL_CERT: + kwargs["ssl_certfile"] = KAFKA_SSL_CERT + if KAFKA_SSL_KEY: + kwargs["ssl_keyfile"] = KAFKA_SSL_KEY + + self.impl = KafkaProducer(**kwargs) + self.mode = "kafka-python" + print("[Kafka] Using kafka-python Producer") + except Exception as e2: + print(f"[Kafka] kafka-python unavailable: {e2}") + self.impl = None + self.mode = None + + def send(self, topic: str, value: dict) -> bool: + if not ENABLE_ALERTS or self.impl is None: + return False + + if self.mode == "confluent": + try: + self.impl.produce(topic, value=json.dumps(value).encode("utf-8")) + self.impl.poll(0) + return True + except Exception as e: + print(f"[Kafka] produce error (confluent): {e}") + return False + elif self.mode == "kafka-python": + try: + fut = self.impl.send(topic, value=value) + fut.get(timeout=5) + return True + except Exception as e: + print(f"[Kafka] produce error (kafka-python): {e}") + return False + + return False + + def flush(self): + try: + if self.mode == "confluent" and self.impl is not None: + self.impl.flush(5) + elif self.mode == "kafka-python" and self.impl is not None: + self.impl.flush() + except Exception: + pass + + +KAFKA_PRODUCER = _KafkaProducer() + + +def _today_date() -> dt.date: + if PROCESS_DATE: + return dt.datetime.strptime(PROCESS_DATE, "%Y-%m-%d").date() + return dt.datetime.now(_tz()).date() + + +# ======== MinIO listing & audio loading ======== + +def list_minio_wavs_for_date( + client: Minio, bucket: str, prefix: str, the_date: dt.date +): + selected = [] + for obj in client.list_objects(bucket, prefix=prefix, recursive=True): + key = obj.object_name + if not key.lower().endswith(".wav"): + continue + + sensor, rec_local = parse_from_name(key) + if rec_local is not None and rec_local.date() == the_date: + selected.append((obj, sensor, rec_local)) + continue + + lm_local = obj.last_modified.astimezone(_tz()) + if lm_local.date() == the_date: + selected.append((obj, sensor, lm_local)) + + return selected + + +def load_audio_from_minio(client: Minio, bucket: str, key: str): + resp = client.get_object(bucket, key) + try: + data = resp.read() + finally: + resp.close() + resp.release_conn() + + bio = BytesIO(data) + audio, sr = sf.read(bio, dtype="float32", always_2d=False) + + if isinstance(audio, np.ndarray) and audio.ndim == 2: + audio = audio.mean(axis=1) + + if sr != SAMPLE_RATE: + audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE) + sr = SAMPLE_RATE + + if len(audio) > N_SAMPLES: + start = (len(audio) - N_SAMPLES) // 2 + audio = audio[start:start + N_SAMPLES] + elif len(audio) < N_SAMPLES: + pad = N_SAMPLES - len(audio) + audio = np.pad(audio, (0, pad), mode="constant") + + return audio.astype(np.float32), sr + + +# ======== Feature extraction ======== + +def extract_ultrasonic_features(audio: np.ndarray, sr: int): + feats: list[float] = [] + + feats.extend( + [ + float(np.mean(audio)), + float(np.std(audio)), + float(np.max(audio)), + float(np.min(audio)), + float(np.var(audio)), + float(np.median(audio)), + ] + ) + + zcr = librosa.feature.zero_crossing_rate(audio, hop_length=HOP_LENGTH)[0] + feats.extend( + [ + float(np.mean(zcr)), + float(np.std(zcr)), + float(np.max(zcr)), + ] + ) + + fft = np.abs(np.fft.fft(audio))[: len(audio) // 2] + feats.extend( + [ + float(np.mean(fft)), + float(np.std(fft)), + float(np.max(fft)), + float(np.argmax(fft)), + ] + ) + + try: + sc = librosa.feature.spectral_centroid( + y=audio, sr=sr, hop_length=HOP_LENGTH + )[0] + ro = librosa.feature.spectral_rolloff( + y=audio, sr=sr, hop_length=HOP_LENGTH + )[0] + feats.extend([float(np.mean(sc)), float(np.mean(ro))]) + except Exception: + feats.extend([0.0, 0.0]) + + rms = librosa.feature.rms(y=audio, hop_length=HOP_LENGTH)[0] + feats.extend( + [ + float(np.mean(rms)), + float(np.std(rms)), + ] + ) + + return np.array(feats, dtype=np.float32) + + +def create_spectrogram_features(audio: np.ndarray, sr: int): + mel = librosa.feature.melspectrogram( + y=audio, + sr=sr, + n_fft=N_FFT, + hop_length=HOP_LENGTH, + n_mels=N_MELS, + fmax=sr // 2, + ) + mel_db = librosa.power_to_db(mel, ref=np.max) + mel_norm = (mel_db - mel_db.min()) / (mel_db.max() - mel_db.min() + 1e-8) + return mel_norm.astype(np.float32) + + +def normalize_features(x: np.ndarray): + return (x - SCALER_MEAN) / SCALER_SCALE + +# ======== DB: Tables & Inserts ======== + +def ensure_predictions_table(conn): + """ + Create ultrasonic_plant_predictions (no device_id/recording_time). + """ + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS ultrasonic_plant_predictions ( + id BIGSERIAL PRIMARY KEY, + file TEXT, + predicted_class TEXT, + confidence DOUBLE PRECISION, + watering_status TEXT, + status TEXT, + prediction_time TIMESTAMPTZ DEFAULT now() + ); + """) + cur.execute( + "CREATE INDEX IF NOT EXISTS idx_upp_pred_time ON ultrasonic_plant_predictions (prediction_time DESC);" + ) + cur.execute( + "CREATE INDEX IF NOT EXISTS idx_upp_class ON ultrasonic_plant_predictions (predicted_class);" + ) + conn.commit() + + +def ensure_alerts_table(conn): + """ + Create alerts table + updated_at trigger + """ + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS alerts ( + alert_id TEXT PRIMARY KEY, + alert_type TEXT, + device_id TEXT, + started_at TIMESTAMPTZ, + ended_at TIMESTAMPTZ, + confidence DOUBLE PRECISION, + area TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + severity INT DEFAULT 1, + image_url TEXT, + vod TEXT, + hls TEXT, + ack BOOLEAN DEFAULT FALSE, + meta JSONB, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now() + ); + """) + + # Trigger for updated_at + cur.execute(""" + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'set_updated_at') THEN + CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $f$ + BEGIN + NEW.updated_at = now(); + RETURN NEW; + END; + $f$ LANGUAGE plpgsql; + END IF; + END$$; + """) + + cur.execute(""" + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_alerts_updated_at') THEN + CREATE TRIGGER trg_alerts_updated_at + BEFORE UPDATE ON alerts + FOR EACH ROW + EXECUTE PROCEDURE set_updated_at(); + END IF; + END$$; + """) + + conn.commit() + + +def insert_prediction_rows(conn, rows): + """ + rows: List of tuples (file, predicted_class, confidence, + watering_status, status, prediction_time) + """ + sql = """ + INSERT INTO ultrasonic_plant_predictions + (file, predicted_class, confidence, watering_status, status, prediction_time) + VALUES %s + """ + with conn.cursor() as cur: + psycopg2.extras.execute_values(cur, sql, rows, page_size=500) + conn.commit() + + +def insert_alert_row(conn, alert: dict, + started_at_dt: dt.datetime, + ended_at_dt: dt.datetime | None = None, + ack: bool = False): + from psycopg2.extras import Json + + sql = """ + INSERT INTO alerts ( + alert_id, alert_type, device_id, started_at, ended_at, + confidence, area, lat, lon, severity, + image_url, vod, hls, ack, meta + ) + VALUES ( + %(alert_id)s, %(alert_type)s, %(device_id)s, + %(started_at)s, %(ended_at)s, + %(confidence)s, %(area)s, %(lat)s, %(lon)s, %(severity)s, + %(image_url)s, %(vod)s, %(hls)s, %(ack)s, %(meta)s + ) + ON CONFLICT (alert_id) + DO UPDATE SET updated_at = now() + """ + + params = { + "alert_id": alert["alert_id"], + "alert_type": alert.get("alert_type"), + "device_id": alert.get("device_id"), + "started_at": started_at_dt, + "ended_at": ended_at_dt, + "confidence": alert.get("confidence"), + "area": alert.get("area"), + "lat": alert.get("lat"), + "lon": alert.get("lon"), + "severity": alert.get("severity"), + "image_url": alert.get("image_url"), + "vod": alert.get("vod"), + "hls": alert.get("hls"), + "ack": ack, + "meta": Json(alert.get("meta", {})), + } + + with conn.cursor() as cur: + cur.execute(sql, params) + conn.commit() + + +# ======== Severity ======== + +def _severity_from_confidence(conf: float) -> int: + if conf >= 0.95: return 5 + if conf >= 0.90: return 4 + if conf >= 0.80: return 3 + if conf >= 0.70: return 2 + return 1 + + +def _iso_utc(dt_aware): + return dt_aware.astimezone(pytz.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + + +# ======== Build Alert Payload (NOW USING DB META) ======== + +def build_alert_payload( + alert_type: str, + device_id: str, + started_at_utc: dt.datetime, + confidence: float, + s3url: str, + area: str | None, + lat: float | None, + lon: float | None, + image_url: str = "", + vod: str = "", + hls: str = "", + extra_meta: dict | None = None, +) -> dict: + + # Backups only if missing + final_area = area if area else DEFAULT_AREA + try: + final_lat = float(lat) if lat is not None else float(DEFAULT_LAT) + except: + final_lat = float(DEFAULT_LAT) + + try: + final_lon = float(lon) if lon is not None else float(DEFAULT_LON) + except: + final_lon = float(DEFAULT_LON) + + image_f = image_url if image_url else DEFAULT_IMAGE_URL + vod_f = vod if vod else DEFAULT_VOD + hls_f = hls if hls else DEFAULT_HLS + + payload = { + "alert_id": str(uuid.uuid4()), + "alert_type": alert_type, + "device_id": device_id, + "started_at": _iso_utc(started_at_utc), + "confidence": round(confidence, 6), + "severity": _severity_from_confidence(confidence), + "area": final_area, + "lat": final_lat, + "lon": final_lon, + "image_url": image_f, + "vod": vod_f, + "hls": hls_f, + "meta": { + "source": "ultrasonic_plant_classifier", + "file": s3url, + }, + } + + if extra_meta: + payload["meta"].update(extra_meta) + + return payload + +# ======== MAIN PROCESSING LOOP ======== + +def main(): + the_date = _today_date() + print(f"[i] Processing MinIO objects for date={the_date} (TZ={TIMEZONE})") + + # MinIO client + client = Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS, + secret_key=MINIO_SECRET, + secure=MINIO_SECURE + ) + + # List WAV files + objs = list_minio_wavs_for_date(client, MINIO_BUCKET, MINIO_PREFIX, the_date) + + if not objs: + print("[i] No WAV files for this date. Exiting.") + return 0 + + # DB + try: + conn = psycopg2.connect(POSTGRES_DSN) + ensure_alerts_table(conn) + ensure_predictions_table(conn) + except Exception as e: + print(f"[!] Postgres connection error: {e}") + return 2 + + batch = [] + ok = 0 + fail = 0 + t0 = time.time() + + # Process each file + for (obj, sensor, rec_local_dt) in objs: + key = obj.object_name + s3url = f"s3://{MINIO_BUCKET}/{key}" + + try: + # If parsing didn't get sensor -> extract from file name + if sensor is None: + sensor, _ = parse_from_name(key) + if sensor is None: + sensor = key.split("/")[-1].split("_")[0] + + # ===== Load audio ===== + audio, sr = load_audio_from_minio(client, MINIO_BUCKET, key) + + # ===== Compute features ===== + feats = extract_ultrasonic_features(audio, sr) + spec = create_spectrogram_features(audio, sr) + feats_norm = normalize_features(feats) + + feats_batch = feats_norm[np.newaxis, :] + spec_batch = spec[np.newaxis, ..., np.newaxis] + + # ===== Prediction ===== + probs = MODEL.predict([feats_batch, spec_batch], verbose=0)[0] + idx = int(np.argmax(probs)) + pred_class = LABEL_ENCODER.classes_[idx] + conf = float(probs[idx]) + + watering_status = CLASS_TO_STATUS.get(pred_class, "Undefined") + if conf < CONFIDENCE_THRESHOLD: + watering_status = f"{watering_status} (Uncertain)" + + # Save prediction row + batch.append(( + s3url, + pred_class, + conf, + watering_status, + "Success", + dt.datetime.utcnow() + )) + ok += 1 + + print(f"[OK] {s3url} -> {pred_class} ({conf:.3f}) [{sensor}]") + + # ===== ALERTS ===== + if ENABLE_ALERTS and pred_class in ("Drought_Tomato", "Drought_Tobacco"): + # normalize time to UTC + rec_utc = ( + rec_local_dt.astimezone(pytz.UTC) + if rec_local_dt.tzinfo + else pytz.UTC.localize(rec_local_dt) + ) + + # Load area/lat/lon from DB metadata or devices + area_db, lat_db, lon_db = get_meta_for_alert(conn, str(sensor), key) + + alert = build_alert_payload( + alert_type=ALERT_TYPE, + device_id=str(sensor), + started_at_utc=rec_utc, + confidence=conf, + s3url=s3url, + area=area_db, # ← from DB + lat=lat_db, # ← from DB + lon=lon_db, # ← from DB + image_url=ALERT_IMAGE_URL, + vod=ALERT_VOD, + hls=ALERT_HLS, + extra_meta={ + "predicted_class": pred_class, + "watering_status": watering_status, + "model_dir": MODEL_DIR, + "sample_rate": SAMPLE_RATE, + "n_fft": N_FFT, + "n_mels": N_MELS + } + ) + + try: + insert_alert_row(conn, alert, started_at_dt=rec_utc, ended_at_dt=None, ack=False) + print(f"[Alert][DB] inserted alert_id={alert['alert_id']} device={sensor} severity={alert['severity']}") + except Exception as e: + print(f"[Alert][DB] insert failed: {e}") + + # Kafka send (optional) + try: + sent = KAFKA_PRODUCER.send(ALERT_TOPIC, alert) + if sent: + print(f"[Alert][Kafka] sent alert_id={alert['alert_id']}") + else: + print(f"[Alert][Kafka] FAILED sending alert") + except Exception as e: + print(f"[Alert][Kafka] send exception: {e}") + + except Exception as e: + fail += 1 + print(f"[ERR] {s3url} -> {e}") + batch.append(( + s3url, + "", + None, + "", + f"Error: {e}", + dt.datetime.utcnow() + )) + + # Insert prediction batch + try: + if batch: + insert_prediction_rows(conn, batch) + print(f"[i] Inserted {len(batch)} prediction rows.") + except Exception as e: + print(f"[!] Error inserting predictions: {e}") + return 3 + + # Close DB + try: + conn.close() + except: + pass + + # Flush Kafka + try: + KAFKA_PRODUCER.flush() + except: + pass + + dt_sec = time.time() - t0 + print(f"Done. processed={len(objs)} ok={ok} fail={fail} elapsed_sec={dt_sec:.1f}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/AgCloud/services/ripeness-baseline/README.md b/AgCloud/services/ripeness-baseline/README.md new file mode 100644 index 000000000..223eb7f65 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/README.md @@ -0,0 +1,148 @@ +# 🍏 Fruit Ripeness Baseline System + +## Overview + +This service implements a **baseline system for tracking fruit ripeness** using simple color and texture heuristics. +It processes sample fruit images, generates weekly rollups, and stores results in PostgreSQL. +Quality flags are added to highlight questionable or low-confidence cases. + +--- + +## Features + +- **Ripeness Heuristics:** + Uses HSV color space and texture analysis (Laplacian variance) to estimate fruit ripeness (Unripe, Ripe, Overripe). +- **Image Processing Pipeline:** + Processes sample fruit images, extracts relevant features, and classifies ripeness. +- **Weekly Rollups:** + Aggregates ripeness data weekly and exports results to a relational database. +- **Quality Flags:** + Flags low-confidence or outlier results for further review. + +--- + +## Project Structure + +- `src/` – Python pipeline (heuristics, quality flags, DB inserts) +- `deploy/sql/` – Database schema, rollup queries, and view definitions +- `README.md` – Documentation + +--- + +## How to Run + +### 1. Prerequisites + +- **Docker & Docker Compose** installed +- Access to the shared **RelDB stack** (`db` service must be running) +- Clone this repository into your workspace + +### 2. Build the Docker Image + +```bash +docker build --no-cache -t ripeness-baseline:local -f deploy/Dockerfile . +``` + +### 3. Run the Pipeline + +Connect the container to the same Docker network as the database (`reldb_airnet`): + +```powershell +docker run --rm --network agcloud-net ` + -e PGHOST=db -e PGPORT=5432 -e PGDATABASE=missions_db ` + -e PGUSER=missions_user -e PGPASSWORD="pg123" ` + -e LOOKBACK_DAYS=7 ` + -e MINIO_URL="http://minio-hot-1:9000" ` + -e MINIO_ACCESS_KEY=minioadmin -e MINIO_SECRET_KEY=minioadmin123 ` + ripeness-baseline:local +``` + +At the end, you will see: +``` +Done. Inserted detections and updated weekly_rollups. +``` + +--- + +## Useful Commands (PowerShell) + +### View Results Per Image (Full History) + +```powershell +docker exec -e PGPASSWORD="pg123" -it db ` + psql -U missions_user -d missions_db -c ` +"SELECT d.detection_id, + i.source_path, + d.ripeness, + d.quality_flags, + to_char(d.created_at,'YYYY-MM-DD HH24:MI:SS') AS created_at + FROM detections d + JOIN images i USING (image_id) + ORDER BY d.created_at DESC;" +``` + +### View Weekly Summaries (View Table) + +```powershell +docker exec -e PGPASSWORD="pg123" -it db ` + psql -U missions_user -d missions_db -c ` +"SELECT * FROM v_weekly_ripeness ORDER BY iso_year, iso_week;" +``` + +### Run Evaluation Script for All Fruit Types... + +```powershell +# Apple +python .\src\evaluate_minio.py --minio-url http://127.0.0.1:9001 ` + --access-key minioadmin --secret-key minioadmin123 ` + --bucket imagery --prefix apple/test --fruit apple ` + --thresholds-json .\thresholds.apple.json ` + --outdir .\eval\apple_test + +# Banana +python .\src\evaluate_minio.py --minio-url http://127.0.0.1:9001 ` + --access-key minioadmin --secret-key minioadmin123 ` + --bucket imagery --prefix banana/test --fruit banana ` + --thresholds-json .\thresholds.banana.json ` + --outdir .\eval\banana_test + +# Orange +python .\src\evaluate_minio.py --minio-url http://127.0.0.1:9001 ` + --access-key minioadmin --secret-key minioadmin123 ` + --bucket imagery --prefix orange/test --fruit orange ` + --thresholds-json .\thresholds.orange.json ` + --outdir .\eval\orange_test + + +``` +--- + +## How It Works + +1. **Define Heuristics:** + Ripeness is determined by average hue, saturation, value, texture variance, and brown/dark pixel ratio. + +2. **Process Images:** + Images are segmented and analyzed. Features are extracted and ripeness is classified. + +3. **Weekly Rollups:** + Results are aggregated by week and stored in the database. + +4. **Quality Flags:** + Results with low confidence or outlier features are flagged for review. + +--- + +## Notes + +- You can change the fruit type (`FRUIT_TYPE`) or green leaf flag threshold (`GREEN_LEAF_FLAG_THR`) as needed. +- Weekly rollups aggregate detections by `(fruit_type, iso_year, iso_week)`. +- Each result includes quality flags (bitmask) and ripeness classification. +- Weekly summaries are updated automatically at the end of each run. + +--- + +## Additional Info + +- Thresholds and heuristics can be adjusted in `src/heuristics.py` for different fruit types or conditions. +- For advanced deployment, see `deploy/k8s-cronjob.yaml` for Kubernetes. \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/deploy/Dockerfile b/AgCloud/services/ripeness-baseline/deploy/Dockerfile new file mode 100644 index 000000000..b1009b496 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/deploy/Dockerfile @@ -0,0 +1,36 @@ +FROM python:3.11-slim + +WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates openssl libgl1 && \ + rm -rf /var/lib/apt/lists/* + +COPY deploy/certs/ /usr/local/share/ca-certificates/ + +RUN set -eux; \ + for f in /usr/local/share/ca-certificates/*.cer; do \ + [ -f "$f" ] && openssl x509 -inform der -in "$f" -out "${f%.cer}.crt" && rm -f "$f" || true; \ + done; \ + update-ca-certificates + +RUN printf "[global]\n\ +cert = /etc/ssl/certs/ca-certificates.crt\n\ +index-url = https://pypi.org/simple\n\ +trusted-host =\n\ + pypi.org\n\ + files.pythonhosted.org\n" > /etc/pip.conf + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +COPY requirements.txt . +RUN python -m pip install --upgrade pip setuptools wheel && \ + pip install --no-cache-dir -r requirements.txt + +COPY src/ /app/src/ +COPY deploy/sql/ /app/deploy/sql/ + +CMD ["python","-u","/app/src/main.py"] diff --git a/AgCloud/services/ripeness-baseline/deploy/k8s-cronjob.yaml b/AgCloud/services/ripeness-baseline/deploy/k8s-cronjob.yaml new file mode 100644 index 000000000..511078bb6 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/deploy/k8s-cronjob.yaml @@ -0,0 +1,61 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: ripeness-rollup + namespace: vectordb +spec: + schedule: "0 3 * * 1" + timeZone: "Asia/Jerusalem" + jobTemplate: + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: ripeness + image: ripeness-baseline:local + imagePullPolicy: Never + env: + - name: PGHOST + value: "192.168.1.124" + - name: PGPORT + value: "5432" + - name: PGDATABASE + value: "missions_db" + - name: PGUSER + value: "missions_user" + - name: PGPASSWORD + value: "Missions!ChangeMe123" + - name: PGSSLMODE + value: "disable" + + - name: MINIO_URL + value: "http://REPLACE_WITH_WINDOWS_IP:9001" + - name: MINIO_BUCKET + value: "imagery" + - name: MINIO_ACCESS_KEY + value: "minioadmin" + - name: MINIO_SECRET_KEY + value: "minioadmin123" + + - name: FRUIT_LIST + value: "apple,banana,pear" + - name: DEFAULT_PREFIX + value: "test" + + command: ["/bin/sh","-lc"] + args: + - | + set -eu + fruits=$(printf "%s" "$FRUIT_LIST" | tr ',' ' ') + for f in $fruits; do + export FRUIT_TYPE="$f" + if [ -n "${DEFAULT_PREFIX:-}" ]; then + export MINIO_PREFIX="${f}/${DEFAULT_PREFIX}" + else + export MINIO_PREFIX="${f}/test" + fi + echo "=== Running $FRUIT_TYPE (prefix=$MINIO_PREFIX) ===" + python -u /app/src/main.py + done + diff --git a/AgCloud/services/ripeness-baseline/deploy/sql/01_schema.sql b/AgCloud/services/ripeness-baseline/deploy/sql/01_schema.sql new file mode 100644 index 000000000..c3233c594 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/deploy/sql/01_schema.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS images ( + image_id BIGSERIAL PRIMARY KEY, + fruit_type VARCHAR(50) NOT NULL, + captured_at TIMESTAMP NOT NULL, + source_path VARCHAR(512) NOT NULL +); + +CREATE TABLE IF NOT EXISTS detections ( + detection_id BIGSERIAL PRIMARY KEY, + image_id BIGINT NOT NULL REFERENCES images(image_id), + mean_h REAL, mean_s REAL, mean_v REAL, + laplacian_var REAL, + brown_ratio REAL, + ripeness VARCHAR(20) NOT NULL, + quality_flags INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS weekly_rollups ( + fruit_type VARCHAR(50) NOT NULL, + iso_year INT NOT NULL, + iso_week INT NOT NULL, + cnt_total INT NOT NULL, + cnt_unripe INT NOT NULL, + cnt_ripe INT NOT NULL, + cnt_overripe INT NOT NULL, + pct_flagged REAL NOT NULL, + mean_brown REAL, + PRIMARY KEY (fruit_type, iso_year, iso_week) +); + + + + diff --git a/AgCloud/services/ripeness-baseline/deploy/sql/02_rollup_view.sql b/AgCloud/services/ripeness-baseline/deploy/sql/02_rollup_view.sql new file mode 100644 index 000000000..1d22e814e --- /dev/null +++ b/AgCloud/services/ripeness-baseline/deploy/sql/02_rollup_view.sql @@ -0,0 +1,15 @@ +CREATE OR REPLACE VIEW v_weekly_ripeness AS +SELECT + fruit_type, + iso_year, + iso_week, + cnt_total, + cnt_unripe, + cnt_ripe, + cnt_overripe, + ROUND( (100.0 * cnt_ripe / NULLIF(cnt_total,0))::numeric, 1 ) AS pct_ripe, + ROUND( (100.0 * cnt_unripe / NULLIF(cnt_total,0))::numeric, 1 ) AS pct_unripe, + ROUND( (100.0 * cnt_overripe / NULLIF(cnt_total,0))::numeric, 1 ) AS pct_overripe, + ROUND( (100.0 * pct_flagged)::numeric, 1 ) AS pct_flagged, + mean_brown +FROM weekly_rollups; diff --git a/AgCloud/services/ripeness-baseline/deploy/sql/03_weekly_upsert.sql b/AgCloud/services/ripeness-baseline/deploy/sql/03_weekly_upsert.sql new file mode 100644 index 000000000..7805f69af --- /dev/null +++ b/AgCloud/services/ripeness-baseline/deploy/sql/03_weekly_upsert.sql @@ -0,0 +1,27 @@ +WITH base AS ( + SELECT + i.fruit_type, + to_char(i.captured_at, 'IYYY')::INT AS iso_year, -- ISO year + to_char(i.captured_at, 'IW')::INT AS iso_week, -- ISO week number (01-53) + COUNT(*) AS cnt_total, + SUM((d.ripeness='Unripe')::INT) AS cnt_unripe, + SUM((d.ripeness='Ripe')::INT) AS cnt_ripe, + SUM((d.ripeness='Overripe')::INT) AS cnt_overripe, + AVG((d.quality_flags>0)::INT)::REAL AS pct_flagged, + AVG(d.brown_ratio)::REAL AS mean_brown + FROM images i + JOIN detections d USING(image_id) + WHERE i.captured_at >= NOW() - INTERVAL '7 days' + GROUP BY 1,2,3 +) +INSERT INTO weekly_rollups + (fruit_type, iso_year, iso_week, cnt_total, cnt_unripe, cnt_ripe, cnt_overripe, pct_flagged, mean_brown) +SELECT fruit_type, iso_year, iso_week, cnt_total, cnt_unripe, cnt_ripe, cnt_overripe, pct_flagged, mean_brown +FROM base +ON CONFLICT (fruit_type, iso_year, iso_week) DO UPDATE +SET cnt_total = EXCLUDED.cnt_total, + cnt_unripe = EXCLUDED.cnt_unripe, + cnt_ripe = EXCLUDED.cnt_ripe, + cnt_overripe= EXCLUDED.cnt_overripe, + pct_flagged = EXCLUDED.pct_flagged, + mean_brown = EXCLUDED.mean_brown; diff --git a/AgCloud/services/ripeness-baseline/docker-compose.yml b/AgCloud/services/ripeness-baseline/docker-compose.yml new file mode 100644 index 000000000..d10ea5cc1 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/docker-compose.yml @@ -0,0 +1,27 @@ +version: "3.8" + +services: + ripeness: + build: + context: . + dockerfile: deploy/Dockerfile + container_name: ripeness-baseline + env_file: ./.env + environment: + PGHOST: host.docker.internal + PGPORT: 5432 + PGDATABASE: missions_db + PGUSER: missions_user + PGPASSWORD: pg123 + SAMPLES_DIR: /app/samples + LOOKBACK_DAYS: ${LOOKBACK_DAYS:-7} + volumes: + - ./samples:/app/samples:ro + - ./eval:/app/eval + - ./thresholds.apple.json:/app/thresholds.apple.json:ro + - ./thresholds.banana.json:/app/thresholds.banana.json:ro + - ./thresholds.orange.json:/app/thresholds.orange.json:ro + restart: "no" + command: ["python", "-u", "/app/src/main.py"] + extra_hosts: + - "host.docker.internal:host-gateway" diff --git a/AgCloud/services/ripeness-baseline/eval/apple_argmax/metrics.json b/AgCloud/services/ripeness-baseline/eval/apple_argmax/metrics.json new file mode 100644 index 000000000..02a660f9b --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/apple_argmax/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.4564740307242136, + "report": { + "unripe": { + "precision": 0.6162162162162163, + "recall": 0.30727762803234504, + "f1-score": 0.41007194244604317, + "support": 371.0 + }, + "ripe": { + "precision": 0.346, + "recall": 0.8759493670886076, + "f1-score": 0.4960573476702509, + "support": 395.0 + }, + "overripe": { + "precision": 0.9010989010989011, + "recall": 0.27287853577371046, + "f1-score": 0.41890166028097064, + "support": 601.0 + }, + "accuracy": 0.4564740307242136, + "macro avg": { + "precision": 0.6211050391050391, + "recall": 0.48536851029822103, + "f1-score": 0.44167698346575496, + "support": 1367.0 + }, + "weighted avg": { + "precision": 0.6633845323896531, + "recall": 0.4564740307242136, + "f1-score": 0.43879973723927906, + "support": 1367.0 + } + }, + "confusion_matrix": [ + [ + 114, + 250, + 7 + ], + [ + 38, + 346, + 11 + ], + [ + 33, + 404, + 164 + ] + ], + "samples": 1367, + "prefix": "apple/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/apple_argmax/per_image.csv b/AgCloud/services/ripeness-baseline/eval/apple_argmax/per_image.csv new file mode 100644 index 000000000..28c174189 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/apple_argmax/per_image.csv @@ -0,0 +1,1368 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +apple/test/overripe/Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,ripe,0.0,0.7545539140701294,0.2454460710287094 +apple/test/overripe/Screen Shot 2018-06-07 at 2.16.54 PM.png,overripe,ripe,0.0,0.8749153017997742,0.12508472800254822 +apple/test/overripe/Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,ripe,0.11619272083044052,0.7942677140235901,0.2057322859764099 +apple/test/overripe/Screen Shot 2018-06-07 at 2.20.04 PM.png,overripe,ripe,0.0,0.7978011965751648,0.20219877362251282 +apple/test/overripe/Screen Shot 2018-06-07 at 2.20.34 PM.png,overripe,ripe,0.0,0.7535206079483032,0.24647939205169678 +apple/test/overripe/Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.426668256521225,0.5733317136764526 +apple/test/overripe/Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,ripe,0.0,0.6973071098327637,0.30269286036491394 +apple/test/overripe/Screen Shot 2018-06-07 at 2.34.49 PM.png,overripe,ripe,0.0,0.9671603441238403,0.032839640974998474 +apple/test/overripe/Screen Shot 2018-06-07 at 2.35.38 PM.png,overripe,overripe,0.0,0.418703556060791,0.581296443939209 +apple/test/overripe/Screen Shot 2018-06-07 at 2.37.53 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,ripe,0.0,0.824446439743042,0.1755535751581192 +apple/test/overripe/Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,ripe,0.0,0.5464485287666321,0.4535514712333679 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.48430266976356506,0.5156973004341125 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.35 PM.png,overripe,overripe,0.0,0.4442184567451477,0.5557815432548523 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.44 PM.png,overripe,ripe,0.0,0.7955507040023804,0.20444931089878082 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.53 PM.png,overripe,ripe,0.0,0.7676244378089905,0.23237557709217072 +apple/test/overripe/Screen Shot 2018-06-07 at 2.41.14 PM.png,overripe,unripe,0.9428315758705139,0.05716843530535698,0.22037874162197113 +apple/test/overripe/Screen Shot 2018-06-07 at 2.42.37 PM.png,overripe,unripe,0.6122614741325378,0.38773855566978455,0.3390651047229767 +apple/test/overripe/Screen Shot 2018-06-07 at 2.43.48 PM.png,overripe,overripe,0.0,0.49070119857788086,0.5092988014221191 +apple/test/overripe/Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,unripe,0.848612368106842,0.15138761699199677,0.23628957569599152 +apple/test/overripe/Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,unripe,0.776132345199585,0.22386763989925385,0.059576887637376785 +apple/test/overripe/Screen Shot 2018-06-07 at 2.47.50 PM.png,overripe,ripe,0.0904172882437706,0.6515184044837952,0.34848159551620483 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.08 PM.png,overripe,overripe,0.0,0.4012884795665741,0.5987115502357483 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.16 PM.png,overripe,overripe,0.0,0.40443962812423706,0.5955603718757629 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.37 PM.png,overripe,ripe,0.0,0.6409111022949219,0.3590888977050781 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.40827131271362305,0.591728687286377 +apple/test/overripe/Screen Shot 2018-06-07 at 2.54.41 PM.png,overripe,overripe,0.0,0.4024764895439148,0.5975235104560852 +apple/test/overripe/Screen Shot 2018-06-07 at 2.56.47 PM.png,overripe,ripe,0.1045917421579361,0.8770256042480469,0.12297438830137253 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.04 PM.png,overripe,overripe,0.0,0.401737779378891,0.5982621908187866 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.30 PM.png,overripe,ripe,0.0,0.5290892124176025,0.47091078758239746 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.48512938618659973,0.5148705840110779 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.09 PM.png,overripe,ripe,0.0,0.5103872418403625,0.48961275815963745 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.13 PM.png,overripe,ripe,0.0,0.5074149966239929,0.49258503317832947 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,unripe,0.7817977666854858,0.21820221841335297,0.3606202006340027 +apple/test/overripe/Screen Shot 2018-06-07 at 3.00.25 PM.png,overripe,ripe,0.0,0.7927172780036926,0.20728273689746857 +apple/test/overripe/Screen Shot 2018-06-07 at 3.01.38 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 3.02.37 PM.png,overripe,ripe,0.011743295006453991,0.5223132967948914,0.47768667340278625 +apple/test/overripe/Screen Shot 2018-06-07 at 3.03.02 PM.png,overripe,ripe,0.22627583146095276,0.5952256917953491,0.4047743082046509 +apple/test/overripe/Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,ripe,0.0,0.6625497937202454,0.33745017647743225 +apple/test/overripe/Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,ripe,0.0,0.5296342968940735,0.4703657031059265 +apple/test/overripe/Screen Shot 2018-06-07 at 3.04.10 PM.png,overripe,ripe,0.0,0.5789469480514526,0.42105305194854736 +apple/test/overripe/Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.42003384232521057,0.579966127872467 +apple/test/overripe/Screen Shot 2018-06-07 at 3.06.30 PM.png,overripe,overripe,0.0,0.45272931456565857,0.5472707152366638 +apple/test/overripe/Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4085937440395355,0.5914062261581421 +apple/test/overripe/Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.48230722546577454,0.5176928043365479 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,unripe,0.8088228702545166,0.1911771148443222,0.15510158240795135 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.17 PM.png,overripe,overripe,0.0,0.47224000096321106,0.5277600288391113 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.24 PM.png,overripe,ripe,0.0,0.5476417541503906,0.4523582458496094 +apple/test/overripe/Screen Shot 2018-06-08 at 2.26.09 PM.png,overripe,ripe,0.0,0.8933544158935547,0.10664559155702591 +apple/test/overripe/Screen Shot 2018-06-08 at 2.26.55 PM.png,overripe,ripe,0.0,0.574117124080658,0.42588290572166443 +apple/test/overripe/Screen Shot 2018-06-08 at 2.28.07 PM.png,overripe,ripe,0.0,0.8121988773345947,0.18780113756656647 +apple/test/overripe/Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.46738964319229126,0.5326103568077087 +apple/test/overripe/Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,ripe,0.0,0.6598382592201233,0.3401617407798767 +apple/test/overripe/Screen Shot 2018-06-08 at 2.30.45 PM.png,overripe,ripe,0.33995410799980164,0.660045862197876,0.23476363718509674 +apple/test/overripe/Screen Shot 2018-06-08 at 2.30.51 PM.png,overripe,ripe,0.0,0.5387656092643738,0.4612343907356262 +apple/test/overripe/Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,ripe,0.0,0.5897746682167053,0.4102253019809723 +apple/test/overripe/Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.46794599294662476,0.5320540070533752 +apple/test/overripe/Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.4029044210910797,0.5970955491065979 +apple/test/overripe/Screen Shot 2018-06-08 at 2.37.03 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-08 at 2.39.51 PM.png,overripe,ripe,0.0,0.8298832178115845,0.17011681199073792 +apple/test/overripe/Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,ripe,0.0,0.6761451959609985,0.32385480403900146 +apple/test/overripe/Screen Shot 2018-06-08 at 2.42.58 PM.png,overripe,ripe,0.0,0.8416235446929932,0.15837645530700684 +apple/test/overripe/Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.47808218002319336,0.5219178199768066 +apple/test/overripe/Screen Shot 2018-06-08 at 2.45.44 PM.png,overripe,ripe,0.0,0.5183576345443726,0.48164236545562744 +apple/test/overripe/Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.40096020698547363,0.5990397930145264 +apple/test/overripe/Screen Shot 2018-06-08 at 2.46.25 PM.png,overripe,overripe,0.0,0.4709493815898895,0.5290505886077881 +apple/test/overripe/Screen Shot 2018-06-08 at 2.48.09 PM.png,overripe,ripe,0.0,0.5950728058815002,0.40492719411849976 +apple/test/overripe/Screen Shot 2018-06-08 at 2.48.43 PM.png,overripe,overripe,0.0,0.4915332794189453,0.5084667205810547 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.14 PM.png,overripe,overripe,0.0,0.49049779772758484,0.5095021724700928 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.25 PM.png,overripe,overripe,0.0,0.452358216047287,0.5476418137550354 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,ripe,0.0,0.7317582368850708,0.2682417631149292 +apple/test/overripe/Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,ripe,0.0,0.7264909148216248,0.27350908517837524 +apple/test/overripe/Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.4019714891910553,0.5980285406112671 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9683067798614502,0.03169320896267891 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.16.41 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,ripe,0.11404334753751755,0.7940260767936707,0.20597393810749054 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,ripe,0.0,0.7950757145881653,0.20492427051067352 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.20.29 PM.png,overripe,overripe,0.0,0.4887690246105194,0.5112309455871582 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4269527494907379,0.5730472803115845 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,ripe,0.0,0.6983228325843811,0.3016771376132965 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.25.26 PM.png,overripe,ripe,0.0,0.5882577300071716,0.41174226999282837 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,ripe,0.0,0.7394410967826843,0.2605589032173157 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.31.59 PM.png,overripe,ripe,0.0,0.8452314138412476,0.15476860105991364 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.34.18 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.35.21 PM.png,overripe,ripe,0.0920504778623581,0.7074757218360901,0.2925243079662323 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,ripe,0.0,0.5425712466239929,0.4574287533760071 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.37.01 PM.png,overripe,overripe,0.0,0.4025708734989166,0.597429096698761 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.37.53 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.38 PM.png,overripe,ripe,0.0,0.5322250723838806,0.467774897813797 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.49 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,ripe,0.0,0.5448402166366577,0.4551598131656647 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.00 PM.png,overripe,ripe,0.0,0.9829521775245667,0.01704784668982029 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,ripe,0.0,0.7483903765678406,0.25160959362983704 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,ripe,0.0,0.9080915451049805,0.09190845489501953 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.41.07 PM.png,overripe,unripe,0.6295706629753113,0.37042930722236633,0.11884623020887375 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.42.25 PM.png,overripe,ripe,0.0,0.9281436204910278,0.07185638695955276 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.42.58 PM.png,overripe,ripe,0.4167487919330597,0.5832511782646179,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,ripe,0.4796822965145111,0.5203177332878113,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,ripe,0.0,0.7026820182800293,0.2973180115222931 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,unripe,0.6610499620437622,0.3389500677585602,0.26377883553504944 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.51 PM.png,overripe,ripe,0.44681820273399353,0.5531817674636841,0.31737861037254333 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.45.09 PM.png,overripe,overripe,0.0,0.40100690722465515,0.5989930629730225 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,unripe,0.5487484335899353,0.4512515962123871,0.035074446350336075 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.47.35 PM.png,overripe,ripe,0.23336206376552582,0.6845308542251587,0.3154691457748413 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,ripe,0.0,0.7797549962997437,0.22024501860141754 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.54.49 PM.png,overripe,ripe,0.0,0.9360671043395996,0.06393289566040039 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.55.27 PM.png,overripe,ripe,0.0,0.9280431270599365,0.07195686548948288 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,ripe,0.0,0.5827373266220093,0.4172627031803131 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.57.26 PM.png,overripe,ripe,0.0,0.6259711384773254,0.37402886152267456 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.48935338854789734,0.5106465816497803 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.4701474905014038,0.5298525094985962 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,unripe,0.8215239644050598,0.17847603559494019,0.3506662845611572 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.59.52 PM.png,overripe,ripe,0.0,0.9186636209487915,0.0813363566994667 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,ripe,0.13751615583896637,0.5227135419845581,0.4772864878177643 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,ripe,0.29777848720550537,0.6839640736579895,0.3160359263420105 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,ripe,0.0,0.8444986343383789,0.1555013805627823 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.02.51 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,ripe,0.0,0.8569608926773071,0.14303909242153168 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,ripe,0.0,0.8946223258972168,0.10537765920162201 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.21.33 PM.png,overripe,ripe,0.0,0.8995535969734192,0.10044639557600021 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.23.48 PM.png,overripe,ripe,0.0,0.8978826999664307,0.10211727023124695 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.31 PM.png,overripe,ripe,0.0,0.6632320284843445,0.3367679715156555 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.37 PM.png,overripe,ripe,0.0,0.7515414357185364,0.24845854938030243 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.4607156217098236,0.539284348487854 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,ripe,0.0,0.955458402633667,0.0445416085422039 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,ripe,0.0,0.9383007287979126,0.061699278652668 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,ripe,0.0,0.6913803815841675,0.3086196482181549 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.30.31 PM.png,overripe,ripe,0.0,0.5019571185112,0.49804291129112244 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.32.10 PM.png,overripe,overripe,0.0,0.4012617766857147,0.5987382531166077 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.33.04 PM.png,overripe,overripe,0.0,0.41342276334762573,0.5865772366523743 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.467265784740448,0.532734215259552 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,unripe,0.5365432500839233,0.46345674991607666,0.10404040664434433 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.938224732875824,0.061775270849466324 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.37.24 PM.png,overripe,ripe,0.0,0.5152910351753235,0.4847089648246765 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.40.30 PM.png,overripe,ripe,0.0,0.5359194874763489,0.4640805125236511 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.46.44 PM.png,overripe,ripe,0.0,0.5454541444778442,0.45454588532447815 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.47.54 PM.png,overripe,overripe,0.0,0.43485355377197266,0.5651464462280273 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.48.15 PM.png,overripe,ripe,0.0,0.6856085062026978,0.31439152359962463 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.50.22 PM.png,overripe,ripe,0.0,0.547704815864563,0.452295184135437 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4461022615432739,0.5538977384567261 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,ripe,0.10958212614059448,0.7939813137054443,0.20601867139339447 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.20.46 PM.png,overripe,ripe,0.0,0.632486879825592,0.36751309037208557 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.23.02 PM.png,overripe,ripe,0.0,0.9823130965232849,0.017686905339360237 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,ripe,0.0,0.9632701873779297,0.036729805171489716 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,ripe,0.08686870336532593,0.5565102100372314,0.44348978996276855 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.37.11 PM.png,overripe,ripe,0.1818079799413681,0.8181920051574707,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.38.49 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,ripe,0.0,0.9184742569923401,0.08152573555707932 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.40.55 PM.png,overripe,ripe,0.3069930672645569,0.6930069327354431,0.21854381263256073 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.42.25 PM.png,overripe,ripe,0.0,0.9505967497825623,0.04940324276685715 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.43.26 PM.png,overripe,ripe,0.0,0.9173040390014648,0.08269596099853516 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.45.44 PM.png,overripe,overripe,0.0,0.4051132798194885,0.5948867201805115 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.46.04 PM.png,overripe,overripe,0.0,0.40443769097328186,0.5955622792243958 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.50.31 PM.png,overripe,ripe,0.0,0.5686571002006531,0.4313429296016693 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.00 PM.png,overripe,ripe,0.0,0.6491890549659729,0.3508109450340271 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.30 PM.png,overripe,ripe,0.0,0.8324998021125793,0.16750018298625946 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,ripe,0.0,0.8485864400863647,0.15141353011131287 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.53.20 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.53.33 PM.png,overripe,ripe,0.0,0.9859579205513,0.014042074792087078 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,ripe,0.051665663719177246,0.7535101771354675,0.24648980796337128 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.55.27 PM.png,overripe,ripe,0.0,0.9271116852760315,0.07288828492164612 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.55.52 PM.png,overripe,overripe,0.0,0.4622405171394348,0.5377594828605652 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,ripe,0.0,0.6168363094329834,0.3831636905670166 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.16 PM.png,overripe,overripe,0.0,0.42570480704307556,0.574295163154602 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,ripe,0.0,0.5843639969825745,0.4156360328197479 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.57.17 PM.png,overripe,overripe,0.0,0.47497987747192383,0.5250201225280762 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.4943205714225769,0.5056794285774231 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.4746500551700592,0.5253499746322632 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.59.09 PM.png,overripe,ripe,0.0,0.5180833339691162,0.4819166660308838 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.00.56 PM.png,overripe,ripe,0.0,0.5245475769042969,0.4754524230957031 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.01.38 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,ripe,0.24870027601718903,0.6739242076873779,0.32607579231262207 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,ripe,0.0,0.8391834497451782,0.16081656515598297 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.35 PM.png,overripe,ripe,0.0,0.6223083734512329,0.3776916265487671 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.41 PM.png,overripe,overripe,0.0,0.46168506145477295,0.538314938545227 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.58 PM.png,overripe,ripe,0.036418166011571884,0.9070121049880981,0.09298786520957947 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.40247201919555664,0.5975279808044434 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,ripe,0.29755207896232605,0.6635130047798157,0.3364869952201843 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.53 PM.png,overripe,ripe,0.0,0.6730727553367615,0.3269272446632385 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,ripe,0.0,0.9001975059509277,0.09980249404907227 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.17.25 PM.png,overripe,ripe,0.0,0.9011508226394653,0.09884918481111526 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4086782932281494,0.5913217067718506 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.24.09 PM.png,overripe,ripe,0.18525253236293793,0.6760610342025757,0.3239389657974243 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,ripe,0.0,0.9540358185768127,0.045964207500219345 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.27.15 PM.png,overripe,ripe,0.0,0.6870434880256653,0.3129565119743347 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,ripe,0.0,0.6871688365936279,0.31283116340637207 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,ripe,0.0,0.7327337861061096,0.2672662138938904 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,ripe,0.0,0.5886185765266418,0.41138145327568054 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.31.23 PM.png,overripe,ripe,0.0,0.6882686018943787,0.31173139810562134 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.32.42 PM.png,overripe,overripe,0.0,0.4039195477962494,0.596080482006073 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.40276506543159485,0.5972349643707275 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.35.03 PM.png,overripe,overripe,0.0,0.43099215626716614,0.5690078139305115 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.40850457549095154,0.5914954543113708 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.40.13 PM.png,overripe,ripe,0.0,0.8202294111251831,0.1797705888748169 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.44.54 PM.png,overripe,ripe,0.0,0.7589153051376343,0.24108467996120453 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.45.58 PM.png,overripe,ripe,0.0,0.8278490900993347,0.17215090990066528 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.47.30 PM.png,overripe,ripe,0.0,0.8913791179656982,0.10862090438604355 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.49.40 PM.png,overripe,overripe,0.0,0.48092421889305115,0.5190757513046265 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.22 PM.png,overripe,ripe,0.0,0.5438693761825562,0.45613065361976624 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4907514750957489,0.5092484951019287 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,ripe,0.0,0.712379515171051,0.287620484828949 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.52.43 PM.png,overripe,ripe,0.0,0.9390769600868225,0.060923025012016296 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,ripe,0.0,0.7551569938659668,0.244842991232872 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.967350423336029,0.03264958783984184 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.7259302735328674,0.27406972646713257,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.20.56 PM.png,overripe,overripe,0.0,0.44718441367149353,0.5528156161308289 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.23.02 PM.png,overripe,ripe,0.0,0.9833796620368958,0.016620351001620293 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,ripe,0.0,0.93206387758255,0.06793615221977234 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.24.35 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.24.59 PM.png,overripe,ripe,0.0,0.9695764183998108,0.03042358160018921 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.31.59 PM.png,overripe,ripe,0.0,0.8330531120300293,0.16694685816764832 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.34.36 PM.png,overripe,ripe,0.0,0.5974652171134949,0.4025348126888275 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.34.49 PM.png,overripe,ripe,0.0,0.9630469679832458,0.03695303946733475 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.37.20 PM.png,overripe,unripe,0.567131519317627,0.43286851048469543,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,ripe,0.0,0.8424391150474548,0.15756088495254517 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.39.26 PM.png,overripe,overripe,0.0,0.4078674018383026,0.5921326279640198 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.40.13 PM.png,overripe,overripe,0.0,0.4262941777706146,0.573705792427063 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,ripe,0.0,0.7483091950416565,0.2516908049583435 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.42.18 PM.png,overripe,ripe,0.3821904957294464,0.617809534072876,0.21698372066020966 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,ripe,0.48610448837280273,0.5138955116271973,0.2841413617134094 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4066220819950104,0.5933778882026672 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.47.27 PM.png,overripe,ripe,0.0,0.7057316303253174,0.2942683696746826 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,ripe,0.0,0.7147276401519775,0.28527238965034485 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.412349134683609,0.5876508355140686 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,ripe,0.0,0.8451262712478638,0.15487371385097504 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.53.20 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.56.34 PM.png,overripe,ripe,0.0,0.9582783579826355,0.0417216531932354 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,ripe,0.0,0.5849897265434265,0.4150102436542511 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.57.42 PM.png,overripe,overripe,0.0,0.4153805077075958,0.5846194624900818 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.57.49 PM.png,overripe,overripe,0.0,0.4582032561302185,0.5417967438697815 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.58.30 PM.png,overripe,ripe,0.0,0.5339092016220093,0.4660908281803131 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.59.38 PM.png,overripe,unripe,0.5518174171447754,0.4481825530529022,0.21782192587852478 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.59.52 PM.png,overripe,ripe,0.0,0.9076318144798279,0.09236818552017212 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,ripe,0.24669331312179565,0.5514664053916931,0.4485335946083069 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.33 PM.png,overripe,ripe,0.0,0.873406708240509,0.12659327685832977 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.40 PM.png,overripe,unripe,0.7814487814903259,0.21855123341083527,0.05215732008218765 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,ripe,0.0,0.5409248471260071,0.4590751528739929 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,ripe,0.2155858874320984,0.668299674987793,0.33170029520988464 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.02.18 PM.png,overripe,ripe,0.0,0.8997437953948975,0.10025618225336075 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.02.37 PM.png,overripe,overripe,0.0,0.4911389648914337,0.5088610053062439 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.03.02 PM.png,overripe,ripe,0.14312675595283508,0.6304293870925903,0.36957064270973206 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.04.24 PM.png,overripe,ripe,0.0,0.5080414414405823,0.4919585585594177 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.29 PM.png,overripe,ripe,0.0,0.7345165014266968,0.26548346877098083 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.40143489837646484,0.5985651016235352 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,ripe,0.2712413966655731,0.6604287028312683,0.3395713269710541 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,ripe,0.0,0.9013089537620544,0.09869106113910675 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4090251326560974,0.5909748673439026 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,ripe,0.0,0.5750827193260193,0.4249172806739807 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.44137096405029297,0.558629035949707 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,ripe,0.0,0.6560968160629272,0.34390321373939514 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.28.42 PM.png,overripe,unripe,0.7839246988296509,0.21607527136802673,0.14804627001285553 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.29.33 PM.png,overripe,ripe,0.0,0.7246161103248596,0.2753838896751404 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.30.51 PM.png,overripe,ripe,0.0,0.536897599697113,0.4631023705005646 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.31.16 PM.png,overripe,overripe,0.0,0.40968266129493713,0.5903173089027405 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,ripe,0.0,0.5343549251556396,0.46564507484436035 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.4676794707775116,0.532320499420166 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.34.42 PM.png,overripe,overripe,0.0,0.414518266916275,0.5854817628860474 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.35.03 PM.png,overripe,overripe,0.0,0.4312627613544464,0.568737268447876 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,ripe,0.0,0.9288630485534668,0.0711369588971138 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,ripe,0.46940869092941284,0.5305913090705872,0.09870533645153046 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.937780499458313,0.06221947818994522 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,ripe,0.0,0.8245388865470886,0.17546111345291138 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.37.19 PM.png,overripe,ripe,0.0,0.8321002125740051,0.16789978742599487 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,ripe,0.0,0.8466804027557373,0.1533195823431015 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.39.26 PM.png,overripe,ripe,0.0,0.5598364472389221,0.44016358256340027 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.41.39 PM.png,overripe,ripe,0.0,0.6395807266235352,0.36041927337646484 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.41.44 PM.png,overripe,ripe,0.0,0.693849503993988,0.30615052580833435 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.06 PM.png,overripe,ripe,0.0007034925511106849,0.7578920722007751,0.24210794270038605 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,ripe,0.0,0.6898362636566162,0.3101637363433838 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.38 PM.png,overripe,ripe,0.0,0.624383807182312,0.3756162226200104 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.52 PM.png,overripe,ripe,0.0,0.9736606478691101,0.026339346542954445 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.43.29 PM.png,overripe,ripe,0.0,0.7659586668014526,0.23404133319854736 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.44.13 PM.png,overripe,overripe,0.0,0.421697199344635,0.578302800655365 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.46.36 PM.png,overripe,overripe,0.0,0.40004780888557434,0.599952220916748 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.46.50 PM.png,overripe,overripe,0.0,0.4016577899456024,0.59834223985672 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.47.03 PM.png,overripe,overripe,0.0,0.40506136417388916,0.5949386358261108 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.47.30 PM.png,overripe,ripe,0.0,0.8886337876319885,0.11136619746685028 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.50.14 PM.png,overripe,overripe,0.0,0.485358864068985,0.5146411061286926 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.17.25 PM.png,overripe,ripe,0.0,0.8494974970817566,0.1505025029182434 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.20.46 PM.png,overripe,ripe,0.0,0.6329468488693237,0.36705315113067627 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,ripe,0.0,0.9032963514328003,0.09670363366603851 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.23.51 PM.png,overripe,ripe,0.0,0.840143620967865,0.159856379032135 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,ripe,0.0641099065542221,0.5555886626243591,0.44441133737564087 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.20 PM.png,overripe,unripe,0.5653679966926575,0.43463197350502014,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.32 PM.png,overripe,overripe,0.0,0.4483490586280823,0.5516509413719177 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,ripe,0.0,0.8418064713478088,0.15819349884986877 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,ripe,0.0,0.7490416765213013,0.25095832347869873 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.41.14 PM.png,overripe,unripe,0.6164188385009766,0.38358113169670105,0.18586483597755432 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.43.26 PM.png,overripe,ripe,0.0,0.9171419739723206,0.08285801112651825 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.43.54 PM.png,overripe,ripe,0.0,0.9392583966255188,0.06074158102273941 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4066635072231293,0.5933365225791931 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.40962210297584534,0.5903778672218323 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.47.13 PM.png,overripe,ripe,0.3001384139060974,0.6998615860939026,0.25409209728240967 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.47.20 PM.png,overripe,ripe,0.10661923885345459,0.7540392875671387,0.24596072733402252 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,ripe,0.0,0.8481209874153137,0.15187904238700867 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,ripe,0.0,0.608056902885437,0.391943097114563 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.56.47 PM.png,overripe,ripe,0.10662014782428741,0.8801792860031128,0.11982069164514542 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.49593791365623474,0.5040620565414429 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.00.17 PM.png,overripe,ripe,0.0,0.8968648910522461,0.1031351238489151 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,ripe,0.214319109916687,0.6683759093284607,0.3316240608692169 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.03.38 PM.png,overripe,ripe,0.02575046196579933,0.8483583927154541,0.1516416370868683 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.4902566075325012,0.5097433924674988 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.04.35 PM.png,overripe,ripe,0.0,0.6225723624229431,0.3774276375770569 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,ripe,0.0,0.8565447926521301,0.14345519244670868 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,ripe,0.28264111280441284,0.6614633798599243,0.3385366201400757 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.58 PM.png,overripe,ripe,0.0,0.7993506789207458,0.20064933598041534 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,unripe,0.7317115664482117,0.26828843355178833,0.1482333093881607 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,ripe,0.0,0.6563419699668884,0.3436580002307892 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.26.34 PM.png,overripe,ripe,0.0,0.578015148639679,0.42198485136032104 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,ripe,0.0,0.6566964387893677,0.3433035910129547 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,ripe,0.0,0.7308566570281982,0.26914334297180176 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.34.51 PM.png,overripe,overripe,0.0,0.41533204913139343,0.5846679210662842 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,ripe,0.0,0.9264752268791199,0.07352479547262192 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.18 PM.png,overripe,ripe,0.16023299098014832,0.7780072093009949,0.22199276089668274 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.9373990297317505,0.0626010000705719 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,ripe,0.0,0.8292298316955566,0.17077018320560455 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.37.19 PM.png,overripe,ripe,0.0,0.8322131037712097,0.1677868813276291 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.37.24 PM.png,overripe,ripe,0.0,0.5228731036186218,0.4771268963813782 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,ripe,0.0,0.8472363352775574,0.15276364982128143 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.38.47 PM.png,overripe,overripe,0.0,0.46913638710975647,0.5308635830879211 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.09 PM.png,overripe,ripe,0.0,0.9557929635047913,0.04420701786875725 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.30 PM.png,overripe,ripe,0.0,0.5363069772720337,0.4636929929256439 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.56 PM.png,overripe,overripe,0.0,0.4049716293811798,0.5950284004211426 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.41.16 PM.png,overripe,overripe,0.0,0.4824846386909485,0.5175153613090515 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,ripe,0.0,0.6909096240997314,0.30909040570259094 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.4775329828262329,0.5224670171737671 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.44.13 PM.png,overripe,overripe,0.0,0.4211606979370117,0.5788393020629883 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.45.05 PM.png,overripe,ripe,0.0,0.7363434433937073,0.2636565864086151 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.47.09 PM.png,overripe,overripe,0.0,0.40773338079452515,0.5922666192054749 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,ripe,0.20428843796253204,0.6009278893470764,0.3990720808506012 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.50.38 PM.png,overripe,ripe,0.0,0.6114640235900879,0.3885359764099121 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,ripe,0.0,0.7080582976341248,0.29194167256355286 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.51.09 PM.png,overripe,overripe,0.0,0.43311309814453125,0.5668869018554688 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.52.05 PM.png,overripe,ripe,0.0,0.6538779735565186,0.34612199664115906 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.52.20 PM.png,overripe,overripe,0.0,0.40624698996543884,0.5937530398368835 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,ripe,0.0,0.7550516128540039,0.24494841694831848 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.7599319219589233,0.24006809294223785,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4281546473503113,0.5718453526496887 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,ripe,0.0,0.9122918248176575,0.08770819008350372 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,ripe,0.0,0.926906168460846,0.07309383898973465 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,ripe,0.0,0.9623044729232788,0.03769555315375328 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,ripe,0.0,0.7388497591018677,0.2611502707004547 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.34.18 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,ripe,0.0,0.5387851595878601,0.4612148106098175 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.37.11 PM.png,overripe,ripe,0.18910716474056244,0.8108928203582764,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.38.38 PM.png,overripe,ripe,0.0,0.5328457951545715,0.4671541750431061 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.35 PM.png,overripe,overripe,0.0,0.4501740634441376,0.54982590675354 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.44 PM.png,overripe,ripe,0.0,0.8042840361595154,0.1957159787416458 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.53 PM.png,overripe,ripe,0.0,0.7690977454185486,0.23090225458145142 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.40.00 PM.png,overripe,ripe,0.0,0.9824215173721313,0.0175784844905138 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.41.23 PM.png,overripe,ripe,0.3441731631755829,0.6558268070220947,0.23758749663829803 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.41.32 PM.png,overripe,ripe,0.0,0.9459468126296997,0.054053205996751785 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.42.58 PM.png,overripe,ripe,0.4022534191608429,0.5977466106414795,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.43.54 PM.png,overripe,ripe,0.0,0.9566748738288879,0.04332513362169266 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,ripe,0.0,0.7034739851951599,0.2965260446071625 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.44.59 PM.png,overripe,overripe,0.0,0.49054017663002014,0.5094598531723022 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4066976308822632,0.5933023691177368 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.46.12 PM.png,overripe,overripe,0.0,0.4086005985736847,0.5913994312286377 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.47.13 PM.png,overripe,ripe,0.3048560619354248,0.6951439380645752,0.2489471733570099 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.50.31 PM.png,overripe,ripe,0.0,0.5756474733352661,0.4243524968624115 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.52.00 PM.png,overripe,ripe,0.0,0.6447377800941467,0.35526221990585327 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.4075360894203186,0.5924639105796814 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.54.08 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,ripe,0.0,0.7431972026824951,0.2568027973175049 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,ripe,0.0,0.9022953510284424,0.09770466387271881 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,ripe,0.0,0.8444233536720276,0.1555766463279724 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.02.24 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,ripe,0.3315379023551941,0.6684620976448059,0.3293115794658661 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,ripe,0.0,0.8890763521194458,0.11092362552881241 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.06.51 PM.png,overripe,ripe,0.0,0.5711193084716797,0.4288806617259979 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.25.43 PM.png,overripe,ripe,0.0,0.6806471347808838,0.3193528652191162 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,ripe,0.0,0.6561587452888489,0.34384122490882874 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,ripe,0.0,0.6910083889961243,0.3089916408061981 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,ripe,0.0,0.6582351326942444,0.341764897108078 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,ripe,0.0,0.5899176001548767,0.4100823998451233 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.16 PM.png,overripe,overripe,0.0,0.40940433740615845,0.5905956625938416 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,ripe,0.0,0.5353731513023376,0.46462681889533997 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.33.49 PM.png,overripe,ripe,0.0,0.8827162981033325,0.11728369444608688 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.34.05 PM.png,overripe,ripe,0.0,0.862308919429779,0.13769106566905975 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.4028860330581665,0.5971139669418335 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,ripe,0.0,0.8274161219596863,0.17258386313915253 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.37.13 PM.png,overripe,ripe,0.0,0.8851184844970703,0.1148814931511879 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.38.33 PM.png,overripe,ripe,0.0,0.6763423085212708,0.32365769147872925 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.4083316922187805,0.5916683077812195 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.13 PM.png,overripe,ripe,0.0,0.8216577768325806,0.17834220826625824 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.38 PM.png,overripe,overripe,0.0,0.4441024363040924,0.55589759349823 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.46 PM.png,overripe,overripe,0.0,0.40169283747673035,0.598307192325592 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.41.54 PM.png,overripe,overripe,0.0,0.4227246344089508,0.5772753357887268 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.42.38 PM.png,overripe,ripe,0.0,0.6231865882873535,0.3768134117126465 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.42.58 PM.png,overripe,ripe,0.0,0.8417767882347107,0.1582232415676117 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.43.29 PM.png,overripe,ripe,0.0,0.7498872876167297,0.25011271238327026 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.47870317101478577,0.5212968587875366 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.44.54 PM.png,overripe,ripe,0.0,0.7565832734107971,0.24341674149036407 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.4012910723686218,0.5987089276313782 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.09 PM.png,overripe,overripe,0.0,0.4076642394065857,0.5923357605934143 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.37 PM.png,overripe,overripe,0.0,0.4300783574581146,0.569921612739563 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,ripe,0.25358760356903076,0.6125152111053467,0.3874848186969757 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.54 PM.png,overripe,overripe,0.0,0.43532735109329224,0.5646726489067078 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.48.29 PM.png,overripe,ripe,0.15717950463294983,0.647361695766449,0.3526383340358734 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.49.27 PM.png,overripe,ripe,0.0,0.842556893825531,0.157443106174469 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.50.55 PM.png,overripe,ripe,0.0,0.5361279845237732,0.4638720154762268 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.40182405710220337,0.5981759428977966 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,ripe,0.0,0.7543689012527466,0.2456311285495758 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9663136005401611,0.03368639200925827 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.16.54 PM.png,overripe,ripe,0.0,0.8741502165794373,0.12584978342056274 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,ripe,0.10810915380716324,0.7941272258758545,0.20587274432182312 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.18.57 PM.png,overripe,ripe,0.0,0.6735761165618896,0.32642385363578796 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,ripe,0.0,0.792866587638855,0.20713339745998383 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,ripe,0.0,0.9250963926315308,0.07490359991788864 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.23.51 PM.png,overripe,ripe,0.0,0.7818387746810913,0.21816124022006989 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.33.47 PM.png,overripe,ripe,0.0,0.5160294771194458,0.4839704930782318 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,ripe,0.0,0.823380708694458,0.1766192764043808 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,ripe,0.0,0.8924233317375183,0.10757669061422348 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,ripe,0.0,0.5478559136390686,0.4521440863609314 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.4860113859176636,0.5139886140823364 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.40.13 PM.png,overripe,overripe,0.0,0.43759825825691223,0.5624017119407654 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.40.55 PM.png,overripe,ripe,0.4234825074672699,0.5765174627304077,0.20679369568824768 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.41.32 PM.png,overripe,ripe,0.0,0.934971272945404,0.06502871960401535 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,ripe,0.428395539522171,0.5716044902801514,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.43.48 PM.png,overripe,overripe,0.0,0.49207803606987,0.5079219937324524 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,ripe,0.0,0.6750780344009399,0.32492196559906006 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4087047278881073,0.5912952423095703 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.41281408071517944,0.5871859192848206 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,unripe,0.76760333776474,0.23239664733409882,0.06069063022732735 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.47.20 PM.png,overripe,ripe,0.10444310307502747,0.7809596657752991,0.21904034912586212 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,ripe,0.0,0.668408989906311,0.3315909802913666 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4140018820762634,0.5859981179237366 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.52.09 PM.png,overripe,ripe,0.0,0.8806607723236084,0.11933925002813339 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.52.30 PM.png,overripe,ripe,0.0,0.8620868921279907,0.13791309297084808 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.53.33 PM.png,overripe,ripe,0.0,0.9933769702911377,0.006623028311878443 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.41039180755615234,0.5896081924438477 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.54.08 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.54.41 PM.png,overripe,overripe,0.0,0.4045596122741699,0.5954403877258301 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,ripe,0.0,0.501268208026886,0.4987318217754364 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,ripe,0.0,0.8970405459403992,0.10295943915843964 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,unripe,0.7745237350463867,0.22547627985477448,0.36066946387290955 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.00.40 PM.png,overripe,unripe,0.8140609860420227,0.1859390288591385,0.057126980274915695 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,ripe,0.0,0.54913729429245,0.45086270570755005 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.03.12 PM.png,overripe,overripe,0.0,0.40924781560897827,0.5907521843910217 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,ripe,0.0,0.5310338139533997,0.46896615624427795 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.04.47 PM.png,overripe,overripe,0.0,0.4800986647605896,0.5199013352394104 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.05.05 PM.png,overripe,ripe,0.0,0.7839930057525635,0.21600699424743652 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,ripe,0.0,0.8970319032669067,0.10296806693077087 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.06.30 PM.png,overripe,overripe,0.0,0.45455679297447205,0.5454431772232056 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.17.25 PM.png,overripe,ripe,0.0,0.8978942036628723,0.10210580378770828 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.21.33 PM.png,overripe,ripe,0.0,0.8923357725143433,0.10766425728797913 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,ripe,0.0,0.5685101747512817,0.4314897954463959 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.24.37 PM.png,overripe,ripe,0.0,0.7581122517585754,0.24188776314258575 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,ripe,0.0,0.6560887098312378,0.3439112603664398 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.27.15 PM.png,overripe,ripe,0.0,0.6853026151657104,0.31469735503196716 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.27.44 PM.png,overripe,ripe,0.0,0.8546470999717712,0.14535290002822876 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4671778678894043,0.5328221321105957 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,ripe,0.0,0.6600779891014099,0.3399220108985901 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,ripe,0.0,0.7159018516540527,0.28409814834594727 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.30.57 PM.png,overripe,ripe,0.0,0.6855120658874512,0.31448793411254883 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.31.08 PM.png,overripe,overripe,0.0,0.46906575560569763,0.53093421459198 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.40459132194519043,0.5954086780548096 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,ripe,0.0,0.9230174422264099,0.07698256522417068 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.35.37 PM.png,overripe,ripe,0.0,0.5560615658760071,0.44393840432167053 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.36.01 PM.png,overripe,overripe,0.0,0.4511595368385315,0.5488404631614685 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.36.23 PM.png,overripe,ripe,0.4390043616294861,0.5609956383705139,0.09869155287742615 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.39.26 PM.png,overripe,ripe,0.0,0.5676177740097046,0.4323822259902954 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.45.50 PM.png,overripe,ripe,0.0,0.5061455368995667,0.49385446310043335 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.46.36 PM.png,overripe,overripe,0.0,0.4019896984100342,0.5980103015899658 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,unripe,0.524116575717926,0.4758833944797516,0.322188138961792 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4475027322769165,0.5524972677230835 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.51.09 PM.png,overripe,overripe,0.0,0.4356294274330139,0.5643705725669861 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,ripe,0.0,0.726266086101532,0.273733913898468 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9682204723358154,0.03177954629063606 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.16.41 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.17.15 PM.png,overripe,ripe,0.0,0.6220037937164307,0.3779962360858917 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.7047738432884216,0.29522618651390076,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,ripe,0.0,0.7940395474433899,0.2059604525566101 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.20.34 PM.png,overripe,ripe,0.0,0.7442492842674255,0.25575071573257446 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,ripe,0.0,0.9108261466026306,0.089173823595047 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,ripe,0.0,0.9762999415397644,0.023700030520558357 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.35.21 PM.png,overripe,ripe,0.12811650335788727,0.6971621513366699,0.3028378486633301 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.36.06 PM.png,overripe,ripe,0.11170769482851028,0.5649522542953491,0.4350477159023285 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.37.01 PM.png,overripe,overripe,0.0,0.4021989107131958,0.5978010892868042 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,ripe,0.0,0.8853477239608765,0.11465225368738174 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.449419766664505,0.5505802035331726 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.39.26 PM.png,overripe,overripe,0.0,0.4092854857444763,0.5907145142555237 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,ripe,0.0,0.8969487547874451,0.10305122286081314 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.44.26 PM.png,overripe,ripe,0.1159982979297638,0.5823452472686768,0.41765475273132324 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.44.59 PM.png,overripe,overripe,0.0,0.47979605197906494,0.5202039480209351 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,unripe,0.8022072315216064,0.19779276847839355,0.05516377091407776 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.47.27 PM.png,overripe,ripe,0.0,0.713334321975708,0.286665678024292 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.47.35 PM.png,overripe,ripe,0.19957226514816284,0.6870830059051514,0.31291699409484863 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.50.09 PM.png,overripe,overripe,0.0,0.4006073474884033,0.5993926525115967 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,ripe,0.0,0.7137728929519653,0.2862270772457123 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,ripe,0.0,0.7796455025672913,0.22035448253154755 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.54.49 PM.png,overripe,ripe,0.0,0.9377363920211792,0.06226362660527229 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,ripe,0.0,0.7107579708099365,0.2892419993877411 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.49930864572525024,0.5006913542747498 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,ripe,0.0,0.9017955660820007,0.09820446372032166 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.57.13 PM.png,overripe,overripe,0.39152562618255615,0.38196709752082825,0.6180329322814941 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.477611780166626,0.522388219833374 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.00.25 PM.png,overripe,ripe,0.0,0.7995904088020325,0.20040959119796753 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.02.02 PM.png,overripe,ripe,0.0,0.6949083209037781,0.30509164929389954 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.12 PM.png,overripe,overripe,0.0,0.4077388346195221,0.5922611355781555 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.21 PM.png,overripe,ripe,0.059224799275398254,0.6320396065711975,0.3679603934288025 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,ripe,0.0,0.6662530303001404,0.33374693989753723 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.58 PM.png,overripe,overripe,0.0,0.45804184675216675,0.5419581532478333 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.04.10 PM.png,overripe,ripe,0.0,0.5738403797149658,0.42615965008735657 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,ripe,0.0,0.8463939428329468,0.15360605716705322 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,ripe,0.0,0.8928197622299194,0.10718020796775818 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.11 PM.png,overripe,overripe,0.0,0.44270071387290955,0.5572992563247681 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,ripe,0.0,0.8948938846588135,0.10510610789060593 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,ripe,0.0,0.5553269982337952,0.44467297196388245 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,unripe,0.6546785235404968,0.3453214466571808,0.142344668507576 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.26.34 PM.png,overripe,ripe,0.08823717385530472,0.6058476567268372,0.39415237307548523 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,ripe,0.0,0.9600381851196289,0.0399618037045002 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.28.00 PM.png,overripe,ripe,0.0,0.7655057907104492,0.2344941943883896 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,ripe,0.0,0.9268267154693604,0.07317330688238144 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4627687335014343,0.5372312664985657 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.30.26 PM.png,overripe,overripe,0.0,0.4296092092990875,0.5703907608985901 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,ripe,0.0,0.5409867167472839,0.4590132534503937 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.34.42 PM.png,overripe,overripe,0.0,0.4157101809978485,0.5842898488044739 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.403055340051651,0.5969446301460266 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.36.23 PM.png,overripe,unripe,0.6771324872970581,0.3228675127029419,0.12590482831001282 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,ripe,0.0,0.8537853956222534,0.14621460437774658 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.41.44 PM.png,overripe,ripe,0.0,0.6924629807472229,0.3075370192527771 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.45.50 PM.png,overripe,ripe,0.0,0.5313133597373962,0.46868664026260376 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.40154924988746643,0.598450779914856 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.47.37 PM.png,overripe,overripe,0.0,0.423509418964386,0.576490581035614 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.00 PM.png,overripe,overripe,0.0,0.4798479378223419,0.5201520919799805 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.29 PM.png,overripe,ripe,0.29401230812072754,0.6650198698043823,0.3349801003932953 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.43 PM.png,overripe,ripe,0.0,0.5002697706222534,0.4997302293777466 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.49.27 PM.png,overripe,ripe,0.0,0.8377478122711182,0.16225217282772064 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4478205442428589,0.5521794557571411 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,ripe,0.0,0.7518887519836426,0.24811123311519623 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,ripe,0.0,0.7280323505401611,0.2719676196575165 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.40210866928100586,0.5978913307189941 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.15.50 PM.png,overripe,unripe,0.6475039720535278,0.3524959981441498,0.027730442583560944 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.967724084854126,0.032275937497615814 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.17.15 PM.png,overripe,ripe,0.0,0.6141247749328613,0.3858751952648163 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.20.04 PM.png,overripe,ripe,0.0,0.7978246808052063,0.2021753489971161 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.20.29 PM.png,overripe,overripe,0.0,0.48689594864845276,0.5131040811538696 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.42696475982666016,0.5730352401733398 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,ripe,0.0,0.6971606612205505,0.30283933877944946 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,ripe,0.0,0.7345425486564636,0.2654574513435364 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.33.47 PM.png,overripe,ripe,0.0,0.5145911574363708,0.48540884256362915 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,ripe,0.0,0.7920956611633301,0.20790430903434753 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.04 PM.png,overripe,ripe,0.0,0.9339483380317688,0.0660516768693924 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,ripe,0.0,0.8242465853691101,0.1757534146308899 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,ripe,0.0,0.8918978571891785,0.10810213536024094 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.4843032956123352,0.5156967043876648 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,ripe,0.44806912541389465,0.551930844783783,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.43.34 PM.png,overripe,overripe,0.0,0.43719637393951416,0.5628036260604858 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,unripe,0.848612368106842,0.15138761699199677,0.23628957569599152 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.45.18 PM.png,overripe,unripe,0.8801013827323914,0.11989860236644745,0.14219418168067932 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.45.35 PM.png,overripe,overripe,0.0,0.41379278898239136,0.5862072110176086 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.46.04 PM.png,overripe,overripe,0.0,0.4046403169631958,0.5953596830368042 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.4106054902076721,0.5893945097923279 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.47.01 PM.png,overripe,overripe,0.0,0.4008193612098694,0.5991806387901306 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4118858873844147,0.5881140828132629 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,unripe,0.7782055139541626,0.2217945009469986,0.36183157563209534 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,ripe,0.1962965428829193,0.5371797680854797,0.46282023191452026 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,ripe,0.0,0.5472190380096436,0.45278096199035645 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.21 PM.png,overripe,ripe,0.0,0.6150673031806946,0.3849326968193054 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,ripe,0.0,0.6624906659126282,0.3375093638896942 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.38 PM.png,overripe,ripe,0.0,0.853050172328949,0.1469498574733734 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.04.47 PM.png,overripe,overripe,0.0,0.4795546531677246,0.5204453468322754 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,ripe,0.0,0.567679226398468,0.43232080340385437 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.4821779131889343,0.5178220868110657 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.25.24 PM.png,overripe,ripe,0.0,0.5476964712142944,0.4523034989833832 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.26.09 PM.png,overripe,ripe,0.0,0.8940961956977844,0.10590382665395737 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,ripe,0.0,0.6556635499000549,0.34433645009994507 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,ripe,0.0,0.9323820471763611,0.0676179751753807 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.30.57 PM.png,overripe,ripe,0.0,0.6856135725975037,0.31438642740249634 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.33.28 PM.png,overripe,ripe,0.0,0.5987611413002014,0.4012388586997986 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.33.49 PM.png,overripe,ripe,0.0,0.8970608115196228,0.1029391661286354 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.46794700622558594,0.5320529937744141 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,unripe,0.6372930407524109,0.3627069890499115,0.10893480479717255 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.9374876022338867,0.06251242756843567 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.37.03 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.40807774662971497,0.5919222831726074 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.39.21 PM.png,overripe,ripe,0.2488100677728653,0.7511899471282959,0.02317122556269169 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.40.38 PM.png,overripe,overripe,0.0,0.44486579298973083,0.5551341772079468 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.40.46 PM.png,overripe,overripe,0.0,0.40149807929992676,0.5985019207000732 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.42.06 PM.png,overripe,ripe,0.0037744492292404175,0.7638024687767029,0.23619753122329712 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,ripe,0.0,0.6761281490325928,0.3238718509674072 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.46.50 PM.png,overripe,overripe,0.0,0.40154266357421875,0.5984573364257812 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.47.03 PM.png,overripe,overripe,0.0,0.4041697680950165,0.5958302617073059 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.48.00 PM.png,overripe,overripe,0.0,0.4795458912849426,0.5204541087150574 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.50.25 PM.png,overripe,overripe,0.0,0.45197761058807373,0.5480223894119263 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.51.43 PM.png,overripe,ripe,0.04209024831652641,0.6678847670555115,0.3321152329444885 +apple/test/ripe/Screen Shot 2018-06-08 at 4.59.44 PM.png,ripe,ripe,0.15061892569065094,0.8493810892105103,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.15 PM.png,ripe,ripe,0.11630692332983017,0.8836930990219116,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,0.9882926344871521,0.011707386001944542 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.4442790448665619,0.5557209253311157 +apple/test/ripe/Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,ripe,0.0963401049375534,0.9036598801612854,0.0010483769001439214 +apple/test/ripe/Screen Shot 2018-06-08 at 5.04.16 PM.png,ripe,ripe,0.26259636878967285,0.7374036312103271,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.04.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,ripe,0.2202623337507248,0.779737651348114,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.40046724677085876,0.5995327234268188 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.18 PM.png,ripe,ripe,0.0,0.9187333583831787,0.08126665651798248 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,ripe,0.16350586712360382,0.8364941477775574,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.8902803659439087,0.10971960425376892,0.029326602816581726 +apple/test/ripe/Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.17 PM.png,ripe,unripe,0.9623035192489624,0.0376964695751667,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.31 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.10.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.25 PM.png,ripe,ripe,0.11337923258543015,0.8866207599639893,0.0067711747251451015 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.31 PM.png,ripe,ripe,0.20574934780597687,0.7942506670951843,0.010814245790243149 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.14.01 PM.png,ripe,ripe,0.23054085671901703,0.7694591283798218,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.15.09 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.15.45 PM.png,ripe,unripe,0.5473261475563049,0.45267385244369507,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,ripe,0.15919406712055206,0.8408059477806091,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,ripe,0.11217188090085983,0.8878281116485596,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.09617671370506287,0.9038233160972595,0.0005068683531135321 +apple/test/ripe/Screen Shot 2018-06-08 at 5.17.58 PM.png,ripe,ripe,0.3810320794582367,0.6189678907394409,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.19.58 PM.png,ripe,ripe,0.0,0.9632399678230286,0.036760035902261734 +apple/test/ripe/Screen Shot 2018-06-08 at 5.21.06 PM.png,ripe,ripe,0.0,0.5320525169372559,0.46794748306274414 +apple/test/ripe/Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,ripe,0.0,0.9012627601623535,0.09873724728822708 +apple/test/ripe/Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,ripe,0.23702529072761536,0.762974739074707,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,ripe,0.0,0.6265431642532349,0.37345680594444275 +apple/test/ripe/Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,ripe,0.15389837324619293,0.8461016416549683,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.06 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.34 PM.png,ripe,unripe,0.6390444040298462,0.3609556257724762,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,unripe,0.6459391117095947,0.3540608882904053,0.2457718402147293 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.04 PM.png,ripe,ripe,0.3078884482383728,0.6921115517616272,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.24 PM.png,ripe,ripe,0.0,0.7673981785774231,0.2326018214225769 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,ripe,0.1997056007385254,0.8002943992614746,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,ripe,0.09978152066469193,0.9002184867858887,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,ripe,0.14325110614299774,0.8567488789558411,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.38 PM.png,ripe,ripe,0.23443204164505005,0.76556795835495,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,ripe,0.013065463863313198,0.7481383681297302,0.2518616318702698 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.6691665649414062,0.33083343505859375,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.34.07 PM.png,ripe,ripe,0.12945596873760223,0.8705440163612366,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 4.59.49 PM.png,ripe,ripe,0.45786169171333313,0.5421382784843445,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.02.08 PM.png,ripe,ripe,0.2890103757381439,0.7109896540641785,0.024732327088713646 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.03.47 PM.png,ripe,ripe,0.18591001629829407,0.8140900135040283,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,ripe,0.2115452140569687,0.7884547710418701,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.04.59 PM.png,ripe,ripe,0.0,0.7632907629013062,0.23670923709869385 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.06.23 PM.png,ripe,ripe,0.10399141162633896,0.8960086107254028,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,ripe,0.19720794260501862,0.8027920722961426,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,ripe,0.21119940280914307,0.7888005971908569,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.9322095513343811,0.06779047101736069,0.027013704180717468 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,ripe,0.11172854900360107,0.8882714509963989,0.0021966041531413794 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.10.29 PM.png,ripe,ripe,0.26160505414009094,0.7383949756622314,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.14.20 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.14.48 PM.png,ripe,ripe,0.4214540719985962,0.5785459280014038,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.17.04 PM.png,ripe,ripe,0.33098089694976807,0.6690191030502319,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.17.58 PM.png,ripe,ripe,0.4431304931640625,0.5568695068359375,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,ripe,0.1513465791940689,0.8486534357070923,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.21.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.22.53 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,ripe,0.41051313281059265,0.589486837387085,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.24.26 PM.png,ripe,ripe,0.0,0.9359968304634094,0.06400319933891296 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.33 PM.png,ripe,ripe,0.0,0.78660649061203,0.21339352428913116 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,ripe,0.0,0.6299331784248352,0.3700668513774872 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.49 PM.png,ripe,ripe,0.0,0.5709038972854614,0.4290961027145386 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,ripe,0.35595038533210754,0.6440496444702148,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.5388264656066895,0.46117353439331055,0.0021030826028436422 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,ripe,0.1552460491657257,0.8447539806365967,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.19 PM.png,ripe,ripe,0.18965311348438263,0.8103469014167786,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.27 PM.png,ripe,ripe,0.1557529866695404,0.844247043132782,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,unripe,0.6407457590103149,0.35925424098968506,0.23931367695331573 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,ripe,0.19908766448497772,0.8009123206138611,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.01.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.02.31 PM.png,ripe,unripe,0.7033951878547668,0.29660478234291077,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.03.34 PM.png,ripe,ripe,0.16179390251636505,0.8382061123847961,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,ripe,0.10202867537736893,0.8979713320732117,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,ripe,0.2107408344745636,0.789259135723114,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.05.06 PM.png,ripe,ripe,0.0,0.7549250721931458,0.24507494270801544 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,ripe,0.21971137821674347,0.7802886366844177,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,ripe,0.24081110954284668,0.7591888904571533,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,ripe,0.21139907836914062,0.7886009216308594,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,ripe,0.15791091322898865,0.842089056968689,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.9567007422447205,0.04329923167824745,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,ripe,0.11104752868413925,0.8889524936676025,0.004727636463940144 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.09.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,ripe,0.2632097601890564,0.7367902398109436,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.11.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.11.35 PM.png,ripe,ripe,0.1848573088645935,0.8151426911354065,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.16 PM.png,ripe,ripe,0.08036331832408905,0.9196366667747498,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.0991649404168129,0.9008350372314453,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.49 PM.png,ripe,ripe,0.0,0.9858324527740479,0.014167574234306812 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.19.28 PM.png,ripe,unripe,0.7247400879859924,0.27525991201400757,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.20.17 PM.png,ripe,ripe,0.12858957052230835,0.8714104294776917,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,ripe,0.1496046632528305,0.8503953218460083,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.21.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,ripe,0.0,0.6137301325798035,0.38626986742019653 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.22.48 PM.png,ripe,ripe,0.0,0.8756043314933777,0.12439566850662231 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.23.51 PM.png,ripe,ripe,0.1044882982969284,0.8955116868019104,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.35 PM.png,ripe,ripe,0.1504228711128235,0.8495771288871765,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.6557435393333435,0.3442564308643341,7.196767546702176e-05 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.26.41 PM.png,ripe,ripe,0.07151981443166733,0.9284802079200745,0.05119211599230766 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,unripe,0.8298211097717285,0.1701788753271103,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,unripe,0.6441575288772583,0.3558424711227417,0.2458137422800064 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.28.32 PM.png,ripe,ripe,0.0,0.7817329168319702,0.2182670682668686 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,ripe,0.1988065540790558,0.8011934757232666,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,ripe,0.15153425931930542,0.8484657406806946,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5249543190002441,0.47504571080207825,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,ripe,0.24576541781425476,0.7542346119880676,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.00.26 PM.png,ripe,ripe,0.23173245787620544,0.7682675123214722,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.01.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.24 PM.png,ripe,ripe,0.18798217177391052,0.8120178580284119,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.4370529353618622,0.5629470944404602 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,ripe,0.10221289843320847,0.8977870941162109,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.04.05 PM.png,ripe,unripe,0.6808990240097046,0.3191010057926178,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.04.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,ripe,0.0,0.8775526881217957,0.12244731187820435 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.18 PM.png,ripe,ripe,0.0,0.6565932035446167,0.3434067666530609 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.27 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,ripe,0.19862088561058044,0.8013791441917419,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.09.25 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,ripe,0.2453845590353012,0.7546154260635376,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.11.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.11.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.14.01 PM.png,ripe,ripe,0.27466338872909546,0.7253366112709045,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.0496506467461586,0.9503493309020996,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.17.10 PM.png,ripe,ripe,0.15734995901584625,0.8426500558853149,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.42 PM.png,ripe,ripe,0.06953629851341248,0.8232388496398926,0.17676113545894623 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.19.47 PM.png,ripe,unripe,0.7659528255462646,0.23404718935489655,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.32 PM.png,ripe,unripe,0.7802921533584595,0.21970783174037933,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,ripe,0.0,0.9718355536460876,0.02816443145275116 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.21.44 PM.png,ripe,unripe,0.6817477345466614,0.318252295255661,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.22.20 PM.png,ripe,ripe,0.0,0.7833824753761292,0.21661750972270966 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.9791838526725769,0.020816123113036156,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.24.35 PM.png,ripe,ripe,0.14938010275363922,0.850619912147522,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,ripe,0.35822367668151855,0.6417763233184814,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,ripe,0.15612784028053284,0.8438721895217896,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.41 PM.png,ripe,ripe,0.07143846899271011,0.9285615086555481,0.05228470638394356 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,unripe,0.8672584891319275,0.1327415108680725,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.28.04 PM.png,ripe,ripe,0.3276796042919159,0.6723203659057617,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,ripe,0.19819389283657074,0.8018060922622681,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.29.24 PM.png,ripe,ripe,0.1906585544347763,0.8093414306640625,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.33.55 PM.png,ripe,ripe,0.0877552404999733,0.9122447371482849,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.34.14 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.34.21 PM.png,ripe,ripe,0.23941953480243683,0.7605804800987244,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 4.59.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,ripe,0.19903016090393066,0.8009698390960693,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.02.48 PM.png,ripe,ripe,0.12944579124450684,0.8705542087554932,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.03.17 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.04.16 PM.png,ripe,ripe,0.2652243971824646,0.7347756028175354,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.18 PM.png,ripe,ripe,0.0,0.6552311778068542,0.34476882219314575 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,ripe,0.21963034570217133,0.7803696393966675,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.40058043599128723,0.5994195938110352 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,ripe,0.24226835370063782,0.7577316761016846,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,ripe,0.24330025911331177,0.7566997408866882,0.022537775337696075 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.07.32 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.08.58 PM.png,ripe,ripe,0.1762090027332306,0.823790967464447,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.09.17 PM.png,ripe,unripe,0.6703948378562927,0.3296051621437073,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.10.37 PM.png,ripe,unripe,0.611264705657959,0.3887353241443634,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.14.07 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.06821701675653458,0.9317829608917236,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.15.28 PM.png,ripe,ripe,0.025007516145706177,0.8013089895248413,0.19869102537631989 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,ripe,0.15736915171146393,0.8426308631896973,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.16.16 PM.png,ripe,ripe,0.05520908907055855,0.9447908997535706,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,ripe,0.21552534401416779,0.7844746708869934,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.18.58 PM.png,ripe,ripe,0.018199164420366287,0.7595773339271545,0.24042265117168427 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.20.32 PM.png,ripe,unripe,0.8416653275489807,0.15833468735218048,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,ripe,0.0,0.9730971455574036,0.02690286748111248 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.44 PM.png,ripe,unripe,0.6558361053466797,0.3441638946533203,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,ripe,0.0,0.6166168451309204,0.383383184671402 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.23.07 PM.png,ripe,ripe,0.32416433095932007,0.6758356690406799,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.23.26 PM.png,ripe,ripe,0.1414884328842163,0.8585115671157837,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,ripe,0.37101882696151733,0.6289811730384827,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,ripe,0.15523412823677063,0.8447659015655518,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.26.24 PM.png,ripe,ripe,0.0,0.8469003438949585,0.1530996412038803 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.06 PM.png,ripe,ripe,0.11088629066944122,0.88911372423172,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.13 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,unripe,0.8342357277870178,0.16576428711414337,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.28.32 PM.png,ripe,ripe,0.0,0.7783722281455994,0.22162774205207825 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,ripe,0.14481689035892487,0.8551831245422363,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.32.33 PM.png,ripe,overripe,0.0,0.40230920910835266,0.597690761089325 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,ripe,0.013051177375018597,0.748881995677948,0.251118004322052 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.33.33 PM.png,ripe,ripe,0.0,0.9065520763397217,0.09344792366027832 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.33.47 PM.png,ripe,ripe,0.185910165309906,0.814089834690094,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.34.07 PM.png,ripe,ripe,0.13719739019870758,0.8628026247024536,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.26 PM.png,ripe,ripe,0.23121313750743866,0.7687868475914001,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.50 PM.png,ripe,ripe,0.19325801730155945,0.8067419528961182,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.02.31 PM.png,ripe,unripe,0.6053993701934814,0.39460062980651855,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.04.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,ripe,0.2110830545425415,0.7889169454574585,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,ripe,0.0,0.8751780986785889,0.12482189387083054 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,ripe,0.21934302151203156,0.7806569933891296,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.23 PM.png,ripe,ripe,0.10235366225242615,0.8976463079452515,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,ripe,0.19094893336296082,0.8090510368347168,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,ripe,0.2451619952917099,0.7548379898071289,0.02296290546655655 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.25 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.056399863213300705,0.9436001181602478,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.13.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.05328867584466934,0.9467113018035889,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.14.48 PM.png,ripe,ripe,0.42169177532196045,0.5783082246780396,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.15.01 PM.png,ripe,ripe,0.3071524202823639,0.6928476095199585,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.15.14 PM.png,ripe,ripe,0.20211414992809296,0.7978858351707458,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.09719979763031006,0.9028002023696899,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.16.49 PM.png,ripe,ripe,0.0,0.9701380133628845,0.02986198477447033 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.17.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.19.15 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.19.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,ripe,0.1498432755470276,0.8501567244529724,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.21.06 PM.png,ripe,ripe,0.0,0.533893883228302,0.466106116771698 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.21.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.22.27 PM.png,ripe,ripe,0.0,0.8628162145614624,0.1371837854385376 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.23.23 PM.png,ripe,ripe,0.13799533247947693,0.8620046377182007,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,ripe,0.0,0.6316184997558594,0.3683815002441406 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,ripe,0.37894052267074585,0.6210594773292542,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.26.05 PM.png,ripe,unripe,0.6357367038726807,0.36426329612731934,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.26.47 PM.png,ripe,ripe,0.0,0.5094195008277893,0.4905804693698883 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,ripe,0.19938556849956512,0.8006144165992737,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.29.24 PM.png,ripe,ripe,0.1826012134552002,0.8173987865447998,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5907950401306152,0.40920495986938477,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.4102914035320282,0.5897085666656494 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,ripe,0.20571556687355042,0.7942844033241272,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.50 PM.png,ripe,ripe,0.18148674070835114,0.8185132741928101,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.01.34 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.02.38 PM.png,ripe,ripe,0.42450520396232605,0.5754948258399963,0.0058945766650140285 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.03.47 PM.png,ripe,ripe,0.1823340654373169,0.8176659345626831,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.04.48 PM.png,ripe,ripe,0.0,0.8122041821479797,0.18779584765434265 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.05.06 PM.png,ripe,ripe,0.0,0.7431919574737549,0.2568080723285675 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.402192622423172,0.5978074073791504 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,ripe,0.22216452658176422,0.777835488319397,0.03951723873615265 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,ripe,0.20733194053173065,0.7926680445671082,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,ripe,0.15749208629131317,0.8425078988075256,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.09.03 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,ripe,0.20499257743358612,0.7950074076652527,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.11.35 PM.png,ripe,ripe,0.17600017786026,0.82399982213974,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.11.59 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.14 PM.png,ripe,ripe,0.0,0.9549225568771362,0.045077428221702576 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.41 PM.png,ripe,ripe,0.14021891355514526,0.8597810864448547,0.012000352144241333 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.47 PM.png,ripe,unripe,0.6739886999130249,0.3260113000869751,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.13.45 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,ripe,0.14388902485370636,0.8561109900474548,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.14 PM.png,ripe,ripe,0.19325049221515656,0.8067495226860046,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,ripe,0.15188966691493988,0.8481103181838989,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,ripe,0.10584209859371185,0.894157886505127,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,ripe,0.20990018546581268,0.7900997996330261,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.21.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.22.48 PM.png,ripe,ripe,0.0,0.8035855293273926,0.19641447067260742 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.23.31 PM.png,ripe,ripe,0.14859332144260406,0.8514066934585571,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.24.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,ripe,0.23061221837997437,0.7693877816200256,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.25.49 PM.png,ripe,ripe,0.0,0.5686710476875305,0.4313289523124695 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.26.29 PM.png,ripe,ripe,0.23698744177818298,0.7630125284194946,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,ripe,0.1933954954147339,0.8066045045852661,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,ripe,0.09427647292613983,0.905723512172699,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.29.31 PM.png,ripe,ripe,0.21284811198711395,0.7871518731117249,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.34.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.34.14 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.00.18 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.01.15 PM.png,ripe,ripe,0.11607584357261658,0.8839241862297058,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.41314947605133057,0.5868505239486694 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.03.10 PM.png,ripe,ripe,0.23961539566516876,0.7603846192359924,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.03.17 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,ripe,0.2161351591348648,0.7838648557662964,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.07.32 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.10.53 PM.png,ripe,unripe,0.9545884132385254,0.0454116091132164,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.11.08 PM.png,ripe,ripe,0.35480260848999023,0.6451973915100098,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.13.25 PM.png,ripe,ripe,0.10460999608039856,0.8953900337219238,0.013388816267251968 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.07 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.20 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,ripe,0.19754354655742645,0.8024564385414124,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.15.01 PM.png,ripe,ripe,0.23189544677734375,0.7681045532226562,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.15.39 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.33 PM.png,ripe,ripe,0.17099793255329132,0.8290020823478699,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.08747463673353195,0.9125253558158875,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.17.04 PM.png,ripe,ripe,0.34844353795051575,0.6515564918518066,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,ripe,0.19694001972675323,0.803059995174408,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.19.28 PM.png,ripe,unripe,0.5799127817153931,0.42008718848228455,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,ripe,0.0,0.9786150455474854,0.021384965628385544 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,ripe,0.0,0.9843376874923706,0.015662286430597305 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,ripe,0.0,0.660161554813385,0.339838445186615 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.23.26 PM.png,ripe,ripe,0.14422036707401276,0.8557796478271484,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,ripe,0.41025641560554504,0.5897436141967773,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,ripe,0.2268822193145752,0.7731177806854248,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,ripe,0.29766714572906494,0.7023328542709351,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.26.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.29.07 PM.png,ripe,ripe,0.21080996096134186,0.7891900539398193,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5416188836097717,0.45838111639022827,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.41264867782592773,0.5873513221740723 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 4.59.49 PM.png,ripe,unripe,0.6871214509010315,0.3128785192966461,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.01.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.03.59 PM.png,ripe,ripe,0.15849345922470093,0.8415065407752991,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,ripe,0.21249507367610931,0.7875049114227295,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.48 PM.png,ripe,ripe,0.0,0.811631977558136,0.18836800754070282 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.59 PM.png,ripe,ripe,0.0,0.7645598649978638,0.23544016480445862 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,ripe,0.0,0.885143518447876,0.11485645920038223 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,ripe,0.2373959869146347,0.7626039981842041,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,ripe,0.21381215751171112,0.7861878275871277,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,ripe,0.11054175347089767,0.8894582390785217,0.0029736622236669064 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.08.52 PM.png,ripe,ripe,0.16331833600997925,0.8366816639900208,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.10.53 PM.png,ripe,unripe,0.8821280598640442,0.11787191033363342,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.13.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.13.31 PM.png,ripe,ripe,0.20729374885559082,0.7927062511444092,0.011411337181925774 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,ripe,0.14572639763355255,0.8542736172676086,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.15.28 PM.png,ripe,ripe,0.025421353057026863,0.8022709488868713,0.19772902131080627 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,ripe,0.11217540502548218,0.8878245949745178,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.18.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.20.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.21.31 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,ripe,0.0,0.9021816253662109,0.09781838953495026 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.23.14 PM.png,ripe,ripe,0.10182032734155655,0.8981796503067017,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,ripe,0.4119238555431366,0.5880761742591858,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,ripe,0.49217575788497925,0.5078242421150208,0.003110304707661271 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.29 PM.png,ripe,ripe,0.24407386779785156,0.7559261322021484,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.58 PM.png,ripe,overripe,0.0,0.417335569858551,0.582664430141449 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.42 PM.png,ripe,ripe,0.0,0.6675575375556946,0.33244243264198303 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.48 PM.png,ripe,ripe,0.0,0.6907384991645813,0.3092614710330963 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,ripe,0.0994064137339592,0.9005935788154602,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,ripe,0.14643268287181854,0.8535673022270203,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.38 PM.png,ripe,ripe,0.2337462306022644,0.7662537693977356,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,ripe,0.01307489164173603,0.7481704950332642,0.25182950496673584 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.6698821783065796,0.3301178514957428,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.41085970401763916,0.5891402959823608 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.33.27 PM.png,ripe,unripe,0.7241566777229309,0.2758433520793915,0.0 +apple/test/unripe/1.jpg,unripe,ripe,0.4562946856021881,0.5437052845954895,0.0 +apple/test/unripe/10.jpg,unripe,unripe,0.8575348258018494,0.14246518909931183,0.0 +apple/test/unripe/100.jpg,unripe,ripe,0.1169804111123085,0.7063207626342773,0.29367923736572266 +apple/test/unripe/101.jpg,unripe,ripe,0.21292909979820251,0.6346324682235718,0.3653675317764282 +apple/test/unripe/102.jpg,unripe,ripe,0.2646982967853546,0.735301673412323,0.0 +apple/test/unripe/103.jpg,unripe,ripe,0.34291598200798035,0.657084047794342,0.0 +apple/test/unripe/104.jpg,unripe,unripe,0.8787909746170044,0.12120901793241501,0.28130730986595154 +apple/test/unripe/105.jpg,unripe,ripe,0.09330707043409348,0.9066929221153259,0.0 +apple/test/unripe/106.jpg,unripe,unripe,0.5518814921379089,0.44811853766441345,0.12091794610023499 +apple/test/unripe/107.jpg,unripe,ripe,0.1337684839963913,0.8662315011024475,0.024314910173416138 +apple/test/unripe/108.jpg,unripe,ripe,0.3316829204559326,0.6683170795440674,0.007372100371867418 +apple/test/unripe/109.jpg,unripe,ripe,0.23637332022190094,0.7636266946792603,0.053370267152786255 +apple/test/unripe/11.jpg,unripe,ripe,0.061089012771844864,0.8497782945632935,0.15022173523902893 +apple/test/unripe/110.jpg,unripe,ripe,0.14138276875019073,0.8432128429412842,0.15678714215755463 +apple/test/unripe/111.jpg,unripe,unripe,0.676794707775116,0.32320529222488403,0.0 +apple/test/unripe/112.jpg,unripe,ripe,0.0,0.9970352649688721,0.002964708721265197 +apple/test/unripe/113.jpg,unripe,ripe,0.2644622325897217,0.7355377674102783,0.10996738076210022 +apple/test/unripe/114.jpg,unripe,unripe,0.6766818761825562,0.32331812381744385,0.0024987375363707542 +apple/test/unripe/115.jpg,unripe,ripe,0.0,0.7436784505844116,0.25632157921791077 +apple/test/unripe/116.jpg,unripe,ripe,0.27026891708374023,0.5800999402999878,0.4199000298976898 +apple/test/unripe/117.jpg,unripe,ripe,0.09980292618274689,0.9001970887184143,0.0 +apple/test/unripe/118.jpg,unripe,ripe,0.40660953521728516,0.5933904647827148,0.17969784140586853 +apple/test/unripe/119.jpg,unripe,ripe,0.441668838262558,0.5583311319351196,0.08933515101671219 +apple/test/unripe/12.jpg,unripe,ripe,0.4263456463813782,0.5736543536186218,0.061614248901605606 +apple/test/unripe/120.jpg,unripe,unripe,0.5968891382217407,0.4031108319759369,0.14588871598243713 +apple/test/unripe/121.jpg,unripe,ripe,0.09330707043409348,0.9066929221153259,0.0 +apple/test/unripe/122.jpg,unripe,unripe,0.5609573125839233,0.4390426576137543,0.05988815426826477 +apple/test/unripe/123.jpg,unripe,unripe,0.5075750350952148,0.49242496490478516,0.14630261063575745 +apple/test/unripe/124.jpg,unripe,ripe,0.0,0.7571467161178589,0.24285326898097992 +apple/test/unripe/125.jpg,unripe,ripe,0.3605206310749054,0.6394793391227722,0.0135036064311862 +apple/test/unripe/126.jpg,unripe,unripe,0.5968891382217407,0.4031108319759369,0.14588871598243713 +apple/test/unripe/127.jpg,unripe,ripe,0.25359612703323364,0.7464038729667664,0.0 +apple/test/unripe/128.jpg,unripe,ripe,0.26322489976882935,0.7367751002311707,0.0 +apple/test/unripe/129.jpg,unripe,unripe,0.721885621547699,0.2781144082546234,0.0 +apple/test/unripe/13.jpg,unripe,ripe,0.3759039044380188,0.6240960955619812,0.1720503717660904 +apple/test/unripe/130.jpg,unripe,ripe,0.32950806617736816,0.6704919338226318,0.0996701642870903 +apple/test/unripe/131.jpg,unripe,unripe,0.6881263256072998,0.3118736743927002,0.0 +apple/test/unripe/132.jpg,unripe,unripe,0.6969317197799683,0.30306828022003174,0.1498396396636963 +apple/test/unripe/133.jpg,unripe,unripe,0.5509782433509827,0.4490217864513397,0.20707941055297852 +apple/test/unripe/134.jpg,unripe,ripe,0.0,0.7863151431083679,0.2136848419904709 +apple/test/unripe/135.jpg,unripe,overripe,0.0,0.43277978897094727,0.5672202110290527 +apple/test/unripe/136.jpg,unripe,unripe,0.5468539595603943,0.4531460404396057,0.1706162989139557 +apple/test/unripe/137.jpg,unripe,ripe,0.1080668568611145,0.8919331431388855,0.0 +apple/test/unripe/138.jpg,unripe,ripe,0.40660953521728516,0.5933904647827148,0.17969784140586853 +apple/test/unripe/139.jpg,unripe,ripe,0.40390151739120483,0.5960984826087952,0.10162189602851868 +apple/test/unripe/14.jpg,unripe,ripe,0.0,0.8517522215843201,0.14824777841567993 +apple/test/unripe/140.jpg,unripe,ripe,0.06341296434402466,0.9365870356559753,0.0 +apple/test/unripe/141.jpg,unripe,ripe,0.02171250246465206,0.7965848445892334,0.2034151554107666 +apple/test/unripe/142.jpg,unripe,overripe,0.0,0.43277978897094727,0.5672202110290527 +apple/test/unripe/143.jpg,unripe,ripe,0.22761428356170654,0.7723857164382935,0.01358797773718834 +apple/test/unripe/144.jpg,unripe,ripe,0.4724130630493164,0.5275869369506836,0.03172523155808449 +apple/test/unripe/145.jpg,unripe,ripe,0.4055708050727844,0.5944291949272156,0.0 +apple/test/unripe/146.jpg,unripe,unripe,0.7483404278755188,0.2516595423221588,0.14118358492851257 +apple/test/unripe/147.jpg,unripe,ripe,0.23561282455921173,0.7643871903419495,0.0 +apple/test/unripe/148.jpg,unripe,ripe,0.0,0.6267208456993103,0.3732791543006897 +apple/test/unripe/149.jpg,unripe,unripe,0.5609573125839233,0.4390426576137543,0.05988815426826477 +apple/test/unripe/15.jpg,unripe,ripe,0.2976350486278534,0.702364981174469,0.1369284838438034 +apple/test/unripe/150.jpg,unripe,ripe,0.0,0.9547246694564819,0.04527535289525986 +apple/test/unripe/151.jpg,unripe,ripe,0.0,0.7571467161178589,0.24285326898097992 +apple/test/unripe/152.jpg,unripe,ripe,0.0,0.6096661686897278,0.3903338611125946 +apple/test/unripe/153.jpg,unripe,ripe,0.0,0.7863151431083679,0.2136848419904709 +apple/test/unripe/154.jpg,unripe,ripe,0.0,0.6613271236419678,0.33867284655570984 +apple/test/unripe/155.jpg,unripe,ripe,0.4781794250011444,0.5218205451965332,0.04777054116129875 +apple/test/unripe/156.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/157.jpg,unripe,ripe,0.042827293276786804,0.7693110108375549,0.23068897426128387 +apple/test/unripe/158.jpg,unripe,ripe,0.2094212770462036,0.7905787229537964,0.0 +apple/test/unripe/159.jpg,unripe,ripe,0.2574476897716522,0.7425523400306702,0.04399367421865463 +apple/test/unripe/16.jpg,unripe,ripe,0.3519147038459778,0.6480852961540222,0.12527358531951904 +apple/test/unripe/160.jpg,unripe,ripe,0.28467851877212524,0.7153214812278748,0.18043844401836395 +apple/test/unripe/161.jpg,unripe,unripe,0.792772114276886,0.20722787082195282,0.05176100507378578 +apple/test/unripe/162.jpg,unripe,ripe,0.0,0.694049060344696,0.30595090985298157 +apple/test/unripe/163.jpg,unripe,ripe,0.019914530217647552,0.7310185432434082,0.2689814865589142 +apple/test/unripe/164.jpg,unripe,ripe,0.2585027813911438,0.7414972186088562,0.0 +apple/test/unripe/165.jpg,unripe,ripe,0.2598665654659271,0.7401334047317505,0.0 +apple/test/unripe/166.jpg,unripe,ripe,0.32363268733024597,0.6763673424720764,0.0 +apple/test/unripe/167.jpg,unripe,ripe,0.0,0.6683453917503357,0.3316546082496643 +apple/test/unripe/168.jpg,unripe,ripe,0.2716045677661896,0.7283954620361328,0.0 +apple/test/unripe/169.jpg,unripe,ripe,0.19273841381072998,0.80726158618927,0.0 +apple/test/unripe/17.jpg,unripe,ripe,0.10964018851518631,0.7979750037193298,0.20202498137950897 +apple/test/unripe/170.jpg,unripe,ripe,0.46620360016822815,0.5337963700294495,0.1002807691693306 +apple/test/unripe/171.jpg,unripe,unripe,0.8821958899497986,0.11780412495136261,0.1638989895582199 +apple/test/unripe/172.jpg,unripe,overripe,0.0,0.36236488819122314,0.6376351118087769 +apple/test/unripe/173.jpg,unripe,unripe,0.5061986446380615,0.4938013553619385,0.0 +apple/test/unripe/174.jpg,unripe,unripe,0.9793816208839417,0.020618358626961708,0.25366684794425964 +apple/test/unripe/175.jpg,unripe,unripe,0.9839369654655457,0.01606305129826069,0.025081967934966087 +apple/test/unripe/176.jpg,unripe,unripe,0.5075750350952148,0.49242496490478516,0.14630261063575745 +apple/test/unripe/177.jpg,unripe,unripe,0.8575008511543274,0.142499178647995,0.0838087797164917 +apple/test/unripe/178.jpg,unripe,ripe,0.34137967228889465,0.658620297908783,0.0 +apple/test/unripe/179.jpg,unripe,ripe,0.25255608558654785,0.7474439144134521,0.0 +apple/test/unripe/18.jpg,unripe,ripe,0.09139034152030945,0.7553049325942993,0.2446950525045395 +apple/test/unripe/180.jpg,unripe,ripe,0.2258203625679016,0.7741796374320984,0.0 +apple/test/unripe/181.jpg,unripe,ripe,0.28467851877212524,0.7153214812278748,0.18043844401836395 +apple/test/unripe/182.jpg,unripe,unripe,0.5511739253997803,0.4488260746002197,0.0 +apple/test/unripe/183.jpg,unripe,unripe,0.9793816208839417,0.020618358626961708,0.25366684794425964 +apple/test/unripe/184.jpg,unripe,ripe,0.2254330962896347,0.7745668888092041,0.19265125691890717 +apple/test/unripe/185.jpg,unripe,ripe,0.32363268733024597,0.6763673424720764,0.0 +apple/test/unripe/186.jpg,unripe,unripe,0.8745660185813904,0.1254339963197708,0.0 +apple/test/unripe/187.jpg,unripe,ripe,0.0,0.6683453917503357,0.3316546082496643 +apple/test/unripe/188.jpg,unripe,unripe,0.5061986446380615,0.4938013553619385,0.0 +apple/test/unripe/189.jpg,unripe,ripe,0.4024812877178192,0.5975187420845032,0.027204278856515884 +apple/test/unripe/19.jpg,unripe,ripe,0.1612040102481842,0.8387959599494934,0.0 +apple/test/unripe/190.jpg,unripe,ripe,0.2716045677661896,0.7283954620361328,0.0 +apple/test/unripe/191.jpg,unripe,ripe,0.07690806686878204,0.8049864768981934,0.19501355290412903 +apple/test/unripe/192.jpg,unripe,ripe,0.3924318552017212,0.6075681447982788,0.019969431683421135 +apple/test/unripe/193.jpg,unripe,unripe,0.9476281404495239,0.05237183719873428,0.0 +apple/test/unripe/194.jpg,unripe,unripe,0.720232367515564,0.27976763248443604,0.0 +apple/test/unripe/195.jpg,unripe,ripe,0.2258203625679016,0.7741796374320984,0.0 +apple/test/unripe/196.jpg,unripe,unripe,0.8821958899497986,0.11780412495136261,0.1638989895582199 +apple/test/unripe/197.jpg,unripe,ripe,0.15885965526103973,0.8175365328788757,0.18246346712112427 +apple/test/unripe/198.jpg,unripe,ripe,0.19273841381072998,0.80726158618927,0.0 +apple/test/unripe/199.jpg,unripe,ripe,0.035680610686540604,0.8789664506912231,0.12103352695703506 +apple/test/unripe/2.jpg,unripe,ripe,0.2583926022052765,0.7416074275970459,0.15277934074401855 +apple/test/unripe/20.jpg,unripe,unripe,0.6253235936164856,0.3746764361858368,0.024918096140027046 +apple/test/unripe/200.jpg,unripe,ripe,0.3478304147720337,0.6521695852279663,0.0 +apple/test/unripe/202.jpg,unripe,ripe,0.18158775568008423,0.8184122443199158,0.17945589125156403 +apple/test/unripe/203.jpg,unripe,unripe,0.8745660185813904,0.1254339963197708,0.0 +apple/test/unripe/205.jpg,unripe,unripe,0.8119770884513855,0.1880229115486145,0.20083031058311462 +apple/test/unripe/206.jpg,unripe,ripe,0.4266219735145569,0.5111111402511597,0.4888888895511627 +apple/test/unripe/207.jpg,unripe,ripe,0.2783106863498688,0.7216893434524536,0.17936572432518005 +apple/test/unripe/208.jpg,unripe,ripe,0.163314089179039,0.8366858959197998,0.0 +apple/test/unripe/209.jpg,unripe,unripe,0.5682976245880127,0.4317023754119873,0.05905454605817795 +apple/test/unripe/21.jpg,unripe,unripe,0.862949013710022,0.13705098628997803,0.11493568122386932 +apple/test/unripe/210.jpg,unripe,ripe,0.0,0.9139112234115601,0.08608875423669815 +apple/test/unripe/211.jpg,unripe,ripe,0.2437860518693924,0.7562139630317688,0.009395972825586796 +apple/test/unripe/212.jpg,unripe,ripe,0.26579931378364563,0.734200656414032,0.0 +apple/test/unripe/213.jpg,unripe,unripe,0.5511739253997803,0.4488260746002197,0.0 +apple/test/unripe/214.jpg,unripe,ripe,0.2683529257774353,0.7316470742225647,0.0 +apple/test/unripe/215.jpg,unripe,ripe,0.1151958703994751,0.6463474035263062,0.35365256667137146 +apple/test/unripe/216.jpg,unripe,ripe,0.2254330962896347,0.7745668888092041,0.19265125691890717 +apple/test/unripe/217.jpg,unripe,unripe,0.9588117599487305,0.04118826240301132,0.0 +apple/test/unripe/218.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/219.jpg,unripe,ripe,0.3478304147720337,0.6521695852279663,0.0 +apple/test/unripe/22.jpg,unripe,unripe,0.8487420082092285,0.15125802159309387,0.0 +apple/test/unripe/220.jpg,unripe,ripe,0.24493707716464996,0.7550629377365112,0.1288294494152069 +apple/test/unripe/221.jpg,unripe,ripe,0.17755930125713348,0.8224406838417053,0.13624915480613708 +apple/test/unripe/222.jpg,unripe,ripe,0.23199278116226196,0.768007218837738,0.030652230605483055 +apple/test/unripe/223.jpg,unripe,ripe,0.028291983529925346,0.7384061813354492,0.26159384846687317 +apple/test/unripe/224.jpg,unripe,ripe,0.1930294930934906,0.806970477104187,0.04066774994134903 +apple/test/unripe/225.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/226.jpg,unripe,ripe,0.3768221437931061,0.6231778860092163,0.0 +apple/test/unripe/227.jpg,unripe,ripe,0.0,0.6868883967399597,0.31311163306236267 +apple/test/unripe/229.jpg,unripe,ripe,0.019914530217647552,0.7310185432434082,0.2689814865589142 +apple/test/unripe/23.jpg,unripe,ripe,0.23961520195007324,0.7603847980499268,0.044185664504766464 +apple/test/unripe/230.jpg,unripe,ripe,0.22234804928302765,0.7776519656181335,0.0 +apple/test/unripe/231.jpg,unripe,ripe,0.0,0.7818809151649475,0.2181190699338913 +apple/test/unripe/232.jpg,unripe,ripe,0.0818558856844902,0.8162839412689209,0.1837160587310791 +apple/test/unripe/233.jpg,unripe,ripe,0.14620132744312286,0.6797456741333008,0.3202543258666992 +apple/test/unripe/235.jpg,unripe,ripe,0.21744121611118317,0.782558798789978,0.0 +apple/test/unripe/236.jpg,unripe,unripe,0.5019802451133728,0.4980197548866272,0.05093478038907051 +apple/test/unripe/237.jpg,unripe,ripe,0.200740247964859,0.7992597222328186,0.0 +apple/test/unripe/238.jpg,unripe,ripe,0.0,0.7660926580429077,0.23390734195709229 +apple/test/unripe/239.jpg,unripe,unripe,0.6433060765266418,0.35669392347335815,0.058849845081567764 +apple/test/unripe/24.jpg,unripe,ripe,0.2685256004333496,0.7314743995666504,0.21776285767555237 +apple/test/unripe/240.jpg,unripe,ripe,0.2491900473833084,0.7508099675178528,0.1133333370089531 +apple/test/unripe/241.jpg,unripe,ripe,0.16602320969104767,0.8339768052101135,0.02986903302371502 +apple/test/unripe/242.jpg,unripe,ripe,0.12806035578250885,0.8719396591186523,0.0 +apple/test/unripe/243.jpg,unripe,unripe,0.8745660185813904,0.1254339963197708,0.0 +apple/test/unripe/244.jpg,unripe,unripe,0.5511739253997803,0.4488260746002197,0.0 +apple/test/unripe/245.jpg,unripe,ripe,0.2680613398551941,0.7319386601448059,0.2042388767004013 +apple/test/unripe/246.jpg,unripe,ripe,0.008209873922169209,0.7508772015571594,0.24912281334400177 +apple/test/unripe/247.jpg,unripe,ripe,0.0,0.5815761685371399,0.4184238016605377 +apple/test/unripe/248.jpg,unripe,unripe,0.964070200920105,0.03592976927757263,0.24980033934116364 +apple/test/unripe/249.jpg,unripe,unripe,0.6823517084121704,0.3176482617855072,0.266000896692276 +apple/test/unripe/25.jpg,unripe,unripe,0.6784923672676086,0.32150763273239136,0.09330445528030396 +apple/test/unripe/250.jpg,unripe,unripe,0.6107896566390991,0.3892103433609009,0.0 +apple/test/unripe/251.jpg,unripe,ripe,0.1115613654255867,0.8884386420249939,0.030110575258731842 +apple/test/unripe/253.jpg,unripe,ripe,0.3768221437931061,0.6231778860092163,0.0 +apple/test/unripe/254.jpg,unripe,unripe,0.6763089299201965,0.32369107007980347,0.23937903344631195 +apple/test/unripe/255.jpg,unripe,unripe,0.8071257472038269,0.1928742527961731,0.15498970448970795 +apple/test/unripe/256.jpg,unripe,unripe,0.9553197026252747,0.044680316001176834,0.0 +apple/test/unripe/257.jpg,unripe,unripe,0.7620083093643188,0.23799166083335876,0.003935629967600107 +apple/test/unripe/258.jpg,unripe,ripe,0.17766821384429932,0.8223317861557007,0.0 +apple/test/unripe/259.jpg,unripe,unripe,0.7236660718917847,0.2763339579105377,0.1929619312286377 +apple/test/unripe/26.jpg,unripe,ripe,0.0,0.7187002301216125,0.28129976987838745 +apple/test/unripe/261.jpg,unripe,ripe,0.0,0.6778004169464111,0.32219961285591125 +apple/test/unripe/262.jpg,unripe,ripe,0.4697762429714203,0.5302237272262573,0.24247796833515167 +apple/test/unripe/263.jpg,unripe,ripe,0.22324194014072418,0.6582126021385193,0.3417873680591583 +apple/test/unripe/264.jpg,unripe,ripe,0.06737788021564484,0.9326221346855164,0.05678063631057739 +apple/test/unripe/265.jpg,unripe,ripe,0.27200382947921753,0.7279961705207825,0.0 +apple/test/unripe/266.jpg,unripe,ripe,0.07615312933921814,0.8176464438438416,0.18235355615615845 +apple/test/unripe/267.jpg,unripe,ripe,0.4391004145145416,0.560899555683136,0.0 +apple/test/unripe/268.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/269.jpg,unripe,ripe,0.20231778919696808,0.7976822257041931,0.020581182092428207 +apple/test/unripe/27.jpg,unripe,unripe,0.9388843774795532,0.06111561134457588,0.0 +apple/test/unripe/270.jpg,unripe,ripe,0.09822190552949905,0.786083459854126,0.21391655504703522 +apple/test/unripe/271.jpg,unripe,unripe,0.5682976245880127,0.4317023754119873,0.05905454605817795 +apple/test/unripe/272.jpg,unripe,ripe,0.1151958703994751,0.6463474035263062,0.35365256667137146 +apple/test/unripe/273.jpg,unripe,ripe,0.0,0.6123222708702087,0.38767772912979126 +apple/test/unripe/274.jpg,unripe,unripe,0.8844173550605774,0.11558262258768082,0.050327885895967484 +apple/test/unripe/275.jpg,unripe,ripe,0.3478304147720337,0.6521695852279663,0.0 +apple/test/unripe/276.jpg,unripe,unripe,0.5298927426338196,0.4701072573661804,0.07491987198591232 +apple/test/unripe/277.jpg,unripe,unripe,0.9868588447570801,0.013141131028532982,0.0 +apple/test/unripe/278.jpg,unripe,ripe,0.28550708293914795,0.714492917060852,0.09802958369255066 +apple/test/unripe/279.jpg,unripe,ripe,0.2437860518693924,0.7562139630317688,0.009395972825586796 +apple/test/unripe/28.jpg,unripe,ripe,0.3301398456096649,0.6698601245880127,0.0 +apple/test/unripe/281.jpg,unripe,unripe,0.7986234426498413,0.2013765424489975,0.0812024176120758 +apple/test/unripe/282.jpg,unripe,ripe,0.0,0.7005393505096436,0.29946064949035645 +apple/test/unripe/283.jpg,unripe,ripe,0.36341744661331177,0.6365825533866882,0.0 +apple/test/unripe/284.jpg,unripe,ripe,0.33728471398353577,0.6627153158187866,0.0 +apple/test/unripe/285.jpg,unripe,ripe,0.12015693634748459,0.8798430562019348,0.11472069472074509 +apple/test/unripe/286.jpg,unripe,unripe,0.6631767749786377,0.3368232250213623,0.0 +apple/test/unripe/287.jpg,unripe,ripe,0.10725508630275726,0.8927448987960815,0.0 +apple/test/unripe/288.jpg,unripe,unripe,0.9868588447570801,0.013141131028532982,0.0 +apple/test/unripe/289.jpg,unripe,unripe,0.6823517084121704,0.3176482617855072,0.266000896692276 +apple/test/unripe/29.jpg,unripe,ripe,0.45876994729042053,0.5412300229072571,0.0 +apple/test/unripe/290.jpg,unripe,ripe,0.12806035578250885,0.8719396591186523,0.0 +apple/test/unripe/291.jpg,unripe,ripe,0.0,0.5815761685371399,0.4184238016605377 +apple/test/unripe/292.jpg,unripe,ripe,0.0,0.9056942462921143,0.09430573880672455 +apple/test/unripe/293.jpg,unripe,ripe,0.0,0.6778004169464111,0.32219961285591125 +apple/test/unripe/294.jpg,unripe,ripe,0.0845990777015686,0.9154009222984314,0.0 +apple/test/unripe/295.jpg,unripe,unripe,0.964070200920105,0.03592976927757263,0.24980033934116364 +apple/test/unripe/297.jpg,unripe,unripe,0.6107896566390991,0.3892103433609009,0.0 +apple/test/unripe/298.jpg,unripe,unripe,0.6763089299201965,0.32369107007980347,0.23937903344631195 +apple/test/unripe/3.jpg,unripe,unripe,0.6315003633499146,0.36849966645240784,0.06869198381900787 +apple/test/unripe/30.jpg,unripe,ripe,0.0,0.5106074213981628,0.48939257860183716 +apple/test/unripe/300.jpg,unripe,ripe,0.0,0.5374586582183838,0.4625413417816162 +apple/test/unripe/301.jpg,unripe,ripe,0.0,0.7039387822151184,0.2960612177848816 +apple/test/unripe/302.jpg,unripe,unripe,0.7236660718917847,0.2763339579105377,0.1929619312286377 +apple/test/unripe/303.jpg,unripe,ripe,0.1115613654255867,0.8884386420249939,0.030110575258731842 +apple/test/unripe/304.jpg,unripe,ripe,0.2206224650144577,0.7793775200843811,0.0 +apple/test/unripe/305.jpg,unripe,ripe,0.3206839859485626,0.679315984249115,0.0 +apple/test/unripe/306.jpg,unripe,ripe,0.1522647589445114,0.8477352261543274,0.037831153720617294 +apple/test/unripe/307.jpg,unripe,ripe,0.008209873922169209,0.7508772015571594,0.24912281334400177 +apple/test/unripe/308.jpg,unripe,unripe,0.5194303393363953,0.4805696904659271,0.3019236624240875 +apple/test/unripe/309.jpg,unripe,ripe,0.46237480640411377,0.5376251935958862,0.0028612304013222456 +apple/test/unripe/31.jpg,unripe,ripe,0.32581856846809387,0.6741814017295837,0.024983027949929237 +apple/test/unripe/310.jpg,unripe,ripe,0.2921709418296814,0.7078290581703186,0.0 +apple/test/unripe/311.jpg,unripe,ripe,0.06737788021564484,0.9326221346855164,0.05678063631057739 +apple/test/unripe/312.jpg,unripe,ripe,0.27200382947921753,0.7279961705207825,0.0 +apple/test/unripe/313.jpg,unripe,ripe,0.07615312933921814,0.8176464438438416,0.18235355615615845 +apple/test/unripe/314.jpg,unripe,ripe,0.4391004145145416,0.560899555683136,0.0 +apple/test/unripe/315.jpg,unripe,ripe,0.1469171941280365,0.8436419367790222,0.1563580334186554 +apple/test/unripe/317.jpg,unripe,unripe,0.605247437953949,0.394752562046051,0.15099723637104034 +apple/test/unripe/318.jpg,unripe,unripe,0.5327056050300598,0.4672944247722626,0.046914514154195786 +apple/test/unripe/32.jpg,unripe,unripe,0.803074836730957,0.19692519307136536,0.0645146518945694 +apple/test/unripe/321.jpg,unripe,ripe,0.30141481757164,0.6985852122306824,0.05563022941350937 +apple/test/unripe/322.jpg,unripe,ripe,0.22324194014072418,0.6582126021385193,0.3417873680591583 +apple/test/unripe/323.jpg,unripe,ripe,0.2026379108428955,0.7973620891571045,0.13464820384979248 +apple/test/unripe/324.jpg,unripe,ripe,0.1268782764673233,0.7774947881698608,0.22250519692897797 +apple/test/unripe/325.jpg,unripe,ripe,0.0,0.9341046810150146,0.06589533388614655 +apple/test/unripe/326.jpg,unripe,ripe,0.2945747673511505,0.7054252028465271,0.17639435827732086 +apple/test/unripe/327.jpg,unripe,unripe,0.5504721403121948,0.44952788949012756,0.02704734168946743 +apple/test/unripe/328.jpg,unripe,unripe,0.8071257472038269,0.1928742527961731,0.15498970448970795 +apple/test/unripe/329.jpg,unripe,unripe,0.9731762409210205,0.026823759078979492,0.22781512141227722 +apple/test/unripe/33.jpg,unripe,ripe,0.07012369483709335,0.7181022763252258,0.28189772367477417 +apple/test/unripe/330.jpg,unripe,ripe,0.0,0.8454614877700806,0.15453852713108063 +apple/test/unripe/331.jpg,unripe,ripe,0.3331201672554016,0.6668798327445984,0.0 +apple/test/unripe/332.jpg,unripe,unripe,0.6348162889480591,0.3651837110519409,0.23759552836418152 +apple/test/unripe/333.jpg,unripe,ripe,0.20231778919696808,0.7976822257041931,0.020581182092428207 +apple/test/unripe/334.jpg,unripe,ripe,0.0,0.5442968010902405,0.45570316910743713 +apple/test/unripe/335.jpg,unripe,ripe,0.16643919050693512,0.8335608243942261,0.0 +apple/test/unripe/336.jpg,unripe,ripe,0.34639573097229004,0.65360426902771,0.1259162873029709 +apple/test/unripe/337.jpg,unripe,ripe,0.13831567764282227,0.8616843223571777,0.0 +apple/test/unripe/338.jpg,unripe,unripe,0.6034168004989624,0.39658322930336,0.23549281060695648 +apple/test/unripe/339.jpg,unripe,overripe,0.39456719160079956,0.4690593481063843,0.5309406518936157 +apple/test/unripe/34.jpg,unripe,ripe,0.4957881569862366,0.5042118430137634,0.2591221332550049 +apple/test/unripe/342.jpg,unripe,ripe,0.2344590425491333,0.6367193460464478,0.36328062415122986 +apple/test/unripe/343.jpg,unripe,ripe,0.05144602805376053,0.5269381999969482,0.47306180000305176 +apple/test/unripe/344.jpg,unripe,ripe,0.0,0.8312223553657532,0.16877762973308563 +apple/test/unripe/346.jpg,unripe,unripe,0.6697567105293274,0.3302432596683502,0.20385321974754333 +apple/test/unripe/347.jpg,unripe,ripe,0.4674018323421478,0.5325981974601746,0.03137117996811867 +apple/test/unripe/348.jpg,unripe,ripe,0.210279181599617,0.7304760217666626,0.269523948431015 +apple/test/unripe/349.jpg,unripe,unripe,0.5927074551582336,0.40729254484176636,0.09056790173053741 +apple/test/unripe/35.jpg,unripe,unripe,0.5958924293518066,0.40410757064819336,0.0316644124686718 +apple/test/unripe/350.jpg,unripe,unripe,0.9731762409210205,0.026823759078979492,0.22781512141227722 +apple/test/unripe/351.jpg,unripe,unripe,0.7614172101020813,0.2385827898979187,0.30010196566581726 +apple/test/unripe/353.jpg,unripe,ripe,0.3331201672554016,0.6668798327445984,0.0 +apple/test/unripe/354.jpg,unripe,ripe,0.09822190552949905,0.786083459854126,0.21391655504703522 +apple/test/unripe/355.jpg,unripe,unripe,0.7252961993217468,0.2747037708759308,0.0 +apple/test/unripe/356.jpg,unripe,unripe,0.8844173550605774,0.11558262258768082,0.050327885895967484 +apple/test/unripe/357.jpg,unripe,ripe,0.4005996584892273,0.5994003415107727,0.2255244255065918 +apple/test/unripe/358.jpg,unripe,ripe,0.41939324140548706,0.5806067585945129,0.0 +apple/test/unripe/359.jpg,unripe,ripe,0.32994186878204346,0.6700581312179565,0.13608315587043762 +apple/test/unripe/36.jpg,unripe,unripe,0.9901036620140076,0.009896308183670044,0.12035609036684036 +apple/test/unripe/361.jpg,unripe,unripe,0.5002100467681885,0.4997899532318115,0.03794838860630989 +apple/test/unripe/362.jpg,unripe,ripe,0.2955261468887329,0.7044738531112671,0.18082484602928162 +apple/test/unripe/363.jpg,unripe,ripe,0.18312303721904755,0.8118589520454407,0.18814103305339813 +apple/test/unripe/364.jpg,unripe,ripe,0.10927099734544754,0.7755218744277954,0.22447814047336578 +apple/test/unripe/365.jpg,unripe,unripe,0.5417800545692444,0.4582199454307556,0.09183943271636963 +apple/test/unripe/366.jpg,unripe,ripe,0.14998960494995117,0.8500103950500488,0.0 +apple/test/unripe/367.jpg,unripe,unripe,0.5749366879463196,0.4250633120536804,0.21835541725158691 +apple/test/unripe/368.jpg,unripe,ripe,0.0,0.504466712474823,0.495533287525177 +apple/test/unripe/369.jpg,unripe,ripe,0.3924318552017212,0.6075681447982788,0.019969431683421135 +apple/test/unripe/37.jpg,unripe,ripe,0.4144032597541809,0.5855967402458191,0.26506394147872925 +apple/test/unripe/370.jpg,unripe,ripe,0.2454131692647934,0.7545868158340454,0.12231694906949997 +apple/test/unripe/371.jpg,unripe,unripe,0.6145668029785156,0.38543322682380676,0.12256770581007004 +apple/test/unripe/373.jpg,unripe,ripe,0.4516255855560303,0.5483744144439697,0.1143302470445633 +apple/test/unripe/374.jpg,unripe,ripe,0.36341744661331177,0.6365825533866882,0.0 +apple/test/unripe/375.jpg,unripe,ripe,0.11548598855733871,0.8845140337944031,0.0 +apple/test/unripe/376.jpg,unripe,ripe,0.16521161794662476,0.8347883820533752,0.11612240970134735 +apple/test/unripe/377.jpg,unripe,unripe,0.8512060642242432,0.14879396557807922,0.05421403422951698 +apple/test/unripe/378.jpg,unripe,ripe,0.3331201672554016,0.6668798327445984,0.0 +apple/test/unripe/38.jpg,unripe,ripe,0.1729108989238739,0.8270891308784485,0.0 +apple/test/unripe/381.jpg,unripe,ripe,0.2684388756752014,0.7315611243247986,0.0 +apple/test/unripe/383.jpg,unripe,ripe,0.0,0.7396394610404968,0.2603605389595032 +apple/test/unripe/384.jpg,unripe,ripe,0.19977840781211853,0.8002216219902039,0.1861814260482788 +apple/test/unripe/385.jpg,unripe,ripe,0.0,0.9877793192863464,0.01222070213407278 +apple/test/unripe/386.jpg,unripe,overripe,0.0,0.4003346860408783,0.5996653437614441 +apple/test/unripe/387.jpg,unripe,unripe,0.919425368309021,0.08057462424039841,0.0852731466293335 +apple/test/unripe/388.jpg,unripe,ripe,0.13282713294029236,0.86717289686203,0.028865208849310875 +apple/test/unripe/389.jpg,unripe,unripe,0.5504721403121948,0.44952788949012756,0.02704734168946743 +apple/test/unripe/39.jpg,unripe,ripe,0.0,0.6811038255691528,0.31889617443084717 +apple/test/unripe/390.jpg,unripe,unripe,0.9102437496185303,0.08975625783205032,0.0 +apple/test/unripe/391.jpg,unripe,ripe,0.015670301392674446,0.5447131395339966,0.4552868902683258 +apple/test/unripe/394.jpg,unripe,unripe,0.5794589519500732,0.42054104804992676,0.016631370410323143 +apple/test/unripe/4.jpg,unripe,ripe,0.38204386830329895,0.6179561018943787,0.0 +apple/test/unripe/40.jpg,unripe,ripe,0.17397251725196838,0.8260274529457092,0.0 +apple/test/unripe/41.jpg,unripe,ripe,0.4957881569862366,0.5042118430137634,0.2591221332550049 +apple/test/unripe/42.jpg,unripe,unripe,0.9744408130645752,0.025559160858392715,0.0 +apple/test/unripe/43.jpg,unripe,ripe,0.3126939833164215,0.6873060464859009,0.30619946122169495 +apple/test/unripe/44.jpg,unripe,ripe,0.3232458829879761,0.6767541170120239,0.0 +apple/test/unripe/45.jpg,unripe,ripe,0.34018588066101074,0.6598141193389893,0.005861182697117329 +apple/test/unripe/46.jpg,unripe,unripe,0.803074836730957,0.19692519307136536,0.0645146518945694 +apple/test/unripe/47.jpg,unripe,unripe,0.5818778276443481,0.41812220215797424,0.0 +apple/test/unripe/48.jpg,unripe,ripe,0.0,0.9571385979652405,0.04286138340830803 +apple/test/unripe/49.jpg,unripe,unripe,0.7608014941215515,0.2391984909772873,0.11866859346628189 +apple/test/unripe/5.jpg,unripe,ripe,0.17705081403255463,0.8229491710662842,0.0 +apple/test/unripe/50.jpg,unripe,ripe,0.3604099452495575,0.6395900249481201,0.16999538242816925 +apple/test/unripe/51.jpg,unripe,unripe,0.6517556309700012,0.3482443690299988,0.025304686278104782 +apple/test/unripe/52.jpg,unripe,ripe,0.17680276930332184,0.8217520117759705,0.17824798822402954 +apple/test/unripe/53.jpg,unripe,ripe,0.28522032499313354,0.7147796750068665,0.05435536429286003 +apple/test/unripe/54.jpg,unripe,unripe,0.6540864109992981,0.3459135591983795,0.10407754778862 +apple/test/unripe/55.jpg,unripe,ripe,0.23298689723014832,0.7414582371711731,0.2585417628288269 +apple/test/unripe/56.jpg,unripe,ripe,0.18667112290859222,0.8133288621902466,0.05177734047174454 +apple/test/unripe/57.jpg,unripe,ripe,0.0,0.6353126168251038,0.36468738317489624 +apple/test/unripe/58.jpg,unripe,ripe,0.08225616812705994,0.9177438020706177,0.0260869562625885 +apple/test/unripe/59.jpg,unripe,unripe,0.8121151328086853,0.18788489699363708,0.10679282248020172 +apple/test/unripe/6.jpg,unripe,unripe,0.8092182874679565,0.19078168272972107,0.16221322119235992 +apple/test/unripe/60.jpg,unripe,unripe,0.5777950286865234,0.4222049415111542,0.03603040426969528 +apple/test/unripe/61.jpg,unripe,ripe,0.08225616812705994,0.9177438020706177,0.0260869562625885 +apple/test/unripe/62.jpg,unripe,unripe,0.5777950286865234,0.4222049415111542,0.03603040426969528 +apple/test/unripe/63.jpg,unripe,unripe,0.5732793807983398,0.42672064900398254,0.08194014430046082 +apple/test/unripe/64.jpg,unripe,ripe,0.2412596195936203,0.7587403655052185,0.0 +apple/test/unripe/65.jpg,unripe,unripe,0.882760763168335,0.11723925918340683,0.023948902264237404 +apple/test/unripe/66.jpg,unripe,ripe,0.3641514480113983,0.6358485817909241,0.0 +apple/test/unripe/67.jpg,unripe,ripe,0.35948479175567627,0.6405152082443237,0.006496966350823641 +apple/test/unripe/68.jpg,unripe,ripe,0.11719897389411926,0.8828010559082031,0.09892863035202026 +apple/test/unripe/69.jpg,unripe,ripe,0.21123431622982025,0.7887656688690186,0.0 +apple/test/unripe/7.jpg,unripe,unripe,0.6398563385009766,0.36014366149902344,0.0 +apple/test/unripe/70.jpg,unripe,unripe,0.8616527318954468,0.13834728300571442,0.0 +apple/test/unripe/71.jpg,unripe,unripe,0.6592984199523926,0.3407016098499298,0.0 +apple/test/unripe/72.jpg,unripe,overripe,0.0,0.4629233479499817,0.5370766520500183 +apple/test/unripe/73.jpg,unripe,ripe,0.32523050904273987,0.6747694611549377,0.202961727976799 +apple/test/unripe/74.jpg,unripe,ripe,0.4938494861125946,0.506150484085083,0.1499136984348297 +apple/test/unripe/75.jpg,unripe,ripe,0.0,0.7277829647064209,0.2722170650959015 +apple/test/unripe/76.jpg,unripe,unripe,0.8581738471984863,0.14182618260383606,0.06792185455560684 +apple/test/unripe/77.jpg,unripe,unripe,0.5566670298576355,0.4433329999446869,0.0 +apple/test/unripe/78.jpg,unripe,unripe,0.9345239400863647,0.06547604501247406,0.09636905044317245 +apple/test/unripe/79.jpg,unripe,ripe,0.12789495289325714,0.7969533801078796,0.20304659008979797 +apple/test/unripe/8.jpg,unripe,ripe,0.3938978612422943,0.6061021089553833,0.0003565062361303717 +apple/test/unripe/80.jpg,unripe,ripe,0.059317946434020996,0.656404435634613,0.3435955345630646 +apple/test/unripe/81.jpg,unripe,ripe,0.014219650998711586,0.6397760510444641,0.3602239191532135 +apple/test/unripe/82.jpg,unripe,ripe,0.3232458829879761,0.6767541170120239,0.0 +apple/test/unripe/83.jpg,unripe,unripe,0.8787909746170044,0.12120901793241501,0.28130730986595154 +apple/test/unripe/84.jpg,unripe,ripe,0.0,0.9970352649688721,0.002964708721265197 +apple/test/unripe/85.jpg,unripe,overripe,0.0,0.43945884704589844,0.5605411529541016 +apple/test/unripe/86.jpg,unripe,unripe,0.6823858618736267,0.3176141679286957,0.1762472242116928 +apple/test/unripe/87.jpg,unripe,ripe,0.41439878940582275,0.5856012105941772,0.025455240160226822 +apple/test/unripe/88.jpg,unripe,ripe,0.16133993864059448,0.8386600613594055,0.0 +apple/test/unripe/89.jpg,unripe,ripe,0.2728308141231537,0.7271691560745239,0.0 +apple/test/unripe/9.jpg,unripe,ripe,0.2904469966888428,0.7095530033111572,0.0 +apple/test/unripe/90.jpg,unripe,ripe,0.2687247693538666,0.7312752604484558,0.026356589049100876 +apple/test/unripe/91.jpg,unripe,ripe,0.12789495289325714,0.7969533801078796,0.20304659008979797 +apple/test/unripe/92.jpg,unripe,ripe,0.3699452579021454,0.630054771900177,0.0 +apple/test/unripe/93.jpg,unripe,unripe,0.9939587712287903,0.006041241344064474,0.07385757565498352 +apple/test/unripe/94.jpg,unripe,ripe,0.11670073121786118,0.8222458362579346,0.17775416374206543 +apple/test/unripe/95.jpg,unripe,ripe,0.1852056086063385,0.8147944211959839,0.1289488524198532 +apple/test/unripe/96.jpg,unripe,unripe,0.5682976245880127,0.4317023754119873,0.05905454605817795 +apple/test/unripe/97.jpg,unripe,ripe,0.20131805539131165,0.7986819744110107,0.0 +apple/test/unripe/98.jpg,unripe,ripe,0.0,0.7736557722091675,0.2263442575931549 +apple/test/unripe/99.jpg,unripe,unripe,0.6766818761825562,0.32331812381744385,0.0024987375363707542 diff --git a/AgCloud/services/ripeness-baseline/eval/apple_argmax/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/apple_argmax/roc_curves.png new file mode 100644 index 000000000..b0eff4b97 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/apple_argmax/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/apple_test/metrics.json b/AgCloud/services/ripeness-baseline/eval/apple_test/metrics.json new file mode 100644 index 000000000..c56066221 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/apple_test/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.6159473299195318, + "report": { + "unripe": { + "precision": 0.4626038781163435, + "recall": 0.4501347708894879, + "f1-score": 0.4562841530054645, + "support": 371.0 + }, + "ripe": { + "precision": 0.5670103092783505, + "recall": 0.4177215189873418, + "f1-score": 0.48104956268221577, + "support": 395.0 + }, + "overripe": { + "precision": 0.7132867132867133, + "recall": 0.848585690515807, + "f1-score": 0.7750759878419453, + "support": 601.0 + }, + "accuracy": 0.6159473299195318, + "macro avg": { + "precision": 0.5809669668938025, + "recall": 0.5721473267975455, + "f1-score": 0.5708032345098751, + "support": 1367.0 + }, + "weighted avg": { + "precision": 0.6029849492548841, + "recall": 0.6159473299195318, + "f1-score": 0.6035966837728688, + "support": 1367.0 + } + }, + "confusion_matrix": [ + [ + 167, + 54, + 150 + ], + [ + 175, + 165, + 55 + ], + [ + 19, + 72, + 510 + ] + ], + "samples": 1367, + "prefix": "apple/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/apple_test/per_image.csv b/AgCloud/services/ripeness-baseline/eval/apple_test/per_image.csv new file mode 100644 index 000000000..ce05f11f8 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/apple_test/per_image.csv @@ -0,0 +1,1368 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +apple/test/overripe/Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7510486841201782,0.24895134568214417 +apple/test/overripe/Screen Shot 2018-06-07 at 2.16.54 PM.png,overripe,overripe,0.0,0.877063512802124,0.12293646484613419 +apple/test/overripe/Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.0,0.7681758999824524,0.23182412981987 +apple/test/overripe/Screen Shot 2018-06-07 at 2.20.04 PM.png,overripe,overripe,0.0,0.8034244179725647,0.1965755969285965 +apple/test/overripe/Screen Shot 2018-06-07 at 2.20.34 PM.png,overripe,overripe,0.0,0.739355206489563,0.2606448233127594 +apple/test/overripe/Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.41611751914024353,0.5838824510574341 +apple/test/overripe/Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,overripe,0.0,0.6947497725486755,0.3052501976490021 +apple/test/overripe/Screen Shot 2018-06-07 at 2.34.49 PM.png,overripe,ripe,0.0,0.9685006737709045,0.03149930760264397 +apple/test/overripe/Screen Shot 2018-06-07 at 2.35.38 PM.png,overripe,overripe,0.0,0.4151459336280823,0.5848540663719177 +apple/test/overripe/Screen Shot 2018-06-07 at 2.37.53 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,overripe,0.0,0.8412008285522461,0.1587991863489151 +apple/test/overripe/Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,overripe,0.0,0.5284951329231262,0.47150489687919617 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.4567440152168274,0.5432559847831726 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.35 PM.png,overripe,overripe,0.0,0.4053076505661011,0.5946923494338989 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.44 PM.png,overripe,overripe,0.0,0.8668062686920166,0.133193701505661 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.53 PM.png,overripe,overripe,0.0,0.7685368657112122,0.23146310448646545 +apple/test/overripe/Screen Shot 2018-06-07 at 2.41.14 PM.png,overripe,overripe,0.4464649558067322,0.5535350441932678,0.28827136754989624 +apple/test/overripe/Screen Shot 2018-06-07 at 2.42.37 PM.png,overripe,overripe,0.4459351897239685,0.5540648102760315,0.3769427537918091 +apple/test/overripe/Screen Shot 2018-06-07 at 2.43.48 PM.png,overripe,overripe,0.0,0.4625967741012573,0.5374032258987427 +apple/test/overripe/Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.7383866906166077,0.26161330938339233,0.2556747794151306 +apple/test/overripe/Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.6497551798820496,0.3502448499202728,0.070245660841465 +apple/test/overripe/Screen Shot 2018-06-07 at 2.47.50 PM.png,overripe,overripe,0.015777839347720146,0.6236642599105835,0.3763357400894165 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.08 PM.png,overripe,overripe,0.0,0.4011174440383911,0.5988825559616089 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.16 PM.png,overripe,overripe,0.0,0.40207988023757935,0.5979201197624207 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.37 PM.png,overripe,overripe,0.0,0.629510223865509,0.37048980593681335 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.40342578291893005,0.5965742468833923 +apple/test/overripe/Screen Shot 2018-06-07 at 2.54.41 PM.png,overripe,overripe,0.0,0.4024507999420166,0.5975492000579834 +apple/test/overripe/Screen Shot 2018-06-07 at 2.56.47 PM.png,overripe,overripe,0.04202081263065338,0.8948892951011658,0.10511071234941483 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.04 PM.png,overripe,overripe,0.0,0.40159422159194946,0.5984057784080505 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.30 PM.png,overripe,overripe,0.0,0.5069993734359741,0.49300065636634827 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.4840458333492279,0.5159541964530945 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.09 PM.png,overripe,overripe,0.0,0.5056963562965393,0.4943036437034607 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.13 PM.png,overripe,overripe,0.0,0.4995913803577423,0.5004086494445801 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.25149595737457275,0.5940654873847961,0.40593454241752625 +apple/test/overripe/Screen Shot 2018-06-07 at 3.00.25 PM.png,overripe,ripe,0.15591633319854736,0.8440836668014526,0.014151695184409618 +apple/test/overripe/Screen Shot 2018-06-07 at 3.01.38 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 3.02.37 PM.png,overripe,overripe,0.0,0.506737470626831,0.49326252937316895 +apple/test/overripe/Screen Shot 2018-06-07 at 3.03.02 PM.png,overripe,overripe,0.0,0.5196657776832581,0.48033425211906433 +apple/test/overripe/Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,overripe,0.0,0.651172399520874,0.3488275706768036 +apple/test/overripe/Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.5122983455657959,0.4877016842365265 +apple/test/overripe/Screen Shot 2018-06-07 at 3.04.10 PM.png,overripe,overripe,0.0,0.4810026288032532,0.5189973711967468 +apple/test/overripe/Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.4000111222267151,0.5999888777732849 +apple/test/overripe/Screen Shot 2018-06-07 at 3.06.30 PM.png,overripe,overripe,0.0,0.45365652441978455,0.5463434457778931 +apple/test/overripe/Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.40372738242149353,0.5962726473808289 +apple/test/overripe/Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.47576385736465454,0.5242361426353455 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,overripe,0.8797266483306885,0.12027335166931152,0.15789514780044556 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.17 PM.png,overripe,overripe,0.0,0.4555940628051758,0.5444059371948242 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.24 PM.png,overripe,overripe,0.0,0.5446174740791321,0.4553825259208679 +apple/test/overripe/Screen Shot 2018-06-08 at 2.26.09 PM.png,overripe,overripe,0.0,0.8908619284629822,0.10913805663585663 +apple/test/overripe/Screen Shot 2018-06-08 at 2.26.55 PM.png,overripe,overripe,0.0,0.5725931525230408,0.42740681767463684 +apple/test/overripe/Screen Shot 2018-06-08 at 2.28.07 PM.png,overripe,overripe,0.0,0.7730071544647217,0.22699284553527832 +apple/test/overripe/Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4274121820926666,0.5725878477096558 +apple/test/overripe/Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6652147769927979,0.33478522300720215 +apple/test/overripe/Screen Shot 2018-06-08 at 2.30.45 PM.png,overripe,overripe,0.0,0.6373330950737,0.36266690492630005 +apple/test/overripe/Screen Shot 2018-06-08 at 2.30.51 PM.png,overripe,overripe,0.0,0.5223477482795715,0.4776522219181061 +apple/test/overripe/Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,overripe,0.0,0.5898973941802979,0.41010260581970215 +apple/test/overripe/Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.46232089400291443,0.537679135799408 +apple/test/overripe/Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.4029760956764221,0.5970239043235779 +apple/test/overripe/Screen Shot 2018-06-08 at 2.37.03 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-08 at 2.39.51 PM.png,overripe,overripe,0.0,0.8342049717903137,0.16579505801200867 +apple/test/overripe/Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.673255205154419,0.32674479484558105 +apple/test/overripe/Screen Shot 2018-06-08 at 2.42.58 PM.png,overripe,overripe,0.0,0.8372034430503845,0.16279654204845428 +apple/test/overripe/Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.4752458930015564,0.5247541069984436 +apple/test/overripe/Screen Shot 2018-06-08 at 2.45.44 PM.png,overripe,overripe,0.0,0.5455124974250793,0.45448753237724304 +apple/test/overripe/Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.4003806710243225,0.5996193289756775 +apple/test/overripe/Screen Shot 2018-06-08 at 2.46.25 PM.png,overripe,overripe,0.0,0.4783133268356323,0.5216866731643677 +apple/test/overripe/Screen Shot 2018-06-08 at 2.48.09 PM.png,overripe,overripe,0.0,0.6010626554489136,0.3989373445510864 +apple/test/overripe/Screen Shot 2018-06-08 at 2.48.43 PM.png,overripe,overripe,0.0,0.5000309944152832,0.4999690055847168 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.14 PM.png,overripe,overripe,0.0,0.47492465376853943,0.525075376033783 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.25 PM.png,overripe,overripe,0.0,0.42561447620391846,0.5743855237960815 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.5853391289710999,0.41466087102890015 +apple/test/overripe/Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,overripe,0.0,0.7285541296005249,0.2714458405971527 +apple/test/overripe/Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.40203016996383667,0.5979698300361633 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9715463519096375,0.028453629463911057 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.16.41 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.0,0.7732822299003601,0.2267177850008011 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,overripe,0.0,0.7907986640930176,0.20920135080814362 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.20.29 PM.png,overripe,overripe,0.0,0.46537700295448303,0.5346230268478394 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4167667329311371,0.5832332372665405 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,overripe,0.0,0.6911649107933044,0.30883508920669556 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.25.26 PM.png,overripe,overripe,0.0,0.5935452580451965,0.40645474195480347 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,overripe,0.0,0.7062181830406189,0.2937818467617035 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.31.59 PM.png,overripe,overripe,0.0,0.7885536551475525,0.2114463448524475 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.34.18 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.35.21 PM.png,overripe,overripe,0.0,0.6125141978263855,0.3874858021736145 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0,0.4591410160064697,0.5408589839935303 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.37.01 PM.png,overripe,overripe,0.0,0.40115800499916077,0.5988420248031616 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.37.53 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.38 PM.png,overripe,overripe,0.0,0.537853479385376,0.4621465504169464 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.49 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,overripe,0.0,0.5255343317985535,0.47446566820144653 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.00 PM.png,overripe,ripe,0.0,0.9893104434013367,0.01068955473601818 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,overripe,0.0,0.757502019405365,0.2424979954957962 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,overripe,0.0,0.8992615342140198,0.10073848068714142 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.41.07 PM.png,overripe,overripe,0.0,0.8325942754745483,0.16740570962429047 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.42.25 PM.png,overripe,ripe,0.0,0.9454150795936584,0.054584894329309464 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.42.58 PM.png,overripe,unripe,0.3104250729084015,0.6895749568939209,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,unripe,0.27771270275115967,0.7222872972488403,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,overripe,0.0,0.7088361382484436,0.2911638617515564 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.6719781756401062,0.3280217945575714,0.26958519220352173 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.51 PM.png,overripe,overripe,0.4149506688117981,0.5850493311882019,0.3329164683818817 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.45.09 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.6820212006568909,0.3179788291454315,0.0685202106833458 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.47.35 PM.png,overripe,overripe,0.12156002968549728,0.6743786334991455,0.3256213963031769 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7754268646240234,0.22457313537597656 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.54.49 PM.png,overripe,ripe,0.0,0.9928542971611023,0.007145698182284832 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.55.27 PM.png,overripe,ripe,0.0,0.9337061047554016,0.06629391759634018 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,overripe,0.0,0.5612080693244934,0.4387919008731842 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.57.26 PM.png,overripe,overripe,0.0,0.618722677230835,0.38127732276916504 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.48349490761756897,0.5165051221847534 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.4259305000305176,0.5740694999694824 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.2830982506275177,0.5942046642303467,0.4057953357696533 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.59.52 PM.png,overripe,overripe,0.0,0.8939687609672546,0.10603123903274536 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,overripe,0.020378662273287773,0.4889051020145416,0.5110949277877808 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.0,0.4954286217689514,0.5045713782310486 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,overripe,0.0,0.8426916599273682,0.15730832517147064 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.02.51 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,overripe,0.0,0.8522940278053284,0.14770597219467163 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,overripe,0.0,0.8701097965240479,0.12989021837711334 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.21.33 PM.png,overripe,overripe,0.0,0.9123019576072693,0.08769804239273071 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.23.48 PM.png,overripe,overripe,0.0,0.9097192287445068,0.09028076380491257 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.31 PM.png,overripe,overripe,0.0,0.6411243081092834,0.35887566208839417 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.37 PM.png,overripe,overripe,0.0,0.7509406208992004,0.24905939400196075 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.4570573568344116,0.5429426431655884 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,ripe,0.0,0.9484506249427795,0.051549363881349564 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,overripe,0.0,0.9039967060089111,0.09600330889225006 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,overripe,0.0,0.6738445162773132,0.32615548372268677 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.30.31 PM.png,overripe,overripe,0.0,0.49428844451904297,0.505711555480957 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.32.10 PM.png,overripe,overripe,0.0,0.4000283479690552,0.5999716520309448 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.33.04 PM.png,overripe,overripe,0.0,0.4028175473213196,0.5971824526786804 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.4613722562789917,0.5386277437210083 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,overripe,0.7866154909133911,0.2133845090866089,0.1290050595998764 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.9359455704689026,0.06405443698167801 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.37.24 PM.png,overripe,overripe,0.0,0.5216484069824219,0.4783516228199005 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.40.30 PM.png,overripe,overripe,0.0,0.5342982411384583,0.46570178866386414 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.46.44 PM.png,overripe,overripe,0.0,0.5430364608764648,0.45696350932121277 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.47.54 PM.png,overripe,overripe,0.0,0.4334219694137573,0.5665780305862427 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.48.15 PM.png,overripe,overripe,0.0,0.6750063300132751,0.32499366998672485 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.50.22 PM.png,overripe,overripe,0.0,0.5433583855628967,0.45664164423942566 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.44550204277038574,0.5544979572296143 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.0,0.7725133299827576,0.22748669981956482 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.20.46 PM.png,overripe,overripe,0.0,0.6202629208564758,0.37973710894584656 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.23.02 PM.png,overripe,ripe,0.0,0.9786281585693359,0.021371841430664062 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,ripe,0.0,0.9604795575141907,0.03952043130993843 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0,0.4533323049545288,0.5466676950454712 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.37.11 PM.png,overripe,unripe,0.17482762038707733,0.8251723647117615,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.38.49 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,overripe,0.0,0.900539219379425,0.09946078062057495 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.40.55 PM.png,overripe,overripe,0.28001394867897034,0.719986081123352,0.26319536566734314 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.42.25 PM.png,overripe,ripe,0.0,0.9454296827316284,0.054570313543081284 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.43.26 PM.png,overripe,overripe,0.0,0.9235553741455078,0.0764446035027504 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.45.44 PM.png,overripe,overripe,0.0,0.40441927313804626,0.5955807566642761 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.46.04 PM.png,overripe,overripe,0.0,0.4034445881843567,0.5965554118156433 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.50.31 PM.png,overripe,overripe,0.0,0.5596411824226379,0.44035884737968445 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.00 PM.png,overripe,overripe,0.0,0.6456087231636047,0.35439130663871765 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.30 PM.png,overripe,overripe,0.0,0.7386246919631958,0.2613752782344818 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7784644961357117,0.22153553366661072 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.53.20 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.53.33 PM.png,overripe,ripe,0.0,0.9908462762832642,0.009153752587735653 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,overripe,0.0,0.7139455080032349,0.28605449199676514 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.55.27 PM.png,overripe,ripe,0.0,0.9370118379592896,0.06298813223838806 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.55.52 PM.png,overripe,overripe,0.0,0.44823309779167175,0.5517669320106506 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.4985315799713135,0.5014684200286865 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.16 PM.png,overripe,overripe,0.0,0.4217042624950409,0.5782957673072815 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,overripe,0.0,0.5606579780578613,0.43934205174446106 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.57.17 PM.png,overripe,overripe,0.0,0.4746156632900238,0.5253843069076538 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.4838150143623352,0.5161849856376648 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.430745005607605,0.569254994392395 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.59.09 PM.png,overripe,overripe,0.0,0.4946509599685669,0.5053490400314331 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.00.56 PM.png,overripe,overripe,0.0,0.50571608543396,0.49428388476371765 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.01.38 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.0,0.4902205467224121,0.5097794532775879 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,overripe,0.0,0.8419982194900513,0.15800176560878754 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.35 PM.png,overripe,overripe,0.0,0.5943114161491394,0.4056885540485382 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.41 PM.png,overripe,overripe,0.0,0.4576709270477295,0.5423290729522705 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.58 PM.png,overripe,overripe,0.0,0.8991056084632874,0.10089437663555145 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.4001377820968628,0.5998622179031372 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.3839193880558014,0.6160805821418762,0.3025771975517273 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.53 PM.png,overripe,overripe,0.0,0.6678308844566345,0.3321690857410431 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.8726078867912292,0.12739208340644836 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.17.25 PM.png,overripe,overripe,0.0,0.922139048576355,0.07786095887422562 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.403615802526474,0.5963841676712036 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.24.09 PM.png,overripe,overripe,0.0,0.6466928124427795,0.35330715775489807 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,ripe,0.0,0.9466292262077332,0.053370777517557144 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.27.15 PM.png,overripe,overripe,0.0,0.673625111579895,0.326374888420105 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,overripe,0.0,0.6713618040084839,0.3286381661891937 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,overripe,0.0,0.6902748942375183,0.3097251057624817 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,overripe,0.0,0.5760325789451599,0.4239674210548401 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.31.23 PM.png,overripe,overripe,0.0,0.6658828258514404,0.3341171443462372 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.32.42 PM.png,overripe,overripe,0.0,0.40090909600257874,0.5990909337997437 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.40282049775123596,0.5971795320510864 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.35.03 PM.png,overripe,overripe,0.0,0.42906108498573303,0.5709388852119446 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.40571436285972595,0.5942856669425964 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.40.13 PM.png,overripe,overripe,0.0,0.8539280295372009,0.14607197046279907 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.44.54 PM.png,overripe,overripe,0.0,0.7608470320701599,0.23915298283100128 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.45.58 PM.png,overripe,overripe,0.0,0.8637757301330566,0.13622424006462097 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.47.30 PM.png,overripe,overripe,0.0,0.9272762537002563,0.07272374629974365 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.49.40 PM.png,overripe,overripe,0.0,0.47151991724967957,0.5284801125526428 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.22 PM.png,overripe,overripe,0.0,0.5445718169212341,0.45542818307876587 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.44615066051483154,0.5538493394851685 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.5921676158905029,0.4078323543071747 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.52.43 PM.png,overripe,ripe,0.0,0.9764948487281799,0.023505177348852158 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7503979802131653,0.2496020495891571 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9721093773841858,0.02789059840142727 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.5107917189598083,0.48920831084251404,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.20.56 PM.png,overripe,overripe,0.0,0.4332394599914551,0.5667605400085449 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.23.02 PM.png,overripe,ripe,0.0,0.9789144396781921,0.02108553983271122 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,overripe,0.0,0.9134517908096313,0.08654821664094925 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.24.35 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.24.59 PM.png,overripe,ripe,0.0,0.9645125865936279,0.035487402230501175 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.31.59 PM.png,overripe,overripe,0.0,0.7924618124961853,0.2075382024049759 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.34.36 PM.png,overripe,overripe,0.0,0.6045212149620056,0.395478755235672 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.34.49 PM.png,overripe,ripe,0.0,0.9679333567619324,0.032066620886325836 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.37.20 PM.png,overripe,unripe,0.7603256702423096,0.23967434465885162,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,overripe,0.0,0.798288881778717,0.20171111822128296 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.39.26 PM.png,overripe,overripe,0.0,0.4018523097038269,0.5981476902961731 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.40.13 PM.png,overripe,overripe,0.0,0.4219289720058441,0.5780709981918335 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,overripe,0.0,0.7577015161514282,0.24229846894741058 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.42.18 PM.png,overripe,overripe,0.1727154701948166,0.7210397720336914,0.278960257768631 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.5885818600654602,0.4114181399345398,0.28067031502723694 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4064505100250244,0.5935494899749756 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.47.27 PM.png,overripe,overripe,0.0,0.6672261953353882,0.33277377486228943 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,overripe,0.0,0.7319165468215942,0.26808348298072815 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4111475646495819,0.5888524055480957 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7811104655265808,0.21888954937458038 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.53.20 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.56.34 PM.png,overripe,ripe,0.0,0.9995189309120178,0.00048107540351338685 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,overripe,0.0,0.5613541007041931,0.4386458992958069 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.57.42 PM.png,overripe,overripe,0.0,0.4012296795845032,0.5987703204154968 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.57.49 PM.png,overripe,overripe,0.0,0.4547550082206726,0.5452449917793274 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.58.30 PM.png,overripe,overripe,0.0,0.5120881199836731,0.4879119098186493 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.59.38 PM.png,overripe,overripe,0.061822112649679184,0.6771993041038513,0.3228006660938263 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.59.52 PM.png,overripe,overripe,0.0,0.9062848687171936,0.093715138733387 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,overripe,0.0,0.46708184480667114,0.5329181551933289 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.33 PM.png,overripe,unripe,0.354729562997818,0.6452704668045044,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.40 PM.png,overripe,unripe,0.41769808530807495,0.582301914691925,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,overripe,0.0,0.5197792649269104,0.4802207350730896 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.0,0.4922020733356476,0.50779789686203 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.02.18 PM.png,overripe,overripe,0.0,0.8980525732040405,0.10194742679595947 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.02.37 PM.png,overripe,overripe,0.0,0.4867391884326935,0.5132607817649841 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.03.02 PM.png,overripe,overripe,0.0,0.5392215847969055,0.4607784152030945 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.04.24 PM.png,overripe,overripe,0.0,0.4931606352329254,0.506839394569397 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.29 PM.png,overripe,overripe,0.0,0.7502533197402954,0.2497466802597046 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.4000743627548218,0.5999256372451782 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.35470518469810486,0.6452948451042175,0.3057732880115509 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.8692718744277954,0.13072814047336578 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4037482440471649,0.5962517857551575 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5730201005935669,0.4269798994064331 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.4383915364742279,0.5616084337234497 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6576327085494995,0.3423672914505005 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.28.42 PM.png,overripe,overripe,0.9027223587036133,0.09727761149406433,0.15483009815216064 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.29.33 PM.png,overripe,overripe,0.0,0.7223621010780334,0.27763786911964417 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.30.51 PM.png,overripe,overripe,0.0,0.5211623907089233,0.4788375794887543 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.31.16 PM.png,overripe,overripe,0.0,0.40848302841186523,0.5915169715881348 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,overripe,0.0,0.5428787469863892,0.45712122321128845 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.4635903835296631,0.5364096164703369 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.34.42 PM.png,overripe,overripe,0.0,0.4156161844730377,0.5843838453292847 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.35.03 PM.png,overripe,overripe,0.0,0.42936787009239197,0.5706321001052856 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,overripe,0.0,0.9047697186470032,0.09523027390241623 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,overripe,0.7800638675689697,0.21993611752986908,0.12854766845703125 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.9378790855407715,0.06212090328335762 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,overripe,0.0,0.8238171339035034,0.1761828511953354 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.37.19 PM.png,overripe,overripe,0.0,0.8321505784988403,0.16784945130348206 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,overripe,0.0,0.7880102396011353,0.21198976039886475 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.39.26 PM.png,overripe,overripe,0.0,0.5613294839859009,0.43867048621177673 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.41.39 PM.png,overripe,overripe,0.0,0.6412169337272644,0.3587830662727356 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.41.44 PM.png,overripe,overripe,0.0,0.6999731659889221,0.3000268340110779 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.06 PM.png,overripe,overripe,0.0,0.7010692358016968,0.2989307641983032 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6934642791748047,0.3065357506275177 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.38 PM.png,overripe,overripe,0.0,0.6238390207290649,0.37616097927093506 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.52 PM.png,overripe,ripe,0.0,0.9718584418296814,0.0281415693461895 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.43.29 PM.png,overripe,overripe,0.0,0.8585364818572998,0.1414635330438614 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.44.13 PM.png,overripe,overripe,0.0,0.4154403805732727,0.5845596194267273 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.46.36 PM.png,overripe,overripe,0.0,0.4000353515148163,0.5999646782875061 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.46.50 PM.png,overripe,overripe,0.0,0.4009704291820526,0.599029541015625 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.47.03 PM.png,overripe,overripe,0.0,0.40370893478393555,0.5962910652160645 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.47.30 PM.png,overripe,overripe,0.0,0.9249141216278076,0.07508587092161179 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.50.14 PM.png,overripe,overripe,0.0,0.476966917514801,0.523033082485199 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.17.25 PM.png,overripe,overripe,0.0,0.8460526466369629,0.15394733846187592 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.20.46 PM.png,overripe,overripe,0.0,0.618087112903595,0.38191288709640503 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,overripe,0.0,0.8651021122932434,0.1348978579044342 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.23.51 PM.png,overripe,overripe,0.0,0.7870917916297913,0.21290822327136993 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0,0.45819979906082153,0.5418002009391785 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.20 PM.png,overripe,unripe,0.7523589730262756,0.24764101207256317,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.32 PM.png,overripe,overripe,0.0,0.44494783878326416,0.5550521612167358 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,overripe,0.0,0.7950502038002014,0.20494982600212097 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,overripe,0.0,0.7571588754653931,0.24284113943576813 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.41.14 PM.png,overripe,overripe,0.6806260347366333,0.3193739652633667,0.2642599046230316 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.43.26 PM.png,overripe,overripe,0.0,0.9220841526985168,0.07791587710380554 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.43.54 PM.png,overripe,ripe,0.0,0.9831035137176514,0.016896475106477737 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4064321517944336,0.5935678482055664 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.4089357852935791,0.5910642147064209 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.47.13 PM.png,overripe,overripe,0.0,0.6612895727157593,0.3387104272842407 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.47.20 PM.png,overripe,overripe,0.0,0.6938263773918152,0.3061736524105072 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7798469662666321,0.2201530635356903 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.49803733825683594,0.5019626617431641 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.56.47 PM.png,overripe,overripe,0.03957240656018257,0.8992016911506653,0.10079827904701233 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.4838564097881317,0.5161436200141907 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.00.17 PM.png,overripe,unripe,0.20792080461978912,0.7920792102813721,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.0,0.493038147687912,0.5069618821144104 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.03.38 PM.png,overripe,overripe,0.0,0.8840967416763306,0.11590326577425003 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.48395782709121704,0.516042172908783 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.04.35 PM.png,overripe,overripe,0.0,0.5953361392021179,0.40466389060020447 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,overripe,0.0,0.8522734045982361,0.1477266252040863 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.36458250880241394,0.6354175209999084,0.3040161430835724 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.58 PM.png,overripe,overripe,0.0,0.7628408074378967,0.23715916275978088 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,overripe,0.8963135480880737,0.10368646681308746,0.16008228063583374 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6576866507530212,0.34231334924697876 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.26.34 PM.png,overripe,overripe,0.029033886268734932,0.6242862343788147,0.3757137656211853 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6649394035339355,0.33506062626838684 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,overripe,0.0,0.6951485872268677,0.3048514425754547 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.34.51 PM.png,overripe,overripe,0.0,0.4146740138530731,0.5853259563446045 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,overripe,0.0,0.9060477018356323,0.09395232051610947 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.18 PM.png,overripe,overripe,0.0,0.7231545448303223,0.27684545516967773 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.937943696975708,0.06205630302429199 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,overripe,0.0,0.8244467973709106,0.17555321753025055 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.37.19 PM.png,overripe,overripe,0.0,0.8306971192359924,0.16930289566516876 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.37.24 PM.png,overripe,overripe,0.0,0.5196489095687866,0.4803510904312134 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,overripe,0.0,0.7875044345855713,0.21249555051326752 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.38.47 PM.png,overripe,overripe,0.0,0.44283029437065125,0.5571697354316711 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.09 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.30 PM.png,overripe,overripe,0.0,0.5303066372871399,0.4696933925151825 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.56 PM.png,overripe,overripe,0.0,0.40001869201660156,0.5999813079833984 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.41.16 PM.png,overripe,overripe,0.0,0.4580136239528656,0.541986346244812 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6924272179603577,0.30757275223731995 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.47307655215263367,0.526923418045044 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.44.13 PM.png,overripe,overripe,0.0,0.41497543454170227,0.5850245356559753 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.45.05 PM.png,overripe,overripe,0.0,0.757743239402771,0.2422567456960678 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.47.09 PM.png,overripe,overripe,0.0,0.4060509204864502,0.5939490795135498 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,overripe,0.24920807778835297,0.6142333745956421,0.3857666254043579 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.50.38 PM.png,overripe,overripe,0.0,0.5519735217094421,0.44802647829055786 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.583672821521759,0.41632720828056335 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.51.09 PM.png,overripe,overripe,0.0,0.4200159013271332,0.5799840688705444 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.52.05 PM.png,overripe,overripe,0.0,0.6523156762123108,0.3476843237876892 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.52.20 PM.png,overripe,overripe,0.0,0.4023451805114746,0.5976548194885254 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7524235248565674,0.2475764900445938 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.49637454748153687,0.5036254525184631,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4168919026851654,0.5831080675125122 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,overripe,0.0,0.8645575046539307,0.13544252514839172 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,overripe,0.0,0.9137449860572815,0.0862550139427185 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,ripe,0.0,0.9618739485740662,0.03812604770064354 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,overripe,0.0,0.706511914730072,0.293488085269928 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.34.18 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0,0.46477556228637695,0.535224437713623 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.37.11 PM.png,overripe,unripe,0.17529533803462982,0.824704647064209,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.38.38 PM.png,overripe,overripe,0.0,0.5377466082572937,0.4622534215450287 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.35 PM.png,overripe,overripe,0.0,0.41284480690956116,0.5871552228927612 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.44 PM.png,overripe,overripe,0.0,0.8704461455345154,0.12955383956432343 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.53 PM.png,overripe,overripe,0.0,0.7720098495483398,0.22799015045166016 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.40.00 PM.png,overripe,ripe,0.0,0.9891064763069153,0.010893509723246098 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.41.23 PM.png,overripe,overripe,0.12770488858222961,0.7178747653961182,0.28212523460388184 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.41.32 PM.png,overripe,ripe,0.0,0.9452999234199524,0.05470006540417671 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.42.58 PM.png,overripe,unripe,0.20567569136619568,0.7943242788314819,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.43.54 PM.png,overripe,ripe,0.0,0.9861157536506653,0.01388426125049591 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,overripe,0.0,0.7130953669548035,0.28690463304519653 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.44.59 PM.png,overripe,overripe,0.0,0.48406314849853516,0.5159368515014648 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4065180718898773,0.5934818983078003 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.46.12 PM.png,overripe,overripe,0.0,0.40837568044662476,0.5916243195533752 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.47.13 PM.png,overripe,overripe,0.0,0.6649071574211121,0.33509281277656555 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.50.31 PM.png,overripe,overripe,0.0,0.5655757188796997,0.4344242513179779 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.52.00 PM.png,overripe,overripe,0.0,0.6512171030044556,0.3487829267978668 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.40232205390930176,0.5976779460906982 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.54.08 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,overripe,0.0,0.7134120464324951,0.2865879535675049 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,overripe,0.0,0.9298586845397949,0.07014130055904388 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,overripe,0.0,0.8428557515144348,0.15714427828788757 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.02.24 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.41983821988105774,0.5801617503166199,0.29152706265449524 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,overripe,0.0,0.8624950051307678,0.13750497996807098 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.06.51 PM.png,overripe,overripe,0.0,0.5567119121551514,0.443288117647171 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.25.43 PM.png,overripe,overripe,0.0,0.6467553377151489,0.3532446622848511 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.65692138671875,0.34307861328125 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,overripe,0.0,0.672228217124939,0.32777178287506104 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6650078296661377,0.3349922001361847 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,overripe,0.0,0.5811682939529419,0.4188316762447357 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.16 PM.png,overripe,overripe,0.0,0.40780648589134216,0.5921934843063354 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,overripe,0.0,0.5433295369148254,0.45667049288749695 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.33.49 PM.png,overripe,overripe,0.0,0.9065655469894409,0.0934344232082367 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.34.05 PM.png,overripe,overripe,0.0,0.8050721883773804,0.19492782652378082 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.40302008390426636,0.5969799160957336 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,overripe,0.0,0.8220764398574829,0.17792357504367828 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.37.13 PM.png,overripe,overripe,0.0,0.9001384973526001,0.09986147284507751 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.38.33 PM.png,overripe,overripe,0.0,0.7053986191749573,0.29460135102272034 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.4058590233325958,0.5941410064697266 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.13 PM.png,overripe,overripe,0.0,0.8514665961265564,0.1485334038734436 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.38 PM.png,overripe,overripe,0.0,0.4369279146194458,0.5630720853805542 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.46 PM.png,overripe,overripe,0.0,0.40104687213897705,0.598953127861023 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.41.54 PM.png,overripe,overripe,0.0,0.41170641779899597,0.5882935523986816 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.42.38 PM.png,overripe,overripe,0.0,0.6237150430679321,0.37628498673439026 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.42.58 PM.png,overripe,overripe,0.0,0.8369565606117249,0.16304342448711395 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.43.29 PM.png,overripe,overripe,0.0,0.8233847618103027,0.17661526799201965 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.4740639925003052,0.5259360074996948 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.44.54 PM.png,overripe,overripe,0.0,0.7609834671020508,0.2390165477991104 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.4001534581184387,0.5998465418815613 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.09 PM.png,overripe,overripe,0.0,0.4072756767272949,0.5927243232727051 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.37 PM.png,overripe,overripe,0.0,0.42806476354599,0.57193523645401 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,overripe,0.3030296564102173,0.6268961429595947,0.3731038272380829 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.54 PM.png,overripe,overripe,0.0,0.43351301550865173,0.5664869546890259 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.48.29 PM.png,overripe,overripe,0.18969136476516724,0.6451812386512756,0.35481876134872437 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.49.27 PM.png,overripe,overripe,0.0,0.8743897676467896,0.12561026215553284 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.50.55 PM.png,overripe,overripe,0.0,0.4552995264530182,0.5447004437446594 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.4019162952899933,0.5980837345123291 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7507896423339844,0.24921034276485443 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9714825749397278,0.02851744554936886 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.16.54 PM.png,overripe,overripe,0.0,0.878029465675354,0.121970534324646 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.0,0.7641037106513977,0.2358963042497635 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.18.57 PM.png,overripe,overripe,0.0,0.6867389678955078,0.3132610321044922 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,overripe,0.0,0.7903935313224792,0.20960646867752075 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,overripe,0.0,0.915632426738739,0.08436758071184158 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.23.51 PM.png,overripe,overripe,0.0,0.7877522110939026,0.21224777400493622 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.33.47 PM.png,overripe,overripe,0.0,0.5192683339118958,0.48073166608810425 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,overripe,0.0,0.8375776410102844,0.16242235898971558 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,overripe,0.0,0.9051539301872253,0.09484604746103287 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,overripe,0.0,0.5279340147972107,0.4720659852027893 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.45817604660987854,0.5418239235877991 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.40.13 PM.png,overripe,overripe,0.0,0.43309399485588074,0.5669059753417969 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.40.55 PM.png,overripe,overripe,0.3390347361564636,0.6609652638435364,0.24599744379520416 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.41.32 PM.png,overripe,ripe,0.0,0.9455589056015015,0.054441098123788834 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,unripe,0.23520340025424957,0.7647966146469116,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.43.48 PM.png,overripe,overripe,0.0,0.46177417039871216,0.5382258296012878 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,overripe,0.0,0.7231303453445435,0.27686968445777893 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4083921015262604,0.591607928276062 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.4126001298427582,0.5873998403549194 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.6328919529914856,0.3671080768108368,0.0721169114112854 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.47.20 PM.png,overripe,overripe,0.0,0.699925422668457,0.30007457733154297 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,overripe,0.0,0.7114904522895813,0.2885095477104187 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4138316512107849,0.5861683487892151 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.52.09 PM.png,overripe,overripe,0.0,0.9201138615608215,0.07988613843917847 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.52.30 PM.png,overripe,overripe,0.0,0.7525773644447327,0.24742262065410614 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.53.33 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.4051350951194763,0.5948649048805237 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.54.08 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.54.41 PM.png,overripe,overripe,0.0,0.40442129969596863,0.5955787301063538 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.4986497759819031,0.5013502240180969 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,overripe,0.0,0.9270163774490356,0.07298363000154495 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.09063193947076797,0.5777403712272644,0.4222596287727356 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.00.40 PM.png,overripe,unripe,0.42857474088668823,0.5714252591133118,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,overripe,0.0,0.5290267467498779,0.47097325325012207 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.03.12 PM.png,overripe,overripe,0.0,0.4084378778934479,0.5915621519088745 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.5121507048606873,0.48784932494163513 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.04.47 PM.png,overripe,overripe,0.0,0.4453652799129486,0.554634690284729 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.05.05 PM.png,overripe,overripe,0.0,0.7708693742752075,0.22913064062595367 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.8810673952102661,0.11893261969089508 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.06.30 PM.png,overripe,overripe,0.0,0.4557962715625763,0.5442037582397461 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.17.25 PM.png,overripe,overripe,0.0,0.9105139970779419,0.0894860029220581 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.21.33 PM.png,overripe,overripe,0.0,0.9064176678657532,0.09358230233192444 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5706241130828857,0.42937585711479187 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.24.37 PM.png,overripe,overripe,0.0,0.7543639540672302,0.24563604593276978 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6581040024757385,0.3418959677219391 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.27.15 PM.png,overripe,overripe,0.0,0.6649149656295776,0.33508506417274475 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.27.44 PM.png,overripe,overripe,0.0,0.7586776614189148,0.241322323679924 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4280434548854828,0.5719565153121948 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6672834753990173,0.33271652460098267 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,overripe,0.0,0.6989516615867615,0.3010483384132385 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.30.57 PM.png,overripe,overripe,0.0,0.6720190644264221,0.32798096537590027 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.31.08 PM.png,overripe,overripe,0.0,0.4663427770137787,0.5336572527885437 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.404620498418808,0.5953795313835144 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,overripe,0.0,0.9065495729446411,0.09345041215419769 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.35.37 PM.png,overripe,overripe,0.0,0.48539507389068604,0.514604926109314 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.36.01 PM.png,overripe,overripe,0.0,0.44680801033973694,0.5531920194625854 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.36.23 PM.png,overripe,overripe,0.9791917204856873,0.020808260887861252,0.1654510200023651 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.39.26 PM.png,overripe,overripe,0.0,0.556162416934967,0.44383761286735535 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.45.50 PM.png,overripe,overripe,0.0,0.5189896821975708,0.4810103178024292 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.46.36 PM.png,overripe,overripe,0.0,0.4018825590610504,0.598117470741272 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,overripe,0.5828765034675598,0.4171234965324402,0.309283584356308 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4475242495536804,0.5524757504463196 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.51.09 PM.png,overripe,overripe,0.0,0.40541985630989075,0.5945801138877869 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,overripe,0.0,0.73084557056427,0.26915445923805237 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9712456464767456,0.028754334896802902 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.16.41 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.17.15 PM.png,overripe,overripe,0.0,0.6404872536659241,0.3595127761363983 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.5234154462814331,0.4765845239162445,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,overripe,0.0,0.7896710634231567,0.21032896637916565 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.20.34 PM.png,overripe,overripe,0.0,0.7371730208396912,0.2628270089626312 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,overripe,0.0,0.8572678565979004,0.1427321583032608 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,ripe,0.0,0.9735211133956909,0.026478858664631844 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.35.21 PM.png,overripe,overripe,0.0,0.5938919186592102,0.4061080515384674 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.36.06 PM.png,overripe,overripe,0.0,0.46881529688835144,0.531184732913971 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.37.01 PM.png,overripe,overripe,0.0,0.40177085995674133,0.5982291102409363 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,overripe,0.0,0.8994163870811462,0.10058359056711197 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.44502636790275574,0.5549736022949219 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.39.26 PM.png,overripe,overripe,0.0,0.4000151455402374,0.599984884262085 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,overripe,0.0,0.8931564092636108,0.10684358328580856 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.44.26 PM.png,overripe,overripe,0.0,0.49945008754730225,0.5005499124526978 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.44.59 PM.png,overripe,overripe,0.0,0.4779168665409088,0.5220831632614136 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.6933403015136719,0.3066597282886505,0.06887143105268478 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.47.27 PM.png,overripe,overripe,0.0,0.6357606053352356,0.3642393946647644 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.47.35 PM.png,overripe,overripe,0.043857477605342865,0.6661539673805237,0.33384600281715393 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.50.09 PM.png,overripe,overripe,0.0,0.40023303031921387,0.5997669696807861 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,overripe,0.0,0.7107493281364441,0.2892506718635559 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7822774648666382,0.21772253513336182 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.54.49 PM.png,overripe,ripe,0.0,0.9932731986045837,0.0067268176935613155 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,overripe,0.0,0.6962710618972778,0.30372896790504456 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.4975249469280243,0.5024750232696533 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,overripe,0.0,0.9244544506072998,0.0755455493927002 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.57.13 PM.png,overripe,overripe,0.0,0.6060338616371155,0.3939661383628845 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.43313777446746826,0.5668622255325317 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.00.25 PM.png,overripe,unripe,0.18098516762256622,0.819014847278595,0.012614683248102665 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.02.02 PM.png,overripe,overripe,0.0,0.7056351900100708,0.2943648099899292 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.12 PM.png,overripe,overripe,0.0,0.40677163004875183,0.5932283997535706 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.21 PM.png,overripe,overripe,0.0,0.5958901643753052,0.4041098356246948 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,overripe,0.0,0.6445797681808472,0.35542023181915283 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.58 PM.png,overripe,overripe,0.0,0.45236510038375854,0.5476348996162415 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.04.10 PM.png,overripe,overripe,0.0,0.4658568501472473,0.5341431498527527 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,overripe,0.0,0.8415022492408752,0.15849775075912476 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.8709063529968262,0.12909366190433502 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.11 PM.png,overripe,overripe,0.0,0.44277846813201904,0.557221531867981 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,overripe,0.0,0.8671301007270813,0.1328699290752411 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5564792156219482,0.44352078437805176 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,overripe,0.8969513177871704,0.1030486598610878,0.1596912145614624 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.26.34 PM.png,overripe,overripe,0.14518886804580688,0.6447352170944214,0.3552647829055786 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,ripe,0.0,0.951077401638031,0.04892260208725929 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.28.00 PM.png,overripe,overripe,0.0,0.7629311680793762,0.2370688021183014 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,overripe,0.0,0.8979910016059875,0.10200896859169006 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4320123791694641,0.5679876208305359 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.30.26 PM.png,overripe,overripe,0.0,0.4130837619304657,0.5869162678718567 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,overripe,0.0,0.5435057878494263,0.45649421215057373 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.34.42 PM.png,overripe,overripe,0.0,0.4164946973323822,0.5835052728652954 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.4032052457332611,0.5967947840690613 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.36.23 PM.png,overripe,overripe,0.8919963240623474,0.1080036535859108,0.1551113873720169 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,overripe,0.0,0.8071441054344177,0.19285592436790466 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.41.44 PM.png,overripe,overripe,0.0,0.6994486451148987,0.3005513548851013 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.45.50 PM.png,overripe,overripe,0.0,0.532703697681427,0.467296302318573 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.40027371048927307,0.5997263193130493 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.47.37 PM.png,overripe,overripe,0.0,0.42370370030403137,0.5762962698936462 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.00 PM.png,overripe,overripe,0.0,0.4799870550632477,0.5200129747390747 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.29 PM.png,overripe,overripe,0.33611994981765747,0.6638800501823425,0.33570191264152527 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.43 PM.png,overripe,overripe,0.0,0.5027751922607422,0.4972247779369354 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.49.27 PM.png,overripe,overripe,0.0,0.8709213733673096,0.12907862663269043 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4466548264026642,0.5533452033996582 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.5924778580665588,0.40752214193344116 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,overripe,0.0,0.7323070168495178,0.26769301295280457 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.4021112024784088,0.5978888273239136 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.15.50 PM.png,overripe,unripe,0.5441098809242249,0.45589011907577515,0.022567344829440117 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,ripe,0.0,0.9718082547187805,0.028191743418574333 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.17.15 PM.png,overripe,overripe,0.0,0.6536400318145752,0.3463599681854248 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.20.04 PM.png,overripe,overripe,0.0,0.8036158084869385,0.19638420641422272 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.20.29 PM.png,overripe,overripe,0.0,0.4672008156776428,0.5327991843223572 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4152916967868805,0.5847082734107971 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,overripe,0.0,0.6955326199531555,0.3044673502445221 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,overripe,0.0,0.706350564956665,0.29364943504333496 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.33.47 PM.png,overripe,overripe,0.0,0.5176990628242493,0.4823009669780731 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,overripe,0.0,0.7892229557037354,0.21077704429626465 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.04 PM.png,overripe,ripe,0.0,0.9339619874954224,0.06603803485631943 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,overripe,0.0,0.8408974409103394,0.15910254418849945 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,overripe,0.0,0.9074229001998901,0.09257707744836807 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.4567457437515259,0.5432542562484741 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,unripe,0.27429473400115967,0.7257052659988403,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.43.34 PM.png,overripe,overripe,0.0,0.4376106262207031,0.5623893737792969 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.7421489953994751,0.2578509747982025,0.2563478648662567 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.45.18 PM.png,overripe,overripe,0.6323552131652832,0.3676447570323944,0.15925393998622894 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.45.35 PM.png,overripe,overripe,0.0,0.4121467173099518,0.5878533124923706 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.46.04 PM.png,overripe,overripe,0.0,0.40354830026626587,0.5964516997337341 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.4104720950126648,0.5895279049873352 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.47.01 PM.png,overripe,overripe,0.0,0.40004679560661316,0.5999531745910645 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4117365777492523,0.5882633924484253 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.21892867982387543,0.5944240093231201,0.4055759906768799 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,overripe,0.023419488221406937,0.4893522560596466,0.510647714138031 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,overripe,0.0,0.5317201018333435,0.4682798981666565 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.21 PM.png,overripe,overripe,0.0,0.5934100151062012,0.40658998489379883 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,overripe,0.0,0.651211142539978,0.3487888276576996 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.38 PM.png,overripe,overripe,0.0,0.8849955201148987,0.11500450223684311 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.04.47 PM.png,overripe,overripe,0.0,0.4464649260044098,0.5535350441932678 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5680946111679077,0.4319054186344147 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.47576484084129333,0.524235188961029 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.25.24 PM.png,overripe,overripe,0.0,0.5446217060089111,0.45537832379341125 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.26.09 PM.png,overripe,overripe,0.0,0.8913058638572693,0.10869413614273071 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6562231779098511,0.3437768518924713 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,overripe,0.0,0.9088462591171265,0.09115372598171234 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.30.57 PM.png,overripe,overripe,0.0,0.6739908456802368,0.3260091245174408 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.33.28 PM.png,overripe,overripe,0.0,0.618596613407135,0.381403386592865 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.33.49 PM.png,overripe,overripe,0.0,0.9042768478393555,0.09572317451238632 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.46267759799957275,0.5373224020004272 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,overripe,0.8326222896575928,0.16737769544124603,0.13146556913852692 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,ripe,0.0,0.9349681735038757,0.06503181159496307 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.37.03 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.40590226650238037,0.5940977334976196 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.39.21 PM.png,overripe,unripe,0.2118154913187027,0.7881845235824585,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.40.38 PM.png,overripe,overripe,0.0,0.4407237470149994,0.559276282787323 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.40.46 PM.png,overripe,overripe,0.0,0.4008391499519348,0.5991608500480652 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.42.06 PM.png,overripe,overripe,0.0,0.7079958915710449,0.29200413823127747 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6744086742401123,0.3255913555622101 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.46.50 PM.png,overripe,overripe,0.0,0.4001723825931549,0.5998275876045227 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.47.03 PM.png,overripe,overripe,0.0,0.4038361608982086,0.596163809299469 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.48.00 PM.png,overripe,overripe,0.0,0.4755285382270813,0.5244714617729187 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.50.25 PM.png,overripe,overripe,0.0,0.4256644546985626,0.5743355751037598 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.51.43 PM.png,overripe,overripe,0.10852902382612228,0.7060053944587708,0.29399460554122925 +apple/test/ripe/Screen Shot 2018-06-08 at 4.59.44 PM.png,ripe,unripe,0.1722540408372879,0.8277459740638733,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.15 PM.png,ripe,unripe,0.11698544770479202,0.8830145597457886,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,0.9643086194992065,0.03569139540195465 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.42867955565452576,0.5713204145431519 +apple/test/ripe/Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,unripe,0.09802675992250443,0.9019732475280762,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.04.16 PM.png,ripe,unripe,0.17173637449741364,0.8282636404037476,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.04.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.22700157761573792,0.7729984521865845,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.4001169204711914,0.5998830795288086 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.18 PM.png,ripe,overripe,0.035962872207164764,0.9126893877983093,0.08731061965227127 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,unripe,0.1614929437637329,0.8385070562362671,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.8564318418502808,0.14356815814971924,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.17 PM.png,ripe,ripe,0.07526206970214844,0.9247379302978516,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.31 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.10.43 PM.png,ripe,ripe,0.018717093393206596,0.9812828898429871,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.25 PM.png,ripe,unripe,0.11281777918338776,0.8871822357177734,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.31 PM.png,ripe,unripe,0.147048681974411,0.8529512882232666,0.019327135756611824 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.14.01 PM.png,ripe,unripe,0.2329966276884079,0.7670033574104309,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.15.09 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.15.45 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,unripe,0.1487225443124771,0.8512774705886841,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,unripe,0.11410532891750336,0.8858946561813354,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,unripe,0.0934695228934288,0.906530499458313,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.17.58 PM.png,ripe,unripe,0.37352901697158813,0.6264709830284119,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.19.58 PM.png,ripe,ripe,0.0,0.9686501026153564,0.031349871307611465 +apple/test/ripe/Screen Shot 2018-06-08 at 5.21.06 PM.png,ripe,overripe,0.0,0.5308315753936768,0.46916845440864563 +apple/test/ripe/Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,ripe,0.0,0.9757035374641418,0.024296460673213005 +apple/test/ripe/Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,unripe,0.23583988845348358,0.7641600966453552,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,overripe,0.0,0.6168880462646484,0.38311195373535156 +apple/test/ripe/Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.148186594247818,0.8518133759498596,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.06 PM.png,ripe,unripe,0.04465050622820854,0.9553495049476624,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.34 PM.png,ripe,unripe,0.48132213950157166,0.518677830696106,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,overripe,0.6321893334388733,0.3678106665611267,0.24935372173786163 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.04 PM.png,ripe,unripe,0.31532952189445496,0.6846704483032227,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.24 PM.png,ripe,overripe,0.0,0.8040108680725098,0.19598916172981262 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.1988549828529358,0.8011450171470642,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,unripe,0.08802516013383865,0.9119748473167419,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.11903131753206253,0.8809686899185181,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.38 PM.png,ripe,unripe,0.22949329018592834,0.770506739616394,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,overripe,0.018106527626514435,0.7562460899353027,0.24375389516353607 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5497590899467468,0.45024093985557556,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.34.07 PM.png,ripe,unripe,0.12951891124248505,0.8704810738563538,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 4.59.49 PM.png,ripe,unripe,0.7558581233024597,0.24414189159870148,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.02.08 PM.png,ripe,unripe,0.13596196472644806,0.8640380501747131,0.028198959305882454 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.03.47 PM.png,ripe,unripe,0.18565353751182556,0.814346432685852,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.2073218822479248,0.7926781177520752,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.04.59 PM.png,ripe,overripe,0.0,0.8025943040847778,0.19740568101406097 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.06.23 PM.png,ripe,unripe,0.08799485117197037,0.9120051264762878,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,unripe,0.1773969829082489,0.8226029872894287,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.20322123169898987,0.7967787384986877,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.8128533363342285,0.18714666366577148,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,unripe,0.12347857654094696,0.8765214085578918,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.10.29 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.14.20 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.14.48 PM.png,ripe,unripe,0.5494385361671448,0.4505614936351776,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.17.04 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.17.58 PM.png,ripe,unripe,0.41764208674430847,0.5823579430580139,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,unripe,0.15182176232337952,0.8481782078742981,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.21.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.22.53 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.4101010859012604,0.5898988842964172,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.24.26 PM.png,ripe,ripe,0.0,0.9560633897781372,0.04393662512302399 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.33 PM.png,ripe,overripe,0.0,0.709980309009552,0.2900196611881256 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,overripe,0.0,0.620879590511322,0.379120409488678 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.49 PM.png,ripe,overripe,0.0,0.5611289143562317,0.4388710558414459 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.2600945234298706,0.7399054765701294,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.28246474266052246,0.7175352573394775,0.007611769251525402 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.14900530874729156,0.8509947061538696,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.19 PM.png,ripe,unripe,0.1907225400209427,0.8092774748802185,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.27 PM.png,ripe,unripe,0.14986295998096466,0.8501370549201965,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,overripe,0.6631062030792236,0.336893767118454,0.2844671607017517 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19901730120182037,0.8009827136993408,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.01.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.02.31 PM.png,ripe,ripe,0.15235164761543274,0.8476483821868896,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.03.34 PM.png,ripe,unripe,0.19222217798233032,0.8077778220176697,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,unripe,0.10722995549440384,0.8927700519561768,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.20519766211509705,0.7948023676872253,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.05.06 PM.png,ripe,overripe,0.0,0.7568162083625793,0.24318377673625946 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.2267218828201294,0.7732781171798706,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,unripe,0.2495804727077484,0.7504194974899292,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.20236726105213165,0.7976327538490295,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,unripe,0.15781788527965546,0.8421820998191833,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.7357653975486755,0.26423460245132446,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,unripe,0.11944113671779633,0.8805588483810425,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.09.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,unripe,0.15896393358707428,0.8410360813140869,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.11.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.11.35 PM.png,ripe,unripe,0.17766690254211426,0.8223330974578857,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.16 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,unripe,0.10328175872564316,0.8967182636260986,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.49 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.19.28 PM.png,ripe,unripe,0.2894679009914398,0.7105320692062378,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.20.17 PM.png,ripe,unripe,0.1332024484872818,0.8667975664138794,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,unripe,0.15120528638362885,0.84879469871521,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.21.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,overripe,0.0,0.6782011985778809,0.32179880142211914 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.22.48 PM.png,ripe,overripe,0.0,0.8722425103187561,0.12775751948356628 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.23.51 PM.png,ripe,unripe,0.1146002858877182,0.8853996992111206,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.35 PM.png,ripe,unripe,0.14087888598442078,0.8591210842132568,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.2711558938026428,0.7288441061973572,0.00806132610887289 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.26.41 PM.png,ripe,unripe,0.07428096234798431,0.9257190227508545,0.04655265063047409 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,ripe,0.15640312433242798,0.843596875667572,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,overripe,0.6511269807815552,0.3488730192184448,0.24644681811332703 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.28.32 PM.png,ripe,overripe,0.0,0.7664070725440979,0.2335929274559021 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19956764578819275,0.8004323840141296,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.11934027820825577,0.8806596994400024,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.45617741346359253,0.5438225865364075,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,unripe,0.15866731107234955,0.8413326740264893,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.00.26 PM.png,ripe,unripe,0.23679853975772858,0.7632014751434326,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.01.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.24 PM.png,ripe,unripe,0.1902765929698944,0.8097233772277832,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.42109414935112,0.5789058804512024 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,unripe,0.10742960125207901,0.8925703763961792,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.04.05 PM.png,ripe,ripe,0.0824795514345169,0.9175204634666443,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.04.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,overripe,0.0,0.8852925300598145,0.11470745503902435 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.18 PM.png,ripe,overripe,0.0,0.6957693696022034,0.304230660200119 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.27 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,unripe,0.18925277888774872,0.8107472062110901,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.09.25 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,unripe,0.1563672125339508,0.8436328172683716,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.11.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.11.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.14.01 PM.png,ripe,unripe,0.2664889097213745,0.7335110902786255,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.061642762273550034,0.9383572340011597,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.17.10 PM.png,ripe,unripe,0.1688077300786972,0.8311922550201416,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.42 PM.png,ripe,overripe,0.03028087504208088,0.8106442093849182,0.1893557757139206 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.19.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.32 PM.png,ripe,unripe,0.2751266062259674,0.724873423576355,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,ripe,0.0,0.959905207157135,0.04009478911757469 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.21.44 PM.png,ripe,unripe,0.9889229536056519,0.011077027767896652,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.22.20 PM.png,ripe,overripe,0.0,0.7748221158981323,0.22517786920070648 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.40668463706970215,0.5933153629302979,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.24.35 PM.png,ripe,unripe,0.1423402577638626,0.8576597571372986,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.26185107231140137,0.7381489276885986,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.14950817823410034,0.8504918217658997,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.41 PM.png,ripe,unripe,0.07432692497968674,0.9256730675697327,0.047213856130838394 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,ripe,0.1525975465774536,0.8474024534225464,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.28.04 PM.png,ripe,unripe,0.31812670826911926,0.6818732619285583,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19969752430915833,0.8003024458885193,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.29.24 PM.png,ripe,unripe,0.1725279986858368,0.8274720311164856,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.33.55 PM.png,ripe,unripe,0.09453246742486954,0.9054675102233887,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.34.14 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.34.21 PM.png,ripe,unripe,0.23913739621639252,0.7608625888824463,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 4.59.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,unripe,0.08056332170963287,0.9194366931915283,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.02.48 PM.png,ripe,unripe,0.21746906638145447,0.7825309038162231,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.03.17 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.04.16 PM.png,ripe,unripe,0.17320123314857483,0.8267987370491028,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.18 PM.png,ripe,overripe,0.0,0.6969361901283264,0.3030638098716736 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.22684386372566223,0.7731561064720154,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.40008077025413513,0.5999191999435425 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,unripe,0.25020796060562134,0.7497920393943787,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,unripe,0.1650659292936325,0.8349340558052063,0.019858766347169876 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.07.32 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.08.58 PM.png,ripe,unripe,0.17690250277519226,0.8230974674224854,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.09.17 PM.png,ripe,ripe,0.10493606328964233,0.8950639367103577,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.10.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.14.07 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.15.28 PM.png,ripe,overripe,0.025809159502387047,0.8062863349914551,0.19371365010738373 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,unripe,0.14354102313518524,0.8564589619636536,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.16.16 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,unripe,0.20071052014827728,0.7992894649505615,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.18.58 PM.png,ripe,overripe,0.015854062512516975,0.759393036365509,0.24060699343681335 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.20.32 PM.png,ripe,unripe,0.3002466857433319,0.6997533440589905,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,ripe,0.0,0.9586840271949768,0.04131597280502319 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.44 PM.png,ripe,unripe,0.997965931892395,0.002034064382314682,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,overripe,0.0,0.6803837418556213,0.31961625814437866 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.23.07 PM.png,ripe,unripe,0.2847970128059387,0.7152029871940613,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.23.26 PM.png,ripe,unripe,0.14177407324314117,0.85822594165802,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.2617780566215515,0.7382219433784485,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.14927741885185242,0.8507225513458252,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.26.24 PM.png,ripe,overripe,0.0,0.8623847961425781,0.13761521875858307 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.06 PM.png,ripe,unripe,0.1758415848016739,0.8241584300994873,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.13 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,ripe,0.14742764830589294,0.8525723814964294,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.28.32 PM.png,ripe,overripe,0.0,0.7667798399925232,0.2332201600074768 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.11956324428319931,0.8804367780685425,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.32.33 PM.png,ripe,overripe,0.0,0.4005420506000519,0.5994579195976257 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,overripe,0.015163491480052471,0.7595457434654236,0.2404542863368988 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.33.33 PM.png,ripe,overripe,0.0,0.8754163384437561,0.1245836541056633 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.33.47 PM.png,ripe,unripe,0.13452443480491638,0.8654755353927612,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.34.07 PM.png,ripe,unripe,0.1318054050207138,0.868194580078125,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.26 PM.png,ripe,unripe,0.2361195981502533,0.7638803720474243,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.50 PM.png,ripe,unripe,0.1909824013710022,0.8090175986289978,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.02.31 PM.png,ripe,ripe,0.1677774339914322,0.832222580909729,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.04.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.20663313567638397,0.7933668494224548,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,overripe,0.0,0.894976019859314,0.10502396523952484 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.2269066423177719,0.7730933427810669,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.23 PM.png,ripe,unripe,0.08797071129083633,0.9120292663574219,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,unripe,0.17675210535526276,0.8232479095458984,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,unripe,0.17679893970489502,0.823201060295105,0.013677413575351238 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.25 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.13.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.14.48 PM.png,ripe,unripe,0.573715090751648,0.42628487944602966,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.15.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.15.14 PM.png,ripe,unripe,0.20429332554340363,0.7957066893577576,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,unripe,0.10276202857494354,0.8972379565238953,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.16.49 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.17.22 PM.png,ripe,unripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.19.15 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.19.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,unripe,0.15059393644332886,0.8494060635566711,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.21.06 PM.png,ripe,overripe,0.0,0.5303571224212646,0.46964287757873535 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.21.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.22.27 PM.png,ripe,overripe,0.0,0.8783290386199951,0.12167093902826309 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.23.23 PM.png,ripe,unripe,0.14002497494220734,0.8599750399589539,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,overripe,0.0,0.6175501942634583,0.38244977593421936 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.2613297998905182,0.7386701703071594,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.26.05 PM.png,ripe,ripe,0.042746879160404205,0.957253098487854,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.26.47 PM.png,ripe,overripe,0.0,0.5049391984939575,0.49506083130836487 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19980210065841675,0.8001978993415833,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.29.24 PM.png,ripe,unripe,0.1685529202222824,0.8314470648765564,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.47477272152900696,0.5252273082733154,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.4007205069065094,0.5992794632911682 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.50 PM.png,ripe,unripe,0.17882029712200165,0.8211796879768372,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.01.34 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.02.38 PM.png,ripe,unripe,0.3037821352481842,0.6962178349494934,0.00885674450546503 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.03.47 PM.png,ripe,unripe,0.17706021666526794,0.8229397535324097,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.04.48 PM.png,ripe,overripe,0.0,0.8914961814880371,0.10850383341312408 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.05.06 PM.png,ripe,overripe,0.0,0.7720029950141907,0.22799701988697052 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.40174537897109985,0.5982546210289001 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,unripe,0.1666603982448578,0.8333396315574646,0.020087910816073418 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.2011433243751526,0.7988566756248474,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,unripe,0.15603581070899963,0.8439642190933228,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.09.03 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.11.35 PM.png,ripe,unripe,0.17052003741264343,0.829479992389679,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.11.59 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.14 PM.png,ripe,ripe,0.0,0.9379247426986694,0.06207524985074997 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.41 PM.png,ripe,unripe,0.18204814195632935,0.8179518580436707,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.47 PM.png,ripe,unripe,0.8232505321502686,0.17674945294857025,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.13.45 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.14 PM.png,ripe,unripe,0.19494804739952087,0.8050519824028015,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.1367204338312149,0.8632795810699463,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,unripe,0.14174257218837738,0.8582574129104614,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,unripe,0.10892204195261002,0.8910779356956482,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,unripe,0.1926030069589615,0.8073970079421997,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.21.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.22.48 PM.png,ripe,overripe,0.0,0.8010453581809998,0.19895464181900024 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.23.31 PM.png,ripe,unripe,0.15131917595863342,0.848680853843689,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.24.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,unripe,0.22879864275455475,0.7712013721466064,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.25.49 PM.png,ripe,overripe,0.0,0.5584725737571716,0.44152742624282837 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.26.29 PM.png,ripe,unripe,0.23223860561847687,0.7677614092826843,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19315741956233978,0.806842565536499,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,unripe,0.08245406299829483,0.9175459146499634,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.29.31 PM.png,ripe,unripe,0.22066596150398254,0.7793340682983398,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.34.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.34.14 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.00.18 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.01.15 PM.png,ripe,unripe,0.11724625527858734,0.8827537298202515,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.40609124302864075,0.5939087867736816 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.03.10 PM.png,ripe,unripe,0.23923443257808685,0.760765552520752,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.03.17 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.2117045819759369,0.7882953882217407,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.07.32 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.10.53 PM.png,ripe,unripe,0.9948655962944031,0.00513438880443573,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.11.08 PM.png,ripe,unripe,0.6422930359840393,0.3577069342136383,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.13.25 PM.png,ripe,unripe,0.11202975362539291,0.8879702687263489,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.07 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.20 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.15.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.15.39 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.33 PM.png,ripe,unripe,0.17420189082622528,0.8257980942726135,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,unripe,0.09559205919504166,0.9044079184532166,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.17.04 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,unripe,0.2001902461051941,0.7998097538948059,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.19.28 PM.png,ripe,unripe,0.22658227384090424,0.7734177112579346,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,ripe,0.0,0.9541412591934204,0.045858755707740784 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,overripe,0.0,0.6782931089401245,0.3217068612575531 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.23.26 PM.png,ripe,unripe,0.1429303139448166,0.8570696711540222,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.4097767472267151,0.5902232527732849,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,unripe,0.22900289297103882,0.7709971070289612,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.26072120666503906,0.7392787933349609,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.26.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.29.07 PM.png,ripe,unripe,0.20510774850845337,0.7948922514915466,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.4711000323295593,0.5288999676704407,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.4012066721916199,0.5987933278083801 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 4.59.49 PM.png,ripe,unripe,0.737617015838623,0.26238301396369934,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.01.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.03.59 PM.png,ripe,unripe,0.14535179734230042,0.854648232460022,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.2091284692287445,0.7908715009689331,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.48 PM.png,ripe,overripe,0.0,0.8871940970420837,0.11280591785907745 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.59 PM.png,ripe,overripe,0.0,0.8042265176773071,0.19577348232269287 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,ripe,0.0,0.9826740622520447,0.017325926572084427 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,unripe,0.24539852142333984,0.7546014785766602,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.20732629299163818,0.7926737070083618,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,unripe,0.12393718212842941,0.87606281042099,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.08.52 PM.png,ripe,unripe,0.16336053609848022,0.8366394639015198,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.10.53 PM.png,ripe,ripe,0.14792589843273163,0.8520740866661072,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.13.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.13.31 PM.png,ripe,unripe,0.1400066614151001,0.8599933385848999,0.019438529387116432 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.051813527941703796,0.948186457157135,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.15.28 PM.png,ripe,overripe,0.02568754181265831,0.8047699928283691,0.19523003697395325 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,unripe,0.11411384493112564,0.8858861327171326,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.18.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.20.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.21.31 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,ripe,0.0,0.9756374955177307,0.024362491443753242 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.23.14 PM.png,ripe,unripe,0.09842202812433243,0.9015779495239258,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.4115223288536072,0.5884776711463928,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.30210453271865845,0.6978954672813416,0.007101335562765598 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.29 PM.png,ripe,unripe,0.23933207988739014,0.7606679201126099,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.58 PM.png,ripe,overripe,0.0,0.4078628420829773,0.5921371579170227 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.42 PM.png,ripe,overripe,0.0,0.6667205095291138,0.33327949047088623 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.48 PM.png,ripe,overripe,0.0,0.6965819001197815,0.3034180700778961 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,unripe,0.08804018795490265,0.9119598269462585,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.11904234439134598,0.8809576630592346,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.38 PM.png,ripe,unripe,0.22846747934818268,0.7715325355529785,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,overripe,0.018119443207979202,0.7562959790229797,0.24370403587818146 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5422958731651306,0.4577041566371918,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.40116187930107117,0.5988381505012512 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.33.27 PM.png,ripe,unripe,0.7810654044151306,0.21893461048603058,0.0 +apple/test/unripe/1.jpg,unripe,unripe,0.19975171983242035,0.8002482652664185,0.0 +apple/test/unripe/10.jpg,unripe,ripe,0.0,0.9879571795463562,0.012042850255966187 +apple/test/unripe/100.jpg,unripe,overripe,0.0,0.6444143056869507,0.3555856943130493 +apple/test/unripe/101.jpg,unripe,overripe,0.0011306814849376678,0.6609887480735779,0.33901122212409973 +apple/test/unripe/102.jpg,unripe,unripe,0.20077070593833923,0.7992292642593384,0.0 +apple/test/unripe/103.jpg,unripe,unripe,0.15873508155345917,0.8412649035453796,0.0 +apple/test/unripe/104.jpg,unripe,ripe,0.0,0.9667677283287048,0.03323227912187576 +apple/test/unripe/105.jpg,unripe,unripe,0.19183248281478882,0.8081675171852112,0.0 +apple/test/unripe/106.jpg,unripe,overripe,0.10239198058843613,0.752912163734436,0.24708785116672516 +apple/test/unripe/107.jpg,unripe,unripe,0.15506672859191895,0.844933271408081,0.0 +apple/test/unripe/108.jpg,unripe,unripe,0.3981587588787079,0.6018412709236145,0.0 +apple/test/unripe/109.jpg,unripe,unripe,0.23104895651340485,0.7689510583877563,0.0 +apple/test/unripe/11.jpg,unripe,unripe,0.09911853075027466,0.9008814692497253,0.06381969153881073 +apple/test/unripe/110.jpg,unripe,unripe,0.07374999672174454,0.9262499809265137,0.01666666753590107 +apple/test/unripe/111.jpg,unripe,unripe,0.9977011680603027,0.002298850566148758,0.0 +apple/test/unripe/112.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/113.jpg,unripe,unripe,0.21617144346237183,0.7838285565376282,0.05155925080180168 +apple/test/unripe/114.jpg,unripe,unripe,0.784197986125946,0.21580202877521515,0.0 +apple/test/unripe/115.jpg,unripe,ripe,0.0,0.9545851349830627,0.045414846390485764 +apple/test/unripe/116.jpg,unripe,overripe,0.0,0.4343191087245941,0.5656809210777283 +apple/test/unripe/117.jpg,unripe,unripe,0.0938388779759407,0.9061611294746399,0.0 +apple/test/unripe/118.jpg,unripe,unripe,0.13558559119701385,0.864414393901825,0.0 +apple/test/unripe/119.jpg,unripe,overripe,0.21204544603824615,0.787954568862915,0.13590964674949646 +apple/test/unripe/12.jpg,unripe,overripe,0.04179413244128227,0.8169068098068237,0.18309317529201508 +apple/test/unripe/120.jpg,unripe,overripe,0.15410521626472473,0.8458948135375977,0.07958923280239105 +apple/test/unripe/121.jpg,unripe,unripe,0.19183248281478882,0.8081675171852112,0.0 +apple/test/unripe/122.jpg,unripe,unripe,0.5659258961677551,0.4340741038322449,0.047266244888305664 +apple/test/unripe/123.jpg,unripe,ripe,0.0,0.9883396029472351,0.011660424061119556 +apple/test/unripe/124.jpg,unripe,overripe,0.0,0.8291446566581726,0.1708553284406662 +apple/test/unripe/125.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/126.jpg,unripe,overripe,0.15410521626472473,0.8458948135375977,0.07958923280239105 +apple/test/unripe/127.jpg,unripe,unripe,0.2259887009859085,0.7740113139152527,0.0 +apple/test/unripe/128.jpg,unripe,unripe,0.3534553349018097,0.6465446949005127,0.0 +apple/test/unripe/129.jpg,unripe,unripe,0.995192289352417,0.004807692486792803,0.0 +apple/test/unripe/13.jpg,unripe,ripe,0.15263237059116364,0.8473676443099976,0.06570238620042801 +apple/test/unripe/130.jpg,unripe,overripe,0.07864879816770554,0.8972156047821045,0.1027844101190567 +apple/test/unripe/131.jpg,unripe,overripe,0.2433762550354004,0.7566237449645996,0.14367513358592987 +apple/test/unripe/132.jpg,unripe,overripe,0.9412127733230591,0.05878719687461853,0.1705596148967743 +apple/test/unripe/133.jpg,unripe,overripe,0.7783231735229492,0.22167684137821198,0.12456193566322327 +apple/test/unripe/134.jpg,unripe,overripe,0.0,0.7467308044433594,0.2532691955566406 +apple/test/unripe/135.jpg,unripe,overripe,0.0,0.4226805567741394,0.5773194432258606 +apple/test/unripe/136.jpg,unripe,overripe,0.0,0.9319005012512207,0.06809952855110168 +apple/test/unripe/137.jpg,unripe,unripe,0.10713089257478714,0.8928691148757935,0.0 +apple/test/unripe/138.jpg,unripe,unripe,0.13558559119701385,0.864414393901825,0.0 +apple/test/unripe/139.jpg,unripe,unripe,0.9071975350379944,0.09280245006084442,0.05712097883224487 +apple/test/unripe/14.jpg,unripe,overripe,0.1198243647813797,0.8801756501197815,0.08913102746009827 +apple/test/unripe/140.jpg,unripe,unripe,0.06339100748300552,0.9366089701652527,0.0 +apple/test/unripe/141.jpg,unripe,overripe,0.025422679260373116,0.8080552220344543,0.19194476306438446 +apple/test/unripe/142.jpg,unripe,overripe,0.0,0.4226805567741394,0.5773194432258606 +apple/test/unripe/143.jpg,unripe,unripe,0.20495618879795074,0.7950438261032104,0.0 +apple/test/unripe/144.jpg,unripe,unripe,0.4728873372077942,0.5271126627922058,0.005469277501106262 +apple/test/unripe/145.jpg,unripe,unripe,0.16237060725688934,0.8376293778419495,0.0 +apple/test/unripe/146.jpg,unripe,overripe,0.38235145807266235,0.6176485419273376,0.15593396127223969 +apple/test/unripe/147.jpg,unripe,unripe,0.2727174758911133,0.7272825241088867,0.0 +apple/test/unripe/148.jpg,unripe,overripe,0.0,0.6133372783660889,0.38666269183158875 +apple/test/unripe/149.jpg,unripe,unripe,0.5659258961677551,0.4340741038322449,0.047266244888305664 +apple/test/unripe/15.jpg,unripe,overripe,0.24180129170417786,0.7581986784934998,0.11214827746152878 +apple/test/unripe/150.jpg,unripe,ripe,0.0,0.986285924911499,0.013714084401726723 +apple/test/unripe/151.jpg,unripe,overripe,0.0,0.8291446566581726,0.1708553284406662 +apple/test/unripe/152.jpg,unripe,overripe,0.0,0.6786095499992371,0.32139045000076294 +apple/test/unripe/153.jpg,unripe,overripe,0.0,0.7467308044433594,0.2532691955566406 +apple/test/unripe/154.jpg,unripe,overripe,0.0,0.6664422750473022,0.33355769515037537 +apple/test/unripe/155.jpg,unripe,unripe,0.11599147319793701,0.884008526802063,0.0 +apple/test/unripe/156.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/157.jpg,unripe,unripe,0.5138461589813232,0.48615384101867676,0.0 +apple/test/unripe/158.jpg,unripe,unripe,0.20939861238002777,0.790601372718811,0.0 +apple/test/unripe/159.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/16.jpg,unripe,overripe,0.5899009704589844,0.4100990295410156,0.1041167676448822 +apple/test/unripe/160.jpg,unripe,overripe,0.0825839638710022,0.874178409576416,0.12582159042358398 +apple/test/unripe/161.jpg,unripe,unripe,0.49730822443962097,0.5026918053627014,0.0 +apple/test/unripe/162.jpg,unripe,overripe,0.0,0.40442967414855957,0.5955703258514404 +apple/test/unripe/163.jpg,unripe,overripe,0.003415559884160757,0.709930419921875,0.290069580078125 +apple/test/unripe/164.jpg,unripe,unripe,0.30979347229003906,0.6902065277099609,0.0 +apple/test/unripe/165.jpg,unripe,unripe,0.26131579279899597,0.7386841773986816,0.0 +apple/test/unripe/166.jpg,unripe,unripe,0.26502323150634766,0.7349767684936523,0.0 +apple/test/unripe/167.jpg,unripe,overripe,0.0,0.7747766971588135,0.22522328794002533 +apple/test/unripe/168.jpg,unripe,unripe,0.2757185697555542,0.7242814302444458,0.0 +apple/test/unripe/169.jpg,unripe,unripe,0.18418724834918976,0.8158127665519714,0.0 +apple/test/unripe/17.jpg,unripe,overripe,0.0,0.6779661178588867,0.32203391194343567 +apple/test/unripe/170.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/171.jpg,unripe,overripe,0.7170455455780029,0.28295445442199707,0.22826698422431946 +apple/test/unripe/172.jpg,unripe,unripe,0.24876141548156738,0.7512385845184326,0.0 +apple/test/unripe/173.jpg,unripe,unripe,0.8924930691719055,0.10750695317983627,0.05783132463693619 +apple/test/unripe/174.jpg,unripe,overripe,0.31919556856155396,0.5533769130706787,0.4466230869293213 +apple/test/unripe/175.jpg,unripe,unripe,0.2617376744747162,0.7382622957229614,0.0 +apple/test/unripe/176.jpg,unripe,ripe,0.0,0.9883396029472351,0.011660424061119556 +apple/test/unripe/177.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/178.jpg,unripe,unripe,0.25701162219047546,0.7429884076118469,0.0 +apple/test/unripe/179.jpg,unripe,unripe,0.14363965392112732,0.8563603162765503,0.0 +apple/test/unripe/18.jpg,unripe,overripe,0.0,0.6427068114280701,0.3572932183742523 +apple/test/unripe/180.jpg,unripe,unripe,0.21563398838043213,0.7843660116195679,0.0 +apple/test/unripe/181.jpg,unripe,overripe,0.0825839638710022,0.874178409576416,0.12582159042358398 +apple/test/unripe/182.jpg,unripe,unripe,0.3150193989276886,0.684980571269989,0.0 +apple/test/unripe/183.jpg,unripe,overripe,0.31919556856155396,0.5533769130706787,0.4466230869293213 +apple/test/unripe/184.jpg,unripe,overripe,0.6070666909217834,0.39293330907821655,0.18304696679115295 +apple/test/unripe/185.jpg,unripe,unripe,0.26502323150634766,0.7349767684936523,0.0 +apple/test/unripe/186.jpg,unripe,unripe,0.931586503982544,0.06841351091861725,0.0 +apple/test/unripe/187.jpg,unripe,overripe,0.0,0.7747766971588135,0.22522328794002533 +apple/test/unripe/188.jpg,unripe,unripe,0.8924930691719055,0.10750695317983627,0.05783132463693619 +apple/test/unripe/189.jpg,unripe,overripe,0.8261280655860901,0.1738719344139099,0.09162010997533798 +apple/test/unripe/19.jpg,unripe,unripe,0.16195766627788544,0.8380423188209534,0.0 +apple/test/unripe/190.jpg,unripe,unripe,0.2757185697555542,0.7242814302444458,0.0 +apple/test/unripe/191.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/192.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/193.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/194.jpg,unripe,unripe,0.44019564986228943,0.559804379940033,0.0 +apple/test/unripe/195.jpg,unripe,unripe,0.21563398838043213,0.7843660116195679,0.0 +apple/test/unripe/196.jpg,unripe,overripe,0.7170455455780029,0.28295445442199707,0.22826698422431946 +apple/test/unripe/197.jpg,unripe,overripe,0.0,0.6137543320655823,0.3862456977367401 +apple/test/unripe/198.jpg,unripe,unripe,0.18418724834918976,0.8158127665519714,0.0 +apple/test/unripe/199.jpg,unripe,overripe,0.0,0.7711687088012695,0.22883130609989166 +apple/test/unripe/2.jpg,unripe,overripe,0.12179306894540787,0.8782069087028503,0.11023597419261932 +apple/test/unripe/20.jpg,unripe,unripe,0.6510513424873352,0.3489486277103424,0.028751179575920105 +apple/test/unripe/200.jpg,unripe,unripe,0.29360419511795044,0.7063958048820496,0.0 +apple/test/unripe/202.jpg,unripe,overripe,0.018572092056274414,0.7197369337081909,0.28026309609413147 +apple/test/unripe/203.jpg,unripe,unripe,0.931586503982544,0.06841351091861725,0.0 +apple/test/unripe/205.jpg,unripe,ripe,0.06587108969688416,0.9341289401054382,0.0 +apple/test/unripe/206.jpg,unripe,unripe,0.7672064900398254,0.23279352486133575,0.04736842215061188 +apple/test/unripe/207.jpg,unripe,unripe,0.6607028245925903,0.3392971456050873,0.05450284108519554 +apple/test/unripe/208.jpg,unripe,unripe,0.16123683750629425,0.8387631773948669,0.0 +apple/test/unripe/209.jpg,unripe,overripe,0.48753759264945984,0.5124624371528625,0.09151709079742432 +apple/test/unripe/21.jpg,unripe,overripe,0.624877393245697,0.37512263655662537,0.08266907930374146 +apple/test/unripe/210.jpg,unripe,ripe,0.03798719868063927,0.9620128273963928,0.03578031435608864 +apple/test/unripe/211.jpg,unripe,unripe,0.2136560082435608,0.7863439917564392,0.0 +apple/test/unripe/212.jpg,unripe,unripe,0.26593640446662903,0.7340636253356934,0.0 +apple/test/unripe/213.jpg,unripe,unripe,0.3150193989276886,0.684980571269989,0.0 +apple/test/unripe/214.jpg,unripe,unripe,0.13787025213241577,0.8621297478675842,0.0 +apple/test/unripe/215.jpg,unripe,overripe,0.29029926657676697,0.7097007632255554,0.2865390181541443 +apple/test/unripe/216.jpg,unripe,overripe,0.6070666909217834,0.39293330907821655,0.18304696679115295 +apple/test/unripe/217.jpg,unripe,unripe,0.23299595713615417,0.7670040726661682,0.0 +apple/test/unripe/218.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/219.jpg,unripe,unripe,0.29360419511795044,0.7063958048820496,0.0 +apple/test/unripe/22.jpg,unripe,unripe,0.14805331826210022,0.8519467115402222,0.0 +apple/test/unripe/220.jpg,unripe,ripe,0.0,0.965606689453125,0.03439329192042351 +apple/test/unripe/221.jpg,unripe,overripe,0.2338857352733612,0.6315112709999084,0.36848875880241394 +apple/test/unripe/222.jpg,unripe,ripe,0.0,0.9822784662246704,0.017721518874168396 +apple/test/unripe/223.jpg,unripe,overripe,0.0,0.4077519476413727,0.5922480821609497 +apple/test/unripe/224.jpg,unripe,unripe,0.20382127165794373,0.7961786985397339,0.0 +apple/test/unripe/225.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/226.jpg,unripe,unripe,0.3102184534072876,0.6897815465927124,0.0 +apple/test/unripe/227.jpg,unripe,unripe,0.09794149547815323,0.902058482170105,0.025522833690047264 +apple/test/unripe/229.jpg,unripe,overripe,0.003415559884160757,0.709930419921875,0.290069580078125 +apple/test/unripe/23.jpg,unripe,unripe,0.21980223059654236,0.7801977396011353,0.0 +apple/test/unripe/230.jpg,unripe,unripe,0.22193500399589539,0.778065025806427,0.0 +apple/test/unripe/231.jpg,unripe,overripe,0.0,0.8807967305183411,0.11920325458049774 +apple/test/unripe/232.jpg,unripe,overripe,0.12057523429393768,0.8794247508049011,0.09293182939291 +apple/test/unripe/233.jpg,unripe,overripe,0.14647309482097626,0.6799642443656921,0.32003578543663025 +apple/test/unripe/235.jpg,unripe,unripe,0.23018962144851685,0.7698103785514832,0.0 +apple/test/unripe/236.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/237.jpg,unripe,unripe,0.19038642942905426,0.8096135854721069,0.0 +apple/test/unripe/238.jpg,unripe,overripe,0.0,0.7550671696662903,0.2449328452348709 +apple/test/unripe/239.jpg,unripe,unripe,0.9487680196762085,0.051231954246759415,0.0 +apple/test/unripe/24.jpg,unripe,overripe,0.3982744812965393,0.6017255187034607,0.16565872728824615 +apple/test/unripe/240.jpg,unripe,overripe,0.0789927989244461,0.6682372093200684,0.33176276087760925 +apple/test/unripe/241.jpg,unripe,unripe,0.2290118932723999,0.7709881067276001,0.0 +apple/test/unripe/242.jpg,unripe,unripe,0.12245374917984009,0.8775462508201599,0.0 +apple/test/unripe/243.jpg,unripe,unripe,0.931586503982544,0.06841351091861725,0.0 +apple/test/unripe/244.jpg,unripe,unripe,0.3150193989276886,0.684980571269989,0.0 +apple/test/unripe/245.jpg,unripe,overripe,0.0,0.7109768986701965,0.2890230715274811 +apple/test/unripe/246.jpg,unripe,overripe,0.0,0.8094335794448853,0.19056640565395355 +apple/test/unripe/247.jpg,unripe,overripe,0.0,0.5829170346260071,0.4170829653739929 +apple/test/unripe/248.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/249.jpg,unripe,overripe,0.3726276457309723,0.5776900053024292,0.4223099648952484 +apple/test/unripe/25.jpg,unripe,overripe,0.0,0.9225916266441345,0.07740835100412369 +apple/test/unripe/250.jpg,unripe,unripe,0.5582658052444458,0.4417341947555542,0.0 +apple/test/unripe/251.jpg,unripe,unripe,0.10953845828771591,0.8904615640640259,0.02561797760426998 +apple/test/unripe/253.jpg,unripe,unripe,0.3102184534072876,0.6897815465927124,0.0 +apple/test/unripe/254.jpg,unripe,overripe,0.0,0.6353689432144165,0.3646310567855835 +apple/test/unripe/255.jpg,unripe,overripe,0.9373970031738281,0.06260297447443008,0.1826958805322647 +apple/test/unripe/256.jpg,unripe,unripe,0.8843420743942261,0.11565794795751572,0.0 +apple/test/unripe/257.jpg,unripe,unripe,0.8362459540367126,0.16375404596328735,0.0 +apple/test/unripe/258.jpg,unripe,unripe,0.1783674657344818,0.8216325044631958,0.0 +apple/test/unripe/259.jpg,unripe,overripe,0.4636349081993103,0.5363650918006897,0.15135085582733154 +apple/test/unripe/26.jpg,unripe,overripe,0.0,0.40607163310050964,0.593928337097168 +apple/test/unripe/261.jpg,unripe,overripe,0.0,0.6844926476478577,0.3155073821544647 +apple/test/unripe/262.jpg,unripe,overripe,0.24231494963169098,0.5683092474937439,0.4316907823085785 +apple/test/unripe/263.jpg,unripe,overripe,0.0,0.5380344986915588,0.46196550130844116 +apple/test/unripe/264.jpg,unripe,unripe,0.06018487364053726,0.939815104007721,0.0222869161516428 +apple/test/unripe/265.jpg,unripe,unripe,0.2605575621128082,0.7394424080848694,0.0 +apple/test/unripe/266.jpg,unripe,overripe,0.0,0.6000387072563171,0.39996132254600525 +apple/test/unripe/267.jpg,unripe,unripe,0.21821895241737366,0.7817810773849487,0.0 +apple/test/unripe/268.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/269.jpg,unripe,unripe,0.18442678451538086,0.8155732154846191,0.0 +apple/test/unripe/27.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/270.jpg,unripe,overripe,0.07106190174818039,0.910317063331604,0.089682936668396 +apple/test/unripe/271.jpg,unripe,overripe,0.48753759264945984,0.5124624371528625,0.09151709079742432 +apple/test/unripe/272.jpg,unripe,overripe,0.29029926657676697,0.7097007632255554,0.2865390181541443 +apple/test/unripe/273.jpg,unripe,overripe,0.06961566209793091,0.7669325470924377,0.23306743800640106 +apple/test/unripe/274.jpg,unripe,unripe,0.9036499857902527,0.0963500440120697,0.03038177825510502 +apple/test/unripe/275.jpg,unripe,unripe,0.29360419511795044,0.7063958048820496,0.0 +apple/test/unripe/276.jpg,unripe,unripe,0.10216616839170456,0.8978338241577148,0.013433458283543587 +apple/test/unripe/277.jpg,unripe,unripe,0.8738874197006226,0.12611258029937744,0.0 +apple/test/unripe/278.jpg,unripe,unripe,0.10113853216171265,0.8988614678382874,0.001953418366611004 +apple/test/unripe/279.jpg,unripe,unripe,0.2136560082435608,0.7863439917564392,0.0 +apple/test/unripe/28.jpg,unripe,unripe,0.2614671587944031,0.7385328412055969,0.0 +apple/test/unripe/281.jpg,unripe,ripe,0.0,0.9917285442352295,0.008271474391222 +apple/test/unripe/282.jpg,unripe,ripe,0.0,0.9659913778305054,0.03400859236717224 +apple/test/unripe/283.jpg,unripe,unripe,0.31914010643959045,0.6808598637580872,0.0 +apple/test/unripe/284.jpg,unripe,unripe,0.33728471398353577,0.6627153158187866,0.0 +apple/test/unripe/285.jpg,unripe,overripe,0.09579335898160934,0.859910249710083,0.140089750289917 +apple/test/unripe/286.jpg,unripe,ripe,0.05833061411976814,0.9416694045066833,0.0 +apple/test/unripe/287.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/288.jpg,unripe,unripe,0.8738874197006226,0.12611258029937744,0.0 +apple/test/unripe/289.jpg,unripe,overripe,0.3726276457309723,0.5776900053024292,0.4223099648952484 +apple/test/unripe/29.jpg,unripe,unripe,0.3333888649940491,0.6666111350059509,0.0 +apple/test/unripe/290.jpg,unripe,unripe,0.12245374917984009,0.8775462508201599,0.0 +apple/test/unripe/291.jpg,unripe,overripe,0.0,0.5829170346260071,0.4170829653739929 +apple/test/unripe/292.jpg,unripe,overripe,0.0,0.9160704016685486,0.08392958343029022 +apple/test/unripe/293.jpg,unripe,overripe,0.0,0.6844926476478577,0.3155073821544647 +apple/test/unripe/294.jpg,unripe,unripe,0.09062404185533524,0.9093759655952454,0.0 +apple/test/unripe/295.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/297.jpg,unripe,unripe,0.5582658052444458,0.4417341947555542,0.0 +apple/test/unripe/298.jpg,unripe,overripe,0.0,0.6353689432144165,0.3646310567855835 +apple/test/unripe/3.jpg,unripe,overripe,0.2803509831428528,0.7196490168571472,0.12420786172151566 +apple/test/unripe/30.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/300.jpg,unripe,overripe,0.0,0.5269841551780701,0.4730158746242523 +apple/test/unripe/301.jpg,unripe,overripe,0.0,0.6615244150161743,0.3384755849838257 +apple/test/unripe/302.jpg,unripe,overripe,0.4636349081993103,0.5363650918006897,0.15135085582733154 +apple/test/unripe/303.jpg,unripe,unripe,0.10953845828771591,0.8904615640640259,0.02561797760426998 +apple/test/unripe/304.jpg,unripe,unripe,0.11091998219490051,0.8890799880027771,0.0 +apple/test/unripe/305.jpg,unripe,unripe,0.3299786150455475,0.6700214147567749,0.0 +apple/test/unripe/306.jpg,unripe,unripe,0.12498888373374939,0.8750110864639282,0.0 +apple/test/unripe/307.jpg,unripe,overripe,0.0,0.8094335794448853,0.19056640565395355 +apple/test/unripe/308.jpg,unripe,overripe,0.0,0.5024498701095581,0.4975501000881195 +apple/test/unripe/309.jpg,unripe,unripe,0.24146385490894318,0.758536159992218,0.052655890583992004 +apple/test/unripe/31.jpg,unripe,overripe,0.07108283042907715,0.8579025268554688,0.14209748804569244 +apple/test/unripe/310.jpg,unripe,unripe,0.2947368323802948,0.7052631378173828,0.0 +apple/test/unripe/311.jpg,unripe,unripe,0.06018487364053726,0.939815104007721,0.0222869161516428 +apple/test/unripe/312.jpg,unripe,unripe,0.2605575621128082,0.7394424080848694,0.0 +apple/test/unripe/313.jpg,unripe,overripe,0.0,0.6000387072563171,0.39996132254600525 +apple/test/unripe/314.jpg,unripe,unripe,0.21821895241737366,0.7817810773849487,0.0 +apple/test/unripe/315.jpg,unripe,overripe,0.0,0.8520809412002563,0.14791905879974365 +apple/test/unripe/317.jpg,unripe,overripe,0.27536600828170776,0.7246339917182922,0.0675673559308052 +apple/test/unripe/318.jpg,unripe,overripe,0.8145894408226013,0.18541055917739868,0.12109769880771637 +apple/test/unripe/32.jpg,unripe,overripe,0.97865229845047,0.02134770154953003,0.13667771220207214 +apple/test/unripe/321.jpg,unripe,unripe,0.15809282660484314,0.8419071435928345,0.0 +apple/test/unripe/322.jpg,unripe,overripe,0.0,0.5380344986915588,0.46196550130844116 +apple/test/unripe/323.jpg,unripe,overripe,0.3420393466949463,0.6579606533050537,0.09250646084547043 +apple/test/unripe/324.jpg,unripe,overripe,0.0,0.6222222447395325,0.3777777850627899 +apple/test/unripe/325.jpg,unripe,ripe,0.0,0.9983065724372864,0.0016934379236772656 +apple/test/unripe/326.jpg,unripe,overripe,0.11779390275478363,0.8822060823440552,0.10566037893295288 +apple/test/unripe/327.jpg,unripe,overripe,0.9900885224342346,0.009911463595926762,0.0857997015118599 +apple/test/unripe/328.jpg,unripe,overripe,0.9373970031738281,0.06260297447443008,0.1826958805322647 +apple/test/unripe/329.jpg,unripe,overripe,0.3505930006504059,0.6494070291519165,0.32419252395629883 +apple/test/unripe/33.jpg,unripe,overripe,0.0108504518866539,0.6980807781219482,0.30191925168037415 +apple/test/unripe/330.jpg,unripe,unripe,0.11787447333335876,0.8821255564689636,0.05639434605836868 +apple/test/unripe/331.jpg,unripe,unripe,0.16268101334571838,0.837319016456604,0.0 +apple/test/unripe/332.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/333.jpg,unripe,unripe,0.18442678451538086,0.8155732154846191,0.0 +apple/test/unripe/334.jpg,unripe,unripe,0.668272852897644,0.33172714710235596,0.02138364687561989 +apple/test/unripe/335.jpg,unripe,unripe,0.1669382005929947,0.8330618143081665,0.0 +apple/test/unripe/336.jpg,unripe,overripe,0.0,0.5485653281211853,0.4514347016811371 +apple/test/unripe/337.jpg,unripe,unripe,0.13906987011432648,0.8609301447868347,0.0 +apple/test/unripe/338.jpg,unripe,overripe,0.496823787689209,0.503176212310791,0.3009856939315796 +apple/test/unripe/339.jpg,unripe,overripe,0.0,0.4326504170894623,0.5673496127128601 +apple/test/unripe/34.jpg,unripe,ripe,0.0,0.988175630569458,0.011824365705251694 +apple/test/unripe/342.jpg,unripe,overripe,0.07729218155145645,0.52812659740448,0.47187340259552 +apple/test/unripe/343.jpg,unripe,overripe,0.0,0.6179575324058533,0.3820424973964691 +apple/test/unripe/344.jpg,unripe,overripe,0.0,0.5090348720550537,0.4909651279449463 +apple/test/unripe/346.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/347.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/348.jpg,unripe,overripe,0.04729737713932991,0.766028106212616,0.23397190868854523 +apple/test/unripe/349.jpg,unripe,overripe,0.6591011881828308,0.3408988118171692,0.09250623732805252 +apple/test/unripe/35.jpg,unripe,unripe,0.6694140434265137,0.33058592677116394,0.0 +apple/test/unripe/350.jpg,unripe,overripe,0.3505930006504059,0.6494070291519165,0.32419252395629883 +apple/test/unripe/351.jpg,unripe,overripe,0.7140542268753052,0.2859457731246948,0.1750599592924118 +apple/test/unripe/353.jpg,unripe,unripe,0.16268101334571838,0.837319016456604,0.0 +apple/test/unripe/354.jpg,unripe,overripe,0.07106190174818039,0.910317063331604,0.089682936668396 +apple/test/unripe/355.jpg,unripe,unripe,0.5118880867958069,0.4881118834018707,0.0 +apple/test/unripe/356.jpg,unripe,unripe,0.9036499857902527,0.0963500440120697,0.03038177825510502 +apple/test/unripe/357.jpg,unripe,overripe,0.0,0.6794895529747009,0.3205104172229767 +apple/test/unripe/358.jpg,unripe,unripe,0.3878248631954193,0.6121751070022583,0.0 +apple/test/unripe/359.jpg,unripe,overripe,0.06262695789337158,0.8115595579147339,0.1884404569864273 +apple/test/unripe/36.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/361.jpg,unripe,overripe,0.0,0.9160729646682739,0.08392702043056488 +apple/test/unripe/362.jpg,unripe,overripe,0.13217242062091827,0.86065274477005,0.13934722542762756 +apple/test/unripe/363.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/364.jpg,unripe,overripe,0.07591696828603745,0.855398416519165,0.14460159838199615 +apple/test/unripe/365.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/366.jpg,unripe,unripe,0.15032091736793518,0.8496790528297424,0.0 +apple/test/unripe/367.jpg,unripe,overripe,0.7017347812652588,0.2982651889324188,0.17811226844787598 +apple/test/unripe/368.jpg,unripe,overripe,0.0,0.5076001286506653,0.4923999011516571 +apple/test/unripe/369.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/37.jpg,unripe,overripe,0.0,0.5934369564056396,0.40656304359436035 +apple/test/unripe/370.jpg,unripe,overripe,0.0747651606798172,0.8110901117324829,0.1889098882675171 +apple/test/unripe/371.jpg,unripe,unripe,0.3126365840435028,0.6873634457588196,0.0 +apple/test/unripe/373.jpg,unripe,overripe,0.0,0.6846068501472473,0.3153931200504303 +apple/test/unripe/374.jpg,unripe,unripe,0.31914010643959045,0.6808598637580872,0.0 +apple/test/unripe/375.jpg,unripe,unripe,0.11587853729724884,0.8841214776039124,0.0 +apple/test/unripe/376.jpg,unripe,overripe,0.12210027873516083,0.877899706363678,0.12203315645456314 +apple/test/unripe/377.jpg,unripe,unripe,0.9988293647766113,0.0011706588556990027,0.013203813694417477 +apple/test/unripe/378.jpg,unripe,unripe,0.16268101334571838,0.837319016456604,0.0 +apple/test/unripe/38.jpg,unripe,unripe,0.15224993228912354,0.8477500677108765,0.0 +apple/test/unripe/381.jpg,unripe,unripe,0.2686980664730072,0.7313019633293152,0.0 +apple/test/unripe/383.jpg,unripe,overripe,0.0,0.7783783674240112,0.22162161767482758 +apple/test/unripe/384.jpg,unripe,overripe,0.23206639289855957,0.7679336071014404,0.2294953167438507 +apple/test/unripe/385.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/386.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/387.jpg,unripe,overripe,0.03990436717867851,0.8144116401672363,0.18558837473392487 +apple/test/unripe/388.jpg,unripe,unripe,0.17802198231220245,0.8219780325889587,0.0 +apple/test/unripe/389.jpg,unripe,overripe,0.9900885224342346,0.009911463595926762,0.0857997015118599 +apple/test/unripe/39.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/390.jpg,unripe,unripe,0.3231586217880249,0.6768413782119751,0.0 +apple/test/unripe/391.jpg,unripe,overripe,0.0,0.4297057092189789,0.5702943205833435 +apple/test/unripe/394.jpg,unripe,unripe,0.5549284219741821,0.44507157802581787,0.0 +apple/test/unripe/4.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/40.jpg,unripe,unripe,0.17864102125167847,0.8213589787483215,0.0 +apple/test/unripe/41.jpg,unripe,ripe,0.0,0.988175630569458,0.011824365705251694 +apple/test/unripe/42.jpg,unripe,unripe,0.7637250423431396,0.23627494275569916,0.0 +apple/test/unripe/43.jpg,unripe,overripe,0.0,0.5049769878387451,0.4950230121612549 +apple/test/unripe/44.jpg,unripe,unripe,0.31095895171165466,0.6890410780906677,0.0 +apple/test/unripe/45.jpg,unripe,unripe,0.825451672077179,0.17454831302165985,0.02707515098154545 +apple/test/unripe/46.jpg,unripe,overripe,0.97865229845047,0.02134770154953003,0.13667771220207214 +apple/test/unripe/47.jpg,unripe,unripe,0.22738155722618103,0.7726184129714966,0.0 +apple/test/unripe/48.jpg,unripe,ripe,0.0,0.9759496450424194,0.024050360545516014 +apple/test/unripe/49.jpg,unripe,overripe,0.9907892942428589,0.009210731834173203,0.08392348140478134 +apple/test/unripe/5.jpg,unripe,unripe,0.176391139626503,0.8236088752746582,0.0 +apple/test/unripe/50.jpg,unripe,overripe,0.5331893563270569,0.4668106734752655,0.23383677005767822 +apple/test/unripe/51.jpg,unripe,unripe,0.9039992094039917,0.0960007905960083,0.0 +apple/test/unripe/52.jpg,unripe,overripe,0.0,0.7217026948928833,0.2782972753047943 +apple/test/unripe/53.jpg,unripe,unripe,0.19356246292591095,0.8064375519752502,0.01843971572816372 +apple/test/unripe/54.jpg,unripe,unripe,0.7380643486976624,0.26193565130233765,0.027710843831300735 +apple/test/unripe/55.jpg,unripe,ripe,0.0,0.9804166555404663,0.019583333283662796 +apple/test/unripe/56.jpg,unripe,unripe,0.11945323646068573,0.8805467486381531,0.0 +apple/test/unripe/57.jpg,unripe,overripe,0.0,0.6349248290061951,0.36507514119148254 +apple/test/unripe/58.jpg,unripe,unripe,0.0832226574420929,0.9167773723602295,0.0 +apple/test/unripe/59.jpg,unripe,ripe,0.0,0.9704920053482056,0.029508015140891075 +apple/test/unripe/6.jpg,unripe,overripe,0.7704282999038696,0.22957171499729156,0.15133178234100342 +apple/test/unripe/60.jpg,unripe,overripe,0.0,0.8285180926322937,0.1714818924665451 +apple/test/unripe/61.jpg,unripe,unripe,0.0832226574420929,0.9167773723602295,0.0 +apple/test/unripe/62.jpg,unripe,overripe,0.0,0.8285180926322937,0.1714818924665451 +apple/test/unripe/63.jpg,unripe,overripe,0.5771305561065674,0.4228694438934326,0.11077184230089188 +apple/test/unripe/64.jpg,unripe,unripe,0.23279911279678345,0.7672008872032166,0.0 +apple/test/unripe/65.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/66.jpg,unripe,unripe,0.14413507282733917,0.855864942073822,0.003317427821457386 +apple/test/unripe/67.jpg,unripe,ripe,0.15283140540122986,0.8471685647964478,0.0 +apple/test/unripe/68.jpg,unripe,unripe,0.13776129484176636,0.8622387051582336,0.026149040088057518 +apple/test/unripe/69.jpg,unripe,unripe,0.18551120162010193,0.8144887685775757,0.010971063748002052 +apple/test/unripe/7.jpg,unripe,unripe,0.8168947696685791,0.1831052452325821,0.0 +apple/test/unripe/70.jpg,unripe,unripe,0.28041958808898926,0.7195804119110107,0.0 +apple/test/unripe/71.jpg,unripe,unripe,0.40971559286117554,0.5902844071388245,0.0 +apple/test/unripe/72.jpg,unripe,overripe,0.0,0.4418943524360657,0.5581056475639343 +apple/test/unripe/73.jpg,unripe,overripe,0.059266623109579086,0.5956621766090393,0.4043377935886383 +apple/test/unripe/74.jpg,unripe,unripe,0.18682722747325897,0.8131727576255798,0.0 +apple/test/unripe/75.jpg,unripe,overripe,0.059182364493608475,0.9236289858818054,0.07637099176645279 +apple/test/unripe/76.jpg,unripe,unripe,0.6382260322570801,0.3617739975452423,0.0012247675331309438 +apple/test/unripe/77.jpg,unripe,unripe,0.3489951491355896,0.6510048508644104,0.0 +apple/test/unripe/78.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/79.jpg,unripe,overripe,0.0,0.6825788617134094,0.3174211382865906 +apple/test/unripe/8.jpg,unripe,unripe,0.23895689845085144,0.761043131351471,0.0 +apple/test/unripe/80.jpg,unripe,overripe,0.08427372574806213,0.7080350518226624,0.29196491837501526 +apple/test/unripe/81.jpg,unripe,overripe,0.039169661700725555,0.7013096213340759,0.2986903786659241 +apple/test/unripe/82.jpg,unripe,unripe,0.31095895171165466,0.6890410780906677,0.0 +apple/test/unripe/83.jpg,unripe,ripe,0.0,0.9667677283287048,0.03323227912187576 +apple/test/unripe/84.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/85.jpg,unripe,overripe,0.0,0.4082304537296295,0.5917695760726929 +apple/test/unripe/86.jpg,unripe,overripe,0.4807778596878052,0.5192221403121948,0.21153444051742554 +apple/test/unripe/87.jpg,unripe,unripe,0.43650466203689575,0.5634953379631042,0.055111248046159744 +apple/test/unripe/88.jpg,unripe,unripe,0.16625991463661194,0.8337401151657104,0.0 +apple/test/unripe/89.jpg,unripe,unripe,0.36872366070747375,0.6312763094902039,0.0 +apple/test/unripe/9.jpg,unripe,unripe,0.2550428509712219,0.7449571490287781,0.0 +apple/test/unripe/90.jpg,unripe,unripe,0.22444623708724976,0.7755537629127502,0.0 +apple/test/unripe/91.jpg,unripe,overripe,0.0,0.6825788617134094,0.3174211382865906 +apple/test/unripe/92.jpg,unripe,unripe,0.24301846325397491,0.7569815516471863,0.0 +apple/test/unripe/93.jpg,unripe,unripe,0.2951611578464508,0.7048388719558716,0.0 +apple/test/unripe/94.jpg,unripe,overripe,0.1031155213713646,0.8968845009803772,0.10298685729503632 +apple/test/unripe/95.jpg,unripe,overripe,0.0,0.4506666660308838,0.5493333339691162 +apple/test/unripe/96.jpg,unripe,overripe,0.48753759264945984,0.5124624371528625,0.09151709079742432 +apple/test/unripe/97.jpg,unripe,unripe,0.19703687727451324,0.802963137626648,0.0 +apple/test/unripe/98.jpg,unripe,unripe,0.3305985927581787,0.6694014072418213,0.04596132040023804 +apple/test/unripe/99.jpg,unripe,unripe,0.784197986125946,0.21580202877521515,0.0 diff --git a/AgCloud/services/ripeness-baseline/eval/apple_test/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/apple_test/roc_curves.png new file mode 100644 index 000000000..efaf43700 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/apple_test/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/apple_tuned/metrics.json b/AgCloud/services/ripeness-baseline/eval/apple_tuned/metrics.json new file mode 100644 index 000000000..a56d0c00e --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/apple_tuned/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.5998536942209217, + "report": { + "unripe": { + "precision": 0.38783269961977185, + "recall": 0.2749326145552561, + "f1-score": 0.3217665615141956, + "support": 371.0 + }, + "ripe": { + "precision": 0.7594339622641509, + "recall": 0.40759493670886077, + "f1-score": 0.5304777594728172, + "support": 395.0 + }, + "overripe": { + "precision": 0.624439461883408, + "recall": 0.9267886855241264, + "f1-score": 0.7461486939048895, + "support": 601.0 + }, + "accuracy": 0.5998536942209217, + "macro avg": { + "precision": 0.5905687079224436, + "recall": 0.5364387455960812, + "f1-score": 0.5327976716306341, + "support": 1367.0 + }, + "weighted avg": { + "precision": 0.599232233537091, + "recall": 0.5998536942209217, + "f1-score": 0.5686536023045852, + "support": 1367.0 + } + }, + "confusion_matrix": [ + [ + 102, + 17, + 252 + ], + [ + 151, + 161, + 83 + ], + [ + 10, + 34, + 557 + ] + ], + "samples": 1367, + "prefix": "apple/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/apple_tuned/per_image.csv b/AgCloud/services/ripeness-baseline/eval/apple_tuned/per_image.csv new file mode 100644 index 000000000..3bb1442b7 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/apple_tuned/per_image.csv @@ -0,0 +1,1368 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +apple/test/overripe/Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7545539140701294,0.2454460710287094 +apple/test/overripe/Screen Shot 2018-06-07 at 2.16.54 PM.png,overripe,overripe,0.0,0.8749153017997742,0.12508472800254822 +apple/test/overripe/Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.11619272083044052,0.7942677140235901,0.2057322859764099 +apple/test/overripe/Screen Shot 2018-06-07 at 2.20.04 PM.png,overripe,overripe,0.0,0.7978011965751648,0.20219877362251282 +apple/test/overripe/Screen Shot 2018-06-07 at 2.20.34 PM.png,overripe,overripe,0.0,0.7535206079483032,0.24647939205169678 +apple/test/overripe/Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.426668256521225,0.5733317136764526 +apple/test/overripe/Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,overripe,0.0,0.6973071098327637,0.30269286036491394 +apple/test/overripe/Screen Shot 2018-06-07 at 2.34.49 PM.png,overripe,overripe,0.0,0.9671603441238403,0.032839640974998474 +apple/test/overripe/Screen Shot 2018-06-07 at 2.35.38 PM.png,overripe,overripe,0.0,0.418703556060791,0.581296443939209 +apple/test/overripe/Screen Shot 2018-06-07 at 2.37.53 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,overripe,0.0,0.824446439743042,0.1755535751581192 +apple/test/overripe/Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,overripe,0.0,0.5464485287666321,0.4535514712333679 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.48430266976356506,0.5156973004341125 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.35 PM.png,overripe,overripe,0.0,0.4442184567451477,0.5557815432548523 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.44 PM.png,overripe,overripe,0.0,0.7955507040023804,0.20444931089878082 +apple/test/overripe/Screen Shot 2018-06-07 at 2.39.53 PM.png,overripe,overripe,0.0,0.7676244378089905,0.23237557709217072 +apple/test/overripe/Screen Shot 2018-06-07 at 2.41.14 PM.png,overripe,overripe,0.9428315758705139,0.05716843530535698,0.22037874162197113 +apple/test/overripe/Screen Shot 2018-06-07 at 2.42.37 PM.png,overripe,overripe,0.6122614741325378,0.38773855566978455,0.3390651047229767 +apple/test/overripe/Screen Shot 2018-06-07 at 2.43.48 PM.png,overripe,overripe,0.0,0.49070119857788086,0.5092988014221191 +apple/test/overripe/Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.848612368106842,0.15138761699199677,0.23628957569599152 +apple/test/overripe/Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.776132345199585,0.22386763989925385,0.059576887637376785 +apple/test/overripe/Screen Shot 2018-06-07 at 2.47.50 PM.png,overripe,overripe,0.0904172882437706,0.6515184044837952,0.34848159551620483 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.08 PM.png,overripe,overripe,0.0,0.4012884795665741,0.5987115502357483 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.16 PM.png,overripe,overripe,0.0,0.40443962812423706,0.5955603718757629 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.37 PM.png,overripe,overripe,0.0,0.6409111022949219,0.3590888977050781 +apple/test/overripe/Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.40827131271362305,0.591728687286377 +apple/test/overripe/Screen Shot 2018-06-07 at 2.54.41 PM.png,overripe,overripe,0.0,0.4024764895439148,0.5975235104560852 +apple/test/overripe/Screen Shot 2018-06-07 at 2.56.47 PM.png,overripe,overripe,0.1045917421579361,0.8770256042480469,0.12297438830137253 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.04 PM.png,overripe,overripe,0.0,0.401737779378891,0.5982621908187866 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.30 PM.png,overripe,overripe,0.0,0.5290892124176025,0.47091078758239746 +apple/test/overripe/Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.48512938618659973,0.5148705840110779 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.09 PM.png,overripe,overripe,0.0,0.5103872418403625,0.48961275815963745 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.13 PM.png,overripe,overripe,0.0,0.5074149966239929,0.49258503317832947 +apple/test/overripe/Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.7817977666854858,0.21820221841335297,0.3606202006340027 +apple/test/overripe/Screen Shot 2018-06-07 at 3.00.25 PM.png,overripe,overripe,0.0,0.7927172780036926,0.20728273689746857 +apple/test/overripe/Screen Shot 2018-06-07 at 3.01.38 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-07 at 3.02.37 PM.png,overripe,overripe,0.011743295006453991,0.5223132967948914,0.47768667340278625 +apple/test/overripe/Screen Shot 2018-06-07 at 3.03.02 PM.png,overripe,overripe,0.22627583146095276,0.5952256917953491,0.4047743082046509 +apple/test/overripe/Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,overripe,0.0,0.6625497937202454,0.33745017647743225 +apple/test/overripe/Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.5296342968940735,0.4703657031059265 +apple/test/overripe/Screen Shot 2018-06-07 at 3.04.10 PM.png,overripe,overripe,0.0,0.5789469480514526,0.42105305194854736 +apple/test/overripe/Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.42003384232521057,0.579966127872467 +apple/test/overripe/Screen Shot 2018-06-07 at 3.06.30 PM.png,overripe,overripe,0.0,0.45272931456565857,0.5472707152366638 +apple/test/overripe/Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4085937440395355,0.5914062261581421 +apple/test/overripe/Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.48230722546577454,0.5176928043365479 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,overripe,0.8088228702545166,0.1911771148443222,0.15510158240795135 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.17 PM.png,overripe,overripe,0.0,0.47224000096321106,0.5277600288391113 +apple/test/overripe/Screen Shot 2018-06-08 at 2.25.24 PM.png,overripe,overripe,0.0,0.5476417541503906,0.4523582458496094 +apple/test/overripe/Screen Shot 2018-06-08 at 2.26.09 PM.png,overripe,overripe,0.0,0.8933544158935547,0.10664559155702591 +apple/test/overripe/Screen Shot 2018-06-08 at 2.26.55 PM.png,overripe,overripe,0.0,0.574117124080658,0.42588290572166443 +apple/test/overripe/Screen Shot 2018-06-08 at 2.28.07 PM.png,overripe,overripe,0.0,0.8121988773345947,0.18780113756656647 +apple/test/overripe/Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.46738964319229126,0.5326103568077087 +apple/test/overripe/Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6598382592201233,0.3401617407798767 +apple/test/overripe/Screen Shot 2018-06-08 at 2.30.45 PM.png,overripe,overripe,0.33995410799980164,0.660045862197876,0.23476363718509674 +apple/test/overripe/Screen Shot 2018-06-08 at 2.30.51 PM.png,overripe,overripe,0.0,0.5387656092643738,0.4612343907356262 +apple/test/overripe/Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,overripe,0.0,0.5897746682167053,0.4102253019809723 +apple/test/overripe/Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.46794599294662476,0.5320540070533752 +apple/test/overripe/Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.4029044210910797,0.5970955491065979 +apple/test/overripe/Screen Shot 2018-06-08 at 2.37.03 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/Screen Shot 2018-06-08 at 2.39.51 PM.png,overripe,overripe,0.0,0.8298832178115845,0.17011681199073792 +apple/test/overripe/Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6761451959609985,0.32385480403900146 +apple/test/overripe/Screen Shot 2018-06-08 at 2.42.58 PM.png,overripe,overripe,0.0,0.8416235446929932,0.15837645530700684 +apple/test/overripe/Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.47808218002319336,0.5219178199768066 +apple/test/overripe/Screen Shot 2018-06-08 at 2.45.44 PM.png,overripe,overripe,0.0,0.5183576345443726,0.48164236545562744 +apple/test/overripe/Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.40096020698547363,0.5990397930145264 +apple/test/overripe/Screen Shot 2018-06-08 at 2.46.25 PM.png,overripe,overripe,0.0,0.4709493815898895,0.5290505886077881 +apple/test/overripe/Screen Shot 2018-06-08 at 2.48.09 PM.png,overripe,overripe,0.0,0.5950728058815002,0.40492719411849976 +apple/test/overripe/Screen Shot 2018-06-08 at 2.48.43 PM.png,overripe,overripe,0.0,0.4915332794189453,0.5084667205810547 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.14 PM.png,overripe,overripe,0.0,0.49049779772758484,0.5095021724700928 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.25 PM.png,overripe,overripe,0.0,0.452358216047287,0.5476418137550354 +apple/test/overripe/Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.7317582368850708,0.2682417631149292 +apple/test/overripe/Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,overripe,0.0,0.7264909148216248,0.27350908517837524 +apple/test/overripe/Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.4019714891910553,0.5980285406112671 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,overripe,0.0,0.9683067798614502,0.03169320896267891 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.16.41 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.11404334753751755,0.7940260767936707,0.20597393810749054 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,overripe,0.0,0.7950757145881653,0.20492427051067352 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.20.29 PM.png,overripe,overripe,0.0,0.4887690246105194,0.5112309455871582 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4269527494907379,0.5730472803115845 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,overripe,0.0,0.6983228325843811,0.3016771376132965 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.25.26 PM.png,overripe,overripe,0.0,0.5882577300071716,0.41174226999282837 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,overripe,0.0,0.7394410967826843,0.2605589032173157 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.31.59 PM.png,overripe,overripe,0.0,0.8452314138412476,0.15476860105991364 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.34.18 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.35.21 PM.png,overripe,overripe,0.0920504778623581,0.7074757218360901,0.2925243079662323 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0,0.5425712466239929,0.4574287533760071 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.37.01 PM.png,overripe,overripe,0.0,0.4025708734989166,0.597429096698761 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.37.53 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.38 PM.png,overripe,overripe,0.0,0.5322250723838806,0.467774897813797 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.49 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,overripe,0.0,0.5448402166366577,0.4551598131656647 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.00 PM.png,overripe,overripe,0.0,0.9829521775245667,0.01704784668982029 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,overripe,0.0,0.7483903765678406,0.25160959362983704 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,overripe,0.0,0.9080915451049805,0.09190845489501953 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.41.07 PM.png,overripe,overripe,0.6295706629753113,0.37042930722236633,0.11884623020887375 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.42.25 PM.png,overripe,overripe,0.0,0.9281436204910278,0.07185638695955276 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.42.58 PM.png,overripe,unripe,0.4167487919330597,0.5832511782646179,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,unripe,0.4796822965145111,0.5203177332878113,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,overripe,0.0,0.7026820182800293,0.2973180115222931 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.6610499620437622,0.3389500677585602,0.26377883553504944 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.44.51 PM.png,overripe,overripe,0.44681820273399353,0.5531817674636841,0.31737861037254333 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.45.09 PM.png,overripe,overripe,0.0,0.40100690722465515,0.5989930629730225 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.5487484335899353,0.4512515962123871,0.035074446350336075 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.47.35 PM.png,overripe,overripe,0.23336206376552582,0.6845308542251587,0.3154691457748413 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7797549962997437,0.22024501860141754 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.54.49 PM.png,overripe,overripe,0.0,0.9360671043395996,0.06393289566040039 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.55.27 PM.png,overripe,overripe,0.0,0.9280431270599365,0.07195686548948288 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,overripe,0.0,0.5827373266220093,0.4172627031803131 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.57.26 PM.png,overripe,overripe,0.0,0.6259711384773254,0.37402886152267456 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.48935338854789734,0.5106465816497803 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.4701474905014038,0.5298525094985962 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.8215239644050598,0.17847603559494019,0.3506662845611572 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 2.59.52 PM.png,overripe,overripe,0.0,0.9186636209487915,0.0813363566994667 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,overripe,0.13751615583896637,0.5227135419845581,0.4772864878177643 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.29777848720550537,0.6839640736579895,0.3160359263420105 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,overripe,0.0,0.8444986343383789,0.1555013805627823 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.02.51 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,overripe,0.0,0.8569608926773071,0.14303909242153168 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,overripe,0.0,0.8946223258972168,0.10537765920162201 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.21.33 PM.png,overripe,overripe,0.0,0.8995535969734192,0.10044639557600021 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.23.48 PM.png,overripe,overripe,0.0,0.8978826999664307,0.10211727023124695 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.31 PM.png,overripe,overripe,0.0,0.6632320284843445,0.3367679715156555 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.37 PM.png,overripe,overripe,0.0,0.7515414357185364,0.24845854938030243 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.4607156217098236,0.539284348487854 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,overripe,0.0,0.955458402633667,0.0445416085422039 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,overripe,0.0,0.9383007287979126,0.061699278652668 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,overripe,0.0,0.6913803815841675,0.3086196482181549 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.30.31 PM.png,overripe,overripe,0.0,0.5019571185112,0.49804291129112244 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.32.10 PM.png,overripe,overripe,0.0,0.4012617766857147,0.5987382531166077 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.33.04 PM.png,overripe,overripe,0.0,0.41342276334762573,0.5865772366523743 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.467265784740448,0.532734215259552 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,overripe,0.5365432500839233,0.46345674991607666,0.10404040664434433 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,overripe,0.0,0.938224732875824,0.061775270849466324 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.37.24 PM.png,overripe,overripe,0.0,0.5152910351753235,0.4847089648246765 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.40.30 PM.png,overripe,overripe,0.0,0.5359194874763489,0.4640805125236511 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.46.44 PM.png,overripe,overripe,0.0,0.5454541444778442,0.45454588532447815 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.47.54 PM.png,overripe,overripe,0.0,0.43485355377197266,0.5651464462280273 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.48.15 PM.png,overripe,overripe,0.0,0.6856085062026978,0.31439152359962463 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.50.22 PM.png,overripe,overripe,0.0,0.547704815864563,0.452295184135437 +apple/test/overripe/rotated_by_15_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4461022615432739,0.5538977384567261 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.10958212614059448,0.7939813137054443,0.20601867139339447 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.20.46 PM.png,overripe,overripe,0.0,0.632486879825592,0.36751309037208557 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.23.02 PM.png,overripe,overripe,0.0,0.9823130965232849,0.017686905339360237 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,overripe,0.0,0.9632701873779297,0.036729805171489716 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.08686870336532593,0.5565102100372314,0.44348978996276855 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.37.11 PM.png,overripe,ripe,0.1818079799413681,0.8181920051574707,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.38.49 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,overripe,0.0,0.9184742569923401,0.08152573555707932 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.40.55 PM.png,overripe,overripe,0.3069930672645569,0.6930069327354431,0.21854381263256073 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.42.25 PM.png,overripe,overripe,0.0,0.9505967497825623,0.04940324276685715 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.43.26 PM.png,overripe,overripe,0.0,0.9173040390014648,0.08269596099853516 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.45.44 PM.png,overripe,overripe,0.0,0.4051132798194885,0.5948867201805115 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.46.04 PM.png,overripe,overripe,0.0,0.40443769097328186,0.5955622792243958 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.50.31 PM.png,overripe,overripe,0.0,0.5686571002006531,0.4313429296016693 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.00 PM.png,overripe,overripe,0.0,0.6491890549659729,0.3508109450340271 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.30 PM.png,overripe,overripe,0.0,0.8324998021125793,0.16750018298625946 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.8485864400863647,0.15141353011131287 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.53.20 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.53.33 PM.png,overripe,overripe,0.0,0.9859579205513,0.014042074792087078 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,overripe,0.051665663719177246,0.7535101771354675,0.24648980796337128 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.55.27 PM.png,overripe,overripe,0.0,0.9271116852760315,0.07288828492164612 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.55.52 PM.png,overripe,overripe,0.0,0.4622405171394348,0.5377594828605652 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.6168363094329834,0.3831636905670166 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.16 PM.png,overripe,overripe,0.0,0.42570480704307556,0.574295163154602 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,overripe,0.0,0.5843639969825745,0.4156360328197479 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.57.17 PM.png,overripe,overripe,0.0,0.47497987747192383,0.5250201225280762 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.4943205714225769,0.5056794285774231 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.4746500551700592,0.5253499746322632 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 2.59.09 PM.png,overripe,overripe,0.0,0.5180833339691162,0.4819166660308838 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.00.56 PM.png,overripe,overripe,0.0,0.5245475769042969,0.4754524230957031 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.01.38 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.24870027601718903,0.6739242076873779,0.32607579231262207 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,overripe,0.0,0.8391834497451782,0.16081656515598297 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.35 PM.png,overripe,overripe,0.0,0.6223083734512329,0.3776916265487671 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.41 PM.png,overripe,overripe,0.0,0.46168506145477295,0.538314938545227 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.04.58 PM.png,overripe,overripe,0.036418166011571884,0.9070121049880981,0.09298786520957947 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.40247201919555664,0.5975279808044434 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.29755207896232605,0.6635130047798157,0.3364869952201843 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.05.53 PM.png,overripe,overripe,0.0,0.6730727553367615,0.3269272446632385 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.9001975059509277,0.09980249404907227 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.17.25 PM.png,overripe,overripe,0.0,0.9011508226394653,0.09884918481111526 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4086782932281494,0.5913217067718506 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.24.09 PM.png,overripe,overripe,0.18525253236293793,0.6760610342025757,0.3239389657974243 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,overripe,0.0,0.9540358185768127,0.045964207500219345 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.27.15 PM.png,overripe,overripe,0.0,0.6870434880256653,0.3129565119743347 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,overripe,0.0,0.6871688365936279,0.31283116340637207 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,overripe,0.0,0.7327337861061096,0.2672662138938904 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,overripe,0.0,0.5886185765266418,0.41138145327568054 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.31.23 PM.png,overripe,overripe,0.0,0.6882686018943787,0.31173139810562134 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.32.42 PM.png,overripe,overripe,0.0,0.4039195477962494,0.596080482006073 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.40276506543159485,0.5972349643707275 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.35.03 PM.png,overripe,overripe,0.0,0.43099215626716614,0.5690078139305115 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.40850457549095154,0.5914954543113708 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.40.13 PM.png,overripe,overripe,0.0,0.8202294111251831,0.1797705888748169 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.44.54 PM.png,overripe,overripe,0.0,0.7589153051376343,0.24108467996120453 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.45.58 PM.png,overripe,overripe,0.0,0.8278490900993347,0.17215090990066528 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.47.30 PM.png,overripe,overripe,0.0,0.8913791179656982,0.10862090438604355 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.49.40 PM.png,overripe,overripe,0.0,0.48092421889305115,0.5190757513046265 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.22 PM.png,overripe,overripe,0.0,0.5438693761825562,0.45613065361976624 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4907514750957489,0.5092484951019287 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.712379515171051,0.287620484828949 +apple/test/overripe/rotated_by_30_Screen Shot 2018-06-08 at 2.52.43 PM.png,overripe,overripe,0.0,0.9390769600868225,0.060923025012016296 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7551569938659668,0.244842991232872 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,overripe,0.0,0.967350423336029,0.03264958783984184 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.7259302735328674,0.27406972646713257,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.20.56 PM.png,overripe,overripe,0.0,0.44718441367149353,0.5528156161308289 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.23.02 PM.png,overripe,overripe,0.0,0.9833796620368958,0.016620351001620293 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,overripe,0.0,0.93206387758255,0.06793615221977234 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.24.35 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.24.59 PM.png,overripe,overripe,0.0,0.9695764183998108,0.03042358160018921 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.31.59 PM.png,overripe,overripe,0.0,0.8330531120300293,0.16694685816764832 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.34.36 PM.png,overripe,overripe,0.0,0.5974652171134949,0.4025348126888275 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.34.49 PM.png,overripe,overripe,0.0,0.9630469679832458,0.03695303946733475 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.37.20 PM.png,overripe,unripe,0.567131519317627,0.43286851048469543,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,overripe,0.0,0.8424391150474548,0.15756088495254517 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.39.26 PM.png,overripe,overripe,0.0,0.4078674018383026,0.5921326279640198 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.40.13 PM.png,overripe,overripe,0.0,0.4262941777706146,0.573705792427063 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,overripe,0.0,0.7483091950416565,0.2516908049583435 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.42.18 PM.png,overripe,overripe,0.3821904957294464,0.617809534072876,0.21698372066020966 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.48610448837280273,0.5138955116271973,0.2841413617134094 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4066220819950104,0.5933778882026672 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.47.27 PM.png,overripe,overripe,0.0,0.7057316303253174,0.2942683696746826 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,overripe,0.0,0.7147276401519775,0.28527238965034485 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.412349134683609,0.5876508355140686 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.8451262712478638,0.15487371385097504 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.53.20 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.56.34 PM.png,overripe,overripe,0.0,0.9582783579826355,0.0417216531932354 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.56.57 PM.png,overripe,overripe,0.0,0.5849897265434265,0.4150102436542511 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.57.42 PM.png,overripe,overripe,0.0,0.4153805077075958,0.5846194624900818 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.57.49 PM.png,overripe,overripe,0.0,0.4582032561302185,0.5417967438697815 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.58.30 PM.png,overripe,overripe,0.0,0.5339092016220093,0.4660908281803131 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.59.38 PM.png,overripe,overripe,0.5518174171447754,0.4481825530529022,0.21782192587852478 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 2.59.52 PM.png,overripe,overripe,0.0,0.9076318144798279,0.09236818552017212 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,overripe,0.24669331312179565,0.5514664053916931,0.4485335946083069 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.33 PM.png,overripe,overripe,0.0,0.873406708240509,0.12659327685832977 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.00.40 PM.png,overripe,overripe,0.7814487814903259,0.21855123341083527,0.05215732008218765 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,overripe,0.0,0.5409248471260071,0.4590751528739929 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.2155858874320984,0.668299674987793,0.33170029520988464 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.02.18 PM.png,overripe,overripe,0.0,0.8997437953948975,0.10025618225336075 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.02.37 PM.png,overripe,overripe,0.0,0.4911389648914337,0.5088610053062439 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.03.02 PM.png,overripe,overripe,0.14312675595283508,0.6304293870925903,0.36957064270973206 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.04.24 PM.png,overripe,overripe,0.0,0.5080414414405823,0.4919585585594177 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.29 PM.png,overripe,overripe,0.0,0.7345165014266968,0.26548346877098083 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.38 PM.png,overripe,overripe,0.0,0.40143489837646484,0.5985651016235352 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.2712413966655731,0.6604287028312683,0.3395713269710541 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.9013089537620544,0.09869106113910675 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-07 at 3.17.32 PM.png,overripe,overripe,0.0,0.4090251326560974,0.5909748673439026 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5750827193260193,0.4249172806739807 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.44137096405029297,0.558629035949707 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6560968160629272,0.34390321373939514 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.28.42 PM.png,overripe,overripe,0.7839246988296509,0.21607527136802673,0.14804627001285553 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.29.33 PM.png,overripe,overripe,0.0,0.7246161103248596,0.2753838896751404 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.30.51 PM.png,overripe,overripe,0.0,0.536897599697113,0.4631023705005646 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.31.16 PM.png,overripe,overripe,0.0,0.40968266129493713,0.5903173089027405 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,overripe,0.0,0.5343549251556396,0.46564507484436035 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.4676794707775116,0.532320499420166 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.34.42 PM.png,overripe,overripe,0.0,0.414518266916275,0.5854817628860474 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.35.03 PM.png,overripe,overripe,0.0,0.4312627613544464,0.568737268447876 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,overripe,0.0,0.9288630485534668,0.0711369588971138 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,overripe,0.46940869092941284,0.5305913090705872,0.09870533645153046 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,overripe,0.0,0.937780499458313,0.06221947818994522 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,overripe,0.0,0.8245388865470886,0.17546111345291138 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.37.19 PM.png,overripe,overripe,0.0,0.8321002125740051,0.16789978742599487 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,overripe,0.0,0.8466804027557373,0.1533195823431015 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.39.26 PM.png,overripe,overripe,0.0,0.5598364472389221,0.44016358256340027 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.41.39 PM.png,overripe,overripe,0.0,0.6395807266235352,0.36041927337646484 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.41.44 PM.png,overripe,overripe,0.0,0.693849503993988,0.30615052580833435 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.06 PM.png,overripe,overripe,0.0007034925511106849,0.7578920722007751,0.24210794270038605 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6898362636566162,0.3101637363433838 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.38 PM.png,overripe,overripe,0.0,0.624383807182312,0.3756162226200104 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.42.52 PM.png,overripe,overripe,0.0,0.9736606478691101,0.026339346542954445 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.43.29 PM.png,overripe,overripe,0.0,0.7659586668014526,0.23404133319854736 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.44.13 PM.png,overripe,overripe,0.0,0.421697199344635,0.578302800655365 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.46.36 PM.png,overripe,overripe,0.0,0.40004780888557434,0.599952220916748 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.46.50 PM.png,overripe,overripe,0.0,0.4016577899456024,0.59834223985672 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.47.03 PM.png,overripe,overripe,0.0,0.40506136417388916,0.5949386358261108 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.47.30 PM.png,overripe,overripe,0.0,0.8886337876319885,0.11136619746685028 +apple/test/overripe/rotated_by_45_Screen Shot 2018-06-08 at 2.50.14 PM.png,overripe,overripe,0.0,0.485358864068985,0.5146411061286926 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.17.25 PM.png,overripe,overripe,0.0,0.8494974970817566,0.1505025029182434 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.20.46 PM.png,overripe,overripe,0.0,0.6329468488693237,0.36705315113067627 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,overripe,0.0,0.9032963514328003,0.09670363366603851 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.23.51 PM.png,overripe,overripe,0.0,0.840143620967865,0.159856379032135 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0641099065542221,0.5555886626243591,0.44441133737564087 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.20 PM.png,overripe,unripe,0.5653679966926575,0.43463197350502014,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.32 PM.png,overripe,overripe,0.0,0.4483490586280823,0.5516509413719177 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,overripe,0.0,0.8418064713478088,0.15819349884986877 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.40.28 PM.png,overripe,overripe,0.0,0.7490416765213013,0.25095832347869873 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.41.14 PM.png,overripe,overripe,0.6164188385009766,0.38358113169670105,0.18586483597755432 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.43.26 PM.png,overripe,overripe,0.0,0.9171419739723206,0.08285801112651825 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.43.54 PM.png,overripe,overripe,0.0,0.9392583966255188,0.06074158102273941 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4066635072231293,0.5933365225791931 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.40962210297584534,0.5903778672218323 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.47.13 PM.png,overripe,overripe,0.3001384139060974,0.6998615860939026,0.25409209728240967 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.47.20 PM.png,overripe,overripe,0.10661923885345459,0.7540392875671387,0.24596072733402252 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.8481209874153137,0.15187904238700867 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.608056902885437,0.391943097114563 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.56.47 PM.png,overripe,overripe,0.10662014782428741,0.8801792860031128,0.11982069164514542 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 2.58.38 PM.png,overripe,overripe,0.0,0.49593791365623474,0.5040620565414429 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.00.17 PM.png,overripe,overripe,0.0,0.8968648910522461,0.1031351238489151 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.01.54 PM.png,overripe,overripe,0.214319109916687,0.6683759093284607,0.3316240608692169 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.03.38 PM.png,overripe,overripe,0.02575046196579933,0.8483583927154541,0.1516416370868683 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.4902566075325012,0.5097433924674988 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.04.35 PM.png,overripe,overripe,0.0,0.6225723624229431,0.3774276375770569 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,overripe,0.0,0.8565447926521301,0.14345519244670868 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.28264111280441284,0.6614633798599243,0.3385366201400757 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-07 at 3.05.58 PM.png,overripe,overripe,0.0,0.7993506789207458,0.20064933598041534 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,overripe,0.7317115664482117,0.26828843355178833,0.1482333093881607 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6563419699668884,0.3436580002307892 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.26.34 PM.png,overripe,overripe,0.0,0.578015148639679,0.42198485136032104 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6566964387893677,0.3433035910129547 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,overripe,0.0,0.7308566570281982,0.26914334297180176 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.34.51 PM.png,overripe,overripe,0.0,0.41533204913139343,0.5846679210662842 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,overripe,0.0,0.9264752268791199,0.07352479547262192 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.18 PM.png,overripe,overripe,0.16023299098014832,0.7780072093009949,0.22199276089668274 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,overripe,0.0,0.9373990297317505,0.0626010000705719 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,overripe,0.0,0.8292298316955566,0.17077018320560455 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.37.19 PM.png,overripe,overripe,0.0,0.8322131037712097,0.1677868813276291 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.37.24 PM.png,overripe,overripe,0.0,0.5228731036186218,0.4771268963813782 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,overripe,0.0,0.8472363352775574,0.15276364982128143 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.38.47 PM.png,overripe,overripe,0.0,0.46913638710975647,0.5308635830879211 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.09 PM.png,overripe,overripe,0.0,0.9557929635047913,0.04420701786875725 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.30 PM.png,overripe,overripe,0.0,0.5363069772720337,0.4636929929256439 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.40.56 PM.png,overripe,overripe,0.0,0.4049716293811798,0.5950284004211426 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.41.16 PM.png,overripe,overripe,0.0,0.4824846386909485,0.5175153613090515 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6909096240997314,0.30909040570259094 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.4775329828262329,0.5224670171737671 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.44.13 PM.png,overripe,overripe,0.0,0.4211606979370117,0.5788393020629883 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.45.05 PM.png,overripe,overripe,0.0,0.7363434433937073,0.2636565864086151 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.47.09 PM.png,overripe,overripe,0.0,0.40773338079452515,0.5922666192054749 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,overripe,0.20428843796253204,0.6009278893470764,0.3990720808506012 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.50.38 PM.png,overripe,overripe,0.0,0.6114640235900879,0.3885359764099121 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.7080582976341248,0.29194167256355286 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.51.09 PM.png,overripe,overripe,0.0,0.43311309814453125,0.5668869018554688 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.52.05 PM.png,overripe,overripe,0.0,0.6538779735565186,0.34612199664115906 +apple/test/overripe/rotated_by_60_Screen Shot 2018-06-08 at 2.52.20 PM.png,overripe,overripe,0.0,0.40624698996543884,0.5937530398368835 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7550516128540039,0.24494841694831848 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.7599319219589233,0.24006809294223785,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.4281546473503113,0.5718453526496887 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,overripe,0.0,0.9122918248176575,0.08770819008350372 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,overripe,0.0,0.926906168460846,0.07309383898973465 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,overripe,0.0,0.9623044729232788,0.03769555315375328 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,overripe,0.0,0.7388497591018677,0.2611502707004547 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.34.18 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.36.21 PM.png,overripe,overripe,0.0,0.5387851595878601,0.4612148106098175 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.37.11 PM.png,overripe,ripe,0.18910716474056244,0.8108928203582764,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.38.38 PM.png,overripe,overripe,0.0,0.5328457951545715,0.4671541750431061 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.35 PM.png,overripe,overripe,0.0,0.4501740634441376,0.54982590675354 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.44 PM.png,overripe,overripe,0.0,0.8042840361595154,0.1957159787416458 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.39.53 PM.png,overripe,overripe,0.0,0.7690977454185486,0.23090225458145142 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.40.00 PM.png,overripe,overripe,0.0,0.9824215173721313,0.0175784844905138 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.41.23 PM.png,overripe,overripe,0.3441731631755829,0.6558268070220947,0.23758749663829803 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.41.32 PM.png,overripe,overripe,0.0,0.9459468126296997,0.054053205996751785 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.42.58 PM.png,overripe,unripe,0.4022534191608429,0.5977466106414795,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.43.54 PM.png,overripe,overripe,0.0,0.9566748738288879,0.04332513362169266 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,overripe,0.0,0.7034739851951599,0.2965260446071625 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.44.59 PM.png,overripe,overripe,0.0,0.49054017663002014,0.5094598531723022 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4066976308822632,0.5933023691177368 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.46.12 PM.png,overripe,overripe,0.0,0.4086005985736847,0.5913994312286377 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.47.13 PM.png,overripe,overripe,0.3048560619354248,0.6951439380645752,0.2489471733570099 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.50.31 PM.png,overripe,overripe,0.0,0.5756474733352661,0.4243524968624115 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.52.00 PM.png,overripe,overripe,0.0,0.6447377800941467,0.35526221990585327 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.4075360894203186,0.5924639105796814 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.54.08 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,overripe,0.0,0.7431972026824951,0.2568027973175049 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,overripe,0.0,0.9022953510284424,0.09770466387271881 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.02.09 PM.png,overripe,overripe,0.0,0.8444233536720276,0.1555766463279724 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.02.24 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.05.46 PM.png,overripe,overripe,0.3315379023551941,0.6684620976448059,0.3293115794658661 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,overripe,0.0,0.8890763521194458,0.11092362552881241 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-07 at 3.06.51 PM.png,overripe,overripe,0.0,0.5711193084716797,0.4288806617259979 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.25.43 PM.png,overripe,overripe,0.0,0.6806471347808838,0.3193528652191162 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6561587452888489,0.34384122490882874 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.28.23 PM.png,overripe,overripe,0.0,0.6910083889961243,0.3089916408061981 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6582351326942444,0.341764897108078 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.03 PM.png,overripe,overripe,0.0,0.5899176001548767,0.4100823998451233 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.16 PM.png,overripe,overripe,0.0,0.40940433740615845,0.5905956625938416 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,overripe,0.0,0.5353731513023376,0.46462681889533997 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.33.49 PM.png,overripe,overripe,0.0,0.8827162981033325,0.11728369444608688 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.34.05 PM.png,overripe,overripe,0.0,0.862308919429779,0.13769106566905975 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.4028860330581665,0.5971139669418335 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.36.55 PM.png,overripe,overripe,0.0,0.8274161219596863,0.17258386313915253 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.37.13 PM.png,overripe,overripe,0.0,0.8851184844970703,0.1148814931511879 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.38.33 PM.png,overripe,overripe,0.0,0.6763423085212708,0.32365769147872925 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.4083316922187805,0.5916683077812195 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.13 PM.png,overripe,overripe,0.0,0.8216577768325806,0.17834220826625824 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.38 PM.png,overripe,overripe,0.0,0.4441024363040924,0.55589759349823 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.40.46 PM.png,overripe,overripe,0.0,0.40169283747673035,0.598307192325592 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.41.54 PM.png,overripe,overripe,0.0,0.4227246344089508,0.5772753357887268 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.42.38 PM.png,overripe,overripe,0.0,0.6231865882873535,0.3768134117126465 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.42.58 PM.png,overripe,overripe,0.0,0.8417767882347107,0.1582232415676117 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.43.29 PM.png,overripe,overripe,0.0,0.7498872876167297,0.25011271238327026 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.43.54 PM.png,overripe,overripe,0.0,0.47870317101478577,0.5212968587875366 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.44.54 PM.png,overripe,overripe,0.0,0.7565832734107971,0.24341674149036407 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.4012910723686218,0.5987089276313782 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.09 PM.png,overripe,overripe,0.0,0.4076642394065857,0.5923357605934143 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.37 PM.png,overripe,overripe,0.0,0.4300783574581146,0.569921612739563 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,overripe,0.25358760356903076,0.6125152111053467,0.3874848186969757 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.47.54 PM.png,overripe,overripe,0.0,0.43532735109329224,0.5646726489067078 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.48.29 PM.png,overripe,overripe,0.15717950463294983,0.647361695766449,0.3526383340358734 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.49.27 PM.png,overripe,overripe,0.0,0.842556893825531,0.157443106174469 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.50.55 PM.png,overripe,overripe,0.0,0.5361279845237732,0.4638720154762268 +apple/test/overripe/rotated_by_75_Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.40182405710220337,0.5981759428977966 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.15.34 PM.png,overripe,overripe,0.0,0.7543689012527466,0.2456311285495758 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,overripe,0.0,0.9663136005401611,0.03368639200925827 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.16.54 PM.png,overripe,overripe,0.0,0.8741502165794373,0.12584978342056274 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.18.25 PM.png,overripe,overripe,0.10810915380716324,0.7941272258758545,0.20587274432182312 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.18.57 PM.png,overripe,overripe,0.0,0.6735761165618896,0.32642385363578796 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,overripe,0.0,0.792866587638855,0.20713339745998383 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.23.24 PM.png,overripe,overripe,0.0,0.9250963926315308,0.07490359991788864 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.23.51 PM.png,overripe,overripe,0.0,0.7818387746810913,0.21816124022006989 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.33.47 PM.png,overripe,overripe,0.0,0.5160294771194458,0.4839704930782318 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,overripe,0.0,0.823380708694458,0.1766192764043808 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,overripe,0.0,0.8924233317375183,0.10757669061422348 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.38.59 PM.png,overripe,overripe,0.0,0.5478559136390686,0.4521440863609314 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.4860113859176636,0.5139886140823364 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.40.13 PM.png,overripe,overripe,0.0,0.43759825825691223,0.5624017119407654 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.40.55 PM.png,overripe,overripe,0.4234825074672699,0.5765174627304077,0.20679369568824768 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.41.32 PM.png,overripe,overripe,0.0,0.934971272945404,0.06502871960401535 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,unripe,0.428395539522171,0.5716044902801514,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.43.48 PM.png,overripe,overripe,0.0,0.49207803606987,0.5079219937324524 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.44.05 PM.png,overripe,overripe,0.0,0.6750780344009399,0.32492196559906006 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.45.55 PM.png,overripe,overripe,0.0,0.4087047278881073,0.5912952423095703 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.41281408071517944,0.5871859192848206 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.76760333776474,0.23239664733409882,0.06069063022732735 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.47.20 PM.png,overripe,overripe,0.10444310307502747,0.7809596657752991,0.21904034912586212 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,overripe,0.0,0.668408989906311,0.3315909802913666 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4140018820762634,0.5859981179237366 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.52.09 PM.png,overripe,overripe,0.0,0.8806607723236084,0.11933925002813339 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.52.30 PM.png,overripe,overripe,0.0,0.8620868921279907,0.13791309297084808 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.53.33 PM.png,overripe,overripe,0.0,0.9933769702911377,0.006623028311878443 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.53.57 PM.png,overripe,overripe,0.0,0.41039180755615234,0.5896081924438477 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.54.08 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.54.41 PM.png,overripe,overripe,0.0,0.4045596122741699,0.5954403877258301 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.501268208026886,0.4987318217754364 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,overripe,0.0,0.8970405459403992,0.10295943915843964 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.7745237350463867,0.22547627985477448,0.36066946387290955 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.00.40 PM.png,overripe,overripe,0.8140609860420227,0.1859390288591385,0.057126980274915695 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,overripe,0.0,0.54913729429245,0.45086270570755005 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.03.12 PM.png,overripe,overripe,0.0,0.40924781560897827,0.5907521843910217 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.04.04 PM.png,overripe,overripe,0.0,0.5310338139533997,0.46896615624427795 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.04.47 PM.png,overripe,overripe,0.0,0.4800986647605896,0.5199013352394104 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.05.05 PM.png,overripe,overripe,0.0,0.7839930057525635,0.21600699424743652 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.8970319032669067,0.10296806693077087 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.06.30 PM.png,overripe,overripe,0.0,0.45455679297447205,0.5454431772232056 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-07 at 3.17.25 PM.png,overripe,overripe,0.0,0.8978942036628723,0.10210580378770828 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.21.33 PM.png,overripe,overripe,0.0,0.8923357725143433,0.10766425728797913 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5685101747512817,0.4314897954463959 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.24.37 PM.png,overripe,overripe,0.0,0.7581122517585754,0.24188776314258575 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6560887098312378,0.3439112603664398 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.27.15 PM.png,overripe,overripe,0.0,0.6853026151657104,0.31469735503196716 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.27.44 PM.png,overripe,overripe,0.0,0.8546470999717712,0.14535290002822876 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4671778678894043,0.5328221321105957 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.29.20 PM.png,overripe,overripe,0.0,0.6600779891014099,0.3399220108985901 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.30.03 PM.png,overripe,overripe,0.0,0.7159018516540527,0.28409814834594727 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.30.57 PM.png,overripe,overripe,0.0,0.6855120658874512,0.31448793411254883 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.31.08 PM.png,overripe,overripe,0.0,0.46906575560569763,0.53093421459198 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.40459132194519043,0.5954086780548096 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.35.25 PM.png,overripe,overripe,0.0,0.9230174422264099,0.07698256522417068 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.35.37 PM.png,overripe,overripe,0.0,0.5560615658760071,0.44393840432167053 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.36.01 PM.png,overripe,overripe,0.0,0.4511595368385315,0.5488404631614685 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.36.23 PM.png,overripe,overripe,0.4390043616294861,0.5609956383705139,0.09869155287742615 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.39.26 PM.png,overripe,overripe,0.0,0.5676177740097046,0.4323822259902954 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.45.50 PM.png,overripe,overripe,0.0,0.5061455368995667,0.49385446310043335 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.46.36 PM.png,overripe,overripe,0.0,0.4019896984100342,0.5980103015899658 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.47.49 PM.png,overripe,overripe,0.524116575717926,0.4758833944797516,0.322188138961792 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4475027322769165,0.5524972677230835 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.51.09 PM.png,overripe,overripe,0.0,0.4356294274330139,0.5643705725669861 +apple/test/overripe/saltandpepper_Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,overripe,0.0,0.726266086101532,0.273733913898468 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,overripe,0.0,0.9682204723358154,0.03177954629063606 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.16.41 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.17.15 PM.png,overripe,overripe,0.0,0.6220037937164307,0.3779962360858917 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.18.13 PM.png,overripe,unripe,0.7047738432884216,0.29522618651390076,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.19.37 PM.png,overripe,overripe,0.0,0.7940395474433899,0.2059604525566101 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.19.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.20.34 PM.png,overripe,overripe,0.0,0.7442492842674255,0.25575071573257446 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.22.00 PM.png,overripe,overripe,0.0,0.9108261466026306,0.089173823595047 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.25.16 PM.png,overripe,overripe,0.0,0.9762999415397644,0.023700030520558357 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.35.21 PM.png,overripe,overripe,0.12811650335788727,0.6971621513366699,0.3028378486633301 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.36.06 PM.png,overripe,overripe,0.11170769482851028,0.5649522542953491,0.4350477159023285 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.37.01 PM.png,overripe,overripe,0.0,0.4021989107131958,0.5978010892868042 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,overripe,0.0,0.8853477239608765,0.11465225368738174 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.449419766664505,0.5505802035331726 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.39.26 PM.png,overripe,overripe,0.0,0.4092854857444763,0.5907145142555237 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.40.48 PM.png,overripe,overripe,0.0,0.8969487547874451,0.10305122286081314 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.44.26 PM.png,overripe,overripe,0.1159982979297638,0.5823452472686768,0.41765475273132324 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.44.59 PM.png,overripe,overripe,0.0,0.47979605197906494,0.5202039480209351 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.46.32 PM.png,overripe,overripe,0.8022072315216064,0.19779276847839355,0.05516377091407776 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.47.27 PM.png,overripe,overripe,0.0,0.713334321975708,0.286665678024292 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.47.35 PM.png,overripe,overripe,0.19957226514816284,0.6870830059051514,0.31291699409484863 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.50.09 PM.png,overripe,overripe,0.0,0.4006073474884033,0.5993926525115967 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.50.52 PM.png,overripe,overripe,0.0,0.7137728929519653,0.2862270772457123 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.51.45 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.52.44 PM.png,overripe,overripe,0.0,0.7796455025672913,0.22035448253154755 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.54.49 PM.png,overripe,overripe,0.0,0.9377363920211792,0.06226362660527229 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.54.58 PM.png,overripe,overripe,0.0,0.7107579708099365,0.2892419993877411 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.56.09 PM.png,overripe,overripe,0.0,0.49930864572525024,0.5006913542747498 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.57.05 PM.png,overripe,overripe,0.0,0.9017955660820007,0.09820446372032166 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.57.13 PM.png,overripe,overripe,0.39152562618255615,0.38196709752082825,0.6180329322814941 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 2.58.47 PM.png,overripe,overripe,0.0,0.477611780166626,0.522388219833374 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.00.25 PM.png,overripe,overripe,0.0,0.7995904088020325,0.20040959119796753 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.02.02 PM.png,overripe,overripe,0.0,0.6949083209037781,0.30509164929389954 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.12 PM.png,overripe,overripe,0.0,0.4077388346195221,0.5922611355781555 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.21 PM.png,overripe,overripe,0.059224799275398254,0.6320396065711975,0.3679603934288025 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,overripe,0.0,0.6662530303001404,0.33374693989753723 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.46 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.03.58 PM.png,overripe,overripe,0.0,0.45804184675216675,0.5419581532478333 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.04.10 PM.png,overripe,overripe,0.0,0.5738403797149658,0.42615965008735657 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.05.13 PM.png,overripe,overripe,0.0,0.8463939428329468,0.15360605716705322 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.06 PM.png,overripe,overripe,0.0,0.8928197622299194,0.10718020796775818 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.11 PM.png,overripe,overripe,0.0,0.44270071387290955,0.5572992563247681 +apple/test/overripe/translation_Screen Shot 2018-06-07 at 3.06.22 PM.png,overripe,overripe,0.0,0.8948938846588135,0.10510610789060593 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.5553269982337952,0.44467297196388245 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.25.04 PM.png,overripe,overripe,0.6546785235404968,0.3453214466571808,0.142344668507576 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.26.34 PM.png,overripe,overripe,0.08823717385530472,0.6058476567268372,0.39415237307548523 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.26.44 PM.png,overripe,overripe,0.0,0.9600381851196289,0.0399618037045002 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.28.00 PM.png,overripe,overripe,0.0,0.7655057907104492,0.2344941943883896 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,overripe,0.0,0.9268267154693604,0.07317330688238144 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.29.10 PM.png,overripe,overripe,0.0,0.4627687335014343,0.5372312664985657 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.30.26 PM.png,overripe,overripe,0.0,0.4296092092990875,0.5703907608985901 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.31.45 PM.png,overripe,overripe,0.0,0.5409867167472839,0.4590132534503937 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.34.42 PM.png,overripe,overripe,0.0,0.4157101809978485,0.5842898488044739 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.34.47 PM.png,overripe,overripe,0.0,0.403055340051651,0.5969446301460266 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.36.23 PM.png,overripe,overripe,0.6771324872970581,0.3228675127029419,0.12590482831001282 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.38.08 PM.png,overripe,overripe,0.0,0.8537853956222534,0.14621460437774658 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.41.44 PM.png,overripe,overripe,0.0,0.6924629807472229,0.3075370192527771 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.45.50 PM.png,overripe,overripe,0.0,0.5313133597373962,0.46868664026260376 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.46.08 PM.png,overripe,overripe,0.0,0.40154924988746643,0.598450779914856 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.47.37 PM.png,overripe,overripe,0.0,0.423509418964386,0.576490581035614 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.00 PM.png,overripe,overripe,0.0,0.4798479378223419,0.5201520919799805 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.29 PM.png,overripe,overripe,0.29401230812072754,0.6650198698043823,0.3349801003932953 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.48.43 PM.png,overripe,overripe,0.0,0.5002697706222534,0.4997302293777466 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.49.27 PM.png,overripe,overripe,0.0,0.8377478122711182,0.16225217282772064 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.50.33 PM.png,overripe,overripe,0.0,0.4478205442428589,0.5521794557571411 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.50.49 PM.png,overripe,overripe,0.0,0.7518887519836426,0.24811123311519623 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.51.28 PM.png,overripe,overripe,0.0,0.7280323505401611,0.2719676196575165 +apple/test/overripe/translation_Screen Shot 2018-06-08 at 2.52.57 PM.png,overripe,overripe,0.0,0.40210866928100586,0.5978913307189941 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.15.50 PM.png,overripe,overripe,0.6475039720535278,0.3524959981441498,0.027730442583560944 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.16.18 PM.png,overripe,overripe,0.0,0.967724084854126,0.032275937497615814 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.17.15 PM.png,overripe,overripe,0.0,0.6141247749328613,0.3858751952648163 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.20.04 PM.png,overripe,overripe,0.0,0.7978246808052063,0.2021753489971161 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.20.29 PM.png,overripe,overripe,0.0,0.48689594864845276,0.5131040811538696 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.21.09 PM.png,overripe,overripe,0.0,0.42696475982666016,0.5730352401733398 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.22.39 PM.png,overripe,overripe,0.0,0.6971606612205505,0.30283933877944946 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.31.43 PM.png,overripe,overripe,0.0,0.7345425486564636,0.2654574513435364 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.33.47 PM.png,overripe,overripe,0.0,0.5145911574363708,0.48540884256362915 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.37.43 PM.png,overripe,overripe,0.0,0.7920956611633301,0.20790430903434753 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.04 PM.png,overripe,overripe,0.0,0.9339483380317688,0.0660516768693924 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.13 PM.png,overripe,overripe,0.0,0.8242465853691101,0.1757534146308899 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.38.28 PM.png,overripe,overripe,0.0,0.8918978571891785,0.10810213536024094 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.39.20 PM.png,overripe,overripe,0.0,0.4843032956123352,0.5156967043876648 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.43.07 PM.png,overripe,unripe,0.44806912541389465,0.551930844783783,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.43.34 PM.png,overripe,overripe,0.0,0.43719637393951416,0.5628036260604858 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.44.36 PM.png,overripe,overripe,0.848612368106842,0.15138761699199677,0.23628957569599152 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.45.18 PM.png,overripe,overripe,0.8801013827323914,0.11989860236644745,0.14219418168067932 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.45.35 PM.png,overripe,overripe,0.0,0.41379278898239136,0.5862072110176086 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.46.04 PM.png,overripe,overripe,0.0,0.4046403169631958,0.5953596830368042 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.46.22 PM.png,overripe,overripe,0.0,0.4106054902076721,0.5893945097923279 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.47.01 PM.png,overripe,overripe,0.0,0.4008193612098694,0.5991806387901306 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.51.01 PM.png,overripe,overripe,0.0,0.4118858873844147,0.5881140828132629 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 2.59.23 PM.png,overripe,overripe,0.7782055139541626,0.2217945009469986,0.36183157563209534 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.00.00 PM.png,overripe,overripe,0.1962965428829193,0.5371797680854797,0.46282023191452026 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.01.09 PM.png,overripe,overripe,0.0,0.5472190380096436,0.45278096199035645 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.21 PM.png,overripe,overripe,0.0,0.6150673031806946,0.3849326968193054 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.31 PM.png,overripe,overripe,0.0,0.6624906659126282,0.3375093638896942 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.03.38 PM.png,overripe,overripe,0.0,0.853050172328949,0.1469498574733734 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-07 at 3.04.47 PM.png,overripe,overripe,0.0,0.4795546531677246,0.5204453468322754 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.23.40 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.24.15 PM.png,overripe,overripe,0.0,0.567679226398468,0.43232080340385437 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.24.46 PM.png,overripe,overripe,0.0,0.4821779131889343,0.5178220868110657 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.25.24 PM.png,overripe,overripe,0.0,0.5476964712142944,0.4523034989833832 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.26.09 PM.png,overripe,overripe,0.0,0.8940961956977844,0.10590382665395737 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.26.14 PM.png,overripe,overripe,0.0,0.6556635499000549,0.34433645009994507 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.28.12 PM.png,overripe,overripe,0.0,0.9323820471763611,0.0676179751753807 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.30.57 PM.png,overripe,overripe,0.0,0.6856135725975037,0.31438642740249634 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.33.28 PM.png,overripe,overripe,0.0,0.5987611413002014,0.4012388586997986 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.33.49 PM.png,overripe,overripe,0.0,0.8970608115196228,0.1029391661286354 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.34.37 PM.png,overripe,overripe,0.0,0.46794700622558594,0.5320529937744141 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.36.31 PM.png,overripe,overripe,0.6372930407524109,0.3627069890499115,0.10893480479717255 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.36.43 PM.png,overripe,overripe,0.0,0.9374876022338867,0.06251242756843567 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.37.03 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.38.54 PM.png,overripe,overripe,0.0,0.40807774662971497,0.5919222831726074 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.39.02 PM.png,overripe,ripe,0.0,1.0,0.0 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.39.21 PM.png,overripe,overripe,0.2488100677728653,0.7511899471282959,0.02317122556269169 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.40.38 PM.png,overripe,overripe,0.0,0.44486579298973083,0.5551341772079468 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.40.46 PM.png,overripe,overripe,0.0,0.40149807929992676,0.5985019207000732 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.42.06 PM.png,overripe,overripe,0.0037744492292404175,0.7638024687767029,0.23619753122329712 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.42.30 PM.png,overripe,overripe,0.0,0.6761281490325928,0.3238718509674072 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.46.50 PM.png,overripe,overripe,0.0,0.40154266357421875,0.5984573364257812 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.47.03 PM.png,overripe,overripe,0.0,0.4041697680950165,0.5958302617073059 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.48.00 PM.png,overripe,overripe,0.0,0.4795458912849426,0.5204541087150574 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.50.25 PM.png,overripe,overripe,0.0,0.45197761058807373,0.5480223894119263 +apple/test/overripe/vertical_flip_Screen Shot 2018-06-08 at 2.51.43 PM.png,overripe,overripe,0.04209024831652641,0.6678847670555115,0.3321152329444885 +apple/test/ripe/Screen Shot 2018-06-08 at 4.59.44 PM.png,ripe,unripe,0.15061892569065094,0.8493810892105103,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.15 PM.png,ripe,ripe,0.11630692332983017,0.8836930990219116,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,overripe,0.0,0.9882926344871521,0.011707386001944542 +apple/test/ripe/Screen Shot 2018-06-08 at 5.01.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.4442790448665619,0.5557209253311157 +apple/test/ripe/Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,ripe,0.0963401049375534,0.9036598801612854,0.0010483769001439214 +apple/test/ripe/Screen Shot 2018-06-08 at 5.04.16 PM.png,ripe,unripe,0.26259636878967285,0.7374036312103271,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.04.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.2202623337507248,0.779737651348114,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.40046724677085876,0.5995327234268188 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.18 PM.png,ripe,overripe,0.0,0.9187333583831787,0.08126665651798248 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,unripe,0.16350586712360382,0.8364941477775574,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,overripe,0.8902803659439087,0.10971960425376892,0.029326602816581726 +apple/test/ripe/Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.17 PM.png,ripe,unripe,0.9623035192489624,0.0376964695751667,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.31 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.09.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.10.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.25 PM.png,ripe,overripe,0.11337923258543015,0.8866207599639893,0.0067711747251451015 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.31 PM.png,ripe,overripe,0.20574934780597687,0.7942506670951843,0.010814245790243149 +apple/test/ripe/Screen Shot 2018-06-08 at 5.13.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.14.01 PM.png,ripe,unripe,0.23054085671901703,0.7694591283798218,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.15.09 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.15.45 PM.png,ripe,unripe,0.5473261475563049,0.45267385244369507,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,unripe,0.15919406712055206,0.8408059477806091,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,ripe,0.11217188090085983,0.8878281116485596,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.09617671370506287,0.9038233160972595,0.0005068683531135321 +apple/test/ripe/Screen Shot 2018-06-08 at 5.17.58 PM.png,ripe,unripe,0.3810320794582367,0.6189678907394409,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.19.58 PM.png,ripe,overripe,0.0,0.9632399678230286,0.036760035902261734 +apple/test/ripe/Screen Shot 2018-06-08 at 5.21.06 PM.png,ripe,overripe,0.0,0.5320525169372559,0.46794748306274414 +apple/test/ripe/Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,overripe,0.0,0.9012627601623535,0.09873724728822708 +apple/test/ripe/Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,unripe,0.23702529072761536,0.762974739074707,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,overripe,0.0,0.6265431642532349,0.37345680594444275 +apple/test/ripe/Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.15389837324619293,0.8461016416549683,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.06 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.34 PM.png,ripe,unripe,0.6390444040298462,0.3609556257724762,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,overripe,0.6459391117095947,0.3540608882904053,0.2457718402147293 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.04 PM.png,ripe,unripe,0.3078884482383728,0.6921115517616272,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.24 PM.png,ripe,overripe,0.0,0.7673981785774231,0.2326018214225769 +apple/test/ripe/Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.1997056007385254,0.8002943992614746,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,ripe,0.09978152066469193,0.9002184867858887,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.14325110614299774,0.8567488789558411,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.38 PM.png,ripe,unripe,0.23443204164505005,0.76556795835495,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,overripe,0.013065463863313198,0.7481383681297302,0.2518616318702698 +apple/test/ripe/Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.6691665649414062,0.33083343505859375,0.0 +apple/test/ripe/Screen Shot 2018-06-08 at 5.34.07 PM.png,ripe,ripe,0.12945596873760223,0.8705440163612366,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 4.59.49 PM.png,ripe,unripe,0.45786169171333313,0.5421382784843445,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.02.08 PM.png,ripe,overripe,0.2890103757381439,0.7109896540641785,0.024732327088713646 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.03.47 PM.png,ripe,unripe,0.18591001629829407,0.8140900135040283,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.2115452140569687,0.7884547710418701,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.04.59 PM.png,ripe,overripe,0.0,0.7632907629013062,0.23670923709869385 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.06.23 PM.png,ripe,ripe,0.10399141162633896,0.8960086107254028,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,unripe,0.19720794260501862,0.8027920722961426,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.21119940280914307,0.7888005971908569,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,overripe,0.9322095513343811,0.06779047101736069,0.027013704180717468 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,ripe,0.11172854900360107,0.8882714509963989,0.0021966041531413794 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.10.29 PM.png,ripe,ripe,0.26160505414009094,0.7383949756622314,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.14.20 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.14.48 PM.png,ripe,unripe,0.4214540719985962,0.5785459280014038,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.17.04 PM.png,ripe,unripe,0.33098089694976807,0.6690191030502319,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.17.58 PM.png,ripe,unripe,0.4431304931640625,0.5568695068359375,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,unripe,0.1513465791940689,0.8486534357070923,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.21.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.22.53 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.41051313281059265,0.589486837387085,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.24.26 PM.png,ripe,overripe,0.0,0.9359968304634094,0.06400319933891296 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.33 PM.png,ripe,overripe,0.0,0.78660649061203,0.21339352428913116 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,overripe,0.0,0.6299331784248352,0.3700668513774872 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.49 PM.png,ripe,overripe,0.0,0.5709038972854614,0.4290961027145386 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.35595038533210754,0.6440496444702148,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.5388264656066895,0.46117353439331055,0.0021030826028436422 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.1552460491657257,0.8447539806365967,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.19 PM.png,ripe,unripe,0.18965311348438263,0.8103469014167786,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.27 PM.png,ripe,unripe,0.1557529866695404,0.844247043132782,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,overripe,0.6407457590103149,0.35925424098968506,0.23931367695331573 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_15_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19908766448497772,0.8009123206138611,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.01.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.02.31 PM.png,ripe,unripe,0.7033951878547668,0.29660478234291077,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.03.34 PM.png,ripe,unripe,0.16179390251636505,0.8382061123847961,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,ripe,0.10202867537736893,0.8979713320732117,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.2107408344745636,0.789259135723114,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.05.06 PM.png,ripe,overripe,0.0,0.7549250721931458,0.24507494270801544 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.21971137821674347,0.7802886366844177,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,unripe,0.24081110954284668,0.7591888904571533,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.21139907836914062,0.7886009216308594,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,unripe,0.15791091322898865,0.842089056968689,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.07.52 PM.png,ripe,unripe,0.9567007422447205,0.04329923167824745,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,overripe,0.11104752868413925,0.8889524936676025,0.004727636463940144 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.09.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,unripe,0.2632097601890564,0.7367902398109436,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.11.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.11.35 PM.png,ripe,unripe,0.1848573088645935,0.8151426911354065,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.16 PM.png,ripe,ripe,0.08036331832408905,0.9196366667747498,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.0991649404168129,0.9008350372314453,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.16.49 PM.png,ripe,overripe,0.0,0.9858324527740479,0.014167574234306812 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.19.28 PM.png,ripe,unripe,0.7247400879859924,0.27525991201400757,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.20.17 PM.png,ripe,ripe,0.12858957052230835,0.8714104294776917,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,unripe,0.1496046632528305,0.8503953218460083,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.21.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,overripe,0.0,0.6137301325798035,0.38626986742019653 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.22.48 PM.png,ripe,overripe,0.0,0.8756043314933777,0.12439566850662231 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.23.51 PM.png,ripe,ripe,0.1044882982969284,0.8955116868019104,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.24.35 PM.png,ripe,unripe,0.1504228711128235,0.8495771288871765,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.6557435393333435,0.3442564308643341,7.196767546702176e-05 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.26.41 PM.png,ripe,overripe,0.07151981443166733,0.9284802079200745,0.05119211599230766 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,unripe,0.8298211097717285,0.1701788753271103,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.27.54 PM.png,ripe,overripe,0.6441575288772583,0.3558424711227417,0.2458137422800064 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.28.32 PM.png,ripe,overripe,0.0,0.7817329168319702,0.2182670682668686 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.1988065540790558,0.8011934757232666,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.15153425931930542,0.8484657406806946,0.0 +apple/test/ripe/rotated_by_30_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5249543190002441,0.47504571080207825,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,unripe,0.24576541781425476,0.7542346119880676,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.00.26 PM.png,ripe,unripe,0.23173245787620544,0.7682675123214722,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.01.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.24 PM.png,ripe,unripe,0.18798217177391052,0.8120178580284119,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.4370529353618622,0.5629470944404602 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.03.40 PM.png,ripe,ripe,0.10221289843320847,0.8977870941162109,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.04.05 PM.png,ripe,unripe,0.6808990240097046,0.3191010057926178,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.04.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,overripe,0.0,0.8775526881217957,0.12244731187820435 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.18 PM.png,ripe,overripe,0.0,0.6565932035446167,0.3434067666530609 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.05.27 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,unripe,0.19862088561058044,0.8013791441917419,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.09.25 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,unripe,0.2453845590353012,0.7546154260635376,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.11.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.11.41 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.14.01 PM.png,ripe,unripe,0.27466338872909546,0.7253366112709045,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.0496506467461586,0.9503493309020996,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.17.10 PM.png,ripe,unripe,0.15734995901584625,0.8426500558853149,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.18.42 PM.png,ripe,overripe,0.06953629851341248,0.8232388496398926,0.17676113545894623 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.19.47 PM.png,ripe,unripe,0.7659528255462646,0.23404718935489655,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.32 PM.png,ripe,unripe,0.7802921533584595,0.21970783174037933,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,overripe,0.0,0.9718355536460876,0.02816443145275116 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.21.44 PM.png,ripe,unripe,0.6817477345466614,0.318252295255661,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.22.20 PM.png,ripe,overripe,0.0,0.7833824753761292,0.21661750972270966 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.9791838526725769,0.020816123113036156,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.24.35 PM.png,ripe,unripe,0.14938010275363922,0.850619912147522,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.35822367668151855,0.6417763233184814,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.15612784028053284,0.8438721895217896,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.41 PM.png,ripe,overripe,0.07143846899271011,0.9285615086555481,0.05228470638394356 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.26.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,unripe,0.8672584891319275,0.1327415108680725,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.28.04 PM.png,ripe,unripe,0.3276796042919159,0.6723203659057617,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19819389283657074,0.8018060922622681,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.29.24 PM.png,ripe,unripe,0.1906585544347763,0.8093414306640625,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.33.55 PM.png,ripe,ripe,0.0877552404999733,0.9122447371482849,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.34.14 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_45_Screen Shot 2018-06-08 at 5.34.21 PM.png,ripe,unripe,0.23941953480243683,0.7605804800987244,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 4.59.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,unripe,0.19903016090393066,0.8009698390960693,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.01.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.02.48 PM.png,ripe,ripe,0.12944579124450684,0.8705542087554932,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.02.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.03.17 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.04.16 PM.png,ripe,unripe,0.2652243971824646,0.7347756028175354,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.18 PM.png,ripe,overripe,0.0,0.6552311778068542,0.34476882219314575 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.21963034570217133,0.7803696393966675,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.40058043599128723,0.5994195938110352 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,unripe,0.24226835370063782,0.7577316761016846,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,overripe,0.24330025911331177,0.7566997408866882,0.022537775337696075 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.07.32 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.08.58 PM.png,ripe,unripe,0.1762090027332306,0.823790967464447,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.09.17 PM.png,ripe,unripe,0.6703948378562927,0.3296051621437073,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.10.37 PM.png,ripe,unripe,0.611264705657959,0.3887353241443634,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.14.07 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.06821701675653458,0.9317829608917236,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.15.28 PM.png,ripe,overripe,0.025007516145706177,0.8013089895248413,0.19869102537631989 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,unripe,0.15736915171146393,0.8426308631896973,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.16.16 PM.png,ripe,ripe,0.05520908907055855,0.9447908997535706,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,unripe,0.21552534401416779,0.7844746708869934,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.18.58 PM.png,ripe,overripe,0.018199164420366287,0.7595773339271545,0.24042265117168427 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.20.32 PM.png,ripe,unripe,0.8416653275489807,0.15833468735218048,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,overripe,0.0,0.9730971455574036,0.02690286748111248 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.44 PM.png,ripe,unripe,0.6558361053466797,0.3441638946533203,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,overripe,0.0,0.6166168451309204,0.383383184671402 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.23.07 PM.png,ripe,unripe,0.32416433095932007,0.6758356690406799,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.23.26 PM.png,ripe,unripe,0.1414884328842163,0.8585115671157837,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.37101882696151733,0.6289811730384827,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.26.19 PM.png,ripe,unripe,0.15523412823677063,0.8447659015655518,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.26.24 PM.png,ripe,overripe,0.0,0.8469003438949585,0.1530996412038803 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.06 PM.png,ripe,ripe,0.11088629066944122,0.88911372423172,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.13 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.27.49 PM.png,ripe,unripe,0.8342357277870178,0.16576428711414337,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.28.32 PM.png,ripe,overripe,0.0,0.7783722281455994,0.22162774205207825 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.14481689035892487,0.8551831245422363,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.32.33 PM.png,ripe,overripe,0.0,0.40230920910835266,0.597690761089325 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,overripe,0.013051177375018597,0.748881995677948,0.251118004322052 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.33.33 PM.png,ripe,overripe,0.0,0.9065520763397217,0.09344792366027832 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.33.47 PM.png,ripe,unripe,0.185910165309906,0.814089834690094,0.0 +apple/test/ripe/rotated_by_60_Screen Shot 2018-06-08 at 5.34.07 PM.png,ripe,ripe,0.13719739019870758,0.8628026247024536,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.26 PM.png,ripe,unripe,0.23121313750743866,0.7687868475914001,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.00.50 PM.png,ripe,unripe,0.19325801730155945,0.8067419528961182,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.02.31 PM.png,ripe,unripe,0.6053993701934814,0.39460062980651855,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.04.11 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.2110830545425415,0.7889169454574585,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,overripe,0.0,0.8751780986785889,0.12482189387083054 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.05.34 PM.png,ripe,unripe,0.21934302151203156,0.7806569933891296,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.23 PM.png,ripe,ripe,0.10235366225242615,0.8976463079452515,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.28 PM.png,ripe,unripe,0.19094893336296082,0.8090510368347168,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,overripe,0.2451619952917099,0.7548379898071289,0.02296290546655655 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.25 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.056399863213300705,0.9436001181602478,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.13.54 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.14.44 PM.png,ripe,ripe,0.05328867584466934,0.9467113018035889,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.14.48 PM.png,ripe,unripe,0.42169177532196045,0.5783082246780396,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.15.01 PM.png,ripe,unripe,0.3071524202823639,0.6928476095199585,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.15.14 PM.png,ripe,unripe,0.20211414992809296,0.7978858351707458,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.09719979763031006,0.9028002023696899,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.16.49 PM.png,ripe,overripe,0.0,0.9701380133628845,0.02986198477447033 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.17.22 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.19.15 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.19.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.42 PM.png,ripe,unripe,0.1498432755470276,0.8501567244529724,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.20.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.21.06 PM.png,ripe,overripe,0.0,0.533893883228302,0.466106116771698 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.21.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.22.27 PM.png,ripe,overripe,0.0,0.8628162145614624,0.1371837854385376 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.23.23 PM.png,ripe,ripe,0.13799533247947693,0.8620046377182007,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.25.43 PM.png,ripe,overripe,0.0,0.6316184997558594,0.3683815002441406 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.37894052267074585,0.6210594773292542,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.26.05 PM.png,ripe,unripe,0.6357367038726807,0.36426329612731934,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.26.47 PM.png,ripe,overripe,0.0,0.5094195008277893,0.4905804693698883 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.19938556849956512,0.8006144165992737,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.29.24 PM.png,ripe,unripe,0.1826012134552002,0.8173987865447998,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5907950401306152,0.40920495986938477,0.0 +apple/test/ripe/rotated_by_75_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.4102914035320282,0.5897085666656494 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.12 PM.png,ripe,unripe,0.20571556687355042,0.7942844033241272,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.00.50 PM.png,ripe,unripe,0.18148674070835114,0.8185132741928101,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.01.34 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.02.38 PM.png,ripe,overripe,0.42450520396232605,0.5754948258399963,0.0058945766650140285 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.03.47 PM.png,ripe,unripe,0.1823340654373169,0.8176659345626831,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.04.48 PM.png,ripe,overripe,0.0,0.8122041821479797,0.18779584765434265 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.05.06 PM.png,ripe,overripe,0.0,0.7431919574737549,0.2568080723285675 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.05.41 PM.png,ripe,overripe,0.0,0.402192622423172,0.5978074073791504 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.06.54 PM.png,ripe,overripe,0.22216452658176422,0.777835488319397,0.03951723873615265 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.20733194053173065,0.7926680445671082,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.07.26 PM.png,ripe,unripe,0.15749208629131317,0.8425078988075256,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.08.05 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.09.03 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.09.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.10.11 PM.png,ripe,unripe,0.20499257743358612,0.7950074076652527,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.11.35 PM.png,ripe,unripe,0.17600017786026,0.82399982213974,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.11.59 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.14 PM.png,ripe,overripe,0.0,0.9549225568771362,0.045077428221702576 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.41 PM.png,ripe,overripe,0.14021891355514526,0.8597810864448547,0.012000352144241333 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.12.47 PM.png,ripe,unripe,0.6739886999130249,0.3260113000869751,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.13.45 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,unripe,0.14388902485370636,0.8561109900474548,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.14 PM.png,ripe,unripe,0.19325049221515656,0.8067495226860046,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.15.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.06 PM.png,ripe,unripe,0.15188966691493988,0.8481103181838989,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,ripe,0.10584209859371185,0.894157886505127,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.16.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,unripe,0.20990018546581268,0.7900997996330261,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.21.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.22.48 PM.png,ripe,overripe,0.0,0.8035855293273926,0.19641447067260742 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.23.31 PM.png,ripe,unripe,0.14859332144260406,0.8514066934585571,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.24.12 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,unripe,0.23061221837997437,0.7693877816200256,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.25.49 PM.png,ripe,overripe,0.0,0.5686710476875305,0.4313289523124695 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.26.29 PM.png,ripe,unripe,0.23698744177818298,0.7630125284194946,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.28.59 PM.png,ripe,unripe,0.1933954954147339,0.8066045045852661,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,ripe,0.09427647292613983,0.905723512172699,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.29.31 PM.png,ripe,unripe,0.21284811198711395,0.7871518731117249,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.34.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/saltandpepper_Screen Shot 2018-06-08 at 5.34.14 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 4.59.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.00.18 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.01.15 PM.png,ripe,ripe,0.11607584357261658,0.8839241862297058,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.02.43 PM.png,ripe,overripe,0.0,0.41314947605133057,0.5868505239486694 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.03.10 PM.png,ripe,unripe,0.23961539566516876,0.7603846192359924,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.03.17 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.2161351591348648,0.7838648557662964,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.07.32 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.08.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.10.53 PM.png,ripe,unripe,0.9545884132385254,0.0454116091132164,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.11.08 PM.png,ripe,unripe,0.35480260848999023,0.6451973915100098,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.13.25 PM.png,ripe,overripe,0.10460999608039856,0.8953900337219238,0.013388816267251968 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.07 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.20 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,unripe,0.19754354655742645,0.8024564385414124,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.15.01 PM.png,ripe,ripe,0.23189544677734375,0.7681045532226562,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.15.39 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.33 PM.png,ripe,unripe,0.17099793255329132,0.8290020823478699,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.37 PM.png,ripe,ripe,0.08747463673353195,0.9125253558158875,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.16.57 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.17.04 PM.png,ripe,unripe,0.34844353795051575,0.6515564918518066,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.18.51 PM.png,ripe,unripe,0.19694001972675323,0.803059995174408,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.19.28 PM.png,ripe,unripe,0.5799127817153931,0.42008718848228455,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.20.51 PM.png,ripe,overripe,0.0,0.9786150455474854,0.021384965628385544 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,overripe,0.0,0.9843376874923706,0.015662286430597305 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.21.56 PM.png,ripe,overripe,0.0,0.660161554813385,0.339838445186615 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.23.26 PM.png,ripe,unripe,0.14422036707401276,0.8557796478271484,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.41025641560554504,0.5897436141967773,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.24.19 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.25.28 PM.png,ripe,unripe,0.2268822193145752,0.7731177806854248,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.25.54 PM.png,ripe,unripe,0.29766714572906494,0.7023328542709351,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.26.52 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.29.07 PM.png,ripe,unripe,0.21080996096134186,0.7891900539398193,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.5416188836097717,0.45838111639022827,0.0 +apple/test/ripe/translation_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.41264867782592773,0.5873513221740723 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 4.59.49 PM.png,ripe,unripe,0.6871214509010315,0.3128785192966461,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.00.35 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.00.43 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.01.01 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.01.08 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.03.59 PM.png,ripe,unripe,0.15849345922470093,0.8415065407752991,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.24 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.42 PM.png,ripe,unripe,0.21249507367610931,0.7875049114227295,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.48 PM.png,ripe,overripe,0.0,0.811631977558136,0.18836800754070282 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.04.59 PM.png,ripe,overripe,0.0,0.7645598649978638,0.23544016480445862 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.05.12 PM.png,ripe,overripe,0.0,0.885143518447876,0.11485645920038223 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.10 PM.png,ripe,unripe,0.2373959869146347,0.7626039981842041,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.40 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.06.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.07.05 PM.png,ripe,unripe,0.21381215751171112,0.7861878275871277,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.08.46 PM.png,ripe,ripe,0.11054175347089767,0.8894582390785217,0.0029736622236669064 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.08.52 PM.png,ripe,unripe,0.16331833600997925,0.8366816639900208,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.09.47 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.10.53 PM.png,ripe,unripe,0.8821280598640442,0.11787191033363342,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.11.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.12.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.13.02 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.13.31 PM.png,ripe,overripe,0.20729374885559082,0.7927062511444092,0.011411337181925774 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.14.56 PM.png,ripe,unripe,0.14572639763355255,0.8542736172676086,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.15.21 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.15.28 PM.png,ripe,overripe,0.025421353057026863,0.8022709488868713,0.19772902131080627 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.16.28 PM.png,ripe,ripe,0.11217540502548218,0.8878245949745178,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.18.26 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.18.37 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.20.56 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.21.31 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.21.51 PM.png,ripe,overripe,0.0,0.9021816253662109,0.09781838953495026 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.23.14 PM.png,ripe,ripe,0.10182032734155655,0.8981796503067017,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.24.04 PM.png,ripe,unripe,0.4119238555431366,0.5880761742591858,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.13 PM.png,ripe,unripe,0.49217575788497925,0.5078242421150208,0.003110304707661271 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.29 PM.png,ripe,unripe,0.24407386779785156,0.7559261322021484,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.36 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.26.58 PM.png,ripe,overripe,0.0,0.417335569858551,0.582664430141449 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.10 PM.png,ripe,ripe,0.0,1.0,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.42 PM.png,ripe,overripe,0.0,0.6675575375556946,0.33244243264198303 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.28.48 PM.png,ripe,overripe,0.0,0.6907384991645813,0.3092614710330963 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.29.13 PM.png,ripe,ripe,0.0994064137339592,0.9005935788154602,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.29.18 PM.png,ripe,unripe,0.14643268287181854,0.8535673022270203,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.38 PM.png,ripe,unripe,0.2337462306022644,0.7662537693977356,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.43 PM.png,ripe,overripe,0.01307489164173603,0.7481704950332642,0.25182950496673584 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.32.50 PM.png,ripe,unripe,0.6698821783065796,0.3301178514957428,0.0 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.33.05 PM.png,ripe,overripe,0.0,0.41085970401763916,0.5891402959823608 +apple/test/ripe/vertical_flip_Screen Shot 2018-06-08 at 5.33.27 PM.png,ripe,unripe,0.7241566777229309,0.2758433520793915,0.0 +apple/test/unripe/1.jpg,unripe,unripe,0.4562946856021881,0.5437052845954895,0.0 +apple/test/unripe/10.jpg,unripe,unripe,0.8575348258018494,0.14246518909931183,0.0 +apple/test/unripe/100.jpg,unripe,overripe,0.1169804111123085,0.7063207626342773,0.29367923736572266 +apple/test/unripe/101.jpg,unripe,overripe,0.21292909979820251,0.6346324682235718,0.3653675317764282 +apple/test/unripe/102.jpg,unripe,unripe,0.2646982967853546,0.735301673412323,0.0 +apple/test/unripe/103.jpg,unripe,unripe,0.34291598200798035,0.657084047794342,0.0 +apple/test/unripe/104.jpg,unripe,overripe,0.8787909746170044,0.12120901793241501,0.28130730986595154 +apple/test/unripe/105.jpg,unripe,ripe,0.09330707043409348,0.9066929221153259,0.0 +apple/test/unripe/106.jpg,unripe,overripe,0.5518814921379089,0.44811853766441345,0.12091794610023499 +apple/test/unripe/107.jpg,unripe,overripe,0.1337684839963913,0.8662315011024475,0.024314910173416138 +apple/test/unripe/108.jpg,unripe,overripe,0.3316829204559326,0.6683170795440674,0.007372100371867418 +apple/test/unripe/109.jpg,unripe,overripe,0.23637332022190094,0.7636266946792603,0.053370267152786255 +apple/test/unripe/11.jpg,unripe,overripe,0.061089012771844864,0.8497782945632935,0.15022173523902893 +apple/test/unripe/110.jpg,unripe,overripe,0.14138276875019073,0.8432128429412842,0.15678714215755463 +apple/test/unripe/111.jpg,unripe,unripe,0.676794707775116,0.32320529222488403,0.0 +apple/test/unripe/112.jpg,unripe,ripe,0.0,0.9970352649688721,0.002964708721265197 +apple/test/unripe/113.jpg,unripe,overripe,0.2644622325897217,0.7355377674102783,0.10996738076210022 +apple/test/unripe/114.jpg,unripe,unripe,0.6766818761825562,0.32331812381744385,0.0024987375363707542 +apple/test/unripe/115.jpg,unripe,overripe,0.0,0.7436784505844116,0.25632157921791077 +apple/test/unripe/116.jpg,unripe,overripe,0.27026891708374023,0.5800999402999878,0.4199000298976898 +apple/test/unripe/117.jpg,unripe,ripe,0.09980292618274689,0.9001970887184143,0.0 +apple/test/unripe/118.jpg,unripe,overripe,0.40660953521728516,0.5933904647827148,0.17969784140586853 +apple/test/unripe/119.jpg,unripe,overripe,0.441668838262558,0.5583311319351196,0.08933515101671219 +apple/test/unripe/12.jpg,unripe,overripe,0.4263456463813782,0.5736543536186218,0.061614248901605606 +apple/test/unripe/120.jpg,unripe,overripe,0.5968891382217407,0.4031108319759369,0.14588871598243713 +apple/test/unripe/121.jpg,unripe,ripe,0.09330707043409348,0.9066929221153259,0.0 +apple/test/unripe/122.jpg,unripe,overripe,0.5609573125839233,0.4390426576137543,0.05988815426826477 +apple/test/unripe/123.jpg,unripe,overripe,0.5075750350952148,0.49242496490478516,0.14630261063575745 +apple/test/unripe/124.jpg,unripe,overripe,0.0,0.7571467161178589,0.24285326898097992 +apple/test/unripe/125.jpg,unripe,overripe,0.3605206310749054,0.6394793391227722,0.0135036064311862 +apple/test/unripe/126.jpg,unripe,overripe,0.5968891382217407,0.4031108319759369,0.14588871598243713 +apple/test/unripe/127.jpg,unripe,unripe,0.25359612703323364,0.7464038729667664,0.0 +apple/test/unripe/128.jpg,unripe,unripe,0.26322489976882935,0.7367751002311707,0.0 +apple/test/unripe/129.jpg,unripe,unripe,0.721885621547699,0.2781144082546234,0.0 +apple/test/unripe/13.jpg,unripe,overripe,0.3759039044380188,0.6240960955619812,0.1720503717660904 +apple/test/unripe/130.jpg,unripe,overripe,0.32950806617736816,0.6704919338226318,0.0996701642870903 +apple/test/unripe/131.jpg,unripe,unripe,0.6881263256072998,0.3118736743927002,0.0 +apple/test/unripe/132.jpg,unripe,overripe,0.6969317197799683,0.30306828022003174,0.1498396396636963 +apple/test/unripe/133.jpg,unripe,overripe,0.5509782433509827,0.4490217864513397,0.20707941055297852 +apple/test/unripe/134.jpg,unripe,overripe,0.0,0.7863151431083679,0.2136848419904709 +apple/test/unripe/135.jpg,unripe,overripe,0.0,0.43277978897094727,0.5672202110290527 +apple/test/unripe/136.jpg,unripe,overripe,0.5468539595603943,0.4531460404396057,0.1706162989139557 +apple/test/unripe/137.jpg,unripe,ripe,0.1080668568611145,0.8919331431388855,0.0 +apple/test/unripe/138.jpg,unripe,overripe,0.40660953521728516,0.5933904647827148,0.17969784140586853 +apple/test/unripe/139.jpg,unripe,overripe,0.40390151739120483,0.5960984826087952,0.10162189602851868 +apple/test/unripe/14.jpg,unripe,overripe,0.0,0.8517522215843201,0.14824777841567993 +apple/test/unripe/140.jpg,unripe,ripe,0.06341296434402466,0.9365870356559753,0.0 +apple/test/unripe/141.jpg,unripe,overripe,0.02171250246465206,0.7965848445892334,0.2034151554107666 +apple/test/unripe/142.jpg,unripe,overripe,0.0,0.43277978897094727,0.5672202110290527 +apple/test/unripe/143.jpg,unripe,overripe,0.22761428356170654,0.7723857164382935,0.01358797773718834 +apple/test/unripe/144.jpg,unripe,overripe,0.4724130630493164,0.5275869369506836,0.03172523155808449 +apple/test/unripe/145.jpg,unripe,unripe,0.4055708050727844,0.5944291949272156,0.0 +apple/test/unripe/146.jpg,unripe,overripe,0.7483404278755188,0.2516595423221588,0.14118358492851257 +apple/test/unripe/147.jpg,unripe,unripe,0.23561282455921173,0.7643871903419495,0.0 +apple/test/unripe/148.jpg,unripe,overripe,0.0,0.6267208456993103,0.3732791543006897 +apple/test/unripe/149.jpg,unripe,overripe,0.5609573125839233,0.4390426576137543,0.05988815426826477 +apple/test/unripe/15.jpg,unripe,overripe,0.2976350486278534,0.702364981174469,0.1369284838438034 +apple/test/unripe/150.jpg,unripe,overripe,0.0,0.9547246694564819,0.04527535289525986 +apple/test/unripe/151.jpg,unripe,overripe,0.0,0.7571467161178589,0.24285326898097992 +apple/test/unripe/152.jpg,unripe,overripe,0.0,0.6096661686897278,0.3903338611125946 +apple/test/unripe/153.jpg,unripe,overripe,0.0,0.7863151431083679,0.2136848419904709 +apple/test/unripe/154.jpg,unripe,overripe,0.0,0.6613271236419678,0.33867284655570984 +apple/test/unripe/155.jpg,unripe,overripe,0.4781794250011444,0.5218205451965332,0.04777054116129875 +apple/test/unripe/156.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/157.jpg,unripe,overripe,0.042827293276786804,0.7693110108375549,0.23068897426128387 +apple/test/unripe/158.jpg,unripe,unripe,0.2094212770462036,0.7905787229537964,0.0 +apple/test/unripe/159.jpg,unripe,overripe,0.2574476897716522,0.7425523400306702,0.04399367421865463 +apple/test/unripe/16.jpg,unripe,overripe,0.3519147038459778,0.6480852961540222,0.12527358531951904 +apple/test/unripe/160.jpg,unripe,overripe,0.28467851877212524,0.7153214812278748,0.18043844401836395 +apple/test/unripe/161.jpg,unripe,overripe,0.792772114276886,0.20722787082195282,0.05176100507378578 +apple/test/unripe/162.jpg,unripe,overripe,0.0,0.694049060344696,0.30595090985298157 +apple/test/unripe/163.jpg,unripe,overripe,0.019914530217647552,0.7310185432434082,0.2689814865589142 +apple/test/unripe/164.jpg,unripe,unripe,0.2585027813911438,0.7414972186088562,0.0 +apple/test/unripe/165.jpg,unripe,unripe,0.2598665654659271,0.7401334047317505,0.0 +apple/test/unripe/166.jpg,unripe,unripe,0.32363268733024597,0.6763673424720764,0.0 +apple/test/unripe/167.jpg,unripe,overripe,0.0,0.6683453917503357,0.3316546082496643 +apple/test/unripe/168.jpg,unripe,unripe,0.2716045677661896,0.7283954620361328,0.0 +apple/test/unripe/169.jpg,unripe,unripe,0.19273841381072998,0.80726158618927,0.0 +apple/test/unripe/17.jpg,unripe,overripe,0.10964018851518631,0.7979750037193298,0.20202498137950897 +apple/test/unripe/170.jpg,unripe,overripe,0.46620360016822815,0.5337963700294495,0.1002807691693306 +apple/test/unripe/171.jpg,unripe,overripe,0.8821958899497986,0.11780412495136261,0.1638989895582199 +apple/test/unripe/172.jpg,unripe,overripe,0.0,0.36236488819122314,0.6376351118087769 +apple/test/unripe/173.jpg,unripe,unripe,0.5061986446380615,0.4938013553619385,0.0 +apple/test/unripe/174.jpg,unripe,overripe,0.9793816208839417,0.020618358626961708,0.25366684794425964 +apple/test/unripe/175.jpg,unripe,overripe,0.9839369654655457,0.01606305129826069,0.025081967934966087 +apple/test/unripe/176.jpg,unripe,overripe,0.5075750350952148,0.49242496490478516,0.14630261063575745 +apple/test/unripe/177.jpg,unripe,overripe,0.8575008511543274,0.142499178647995,0.0838087797164917 +apple/test/unripe/178.jpg,unripe,unripe,0.34137967228889465,0.658620297908783,0.0 +apple/test/unripe/179.jpg,unripe,unripe,0.25255608558654785,0.7474439144134521,0.0 +apple/test/unripe/18.jpg,unripe,overripe,0.09139034152030945,0.7553049325942993,0.2446950525045395 +apple/test/unripe/180.jpg,unripe,unripe,0.2258203625679016,0.7741796374320984,0.0 +apple/test/unripe/181.jpg,unripe,overripe,0.28467851877212524,0.7153214812278748,0.18043844401836395 +apple/test/unripe/182.jpg,unripe,unripe,0.5511739253997803,0.4488260746002197,0.0 +apple/test/unripe/183.jpg,unripe,overripe,0.9793816208839417,0.020618358626961708,0.25366684794425964 +apple/test/unripe/184.jpg,unripe,overripe,0.2254330962896347,0.7745668888092041,0.19265125691890717 +apple/test/unripe/185.jpg,unripe,unripe,0.32363268733024597,0.6763673424720764,0.0 +apple/test/unripe/186.jpg,unripe,unripe,0.8745660185813904,0.1254339963197708,0.0 +apple/test/unripe/187.jpg,unripe,overripe,0.0,0.6683453917503357,0.3316546082496643 +apple/test/unripe/188.jpg,unripe,unripe,0.5061986446380615,0.4938013553619385,0.0 +apple/test/unripe/189.jpg,unripe,overripe,0.4024812877178192,0.5975187420845032,0.027204278856515884 +apple/test/unripe/19.jpg,unripe,unripe,0.1612040102481842,0.8387959599494934,0.0 +apple/test/unripe/190.jpg,unripe,unripe,0.2716045677661896,0.7283954620361328,0.0 +apple/test/unripe/191.jpg,unripe,overripe,0.07690806686878204,0.8049864768981934,0.19501355290412903 +apple/test/unripe/192.jpg,unripe,overripe,0.3924318552017212,0.6075681447982788,0.019969431683421135 +apple/test/unripe/193.jpg,unripe,unripe,0.9476281404495239,0.05237183719873428,0.0 +apple/test/unripe/194.jpg,unripe,unripe,0.720232367515564,0.27976763248443604,0.0 +apple/test/unripe/195.jpg,unripe,unripe,0.2258203625679016,0.7741796374320984,0.0 +apple/test/unripe/196.jpg,unripe,overripe,0.8821958899497986,0.11780412495136261,0.1638989895582199 +apple/test/unripe/197.jpg,unripe,overripe,0.15885965526103973,0.8175365328788757,0.18246346712112427 +apple/test/unripe/198.jpg,unripe,unripe,0.19273841381072998,0.80726158618927,0.0 +apple/test/unripe/199.jpg,unripe,overripe,0.035680610686540604,0.8789664506912231,0.12103352695703506 +apple/test/unripe/2.jpg,unripe,overripe,0.2583926022052765,0.7416074275970459,0.15277934074401855 +apple/test/unripe/20.jpg,unripe,overripe,0.6253235936164856,0.3746764361858368,0.024918096140027046 +apple/test/unripe/200.jpg,unripe,unripe,0.3478304147720337,0.6521695852279663,0.0 +apple/test/unripe/202.jpg,unripe,overripe,0.18158775568008423,0.8184122443199158,0.17945589125156403 +apple/test/unripe/203.jpg,unripe,unripe,0.8745660185813904,0.1254339963197708,0.0 +apple/test/unripe/205.jpg,unripe,overripe,0.8119770884513855,0.1880229115486145,0.20083031058311462 +apple/test/unripe/206.jpg,unripe,overripe,0.4266219735145569,0.5111111402511597,0.4888888895511627 +apple/test/unripe/207.jpg,unripe,overripe,0.2783106863498688,0.7216893434524536,0.17936572432518005 +apple/test/unripe/208.jpg,unripe,unripe,0.163314089179039,0.8366858959197998,0.0 +apple/test/unripe/209.jpg,unripe,overripe,0.5682976245880127,0.4317023754119873,0.05905454605817795 +apple/test/unripe/21.jpg,unripe,overripe,0.862949013710022,0.13705098628997803,0.11493568122386932 +apple/test/unripe/210.jpg,unripe,overripe,0.0,0.9139112234115601,0.08608875423669815 +apple/test/unripe/211.jpg,unripe,overripe,0.2437860518693924,0.7562139630317688,0.009395972825586796 +apple/test/unripe/212.jpg,unripe,unripe,0.26579931378364563,0.734200656414032,0.0 +apple/test/unripe/213.jpg,unripe,unripe,0.5511739253997803,0.4488260746002197,0.0 +apple/test/unripe/214.jpg,unripe,unripe,0.2683529257774353,0.7316470742225647,0.0 +apple/test/unripe/215.jpg,unripe,overripe,0.1151958703994751,0.6463474035263062,0.35365256667137146 +apple/test/unripe/216.jpg,unripe,overripe,0.2254330962896347,0.7745668888092041,0.19265125691890717 +apple/test/unripe/217.jpg,unripe,unripe,0.9588117599487305,0.04118826240301132,0.0 +apple/test/unripe/218.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/219.jpg,unripe,unripe,0.3478304147720337,0.6521695852279663,0.0 +apple/test/unripe/22.jpg,unripe,unripe,0.8487420082092285,0.15125802159309387,0.0 +apple/test/unripe/220.jpg,unripe,overripe,0.24493707716464996,0.7550629377365112,0.1288294494152069 +apple/test/unripe/221.jpg,unripe,overripe,0.17755930125713348,0.8224406838417053,0.13624915480613708 +apple/test/unripe/222.jpg,unripe,overripe,0.23199278116226196,0.768007218837738,0.030652230605483055 +apple/test/unripe/223.jpg,unripe,overripe,0.028291983529925346,0.7384061813354492,0.26159384846687317 +apple/test/unripe/224.jpg,unripe,overripe,0.1930294930934906,0.806970477104187,0.04066774994134903 +apple/test/unripe/225.jpg,unripe,ripe,0.0,1.0,0.0 +apple/test/unripe/226.jpg,unripe,unripe,0.3768221437931061,0.6231778860092163,0.0 +apple/test/unripe/227.jpg,unripe,overripe,0.0,0.6868883967399597,0.31311163306236267 +apple/test/unripe/229.jpg,unripe,overripe,0.019914530217647552,0.7310185432434082,0.2689814865589142 +apple/test/unripe/23.jpg,unripe,overripe,0.23961520195007324,0.7603847980499268,0.044185664504766464 +apple/test/unripe/230.jpg,unripe,unripe,0.22234804928302765,0.7776519656181335,0.0 +apple/test/unripe/231.jpg,unripe,overripe,0.0,0.7818809151649475,0.2181190699338913 +apple/test/unripe/232.jpg,unripe,overripe,0.0818558856844902,0.8162839412689209,0.1837160587310791 +apple/test/unripe/233.jpg,unripe,overripe,0.14620132744312286,0.6797456741333008,0.3202543258666992 +apple/test/unripe/235.jpg,unripe,unripe,0.21744121611118317,0.782558798789978,0.0 +apple/test/unripe/236.jpg,unripe,overripe,0.5019802451133728,0.4980197548866272,0.05093478038907051 +apple/test/unripe/237.jpg,unripe,unripe,0.200740247964859,0.7992597222328186,0.0 +apple/test/unripe/238.jpg,unripe,overripe,0.0,0.7660926580429077,0.23390734195709229 +apple/test/unripe/239.jpg,unripe,overripe,0.6433060765266418,0.35669392347335815,0.058849845081567764 +apple/test/unripe/24.jpg,unripe,overripe,0.2685256004333496,0.7314743995666504,0.21776285767555237 +apple/test/unripe/240.jpg,unripe,overripe,0.2491900473833084,0.7508099675178528,0.1133333370089531 +apple/test/unripe/241.jpg,unripe,overripe,0.16602320969104767,0.8339768052101135,0.02986903302371502 +apple/test/unripe/242.jpg,unripe,ripe,0.12806035578250885,0.8719396591186523,0.0 +apple/test/unripe/243.jpg,unripe,unripe,0.8745660185813904,0.1254339963197708,0.0 +apple/test/unripe/244.jpg,unripe,unripe,0.5511739253997803,0.4488260746002197,0.0 +apple/test/unripe/245.jpg,unripe,overripe,0.2680613398551941,0.7319386601448059,0.2042388767004013 +apple/test/unripe/246.jpg,unripe,overripe,0.008209873922169209,0.7508772015571594,0.24912281334400177 +apple/test/unripe/247.jpg,unripe,overripe,0.0,0.5815761685371399,0.4184238016605377 +apple/test/unripe/248.jpg,unripe,overripe,0.964070200920105,0.03592976927757263,0.24980033934116364 +apple/test/unripe/249.jpg,unripe,overripe,0.6823517084121704,0.3176482617855072,0.266000896692276 +apple/test/unripe/25.jpg,unripe,overripe,0.6784923672676086,0.32150763273239136,0.09330445528030396 +apple/test/unripe/250.jpg,unripe,unripe,0.6107896566390991,0.3892103433609009,0.0 +apple/test/unripe/251.jpg,unripe,overripe,0.1115613654255867,0.8884386420249939,0.030110575258731842 +apple/test/unripe/253.jpg,unripe,unripe,0.3768221437931061,0.6231778860092163,0.0 +apple/test/unripe/254.jpg,unripe,overripe,0.6763089299201965,0.32369107007980347,0.23937903344631195 +apple/test/unripe/255.jpg,unripe,overripe,0.8071257472038269,0.1928742527961731,0.15498970448970795 +apple/test/unripe/256.jpg,unripe,unripe,0.9553197026252747,0.044680316001176834,0.0 +apple/test/unripe/257.jpg,unripe,unripe,0.7620083093643188,0.23799166083335876,0.003935629967600107 +apple/test/unripe/258.jpg,unripe,unripe,0.17766821384429932,0.8223317861557007,0.0 +apple/test/unripe/259.jpg,unripe,overripe,0.7236660718917847,0.2763339579105377,0.1929619312286377 +apple/test/unripe/26.jpg,unripe,overripe,0.0,0.7187002301216125,0.28129976987838745 +apple/test/unripe/261.jpg,unripe,overripe,0.0,0.6778004169464111,0.32219961285591125 +apple/test/unripe/262.jpg,unripe,overripe,0.4697762429714203,0.5302237272262573,0.24247796833515167 +apple/test/unripe/263.jpg,unripe,overripe,0.22324194014072418,0.6582126021385193,0.3417873680591583 +apple/test/unripe/264.jpg,unripe,overripe,0.06737788021564484,0.9326221346855164,0.05678063631057739 +apple/test/unripe/265.jpg,unripe,unripe,0.27200382947921753,0.7279961705207825,0.0 +apple/test/unripe/266.jpg,unripe,overripe,0.07615312933921814,0.8176464438438416,0.18235355615615845 +apple/test/unripe/267.jpg,unripe,unripe,0.4391004145145416,0.560899555683136,0.0 +apple/test/unripe/268.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +apple/test/unripe/269.jpg,unripe,overripe,0.20231778919696808,0.7976822257041931,0.020581182092428207 +apple/test/unripe/27.jpg,unripe,unripe,0.9388843774795532,0.06111561134457588,0.0 +apple/test/unripe/270.jpg,unripe,overripe,0.09822190552949905,0.786083459854126,0.21391655504703522 +apple/test/unripe/271.jpg,unripe,overripe,0.5682976245880127,0.4317023754119873,0.05905454605817795 +apple/test/unripe/272.jpg,unripe,overripe,0.1151958703994751,0.6463474035263062,0.35365256667137146 +apple/test/unripe/273.jpg,unripe,overripe,0.0,0.6123222708702087,0.38767772912979126 +apple/test/unripe/274.jpg,unripe,overripe,0.8844173550605774,0.11558262258768082,0.050327885895967484 +apple/test/unripe/275.jpg,unripe,unripe,0.3478304147720337,0.6521695852279663,0.0 +apple/test/unripe/276.jpg,unripe,overripe,0.5298927426338196,0.4701072573661804,0.07491987198591232 +apple/test/unripe/277.jpg,unripe,unripe,0.9868588447570801,0.013141131028532982,0.0 +apple/test/unripe/278.jpg,unripe,overripe,0.28550708293914795,0.714492917060852,0.09802958369255066 +apple/test/unripe/279.jpg,unripe,overripe,0.2437860518693924,0.7562139630317688,0.009395972825586796 +apple/test/unripe/28.jpg,unripe,unripe,0.3301398456096649,0.6698601245880127,0.0 +apple/test/unripe/281.jpg,unripe,overripe,0.7986234426498413,0.2013765424489975,0.0812024176120758 +apple/test/unripe/282.jpg,unripe,overripe,0.0,0.7005393505096436,0.29946064949035645 +apple/test/unripe/283.jpg,unripe,unripe,0.36341744661331177,0.6365825533866882,0.0 +apple/test/unripe/284.jpg,unripe,unripe,0.33728471398353577,0.6627153158187866,0.0 +apple/test/unripe/285.jpg,unripe,overripe,0.12015693634748459,0.8798430562019348,0.11472069472074509 +apple/test/unripe/286.jpg,unripe,unripe,0.6631767749786377,0.3368232250213623,0.0 +apple/test/unripe/287.jpg,unripe,ripe,0.10725508630275726,0.8927448987960815,0.0 +apple/test/unripe/288.jpg,unripe,unripe,0.9868588447570801,0.013141131028532982,0.0 +apple/test/unripe/289.jpg,unripe,overripe,0.6823517084121704,0.3176482617855072,0.266000896692276 +apple/test/unripe/29.jpg,unripe,unripe,0.45876994729042053,0.5412300229072571,0.0 +apple/test/unripe/290.jpg,unripe,ripe,0.12806035578250885,0.8719396591186523,0.0 +apple/test/unripe/291.jpg,unripe,overripe,0.0,0.5815761685371399,0.4184238016605377 +apple/test/unripe/292.jpg,unripe,overripe,0.0,0.9056942462921143,0.09430573880672455 +apple/test/unripe/293.jpg,unripe,overripe,0.0,0.6778004169464111,0.32219961285591125 +apple/test/unripe/294.jpg,unripe,ripe,0.0845990777015686,0.9154009222984314,0.0 +apple/test/unripe/295.jpg,unripe,overripe,0.964070200920105,0.03592976927757263,0.24980033934116364 +apple/test/unripe/297.jpg,unripe,unripe,0.6107896566390991,0.3892103433609009,0.0 +apple/test/unripe/298.jpg,unripe,overripe,0.6763089299201965,0.32369107007980347,0.23937903344631195 +apple/test/unripe/3.jpg,unripe,overripe,0.6315003633499146,0.36849966645240784,0.06869198381900787 +apple/test/unripe/30.jpg,unripe,overripe,0.0,0.5106074213981628,0.48939257860183716 +apple/test/unripe/300.jpg,unripe,overripe,0.0,0.5374586582183838,0.4625413417816162 +apple/test/unripe/301.jpg,unripe,overripe,0.0,0.7039387822151184,0.2960612177848816 +apple/test/unripe/302.jpg,unripe,overripe,0.7236660718917847,0.2763339579105377,0.1929619312286377 +apple/test/unripe/303.jpg,unripe,overripe,0.1115613654255867,0.8884386420249939,0.030110575258731842 +apple/test/unripe/304.jpg,unripe,unripe,0.2206224650144577,0.7793775200843811,0.0 +apple/test/unripe/305.jpg,unripe,unripe,0.3206839859485626,0.679315984249115,0.0 +apple/test/unripe/306.jpg,unripe,overripe,0.1522647589445114,0.8477352261543274,0.037831153720617294 +apple/test/unripe/307.jpg,unripe,overripe,0.008209873922169209,0.7508772015571594,0.24912281334400177 +apple/test/unripe/308.jpg,unripe,overripe,0.5194303393363953,0.4805696904659271,0.3019236624240875 +apple/test/unripe/309.jpg,unripe,unripe,0.46237480640411377,0.5376251935958862,0.0028612304013222456 +apple/test/unripe/31.jpg,unripe,overripe,0.32581856846809387,0.6741814017295837,0.024983027949929237 +apple/test/unripe/310.jpg,unripe,unripe,0.2921709418296814,0.7078290581703186,0.0 +apple/test/unripe/311.jpg,unripe,overripe,0.06737788021564484,0.9326221346855164,0.05678063631057739 +apple/test/unripe/312.jpg,unripe,unripe,0.27200382947921753,0.7279961705207825,0.0 +apple/test/unripe/313.jpg,unripe,overripe,0.07615312933921814,0.8176464438438416,0.18235355615615845 +apple/test/unripe/314.jpg,unripe,unripe,0.4391004145145416,0.560899555683136,0.0 +apple/test/unripe/315.jpg,unripe,overripe,0.1469171941280365,0.8436419367790222,0.1563580334186554 +apple/test/unripe/317.jpg,unripe,overripe,0.605247437953949,0.394752562046051,0.15099723637104034 +apple/test/unripe/318.jpg,unripe,overripe,0.5327056050300598,0.4672944247722626,0.046914514154195786 +apple/test/unripe/32.jpg,unripe,overripe,0.803074836730957,0.19692519307136536,0.0645146518945694 +apple/test/unripe/321.jpg,unripe,overripe,0.30141481757164,0.6985852122306824,0.05563022941350937 +apple/test/unripe/322.jpg,unripe,overripe,0.22324194014072418,0.6582126021385193,0.3417873680591583 +apple/test/unripe/323.jpg,unripe,overripe,0.2026379108428955,0.7973620891571045,0.13464820384979248 +apple/test/unripe/324.jpg,unripe,overripe,0.1268782764673233,0.7774947881698608,0.22250519692897797 +apple/test/unripe/325.jpg,unripe,overripe,0.0,0.9341046810150146,0.06589533388614655 +apple/test/unripe/326.jpg,unripe,overripe,0.2945747673511505,0.7054252028465271,0.17639435827732086 +apple/test/unripe/327.jpg,unripe,overripe,0.5504721403121948,0.44952788949012756,0.02704734168946743 +apple/test/unripe/328.jpg,unripe,overripe,0.8071257472038269,0.1928742527961731,0.15498970448970795 +apple/test/unripe/329.jpg,unripe,overripe,0.9731762409210205,0.026823759078979492,0.22781512141227722 +apple/test/unripe/33.jpg,unripe,overripe,0.07012369483709335,0.7181022763252258,0.28189772367477417 +apple/test/unripe/330.jpg,unripe,overripe,0.0,0.8454614877700806,0.15453852713108063 +apple/test/unripe/331.jpg,unripe,unripe,0.3331201672554016,0.6668798327445984,0.0 +apple/test/unripe/332.jpg,unripe,overripe,0.6348162889480591,0.3651837110519409,0.23759552836418152 +apple/test/unripe/333.jpg,unripe,overripe,0.20231778919696808,0.7976822257041931,0.020581182092428207 +apple/test/unripe/334.jpg,unripe,overripe,0.0,0.5442968010902405,0.45570316910743713 +apple/test/unripe/335.jpg,unripe,unripe,0.16643919050693512,0.8335608243942261,0.0 +apple/test/unripe/336.jpg,unripe,overripe,0.34639573097229004,0.65360426902771,0.1259162873029709 +apple/test/unripe/337.jpg,unripe,ripe,0.13831567764282227,0.8616843223571777,0.0 +apple/test/unripe/338.jpg,unripe,overripe,0.6034168004989624,0.39658322930336,0.23549281060695648 +apple/test/unripe/339.jpg,unripe,overripe,0.39456719160079956,0.4690593481063843,0.5309406518936157 +apple/test/unripe/34.jpg,unripe,overripe,0.4957881569862366,0.5042118430137634,0.2591221332550049 +apple/test/unripe/342.jpg,unripe,overripe,0.2344590425491333,0.6367193460464478,0.36328062415122986 +apple/test/unripe/343.jpg,unripe,overripe,0.05144602805376053,0.5269381999969482,0.47306180000305176 +apple/test/unripe/344.jpg,unripe,overripe,0.0,0.8312223553657532,0.16877762973308563 +apple/test/unripe/346.jpg,unripe,overripe,0.6697567105293274,0.3302432596683502,0.20385321974754333 +apple/test/unripe/347.jpg,unripe,overripe,0.4674018323421478,0.5325981974601746,0.03137117996811867 +apple/test/unripe/348.jpg,unripe,overripe,0.210279181599617,0.7304760217666626,0.269523948431015 +apple/test/unripe/349.jpg,unripe,overripe,0.5927074551582336,0.40729254484176636,0.09056790173053741 +apple/test/unripe/35.jpg,unripe,overripe,0.5958924293518066,0.40410757064819336,0.0316644124686718 +apple/test/unripe/350.jpg,unripe,overripe,0.9731762409210205,0.026823759078979492,0.22781512141227722 +apple/test/unripe/351.jpg,unripe,overripe,0.7614172101020813,0.2385827898979187,0.30010196566581726 +apple/test/unripe/353.jpg,unripe,unripe,0.3331201672554016,0.6668798327445984,0.0 +apple/test/unripe/354.jpg,unripe,overripe,0.09822190552949905,0.786083459854126,0.21391655504703522 +apple/test/unripe/355.jpg,unripe,unripe,0.7252961993217468,0.2747037708759308,0.0 +apple/test/unripe/356.jpg,unripe,overripe,0.8844173550605774,0.11558262258768082,0.050327885895967484 +apple/test/unripe/357.jpg,unripe,overripe,0.4005996584892273,0.5994003415107727,0.2255244255065918 +apple/test/unripe/358.jpg,unripe,unripe,0.41939324140548706,0.5806067585945129,0.0 +apple/test/unripe/359.jpg,unripe,overripe,0.32994186878204346,0.6700581312179565,0.13608315587043762 +apple/test/unripe/36.jpg,unripe,overripe,0.9901036620140076,0.009896308183670044,0.12035609036684036 +apple/test/unripe/361.jpg,unripe,overripe,0.5002100467681885,0.4997899532318115,0.03794838860630989 +apple/test/unripe/362.jpg,unripe,overripe,0.2955261468887329,0.7044738531112671,0.18082484602928162 +apple/test/unripe/363.jpg,unripe,overripe,0.18312303721904755,0.8118589520454407,0.18814103305339813 +apple/test/unripe/364.jpg,unripe,overripe,0.10927099734544754,0.7755218744277954,0.22447814047336578 +apple/test/unripe/365.jpg,unripe,overripe,0.5417800545692444,0.4582199454307556,0.09183943271636963 +apple/test/unripe/366.jpg,unripe,unripe,0.14998960494995117,0.8500103950500488,0.0 +apple/test/unripe/367.jpg,unripe,overripe,0.5749366879463196,0.4250633120536804,0.21835541725158691 +apple/test/unripe/368.jpg,unripe,overripe,0.0,0.504466712474823,0.495533287525177 +apple/test/unripe/369.jpg,unripe,overripe,0.3924318552017212,0.6075681447982788,0.019969431683421135 +apple/test/unripe/37.jpg,unripe,overripe,0.4144032597541809,0.5855967402458191,0.26506394147872925 +apple/test/unripe/370.jpg,unripe,overripe,0.2454131692647934,0.7545868158340454,0.12231694906949997 +apple/test/unripe/371.jpg,unripe,overripe,0.6145668029785156,0.38543322682380676,0.12256770581007004 +apple/test/unripe/373.jpg,unripe,overripe,0.4516255855560303,0.5483744144439697,0.1143302470445633 +apple/test/unripe/374.jpg,unripe,unripe,0.36341744661331177,0.6365825533866882,0.0 +apple/test/unripe/375.jpg,unripe,ripe,0.11548598855733871,0.8845140337944031,0.0 +apple/test/unripe/376.jpg,unripe,overripe,0.16521161794662476,0.8347883820533752,0.11612240970134735 +apple/test/unripe/377.jpg,unripe,overripe,0.8512060642242432,0.14879396557807922,0.05421403422951698 +apple/test/unripe/378.jpg,unripe,unripe,0.3331201672554016,0.6668798327445984,0.0 +apple/test/unripe/38.jpg,unripe,unripe,0.1729108989238739,0.8270891308784485,0.0 +apple/test/unripe/381.jpg,unripe,unripe,0.2684388756752014,0.7315611243247986,0.0 +apple/test/unripe/383.jpg,unripe,overripe,0.0,0.7396394610404968,0.2603605389595032 +apple/test/unripe/384.jpg,unripe,overripe,0.19977840781211853,0.8002216219902039,0.1861814260482788 +apple/test/unripe/385.jpg,unripe,overripe,0.0,0.9877793192863464,0.01222070213407278 +apple/test/unripe/386.jpg,unripe,overripe,0.0,0.4003346860408783,0.5996653437614441 +apple/test/unripe/387.jpg,unripe,overripe,0.919425368309021,0.08057462424039841,0.0852731466293335 +apple/test/unripe/388.jpg,unripe,overripe,0.13282713294029236,0.86717289686203,0.028865208849310875 +apple/test/unripe/389.jpg,unripe,overripe,0.5504721403121948,0.44952788949012756,0.02704734168946743 +apple/test/unripe/39.jpg,unripe,overripe,0.0,0.6811038255691528,0.31889617443084717 +apple/test/unripe/390.jpg,unripe,unripe,0.9102437496185303,0.08975625783205032,0.0 +apple/test/unripe/391.jpg,unripe,overripe,0.015670301392674446,0.5447131395339966,0.4552868902683258 +apple/test/unripe/394.jpg,unripe,overripe,0.5794589519500732,0.42054104804992676,0.016631370410323143 +apple/test/unripe/4.jpg,unripe,unripe,0.38204386830329895,0.6179561018943787,0.0 +apple/test/unripe/40.jpg,unripe,unripe,0.17397251725196838,0.8260274529457092,0.0 +apple/test/unripe/41.jpg,unripe,overripe,0.4957881569862366,0.5042118430137634,0.2591221332550049 +apple/test/unripe/42.jpg,unripe,unripe,0.9744408130645752,0.025559160858392715,0.0 +apple/test/unripe/43.jpg,unripe,overripe,0.3126939833164215,0.6873060464859009,0.30619946122169495 +apple/test/unripe/44.jpg,unripe,unripe,0.3232458829879761,0.6767541170120239,0.0 +apple/test/unripe/45.jpg,unripe,overripe,0.34018588066101074,0.6598141193389893,0.005861182697117329 +apple/test/unripe/46.jpg,unripe,overripe,0.803074836730957,0.19692519307136536,0.0645146518945694 +apple/test/unripe/47.jpg,unripe,unripe,0.5818778276443481,0.41812220215797424,0.0 +apple/test/unripe/48.jpg,unripe,overripe,0.0,0.9571385979652405,0.04286138340830803 +apple/test/unripe/49.jpg,unripe,overripe,0.7608014941215515,0.2391984909772873,0.11866859346628189 +apple/test/unripe/5.jpg,unripe,unripe,0.17705081403255463,0.8229491710662842,0.0 +apple/test/unripe/50.jpg,unripe,overripe,0.3604099452495575,0.6395900249481201,0.16999538242816925 +apple/test/unripe/51.jpg,unripe,overripe,0.6517556309700012,0.3482443690299988,0.025304686278104782 +apple/test/unripe/52.jpg,unripe,overripe,0.17680276930332184,0.8217520117759705,0.17824798822402954 +apple/test/unripe/53.jpg,unripe,overripe,0.28522032499313354,0.7147796750068665,0.05435536429286003 +apple/test/unripe/54.jpg,unripe,overripe,0.6540864109992981,0.3459135591983795,0.10407754778862 +apple/test/unripe/55.jpg,unripe,overripe,0.23298689723014832,0.7414582371711731,0.2585417628288269 +apple/test/unripe/56.jpg,unripe,overripe,0.18667112290859222,0.8133288621902466,0.05177734047174454 +apple/test/unripe/57.jpg,unripe,overripe,0.0,0.6353126168251038,0.36468738317489624 +apple/test/unripe/58.jpg,unripe,overripe,0.08225616812705994,0.9177438020706177,0.0260869562625885 +apple/test/unripe/59.jpg,unripe,overripe,0.8121151328086853,0.18788489699363708,0.10679282248020172 +apple/test/unripe/6.jpg,unripe,overripe,0.8092182874679565,0.19078168272972107,0.16221322119235992 +apple/test/unripe/60.jpg,unripe,overripe,0.5777950286865234,0.4222049415111542,0.03603040426969528 +apple/test/unripe/61.jpg,unripe,overripe,0.08225616812705994,0.9177438020706177,0.0260869562625885 +apple/test/unripe/62.jpg,unripe,overripe,0.5777950286865234,0.4222049415111542,0.03603040426969528 +apple/test/unripe/63.jpg,unripe,overripe,0.5732793807983398,0.42672064900398254,0.08194014430046082 +apple/test/unripe/64.jpg,unripe,unripe,0.2412596195936203,0.7587403655052185,0.0 +apple/test/unripe/65.jpg,unripe,overripe,0.882760763168335,0.11723925918340683,0.023948902264237404 +apple/test/unripe/66.jpg,unripe,unripe,0.3641514480113983,0.6358485817909241,0.0 +apple/test/unripe/67.jpg,unripe,overripe,0.35948479175567627,0.6405152082443237,0.006496966350823641 +apple/test/unripe/68.jpg,unripe,overripe,0.11719897389411926,0.8828010559082031,0.09892863035202026 +apple/test/unripe/69.jpg,unripe,unripe,0.21123431622982025,0.7887656688690186,0.0 +apple/test/unripe/7.jpg,unripe,unripe,0.6398563385009766,0.36014366149902344,0.0 +apple/test/unripe/70.jpg,unripe,unripe,0.8616527318954468,0.13834728300571442,0.0 +apple/test/unripe/71.jpg,unripe,unripe,0.6592984199523926,0.3407016098499298,0.0 +apple/test/unripe/72.jpg,unripe,overripe,0.0,0.4629233479499817,0.5370766520500183 +apple/test/unripe/73.jpg,unripe,overripe,0.32523050904273987,0.6747694611549377,0.202961727976799 +apple/test/unripe/74.jpg,unripe,overripe,0.4938494861125946,0.506150484085083,0.1499136984348297 +apple/test/unripe/75.jpg,unripe,overripe,0.0,0.7277829647064209,0.2722170650959015 +apple/test/unripe/76.jpg,unripe,overripe,0.8581738471984863,0.14182618260383606,0.06792185455560684 +apple/test/unripe/77.jpg,unripe,unripe,0.5566670298576355,0.4433329999446869,0.0 +apple/test/unripe/78.jpg,unripe,overripe,0.9345239400863647,0.06547604501247406,0.09636905044317245 +apple/test/unripe/79.jpg,unripe,overripe,0.12789495289325714,0.7969533801078796,0.20304659008979797 +apple/test/unripe/8.jpg,unripe,unripe,0.3938978612422943,0.6061021089553833,0.0003565062361303717 +apple/test/unripe/80.jpg,unripe,overripe,0.059317946434020996,0.656404435634613,0.3435955345630646 +apple/test/unripe/81.jpg,unripe,overripe,0.014219650998711586,0.6397760510444641,0.3602239191532135 +apple/test/unripe/82.jpg,unripe,unripe,0.3232458829879761,0.6767541170120239,0.0 +apple/test/unripe/83.jpg,unripe,overripe,0.8787909746170044,0.12120901793241501,0.28130730986595154 +apple/test/unripe/84.jpg,unripe,ripe,0.0,0.9970352649688721,0.002964708721265197 +apple/test/unripe/85.jpg,unripe,overripe,0.0,0.43945884704589844,0.5605411529541016 +apple/test/unripe/86.jpg,unripe,overripe,0.6823858618736267,0.3176141679286957,0.1762472242116928 +apple/test/unripe/87.jpg,unripe,overripe,0.41439878940582275,0.5856012105941772,0.025455240160226822 +apple/test/unripe/88.jpg,unripe,unripe,0.16133993864059448,0.8386600613594055,0.0 +apple/test/unripe/89.jpg,unripe,unripe,0.2728308141231537,0.7271691560745239,0.0 +apple/test/unripe/9.jpg,unripe,unripe,0.2904469966888428,0.7095530033111572,0.0 +apple/test/unripe/90.jpg,unripe,overripe,0.2687247693538666,0.7312752604484558,0.026356589049100876 +apple/test/unripe/91.jpg,unripe,overripe,0.12789495289325714,0.7969533801078796,0.20304659008979797 +apple/test/unripe/92.jpg,unripe,unripe,0.3699452579021454,0.630054771900177,0.0 +apple/test/unripe/93.jpg,unripe,overripe,0.9939587712287903,0.006041241344064474,0.07385757565498352 +apple/test/unripe/94.jpg,unripe,overripe,0.11670073121786118,0.8222458362579346,0.17775416374206543 +apple/test/unripe/95.jpg,unripe,overripe,0.1852056086063385,0.8147944211959839,0.1289488524198532 +apple/test/unripe/96.jpg,unripe,overripe,0.5682976245880127,0.4317023754119873,0.05905454605817795 +apple/test/unripe/97.jpg,unripe,unripe,0.20131805539131165,0.7986819744110107,0.0 +apple/test/unripe/98.jpg,unripe,overripe,0.0,0.7736557722091675,0.2263442575931549 +apple/test/unripe/99.jpg,unripe,unripe,0.6766818761825562,0.32331812381744385,0.0024987375363707542 diff --git a/AgCloud/services/ripeness-baseline/eval/apple_tuned/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/apple_tuned/roc_curves.png new file mode 100644 index 000000000..b0eff4b97 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/apple_tuned/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/banana_argmax/metrics.json b/AgCloud/services/ripeness-baseline/eval/banana_argmax/metrics.json new file mode 100644 index 000000000..550519d20 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/banana_argmax/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.2036613272311213, + "report": { + "unripe": { + "precision": 0.4304635761589404, + "recall": 0.1625, + "f1-score": 0.23593466424682397, + "support": 400.0 + }, + "ripe": { + "precision": 0.022452504317789293, + "recall": 0.03412073490813648, + "f1-score": 0.027083333333333334, + "support": 381.0 + }, + "overripe": { + "precision": 0.3253012048192771, + "recall": 0.35660377358490564, + "f1-score": 0.3402340234023402, + "support": 530.0 + }, + "accuracy": 0.2036613272311213, + "macro avg": { + "precision": 0.2594057617653356, + "recall": 0.18440816949768069, + "f1-score": 0.20108400699416584, + "support": 1311.0 + }, + "weighted avg": { + "precision": 0.2693741214056985, + "recall": 0.2036613272311213, + "f1-score": 0.21740400312888628, + "support": 1311.0 + } + }, + "confusion_matrix": [ + [ + 65, + 292, + 43 + ], + [ + 19, + 13, + 349 + ], + [ + 67, + 274, + 189 + ] + ], + "samples": 1311, + "prefix": "banana/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/banana_argmax/per_image.csv b/AgCloud/services/ripeness-baseline/eval/banana_argmax/per_image.csv new file mode 100644 index 000000000..2e0a95964 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/banana_argmax/per_image.csv @@ -0,0 +1,1312 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +banana/test/overripe/Screen Shot 2018-06-12 at 8.47.41 PM.png,overripe,ripe,0.0,0.518994927406311,0.4810050427913666 +banana/test/overripe/Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42206040024757385,0.5779396295547485 +banana/test/overripe/Screen Shot 2018-06-12 at 8.48.40 PM.png,overripe,overripe,0.0,0.42832913994789124,0.5716708302497864 +banana/test/overripe/Screen Shot 2018-06-12 at 8.49.25 PM.png,overripe,ripe,0.0,0.7199916243553162,0.2800084054470062 +banana/test/overripe/Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,ripe,0.0,0.5577014088630676,0.4422985911369324 +banana/test/overripe/Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,ripe,0.0,0.709297776222229,0.290702223777771 +banana/test/overripe/Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.47075432538986206,0.5292456746101379 +banana/test/overripe/Screen Shot 2018-06-12 at 8.52.16 PM.png,overripe,unripe,0.667235791683197,0.3327641785144806,0.1479686200618744 +banana/test/overripe/Screen Shot 2018-06-12 at 8.52.21 PM.png,overripe,overripe,0.0,0.4193042516708374,0.5806957483291626 +banana/test/overripe/Screen Shot 2018-06-12 at 8.53.09 PM.png,overripe,ripe,0.4044839143753052,0.5955160856246948,0.18102799355983734 +banana/test/overripe/Screen Shot 2018-06-12 at 8.53.47 PM.png,overripe,ripe,0.32728666067123413,0.6727133393287659,0.21059417724609375 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,ripe,0.3154248595237732,0.6352924108505249,0.3647075891494751 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.07 PM.png,overripe,ripe,0.19021277129650116,0.8097872138023376,0.13643395900726318 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,ripe,0.3981633186340332,0.6018366813659668,0.13918264210224152 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.41397202014923096,0.586027979850769 +banana/test/overripe/Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,ripe,0.0,0.5972847938537598,0.40271520614624023 +banana/test/overripe/Screen Shot 2018-06-12 at 8.57.14 PM.png,overripe,overripe,0.0,0.4320329427719116,0.5679670572280884 +banana/test/overripe/Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,ripe,0.1500568985939026,0.8499431014060974,0.08232235163450241 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.38 PM.png,overripe,overripe,0.0,0.4314533770084381,0.5685465931892395 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.43 PM.png,overripe,overripe,0.0,0.41014915704727173,0.5898508429527283 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.407118022441864,0.592881977558136 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.02 PM.png,overripe,overripe,0.0,0.4045616090297699,0.5954383611679077 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.15 PM.png,overripe,overripe,0.0,0.4761301577091217,0.5238698720932007 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,ripe,0.32199764251708984,0.5733931064605713,0.4266068637371063 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.44 PM.png,overripe,unripe,0.6172191500663757,0.38278084993362427,0.3783235549926758 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,unripe,0.6168419122695923,0.3831580877304077,0.43098539113998413 +banana/test/overripe/Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,ripe,0.0,0.5055989623069763,0.4944010376930237 +banana/test/overripe/Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,ripe,0.096991628408432,0.599040150642395,0.4009598195552826 +banana/test/overripe/Screen Shot 2018-06-12 at 9.01.26 PM.png,overripe,overripe,0.0,0.4006190299987793,0.5993809700012207 +banana/test/overripe/Screen Shot 2018-06-12 at 9.03.34 PM.png,overripe,ripe,0.11833731830120087,0.5525362491607666,0.4474637806415558 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,ripe,0.0,0.5552470684051514,0.444752961397171 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.46078523993492126,0.5392147898674011 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,unripe,0.9270422458648682,0.07295773923397064,0.3303984999656677 +banana/test/overripe/Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,ripe,0.0,0.6510867476463318,0.3489132821559906 +banana/test/overripe/Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,ripe,0.38904502987861633,0.6109549403190613,0.33104702830314636 +banana/test/overripe/Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.4519452452659607,0.5480547547340393 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,ripe,0.0,0.5278568863868713,0.4721430838108063 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.29 PM.png,overripe,ripe,0.3696404695510864,0.5534030199050903,0.44659698009490967 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,ripe,0.0,0.6095148921012878,0.39048513770103455 +banana/test/overripe/Screen Shot 2018-06-12 at 9.10.20 PM.png,overripe,overripe,0.0,0.4696422517299652,0.5303577780723572 +banana/test/overripe/Screen Shot 2018-06-12 at 9.11.00 PM.png,overripe,ripe,0.0,0.557987630367279,0.44201236963272095 +banana/test/overripe/Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,ripe,0.0,0.6629846096038818,0.3370153605937958 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.40 PM.png,overripe,overripe,0.0,0.4004119336605072,0.5995880961418152 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,ripe,0.0,0.9653072357177734,0.034692756831645966 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,ripe,0.12668900191783905,0.626222550868988,0.37377744913101196 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.16 PM.png,overripe,unripe,0.7571433782577515,0.24285665154457092,0.18584129214286804 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,ripe,0.0,0.5574024319648743,0.4425975978374481 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.34 PM.png,overripe,overripe,0.0,0.4543142318725586,0.5456857681274414 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.44 PM.png,overripe,ripe,0.0,0.5824558138847351,0.4175442159175873 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,ripe,0.19090639054775238,0.5990057587623596,0.4009942412376404 +banana/test/overripe/Screen Shot 2018-06-12 at 9.14.07 PM.png,overripe,overripe,0.0,0.4175000488758087,0.5824999213218689 +banana/test/overripe/Screen Shot 2018-06-12 at 9.14.22 PM.png,overripe,overripe,0.0,0.4019697606563568,0.5980302095413208 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.41344261169433594,0.5865573883056641 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.28 PM.png,overripe,unripe,0.7762260437011719,0.22377397119998932,0.3118768334388733 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,ripe,0.0,0.7262567281723022,0.27374327182769775 +banana/test/overripe/Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,unripe,0.7642717361450195,0.23572827875614166,0.1272905170917511 +banana/test/overripe/Screen Shot 2018-06-12 at 9.18.57 PM.png,overripe,ripe,0.0,0.5646048188209534,0.435395210981369 +banana/test/overripe/Screen Shot 2018-06-12 at 9.19.17 PM.png,overripe,ripe,0.0,0.5958848595619202,0.40411514043807983 +banana/test/overripe/Screen Shot 2018-06-12 at 9.21.25 PM.png,overripe,overripe,0.0,0.40166544914245605,0.598334550857544 +banana/test/overripe/Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,ripe,0.0,0.6837197542190552,0.3162802457809448 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4030764102935791,0.5969235897064209 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.4107680022716522,0.5892319679260254 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.28 PM.png,overripe,unripe,0.8895150423049927,0.11048497259616852,0.39010170102119446 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,ripe,0.44345948100090027,0.5565405488014221,0.2643858790397644 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.26 PM.png,overripe,ripe,0.0,0.7626296877861023,0.2373703122138977 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,ripe,0.0,0.7602983117103577,0.23970165848731995 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.42068740725517273,0.5793125629425049 +banana/test/overripe/Screen Shot 2018-06-12 at 9.28.04 PM.png,overripe,unripe,0.5855558514595032,0.41444411873817444,0.23162424564361572 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42225202918052673,0.5777479410171509 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.2923990786075592,0.7076008915901184 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,ripe,0.0,0.7093083262443542,0.29069164395332336 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4694567918777466,0.5305432081222534 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.52.48 PM.png,overripe,ripe,0.0,0.9372448325157166,0.06275517493486404 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,ripe,0.0,0.9111128449440002,0.08888714015483856 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,ripe,0.4091706871986389,0.5908293128013611,0.18992936611175537 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.42349129915237427,0.5765087008476257 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,ripe,0.430692195892334,0.569307804107666,0.15018440783023834 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.16 PM.png,overripe,ripe,0.0,0.8058255314826965,0.19417443871498108 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.37 PM.png,overripe,unripe,0.860004723072052,0.13999530673027039,0.29752403497695923 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,ripe,0.0,0.5959540605545044,0.404045969247818 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.59 PM.png,overripe,ripe,0.0,0.7087627053260803,0.2912372946739197 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.57.04 PM.png,overripe,unripe,0.7473717927932739,0.2526282072067261,0.1737419217824936 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,unripe,0.7669326663017273,0.2330673336982727,0.11344532668590546 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.15 PM.png,overripe,overripe,0.0,0.4768418073654175,0.5231581926345825 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,ripe,0.32334139943122864,0.5722593665122986,0.4277406632900238 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.32 PM.png,overripe,ripe,0.33486294746398926,0.5714527368545532,0.4285472631454468 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,unripe,0.6037713885307312,0.3962285816669464,0.436612069606781 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,ripe,0.0,0.5189968943595886,0.48100313544273376 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.00.49 PM.png,overripe,ripe,0.0,0.6620721220970154,0.33792784810066223 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.41573527455329895,0.5842646956443787 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.44031935930252075,0.5596806406974792 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,ripe,0.016604125499725342,0.6794905066490173,0.32050949335098267 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,ripe,0.1282028704881668,0.7799780964851379,0.22002193331718445 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.46152961254119873,0.5384703874588013 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.05.02 PM.png,overripe,unripe,0.876327395439148,0.12367259711027145,0.3767489194869995 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.06.13 PM.png,overripe,ripe,0.13691593706607819,0.5625618100166321,0.4374381899833679 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.07.35 PM.png,overripe,ripe,0.0,0.7051929831504822,0.2948070466518402 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,ripe,0.0,0.5259113907814026,0.474088579416275 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,ripe,0.0,0.6974937319755554,0.30250629782676697 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,ripe,0.0,0.609123945236206,0.39087608456611633 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.04 PM.png,overripe,ripe,0.0,0.553801417350769,0.44619855284690857 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,ripe,0.10625192523002625,0.5704038739204407,0.4295961260795593 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.49855419993400574,0.5014457702636719 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,ripe,0.0,0.6642929315567017,0.33570706844329834 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.11.47 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.40682414174079895,0.5931758284568787 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,ripe,0.0,0.9641626477241516,0.03583735600113869 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,ripe,0.13838624954223633,0.6232191324234009,0.3767808675765991 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.4272346496582031,0.5727653503417969 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.4114099442958832,0.5885900855064392 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.18.43 PM.png,overripe,overripe,0.0,0.4790213406085968,0.5209786891937256 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.18.50 PM.png,overripe,unripe,0.9545795321464539,0.045420464128255844,0.272438108921051 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,ripe,0.05336310341954231,0.5470808148384094,0.4529191553592682 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4743472635746002,0.5256527066230774 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.36 PM.png,overripe,overripe,0.0,0.4905504286289215,0.5094496011734009 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.21.05 PM.png,overripe,overripe,0.0,0.4008191227912903,0.5991808772087097 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.22.19 PM.png,overripe,overripe,0.0,0.430061012506485,0.5699390172958374 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.22.50 PM.png,overripe,ripe,0.0,0.7324366569519043,0.2675633430480957 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,ripe,0.12080598622560501,0.8791940212249756,0.09513043612241745 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.11 PM.png,overripe,ripe,0.0,0.7422084808349609,0.25779151916503906 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,ripe,0.0,0.7596266269683838,0.2403733879327774 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,ripe,0.0,0.6120586395263672,0.3879413604736328 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.46 PM.png,overripe,ripe,0.0,0.632862389087677,0.3671375811100006 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,ripe,0.0,0.7616807222366333,0.2383192777633667 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.2932327389717102,0.7067672610282898 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,ripe,0.42225438356399536,0.5777456164360046,0.3469010591506958 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.07 PM.png,overripe,ripe,0.20600689947605133,0.7939931154251099,0.14732812345027924 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,ripe,0.42930981516838074,0.5706902146339417,0.15038400888442993 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,unripe,0.6561015844345093,0.3438984453678131,0.23757719993591309 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.37 PM.png,overripe,ripe,0.0,0.6641075611114502,0.3358924388885498 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.43 PM.png,overripe,ripe,0.0,0.5799486637115479,0.42005136609077454 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.55.15 PM.png,overripe,ripe,0.0,0.5579839944839478,0.44201603531837463 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.56.11 PM.png,overripe,ripe,0.0,0.5840222239494324,0.4159777760505676 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.56.41 PM.png,overripe,ripe,0.07862403988838196,0.8707615733146667,0.12923841178417206 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.36930447816848755,0.6306955218315125 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.57.29 PM.png,overripe,overripe,0.0,0.487086683511734,0.5129133462905884 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,unripe,0.7532721161842346,0.24672789871692657,0.11264381557703018 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,ripe,0.46541187167167664,0.5345881581306458,0.0751504972577095 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,ripe,0.3279096782207489,0.5761774182319641,0.4238225817680359 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.00.31 PM.png,overripe,overripe,0.0,0.4629867374897003,0.5370132327079773 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.00.49 PM.png,overripe,ripe,0.0,0.6623407602310181,0.3376592695713043 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,ripe,0.06528335809707642,0.6431384086608887,0.35686159133911133 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.02.52 PM.png,overripe,unripe,0.9464337825775146,0.05356622114777565,0.2509620487689972 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.03.25 PM.png,overripe,unripe,0.8060479164123535,0.19395211338996887,0.39888882637023926 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,ripe,0.10719307512044907,0.7867035269737244,0.21329650282859802 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,ripe,0.0,0.5363625288009644,0.46363750100135803 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,ripe,0.49722376465797424,0.5027762055397034,0.2618366777896881 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,ripe,0.0,0.6207407116889954,0.379259318113327 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.4471229016780853,0.5528771281242371 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.00 PM.png,overripe,overripe,0.0,0.40048736333847046,0.5995126366615295 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.12 PM.png,overripe,overripe,0.0,0.4552210569381714,0.5447789430618286 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.26 PM.png,overripe,ripe,0.0,0.5577588081359863,0.4422411620616913 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.43 PM.png,overripe,ripe,0.16492566466331482,0.6882914900779724,0.3117085099220276 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,ripe,0.092755027115345,0.5667913556098938,0.4332086145877838 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.10.45 PM.png,overripe,unripe,0.6769934892654419,0.3230064809322357,0.0 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.05 PM.png,overripe,overripe,0.0,0.4608272612094879,0.5391727685928345 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.17 PM.png,overripe,unripe,0.5491920709609985,0.45080792903900146,0.14614258706569672 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.27 PM.png,overripe,ripe,0.0,0.7357856631278992,0.26421433687210083 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.58 PM.png,overripe,overripe,0.18038751184940338,0.47363460063934326,0.5263653993606567 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.12.17 PM.png,overripe,overripe,0.0,0.45771655440330505,0.5422834157943726 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.13.10 PM.png,overripe,ripe,0.0,0.9363075494766235,0.06369245052337646 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.4018345773220062,0.5981653928756714 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.34 PM.png,overripe,overripe,0.0,0.41054949164390564,0.5894505381584167 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,ripe,0.0,0.7643312811851501,0.23566873371601105 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.53 PM.png,overripe,ripe,0.0,0.5700190663337708,0.42998093366622925 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,ripe,0.0,0.7285685539245605,0.27143144607543945 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.18.11 PM.png,overripe,ripe,0.0,0.5845561027526855,0.41544386744499207 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.18.50 PM.png,overripe,unripe,0.9015335440635681,0.09846646338701248,0.25223612785339355 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.08 PM.png,overripe,ripe,0.21922332048416138,0.5993502736091614,0.40064969658851624 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,ripe,0.04551864042878151,0.5481064319610596,0.45189356803894043 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.42 PM.png,overripe,unripe,0.822131335735321,0.17786864936351776,0.16532373428344727 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.49 PM.png,overripe,overripe,0.0,0.4043714106082916,0.5956286191940308 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.20.36 PM.png,overripe,overripe,0.0,0.49349918961524963,0.506500780582428 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,ripe,0.0,0.6843619346618652,0.3156380355358124 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.25.09 PM.png,overripe,overripe,0.0,0.42234501242637634,0.5776549577713013 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,ripe,0.3865452706813812,0.6134547591209412,0.2693168520927429 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.26.01 PM.png,overripe,ripe,0.0,0.8650020360946655,0.13499796390533447 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.26.13 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,ripe,0.0,0.6915556192398071,0.30844438076019287 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.31 PM.png,overripe,ripe,0.0,0.8348144888877869,0.16518552601337433 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.41934001445770264,0.5806599855422974 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.47.28 PM.png,overripe,ripe,0.4300452470779419,0.5699547529220581,0.3990974426269531 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42258188128471375,0.5774181485176086 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,ripe,0.0,0.7628708481788635,0.23712916672229767 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.29060453176498413,0.7093954682350159 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.53.09 PM.png,overripe,ripe,0.44640639424324036,0.5535935759544373,0.18370641767978668 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.4369044899940491,0.5630955100059509 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,unripe,0.7128736972808838,0.2871263325214386,0.24525289237499237 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.58 PM.png,overripe,overripe,0.0,0.44183143973350525,0.5581685900688171 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.4518700838088989,0.5481299161911011 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,ripe,0.0,0.5985235571861267,0.4014764428138733 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.38556787371635437,0.6144320964813232 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,ripe,0.0,0.5909960269927979,0.40900397300720215 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,unripe,0.8564693927764893,0.14353063702583313,0.11759375035762787 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.58.38 PM.png,overripe,overripe,0.0,0.4619027078151703,0.5380973219871521 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.46374693512916565,0.536253035068512 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,ripe,0.0,0.5310365557670593,0.46896347403526306 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.27 PM.png,overripe,overripe,0.0,0.4929629862308502,0.5070369839668274 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.32 PM.png,overripe,ripe,0.0,0.9349110722541809,0.06508895754814148 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.4448396861553192,0.5551602840423584 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.02.15 PM.png,overripe,ripe,0.036078158766031265,0.7250709533691406,0.2749290466308594 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.02.32 PM.png,overripe,ripe,0.016653841361403465,0.7159335017204285,0.28406646847724915 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.01 PM.png,overripe,overripe,0.0,0.416714072227478,0.583285927772522 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.12 PM.png,overripe,ripe,0.08530794084072113,0.7214001417160034,0.2785998284816742 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.21 PM.png,overripe,unripe,0.7167579531669617,0.2832420766353607,0.3230315148830414 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,ripe,0.05061882734298706,0.8017657399177551,0.19823426008224487 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.4626938998699188,0.5373061299324036 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.05.32 PM.png,overripe,overripe,0.0,0.4959444999694824,0.5040555000305176 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,ripe,0.0,0.6650514006614685,0.3349485993385315 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,ripe,0.42296627163887024,0.5770337581634521,0.31120434403419495 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.07.35 PM.png,overripe,ripe,0.0,0.7058061957359314,0.2941937744617462 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.09.09 PM.png,overripe,overripe,0.0,0.45207205414772034,0.5479279160499573 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,ripe,0.06998586654663086,0.5522445440292358,0.44775545597076416 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.4250321388244629,0.5749678611755371 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,ripe,0.19400057196617126,0.6083990335464478,0.39160096645355225 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.4837188422679901,0.5162811875343323 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.07880022376775742,0.41323742270469666,0.5867626070976257 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,ripe,0.0,0.7322930693626404,0.26770690083503723 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.17.44 PM.png,overripe,ripe,0.0,0.6281085014343262,0.37189149856567383 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.18.43 PM.png,overripe,overripe,0.0,0.48793166875839233,0.5120683312416077 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4790334701538086,0.5209665298461914 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.56 PM.png,overripe,overripe,0.0,0.487467497587204,0.5125324726104736 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.22.37 PM.png,overripe,ripe,0.0,0.7536795735359192,0.24632041156291962 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.22.57 PM.png,overripe,overripe,0.0,0.4589863121509552,0.5410137176513672 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.23.24 PM.png,overripe,overripe,0.0,0.427823930978775,0.5721760988235474 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.406148761510849,0.5938512682914734 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.40087518095970154,0.5991247892379761 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.47.28 PM.png,overripe,unripe,0.5188015103340149,0.4811984598636627,0.3791777789592743 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.49.04 PM.png,overripe,ripe,0.0,0.5210857391357422,0.4789142310619354 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.49.30 PM.png,overripe,overripe,0.0,0.4237978756427765,0.5762020945549011 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.50.40 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.51.38 PM.png,overripe,overripe,0.0,0.4002718925476074,0.5997281074523926 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4739341735839844,0.5260658264160156 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.52.37 PM.png,overripe,ripe,0.2937920093536377,0.7062079906463623,0.10951603949069977 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,ripe,0.0,0.9023623466491699,0.09763766825199127 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,unripe,0.5097864270210266,0.4902136027812958,0.2157808244228363 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,unripe,0.6782633066177368,0.32173672318458557,0.23851622641086578 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.55.08 PM.png,overripe,overripe,0.0,0.444760799407959,0.555239200592041 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,ripe,0.0,0.5867201685905457,0.41327980160713196 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.21 PM.png,overripe,ripe,0.0,0.5185762047767639,0.4814237654209137 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.4500170648097992,0.5499829053878784 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,ripe,0.0,0.5983180999755859,0.40168190002441406 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.40120476484298706,0.5987952351570129 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,ripe,0.0,0.5910417437553406,0.4089582860469818 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,ripe,0.44182518124580383,0.5581748485565186,0.08216758072376251 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.32 PM.png,overripe,ripe,0.0,0.9695771336555481,0.030422843992710114 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.35 PM.png,overripe,ripe,0.30009040236473083,0.6999096274375916,0.2437742054462433 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.44117316603660583,0.5588268637657166 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.02.09 PM.png,overripe,ripe,0.0,0.6998764276504517,0.30012354254722595 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,ripe,0.0,0.6660088896751404,0.33399108052253723 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.03.30 PM.png,overripe,overripe,0.0015817406820133328,0.47638797760009766,0.5236120223999023 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.03.34 PM.png,overripe,ripe,0.1574755311012268,0.5649651885032654,0.435034841299057 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.04.01 PM.png,overripe,overripe,0.0,0.4834127724170685,0.5165872573852539 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.05.21 PM.png,overripe,overripe,0.0,0.4040911793708801,0.5959088206291199 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,ripe,0.43597885966300964,0.564021110534668,0.30908527970314026 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.4378005564212799,0.5621994733810425 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.08.00 PM.png,overripe,overripe,0.0,0.4005366265773773,0.5994633436203003 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,ripe,0.22187215089797974,0.5990240573883057,0.40097594261169434 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.11.23 PM.png,overripe,overripe,0.0,0.4071483314037323,0.5928516983985901 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.12.27 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,ripe,0.005762064829468727,0.9844754338264465,0.015524537302553654 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.16 PM.png,overripe,ripe,0.229378804564476,0.7706211805343628,0.0883166491985321 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,ripe,0.012014739215373993,0.5767086148262024,0.4232913851737976 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.34 PM.png,overripe,overripe,0.0,0.4222790002822876,0.5777209997177124 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.41723835468292236,0.5827616453170776 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,ripe,0.0,0.7321774959564209,0.2678225338459015 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.4811819791793823,0.5188180208206177 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,unripe,0.7739754319190979,0.2260245531797409,0.11981095373630524 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,ripe,0.0468892976641655,0.5450760722160339,0.45492392778396606 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.20.01 PM.png,overripe,ripe,0.33310776948928833,0.6647689342498779,0.33523106575012207 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.20.47 PM.png,overripe,overripe,0.0,0.4083249270915985,0.5916751027107239 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.23.15 PM.png,overripe,ripe,0.43818315863609314,0.5618168711662292,0.14291292428970337 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4024614691734314,0.5975385308265686 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.04 PM.png,overripe,ripe,0.4472711384296417,0.5527288913726807,0.2147432565689087 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.34 PM.png,overripe,overripe,0.0,0.4423665702342987,0.5576333999633789 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,unripe,0.5294066667556763,0.47059333324432373,0.318183958530426 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.40053874254226685,0.5994612574577332 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.41987958550453186,0.5801204442977905 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.47.41 PM.png,overripe,ripe,0.0,0.5250896215438843,0.4749104082584381 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,ripe,0.0,0.5697281956672668,0.43027177453041077 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.04 PM.png,overripe,ripe,0.0,0.5481178760528564,0.45188212394714355 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.47 PM.png,overripe,ripe,0.0,0.9659353494644165,0.03406466543674469 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.54 PM.png,overripe,ripe,0.0,0.7318786382675171,0.2681213319301605 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.30 PM.png,overripe,ripe,0.0,0.6876980066299438,0.31230202317237854 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4704473614692688,0.5295526385307312 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.56 PM.png,overripe,ripe,0.21594563126564026,0.7723483443260193,0.2276516556739807 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,ripe,0.0,0.7067722678184509,0.2932277321815491 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.52.11 PM.png,overripe,ripe,0.31783631443977356,0.600331723690033,0.39966827630996704 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,unripe,0.9428635239601135,0.057136498391628265,0.3646189272403717 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,unripe,0.5536673665046692,0.4463326036930084,0.21723736822605133 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.55.08 PM.png,overripe,overripe,0.0,0.44686880707740784,0.5531311631202698 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.55.35 PM.png,overripe,ripe,0.0,0.678168773651123,0.32183122634887695 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,ripe,0.0,0.5875664353370667,0.41243356466293335 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.44791969656944275,0.5520803332328796 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.57.46 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.58.07 PM.png,overripe,ripe,0.0,0.9840808510780334,0.015919169411063194 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40714243054389954,0.5928575992584229 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.59.32 PM.png,overripe,ripe,0.16938765347003937,0.5893108248710632,0.41068920493125916 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.00.11 PM.png,overripe,ripe,0.0,0.8064447045326233,0.1935552954673767 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.00.17 PM.png,overripe,overripe,0.0,0.42036008834838867,0.5796399116516113 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,ripe,0.16116876900196075,0.650871753692627,0.34912824630737305 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.4120545983314514,0.5879454016685486 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.26 PM.png,overripe,overripe,0.0,0.4004873037338257,0.5995126962661743 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.43730252981185913,0.5626974701881409 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.02.03 PM.png,overripe,ripe,0.29318201541900635,0.7068179845809937,0.24836911261081696 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.03.25 PM.png,overripe,unripe,0.818538248538971,0.18146175146102905,0.3989396393299103 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.03.40 PM.png,overripe,unripe,0.9131476283073425,0.08685235679149628,0.34103259444236755 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.01 PM.png,overripe,overripe,0.0,0.4898202419281006,0.5101797580718994 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.08 PM.png,overripe,ripe,0.0,0.775429904460907,0.22457008063793182 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,unripe,0.5265953540802002,0.4734046161174774,0.26254868507385254 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.05.08 PM.png,overripe,unripe,0.9430190324783325,0.05698094516992569,0.3412940204143524 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,ripe,0.0,0.7026721835136414,0.29732784628868103 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,unripe,0.5027430057525635,0.4972569942474365,0.30651968717575073 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.07.26 PM.png,overripe,ripe,0.0,0.6439215540885925,0.35607847571372986 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.09.16 PM.png,overripe,overripe,0.0,0.4392331540584564,0.5607668161392212 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,ripe,0.0,0.6948384046554565,0.30516159534454346 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.10.10 PM.png,overripe,ripe,0.0,0.576144814491272,0.423855185508728 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.10.45 PM.png,overripe,unripe,0.8485866189002991,0.15141336619853973,0.0 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.05 PM.png,overripe,overripe,0.0,0.4609310030937195,0.5390689969062805 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,ripe,0.0,0.6696222424507141,0.3303777277469635 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.43 PM.png,overripe,unripe,0.9137662053108215,0.08623377233743668,0.19863319396972656 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.52 PM.png,overripe,unripe,0.5105805397033691,0.48941943049430847,0.28044217824935913 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.11 PM.png,overripe,overripe,0.0,0.4213138222694397,0.5786861777305603 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.40727171301841736,0.592728316783905 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,ripe,0.13972267508506775,0.6248213052749634,0.3751786947250366 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,ripe,0.2718159556388855,0.6032296419143677,0.3967703878879547 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.43030551075935364,0.569694459438324 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.54 PM.png,overripe,ripe,0.1474388837814331,0.5679682493209839,0.4320317804813385 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.14.22 PM.png,overripe,overripe,0.0,0.4010840952396393,0.5989159345626831 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.4011707603931427,0.5988292098045349 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.16.28 PM.png,overripe,unripe,0.9243571162223816,0.0756429135799408,0.2309676557779312 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.17.44 PM.png,overripe,ripe,0.0,0.6393946409225464,0.3606053590774536 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.18.11 PM.png,overripe,ripe,0.0,0.5561631917953491,0.44383683800697327 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.19.03 PM.png,overripe,ripe,0.0,0.6624022126197815,0.3375977873802185 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.20.01 PM.png,overripe,ripe,0.29121193289756775,0.677950918674469,0.322049081325531 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.22.57 PM.png,overripe,overripe,0.0,0.4748762249946594,0.5251237750053406 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.23.15 PM.png,overripe,unripe,0.5625900626182556,0.4374099373817444,0.14382266998291016 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.40582725405693054,0.5941727757453918 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.28 PM.png,overripe,unripe,0.8638418316841125,0.13615818321704865,0.4155179262161255 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,unripe,0.5551921725273132,0.4448077976703644,0.2610897719860077 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,ripe,0.11531119793653488,0.8846887946128845,0.09209687262773514 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,ripe,0.0,0.6782616376876831,0.3217383325099945 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,ripe,0.0,0.7672150731086731,0.2327849566936493 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.46 PM.png,overripe,ripe,0.0,0.6319037675857544,0.3680962324142456 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.4240608811378479,0.5759391188621521 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.48.40 PM.png,overripe,overripe,0.0,0.43042683601379395,0.569573163986206 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,ripe,0.0,0.7623252868652344,0.23767472803592682 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,ripe,0.0,0.558709442615509,0.4412905275821686 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.04 PM.png,overripe,ripe,0.0,0.5481931567192078,0.45180684328079224 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.26 PM.png,overripe,ripe,0.0,0.7081860303878784,0.2918139696121216 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.54 PM.png,overripe,ripe,0.0,0.7358757853507996,0.26412418484687805 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.51.38 PM.png,overripe,overripe,0.0,0.40431931614875793,0.5956807136535645 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,ripe,0.0,0.7236613631248474,0.2763386368751526 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.37 PM.png,overripe,ripe,0.36805829405784607,0.6319416761398315,0.12030328065156937 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.42 PM.png,overripe,unripe,0.6024632453918457,0.3975367546081543,0.2390647679567337 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,ripe,0.0,0.9031139016151428,0.09688610583543777 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,unripe,0.5595387816429138,0.44046124815940857,0.217310830950737 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.4231916666030884,0.5768083333969116 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,unripe,0.5568576455116272,0.4431423246860504,0.1838689148426056 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.41595780849456787,0.5840421915054321 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.55.47 PM.png,overripe,ripe,0.0,0.56788170337677,0.43211832642555237 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.56.21 PM.png,overripe,ripe,0.0,0.8804879784584045,0.11951202899217606 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,ripe,0.14361675083637238,0.8563832640647888,0.07992758601903915 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,ripe,0.3874566853046417,0.6125432848930359,0.07197075337171555 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.4089244306087494,0.591075599193573 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,unripe,0.6038687825202942,0.3961312174797058,0.4308454096317291 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.4678408205509186,0.532159149646759 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,ripe,0.0,0.5063828825950623,0.49361714720726013 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.00.31 PM.png,overripe,overripe,0.0,0.4658997356891632,0.5341002345085144 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.4160113036632538,0.5839886665344238 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.4439060389995575,0.5560939311981201 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.32 PM.png,overripe,ripe,0.0,0.7083366513252258,0.29166337847709656 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,ripe,0.01209096796810627,0.6810184717178345,0.3189815580844879 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.52 PM.png,overripe,ripe,0.4016786813735962,0.5890161991119385,0.4109838306903839 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.03.30 PM.png,overripe,overripe,0.0,0.4590902626514435,0.5409097671508789 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.03.55 PM.png,overripe,unripe,0.8802447319030762,0.11975526809692383,0.2900313436985016 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,unripe,0.9305940270423889,0.06940599530935287,0.32746097445487976 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.05.02 PM.png,overripe,unripe,0.9065488576889038,0.09345114231109619,0.3777277171611786 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,ripe,0.0,0.6515752077102661,0.3484247922897339 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.06.21 PM.png,overripe,overripe,0.0,0.44775259494781494,0.5522474050521851 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,ripe,0.0,0.6158132553100586,0.3841867446899414 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.07.39 PM.png,overripe,ripe,0.0,0.6144538521766663,0.38554614782333374 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.08.22 PM.png,overripe,overripe,0.0,0.4736316502094269,0.5263683199882507 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,ripe,0.0,0.5269660353660583,0.47303399443626404 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.10 PM.png,overripe,ripe,0.0,0.5380335450172424,0.4619664251804352 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,ripe,0.0908670499920845,0.5586211681365967,0.4413788318634033 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.41 PM.png,overripe,overripe,0.0,0.490922749042511,0.509077250957489 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,ripe,0.0,0.5017094016075134,0.4982905983924866 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.11.17 PM.png,overripe,unripe,0.5789666771888733,0.4210332930088043,0.33436092734336853 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.11 PM.png,overripe,overripe,0.0,0.42478039860725403,0.5752196311950684 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.40 PM.png,overripe,overripe,0.0,0.4021092355251312,0.5978907942771912 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.49 PM.png,overripe,ripe,0.0,0.9378576278686523,0.06214238330721855 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,ripe,0.12266360223293304,0.6249702572822571,0.37502971291542053 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.44794145226478577,0.5520585775375366 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.48337119817733765,0.5166288018226624 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.4199988543987274,0.5800011157989502 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.47120413184165955,0.5287958383560181 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.18.07 PM.png,overripe,unripe,0.6875103712081909,0.31248965859413147,0.4850299060344696 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,ripe,0.03455352783203125,0.5449650287628174,0.455035001039505 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.19.42 PM.png,overripe,unripe,0.6564497351646423,0.34355026483535767,0.14597618579864502 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.43772608041763306,0.5622739195823669 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.46779221296310425,0.5322077870368958 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.22.37 PM.png,overripe,ripe,0.0,0.7639629244804382,0.23603709042072296 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.23.24 PM.png,overripe,overripe,0.0,0.435380756855011,0.564619243144989 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4048506021499634,0.5951493978500366 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.25.12 PM.png,overripe,overripe,0.0,0.41544586420059204,0.584554135799408 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,ripe,0.0,0.6143311858177185,0.3856688141822815 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.50.20 PM.png,overripe,overripe,0.0,0.415733277797699,0.584266722202301 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,ripe,0.0,0.7082611918449402,0.2917388081550598 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.46856704354286194,0.5314329862594604 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.56 PM.png,overripe,ripe,0.21707652509212494,0.7777199745178223,0.22228004038333893 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,ripe,0.0,0.7452894449234009,0.2547105550765991 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,unripe,0.9413067102432251,0.0586932972073555,0.3624807894229889 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,ripe,0.3857897222042084,0.6142102479934692,0.3538011312484741 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,ripe,0.4323126971721649,0.5676873326301575,0.15199248492717743 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.37 PM.png,overripe,ripe,0.0,0.5508689284324646,0.4491311013698578 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.43 PM.png,overripe,ripe,0.0,0.6196020841598511,0.38039788603782654 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.4112548232078552,0.5887451767921448 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.55.21 PM.png,overripe,ripe,0.0,0.6137250065803528,0.3862749934196472 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.55.28 PM.png,overripe,overripe,0.0,0.40180298686027527,0.5981969833374023 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,ripe,0.10174223780632019,0.8982577323913574,0.07811367511749268 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.59.44 PM.png,overripe,unripe,0.643505334854126,0.356494665145874,0.3646322190761566 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.46659156680107117,0.5334084630012512 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,ripe,0.0,0.5200329422950745,0.47996705770492554 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.4405238926410675,0.5594761371612549 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.02.38 PM.png,overripe,ripe,0.33342039585113525,0.6665796041488647,0.2894760072231293 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.03.17 PM.png,overripe,unripe,0.8222827911376953,0.1777171939611435,0.3974619507789612 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.03.40 PM.png,overripe,unripe,0.9609435796737671,0.03905642777681351,0.35723549127578735 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.05.12 PM.png,overripe,overripe,0.0,0.45372143387794495,0.5462785959243774 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.05.52 PM.png,overripe,ripe,0.0,0.8409667611122131,0.15903325378894806 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.06.13 PM.png,overripe,ripe,0.06695782393217087,0.5748559832572937,0.4251440167427063 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.06.40 PM.png,overripe,overripe,0.0,0.42634764313697815,0.5736523866653442 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.07.04 PM.png,overripe,ripe,0.0,0.5824512839317322,0.4175487160682678 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,ripe,0.0,0.6958288550376892,0.3041711151599884 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,ripe,0.0,0.6108704209327698,0.3891295790672302 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.10.27 PM.png,overripe,overripe,0.0,0.4930756986141205,0.5069242715835571 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,ripe,0.0,0.5007198452949524,0.49928018450737 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.11.52 PM.png,overripe,unripe,0.68659508228302,0.31340494751930237,0.22047559916973114 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.12.49 PM.png,overripe,ripe,0.0,0.8481088876724243,0.15189111232757568 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.13.10 PM.png,overripe,ripe,0.0,0.9350239634513855,0.06497606635093689 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.42875319719314575,0.5712468028068542 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.4454348385334015,0.5545651912689209 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.41765475273132324,0.5823452472686768 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,ripe,0.0,0.7640902400016785,0.23590974509716034 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.17.27 PM.png,overripe,overripe,0.0,0.43881121277809143,0.5611887574195862 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4757011830806732,0.5242987871170044 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.20 PM.png,overripe,ripe,0.0,0.856823742389679,0.14317624270915985 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.429749071598053,0.570250928401947 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.4643518924713135,0.5356481075286865 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,ripe,0.0,0.7005571722984314,0.2994428277015686 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.25.12 PM.png,overripe,overripe,0.0,0.4115293323993683,0.5884706974029541 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.25.18 PM.png,overripe,ripe,0.0,0.521875262260437,0.4781247079372406 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.26.01 PM.png,overripe,ripe,0.0,0.864278256893158,0.13572175800800323 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.26.21 PM.png,overripe,overripe,0.0,0.4754360020160675,0.5245640277862549 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.22 PM.png,overripe,ripe,0.0,0.7687398791313171,0.23126013576984406 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,ripe,0.0,0.7598547339439392,0.2401452660560608 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,ripe,0.0,0.609870970249176,0.390129029750824 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.52 PM.png,overripe,overripe,0.0,0.412906676530838,0.5870933532714844 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.4208032488822937,0.5791967511177063 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.07 PM.png,overripe,unripe,0.7796928286552429,0.22030718624591827,0.14322581887245178 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.46 PM.png,overripe,overripe,0.0,0.4252093434333801,0.5747906565666199 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.50.15 PM.png,overripe,overripe,0.0,0.4632422924041748,0.5367577075958252 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.52.21 PM.png,overripe,overripe,0.0,0.41961589455604553,0.5803840756416321 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,unripe,0.9601154327392578,0.0398845411837101,0.3612441420555115 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.21 PM.png,overripe,ripe,0.0,0.6202139258384705,0.37978607416152954 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.41 PM.png,overripe,ripe,0.0,0.6717962622642517,0.3282037675380707 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.53 PM.png,overripe,ripe,0.0,0.5835232138633728,0.4164767563343048 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.56.06 PM.png,overripe,ripe,0.0,0.6676844358444214,0.332315593957901 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.56.11 PM.png,overripe,ripe,0.0,0.5821190476417542,0.41788095235824585 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,ripe,0.0,0.5984551906585693,0.4015447795391083 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.58.07 PM.png,overripe,ripe,0.0,0.9750568270683289,0.02494315803050995 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.4143444895744324,0.5856555104255676 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,ripe,0.3076438903808594,0.5710887908935547,0.4289112091064453 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,unripe,0.6049919724464417,0.39500799775123596,0.43183979392051697 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.00.17 PM.png,overripe,overripe,0.0,0.4210910499095917,0.5789089202880859 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,ripe,0.09842128306627274,0.599302351474762,0.4006976783275604 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.44275033473968506,0.5572496652603149 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.02.09 PM.png,overripe,ripe,0.0,0.7079663872718811,0.2920336425304413 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,ripe,0.0,0.5544304251670837,0.44556957483291626 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,ripe,0.4767523407936096,0.5232476592063904,0.2387540489435196 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,unripe,0.9567938446998596,0.04320615530014038,0.32441797852516174 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.05.08 PM.png,overripe,unripe,0.9484178423881531,0.05158214643597603,0.3626604676246643 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.05.21 PM.png,overripe,overripe,0.0,0.4044286012649536,0.5955713987350464 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.06.05 PM.png,overripe,ripe,0.0,0.5896925926208496,0.4103074371814728 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.06.27 PM.png,overripe,overripe,0.0,0.4673916697502136,0.5326083302497864 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,ripe,0.0,0.6155884861946106,0.3844115138053894 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.08.59 PM.png,overripe,overripe,0.0,0.465396910905838,0.5346030592918396 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.09 PM.png,overripe,overripe,0.0,0.4496890902519226,0.5503109097480774 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.22 PM.png,overripe,overripe,0.0,0.43580561876296997,0.56419438123703 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.29 PM.png,overripe,ripe,0.36213988065719604,0.5522128939628601,0.4477871060371399 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,ripe,0.0,0.6092892289161682,0.3907108008861542 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.27 PM.png,overripe,overripe,0.0,0.4904716908931732,0.5095283389091492 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.32 PM.png,overripe,ripe,0.0,0.5515487790107727,0.4484512507915497 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,ripe,0.0,0.5004364252090454,0.499563604593277 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.11.00 PM.png,overripe,ripe,0.0,0.5587450861930847,0.4412549138069153 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.11.27 PM.png,overripe,ripe,0.0,0.7147388458251953,0.2852611839771271 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.40641239285469055,0.5935876369476318 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,ripe,0.0,0.5548837184906006,0.4451162815093994 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,ripe,0.19557824730873108,0.5998688340187073,0.40013113617897034 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.44573774933815,0.5542622804641724 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.4828687012195587,0.5171313285827637 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.40196922421455383,0.5980308055877686 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,ripe,0.0,0.7641022801399231,0.2358977347612381 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,ripe,0.0,0.726243257522583,0.2737567126750946 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.46988993883132935,0.5301100611686707 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,unripe,0.7637637257575989,0.23623628914356232,0.12722226977348328 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.18.07 PM.png,overripe,unripe,0.6703135967254639,0.32968637347221375,0.48752960562705994 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.18.57 PM.png,overripe,ripe,0.0,0.5532443523406982,0.44675564765930176 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.19.08 PM.png,overripe,ripe,0.07487442344427109,0.5838508605957031,0.4161491394042969 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.19.28 PM.png,overripe,ripe,0.12017899751663208,0.6798291206359863,0.32017087936401367 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4755699038505554,0.5244300961494446 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.21.05 PM.png,overripe,overripe,0.0,0.4008321464061737,0.5991678237915039 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.435777872800827,0.5642220973968506 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.46489372849464417,0.5351063013076782 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.22.50 PM.png,overripe,ripe,0.0,0.7332583069801331,0.26674169301986694 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4030759632587433,0.5969240665435791 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.18 PM.png,overripe,ripe,0.0,0.5318275690078735,0.46817243099212646 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.41079801321029663,0.5892019867897034 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.4011690318584442,0.5988309383392334 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,ripe,0.10263717174530029,0.8973628282546997,0.08496232330799103 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.27.11 PM.png,overripe,ripe,0.0,0.7476887106895447,0.25231125950813293 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,ripe,0.0,0.6985558867454529,0.30144408345222473 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.28.09 PM.png,overripe,ripe,0.389201283454895,0.610798716545105,0.2528305649757385 +banana/test/ripe/Screen Shot 2018-06-12 at 10.00.37 PM.png,ripe,overripe,0.0,0.4007880687713623,0.5992119312286377 +banana/test/ripe/Screen Shot 2018-06-12 at 10.01.07 PM.png,ripe,overripe,0.0,0.4193786382675171,0.5806213617324829 +banana/test/ripe/Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.4005930721759796,0.5994069576263428 +banana/test/ripe/Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.41957440972328186,0.5804256200790405 +banana/test/ripe/Screen Shot 2018-06-12 at 10.05.54 PM.png,ripe,ripe,0.0,0.9891670346260071,0.010832937434315681 +banana/test/ripe/Screen Shot 2018-06-12 at 10.06.38 PM.png,ripe,overripe,0.0,0.4090784788131714,0.5909215211868286 +banana/test/ripe/Screen Shot 2018-06-12 at 10.07.21 PM.png,ripe,overripe,0.0,0.4003419280052185,0.5996580719947815 +banana/test/ripe/Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4537760615348816,0.5462239384651184 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.4114197790622711,0.5885802507400513 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.10 PM.png,ripe,overripe,0.0,0.4001697599887848,0.5998302102088928 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.15 PM.png,ripe,overripe,0.0,0.41128334403038025,0.5887166261672974 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4627842307090759,0.5372157692909241 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.427450567483902,0.5725494623184204 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.33 PM.png,ripe,overripe,0.0,0.44220709800720215,0.5577929019927979 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.47 PM.png,ripe,overripe,0.0,0.41586291790008545,0.5841370820999146 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.58 PM.png,ripe,unripe,0.5566430687904358,0.4433569312095642,0.3552466034889221 +banana/test/ripe/Screen Shot 2018-06-12 at 9.40.26 PM.png,ripe,overripe,0.0,0.41507408022880554,0.5849258899688721 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.40182408690452576,0.5981759428977966 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.30 PM.png,ripe,overripe,0.0,0.40435588359832764,0.5956441164016724 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.40047335624694824,0.5995266437530518 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4003332555294037,0.5996667742729187 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.53 PM.png,ripe,overripe,0.0,0.40897101163864136,0.5910289883613586 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.59 PM.png,ripe,overripe,0.0,0.40793779492378235,0.5920621752738953 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.02 PM.png,ripe,overripe,0.0,0.40057823061943054,0.5994217395782471 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.15 PM.png,ripe,ripe,0.0,0.6766049265861511,0.3233950734138489 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.22 PM.png,ripe,overripe,0.0,0.4060789942741394,0.5939210057258606 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.34 PM.png,ripe,overripe,0.0,0.40001046657562256,0.5999895334243774 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.40260401368141174,0.5973960161209106 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.40208160877227783,0.5979183912277222 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.55 PM.png,ripe,overripe,0.0,0.4698328375816345,0.5301671624183655 +banana/test/ripe/Screen Shot 2018-06-12 at 9.49.00 PM.png,ripe,overripe,0.0,0.40289607644081116,0.5971038937568665 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.4110104739665985,0.5889894962310791 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.44 PM.png,ripe,overripe,0.0,0.4094780385494232,0.5905219912528992 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.4281330704689026,0.5718669295310974 +banana/test/ripe/Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.4034457504749298,0.5965542793273926 +banana/test/ripe/Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.41936612129211426,0.5806338787078857 +banana/test/ripe/Screen Shot 2018-06-12 at 9.54.35 PM.png,ripe,overripe,0.0,0.40099674463272095,0.599003255367279 +banana/test/ripe/Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4093055725097656,0.5906944274902344 +banana/test/ripe/Screen Shot 2018-06-12 at 9.55.46 PM.png,ripe,overripe,0.0,0.4252602458000183,0.5747397541999817 +banana/test/ripe/Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4047488570213318,0.5952511429786682 +banana/test/ripe/Screen Shot 2018-06-12 at 9.56.03 PM.png,ripe,overripe,0.0,0.46486005187034607,0.5351399779319763 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.17 PM.png,ripe,overripe,0.0,0.40740665793418884,0.5925933718681335 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4194507598876953,0.5805492401123047 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.31 PM.png,ripe,overripe,0.0,0.4000087380409241,0.5999912619590759 +banana/test/ripe/Screen Shot 2018-06-12 at 9.58.36 PM.png,ripe,overripe,0.0,0.4399474561214447,0.5600525736808777 +banana/test/ripe/Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.410834938287735,0.5891650915145874 +banana/test/ripe/Screen Shot 2018-06-12 at 9.59.48 PM.png,ripe,overripe,0.0,0.4046463668346405,0.5953536629676819 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.07 PM.png,ripe,overripe,0.0,0.40390744805336,0.5960925221443176 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.12 PM.png,ripe,overripe,0.26543116569519043,0.4705834984779358,0.5294165015220642 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.4024111032485962,0.5975888967514038 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.42352014780044556,0.5764798521995544 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.13 PM.png,ripe,overripe,0.0,0.4008117616176605,0.5991882681846619 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.42146095633506775,0.5785390734672546 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,unripe,0.5437561869621277,0.4562438130378723,0.3155919015407562 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.4025254547595978,0.5974745154380798 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4176027476787567,0.5823972821235657 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.40551331639289856,0.594486653804779 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.38.29 PM.png,ripe,overripe,0.0,0.4243466556072235,0.5756533145904541 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.39.53 PM.png,ripe,overripe,0.0,0.40158501267433167,0.598414957523346 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.40.43 PM.png,ripe,overripe,0.0,0.4023998975753784,0.5976001024246216 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.4018467366695404,0.598153293132782 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.42.29 PM.png,ripe,overripe,0.0,0.43577131628990173,0.5642287135124207 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.43.27 PM.png,ripe,overripe,0.0,0.41725894808769226,0.5827410817146301 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.40207862854003906,0.5979213714599609 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.4161137640476227,0.5838862061500549 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.40523719787597656,0.5947628021240234 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.4003817141056061,0.5996182560920715 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.48 PM.png,ripe,overripe,0.0,0.41905274987220764,0.5809472799301147 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.49.15 PM.png,ripe,overripe,0.0,0.41059574484825134,0.5894042253494263 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.49.37 PM.png,ripe,overripe,0.0,0.4071851670742035,0.5928148031234741 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.4271238148212433,0.5728761553764343 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.50.53 PM.png,ripe,ripe,0.0,0.5314193964004517,0.46858057379722595 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.55.19 PM.png,ripe,overripe,0.0,0.41569429636001587,0.5843057036399841 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4003489315509796,0.5996510982513428 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.41411662101745605,0.585883378982544 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.07 PM.png,ripe,overripe,0.0,0.4141521453857422,0.5858478546142578 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4191780686378479,0.5808219313621521 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.414993554353714,0.5850064754486084 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.02 PM.png,ripe,overripe,0.0,0.4001361131668091,0.5998638868331909 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.4025035500526428,0.5974964499473572 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.12 PM.png,ripe,overripe,0.0,0.40847378969192505,0.591526210308075 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.17 PM.png,ripe,overripe,0.0,0.4009867310523987,0.5990132689476013 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,unripe,0.9269781112670898,0.07302191853523254,0.20580324530601501 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.00.00 PM.png,ripe,overripe,0.0,0.4041305184364319,0.5958694815635681 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.4048006236553192,0.5951993465423584 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.07 PM.png,ripe,overripe,0.0,0.4112211763858795,0.5887788534164429 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4147005081176758,0.5852994918823242 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.52 PM.png,ripe,overripe,0.0,0.40533167123794556,0.5946683287620544 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.41239145398139954,0.5876085162162781 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.40006208419799805,0.599937915802002 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.36 PM.png,ripe,overripe,0.0,0.40050387382507324,0.5994961261749268 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.42393434047698975,0.5760656595230103 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.05.20 PM.png,ripe,overripe,0.0,0.4563998281955719,0.5436002016067505 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.24 PM.png,ripe,overripe,0.0,0.42072027921676636,0.5792797207832336 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.33 PM.png,ripe,unripe,0.9205976128578186,0.0794023796916008,0.15894976258277893 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.59 PM.png,ripe,overripe,0.0,0.40596312284469604,0.594036877155304 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.42324399948120117,0.5767560005187988 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.41.57 PM.png,ripe,overripe,0.0,0.4495638906955719,0.5504360795021057 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4001515507698059,0.5998484492301941 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.40050721168518066,0.5994927883148193 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.40052375197410583,0.5994762778282166 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.48 PM.png,ripe,overripe,0.0,0.41308581829071045,0.5869141817092896 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.27 PM.png,ripe,overripe,0.0,0.41491541266441345,0.5850846171379089 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4025897979736328,0.5974102020263672 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.40736591815948486,0.5926340818405151 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.49.23 PM.png,ripe,overripe,0.0,0.4056994915008545,0.5943005084991455 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.49.45 PM.png,ripe,overripe,0.0,0.4031417965888977,0.5968582034111023 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.50.12 PM.png,ripe,overripe,0.0,0.4284534752368927,0.5715465545654297 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.50.44 PM.png,ripe,overripe,0.0,0.40831124782562256,0.5916887521743774 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.41503265500068665,0.584967315196991 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.37 PM.png,ripe,overripe,0.0,0.47506579756736755,0.5249342322349548 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.45 PM.png,ripe,unripe,0.6214478611946106,0.3785521686077118,0.33141323924064636 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.54.02 PM.png,ripe,overripe,0.0,0.40042009949684143,0.599579930305481 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4045157730579376,0.5954842567443848 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.55.19 PM.png,ripe,overripe,0.0,0.41687649488449097,0.583123505115509 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.56.03 PM.png,ripe,overripe,0.0,0.4497866928577423,0.5502132773399353 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4005286693572998,0.5994713306427002 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.57.08 PM.png,ripe,overripe,0.0,0.41743993759155273,0.5825600624084473 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.59.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,unripe,0.7384001612663269,0.2615998387336731,0.3058492839336395 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.44621244072914124,0.5537875890731812 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.00.49 PM.png,ripe,overripe,0.0,0.40384724736213684,0.5961527228355408 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.01.27 PM.png,ripe,overripe,0.0,0.4146825075149536,0.5853174924850464 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.4059502184391022,0.5940497517585754 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.42409372329711914,0.5759062767028809 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.13 PM.png,ripe,overripe,0.0,0.40090423822402954,0.5990957617759705 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.35 PM.png,ripe,overripe,0.0,0.40064558386802673,0.5993543863296509 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.54 PM.png,ripe,ripe,0.0,0.9880609512329102,0.011939048767089844 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.06.59 PM.png,ripe,overripe,0.0,0.4063032865524292,0.5936967134475708 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.42066577076911926,0.5793341994285583 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.38.29 PM.png,ripe,overripe,0.0,0.40770208835601807,0.5922979116439819 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.00 PM.png,ripe,overripe,0.0,0.4006887972354889,0.5993111729621887 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.46007972955703735,0.5399202704429626 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4174063801765442,0.5825936198234558 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.22 PM.png,ripe,overripe,0.0,0.40022435784339905,0.5997756123542786 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.26 PM.png,ripe,overripe,0.0,0.4080269932746887,0.5919730067253113 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.38 PM.png,ripe,overripe,0.0,0.41064733266830444,0.5893526673316956 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.49 PM.png,ripe,overripe,0.0,0.4008491039276123,0.5991508960723877 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.41.18 PM.png,ripe,overripe,0.0,0.4048261046409607,0.5951738953590393 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.41.38 PM.png,ripe,ripe,0.4426426887512207,0.5573573112487793,0.06448783725500107 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.42.29 PM.png,ripe,overripe,0.0,0.4400847256183624,0.55991530418396 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.415728896856308,0.5842710733413696 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.44.49 PM.png,ripe,overripe,0.0,0.40623176097869873,0.5937682390213013 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.40071240067481995,0.5992876291275024 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.45.28 PM.png,ripe,overripe,0.0,0.4020940065383911,0.5979059934616089 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,unripe,0.9305604100227356,0.06943961977958679,0.22025297582149506 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.47.27 PM.png,ripe,overripe,0.0,0.41550230979919434,0.5844976902008057 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.48.04 PM.png,ripe,ripe,0.0,0.6243352293968201,0.37566477060317993 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.48.39 PM.png,ripe,overripe,0.0,0.42714929580688477,0.5728507041931152 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.49.23 PM.png,ripe,overripe,0.0,0.40532737970352173,0.5946726202964783 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.50.53 PM.png,ripe,overripe,0.0,0.4003124237060547,0.5996875762939453 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.51.44 PM.png,ripe,unripe,0.7765782475471497,0.22342178225517273,0.23243741691112518 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.51.58 PM.png,ripe,overripe,0.0,0.40052443742752075,0.5994755625724792 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.26 PM.png,ripe,overripe,0.0,0.4001431167125702,0.5998568534851074 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.41072529554367065,0.5892747044563293 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.50 PM.png,ripe,overripe,0.0,0.40097543597221375,0.5990245938301086 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.403841495513916,0.596158504486084 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.53.22 PM.png,ripe,overripe,0.0,0.42305928468704224,0.5769407153129578 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.07 PM.png,ripe,overripe,0.0,0.404362291097641,0.5956376791000366 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.18 PM.png,ripe,ripe,0.30633553862571716,0.6936644315719604,0.013877339661121368 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4045843482017517,0.5954156517982483 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.55.13 PM.png,ripe,overripe,0.0,0.40077102184295654,0.5992289781570435 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4023093581199646,0.5976906418800354 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.56.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4005708694458008,0.5994291305541992 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.57.38 PM.png,ripe,overripe,0.0,0.40197622776031494,0.5980237722396851 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.57.42 PM.png,ripe,overripe,0.0,0.40021201968193054,0.5997879505157471 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.58.49 PM.png,ripe,overripe,0.0,0.4342198073863983,0.5657801628112793 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,unripe,0.7340751886367798,0.2659248113632202,0.3071208596229553 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.00.07 PM.png,ripe,overripe,0.0,0.40327388048171997,0.59672611951828 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.4006426930427551,0.5993573069572449 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.41137227416038513,0.5886276960372925 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4005414843559265,0.5994585156440735 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,unripe,0.5357707142829895,0.4642293155193329,0.3427898585796356 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.07.21 PM.png,ripe,overripe,0.0,0.40015217661857605,0.5998478531837463 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4194410741329193,0.5805588960647583 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.42040956020355225,0.5795904397964478 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.40056130290031433,0.5994387269020081 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4661208987236023,0.5338791012763977 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4551837742328644,0.544816255569458 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.41.57 PM.png,ripe,overripe,0.0,0.4483410716056824,0.5516589283943176 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4000105559825897,0.5999894738197327 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.41670507192611694,0.5832949280738831 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,unripe,0.9257851839065552,0.07421480119228363,0.219720259308815 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4427525997161865,0.5572474002838135 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.40 PM.png,ripe,overripe,0.0,0.40862080454826355,0.5913791656494141 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.55 PM.png,ripe,ripe,0.44245538115501404,0.5575445890426636,0.06122085824608803 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.402848482131958,0.597151517868042 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.46 PM.png,ripe,overripe,0.0,0.40063321590423584,0.5993667840957642 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.48.04 PM.png,ripe,ripe,0.0,0.6180996894836426,0.3819003403186798 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.48.54 PM.png,ripe,overripe,0.0,0.41036540269851685,0.5896345973014832 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.4372384548187256,0.5627615451812744 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.51.36 PM.png,ripe,overripe,0.0,0.4194974899291992,0.5805025100708008 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.41079530119895935,0.5892046689987183 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.30 PM.png,ripe,overripe,0.0,0.4178354740142822,0.5821645259857178 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.41 PM.png,ripe,overripe,0.0,0.40721720457077026,0.5927827954292297 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.46 PM.png,ripe,overripe,0.0,0.4045797884464264,0.595420241355896 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4043155014514923,0.5956845283508301 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.55.27 PM.png,ripe,ripe,0.12415898591279984,0.5343034267425537,0.4656966030597687 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.55.33 PM.png,ripe,overripe,0.0,0.40037980675697327,0.5996202230453491 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.56.23 PM.png,ripe,overripe,0.0,0.4135597050189972,0.5864402651786804 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.56.56 PM.png,ripe,overripe,0.0,0.4083127975463867,0.5916872024536133 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.57.42 PM.png,ripe,overripe,0.0,0.40003088116645813,0.5999691486358643 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.41579756140708923,0.5842024087905884 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,unripe,0.9599857330322266,0.04001424461603165,0.21652738749980927 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.21 PM.png,ripe,overripe,0.0,0.4006800949573517,0.5993199348449707 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.37 PM.png,ripe,overripe,0.0,0.40544962882995605,0.594550371170044 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.45223766565322876,0.5477623343467712 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.01.52 PM.png,ripe,overripe,0.0,0.40577489137649536,0.5942251086235046 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.01.58 PM.png,ripe,overripe,0.0,0.40764129161834717,0.5923587083816528 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.02.01 PM.png,ripe,overripe,0.0,0.4872952401638031,0.5127047300338745 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.02.36 PM.png,ripe,overripe,0.0,0.40056294202804565,0.5994370579719543 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4004143178462982,0.5995857119560242 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4093019366264343,0.5906980633735657 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.06.07 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4180161952972412,0.5819838047027588 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.38.22 PM.png,ripe,overripe,0.0,0.41693100333213806,0.5830689668655396 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4030255675315857,0.5969744324684143 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.39.58 PM.png,ripe,unripe,0.5704057216644287,0.4295942485332489,0.35181981325149536 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.02 PM.png,ripe,overripe,0.0,0.40161988139152527,0.5983801484107971 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.22 PM.png,ripe,overripe,0.0,0.40013647079467773,0.5998635292053223 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.32 PM.png,ripe,overripe,0.0,0.40023988485336304,0.599760115146637 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.40116268396377563,0.5988373160362244 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.38 PM.png,ripe,ripe,0.48332223296165466,0.516677737236023,0.07653366029262543 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.40658581256866455,0.5934141874313354 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.418486088514328,0.5815138816833496 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.32 PM.png,ripe,overripe,0.0,0.40592309832572937,0.594076931476593 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.59 PM.png,ripe,overripe,0.0,0.4006735384464264,0.5993264317512512 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4008752405643463,0.5991247296333313 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.416350394487381,0.5836495757102966 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.48.14 PM.png,ripe,overripe,0.0,0.4005959928035736,0.5994040369987488 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.49.15 PM.png,ripe,overripe,0.0,0.41090235114097595,0.5890976786613464 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.49.45 PM.png,ripe,overripe,0.0,0.40322113037109375,0.5967788696289062 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.51.00 PM.png,ripe,overripe,0.0,0.4641489088535309,0.5358511209487915 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.52.06 PM.png,ripe,ripe,0.20453080534934998,0.7365314960479736,0.26346850395202637 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.40370848774909973,0.5962914824485779 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.22 PM.png,ripe,overripe,0.0,0.4201815724372864,0.5798184275627136 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.41 PM.png,ripe,overripe,0.0,0.4072461724281311,0.5927538275718689 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.54.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.42 PM.png,ripe,overripe,0.0,0.40088993310928345,0.5991100668907166 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.46 PM.png,ripe,overripe,0.0,0.4197326898574829,0.5802673101425171 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.40050992369651794,0.5994900465011597 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.40252330899238586,0.5974766612052917 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.58.07 PM.png,ripe,overripe,0.0,0.41369524598121643,0.5863047242164612 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.41885003447532654,0.5811499357223511 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.59.02 PM.png,ripe,overripe,0.0,0.40041372179985046,0.5995862483978271 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.00 PM.png,ripe,overripe,0.0,0.407134473323822,0.592865526676178 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.44882702827453613,0.5511729717254639 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.49 PM.png,ripe,overripe,0.0,0.40540406107902527,0.5945959687232971 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4423450231552124,0.5576549768447876 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.02.01 PM.png,ripe,overripe,0.0,0.48280438780784607,0.5171955823898315 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4018447697162628,0.5981552004814148 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.4059360921382904,0.5940638780593872 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4252917170524597,0.5747082829475403 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.4045071601867676,0.5954928398132324 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.24 PM.png,ripe,overripe,0.0,0.4204998016357422,0.5795001983642578 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.38 PM.png,ripe,overripe,0.0,0.4113009572029114,0.5886990427970886 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.07.29 PM.png,ripe,overripe,0.0,0.42436814308166504,0.575631856918335 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4555136263370514,0.5444863438606262 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.4275374710559845,0.5724625587463379 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.38.38 PM.png,ripe,overripe,0.0,0.4575827717781067,0.5424172282218933 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.00 PM.png,ripe,overripe,0.0,0.4026916027069092,0.5973083972930908 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.33 PM.png,ripe,overripe,0.0,0.4439225196838379,0.5560774803161621 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.47 PM.png,ripe,overripe,0.0,0.4176863133907318,0.5823137164115906 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.40.43 PM.png,ripe,overripe,0.0,0.40331870317459106,0.5966812968254089 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.40.49 PM.png,ripe,overripe,0.0,0.40270891785621643,0.597291111946106 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4843701720237732,0.5156298279762268 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.4037852883338928,0.5962147116661072 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.42.03 PM.png,ripe,unripe,0.7292352318763733,0.2707647979259491,0.24920423328876495 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.4166894853115082,0.5833104848861694 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.40224605798721313,0.5977539420127869 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.43.53 PM.png,ripe,overripe,0.0,0.4107225835323334,0.5892773866653442 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.44.06 PM.png,ripe,overripe,0.0,0.4124692976474762,0.5875306725502014 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.45.02 PM.png,ripe,overripe,0.0,0.40254995226860046,0.5974500775337219 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.40820205211639404,0.591797947883606 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4489513635635376,0.5510486364364624 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.40 PM.png,ripe,overripe,0.0,0.41013452410697937,0.589865505695343 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.402524471282959,0.597475528717041 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4021369218826294,0.5978630781173706 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.48.54 PM.png,ripe,overripe,0.0,0.4122954308986664,0.5877045392990112 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.50.38 PM.png,ripe,overripe,0.0,0.4051496088504791,0.5948503613471985 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.42978110909461975,0.5702189207077026 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.42129603028297424,0.5787039399147034 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.40209051966667175,0.5979095101356506 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.00.12 PM.png,ripe,overripe,0.15295647084712982,0.4581502377986908,0.5418497920036316 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4390937089920044,0.5609062910079956 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.4005271792411804,0.5994728207588196 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.41334807872772217,0.5866519212722778 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4002232253551483,0.5997768044471741 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.04.54 PM.png,ripe,overripe,0.0,0.40059494972229004,0.59940505027771 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.05.20 PM.png,ripe,overripe,0.0,0.45258376002311707,0.5474162697792053 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4202110171318054,0.5797889828681946 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.38.51 PM.png,ripe,overripe,0.0,0.4000156819820404,0.599984347820282 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.40.38 PM.png,ripe,overripe,0.0,0.4100465476512909,0.5899534225463867 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.41.30 PM.png,ripe,overripe,0.0,0.40407970547676086,0.5959203243255615 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.4015922248363495,0.5984078049659729 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.42.18 PM.png,ripe,ripe,0.0,0.5197359323501587,0.4802640974521637 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.43.27 PM.png,ripe,overripe,0.0,0.4180302321910858,0.5819697380065918 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.44.19 PM.png,ripe,overripe,0.0,0.42040395736694336,0.5795960426330566 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.40053072571754456,0.5994693040847778 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.45.34 PM.png,ripe,overripe,0.0,0.4003058671951294,0.5996941328048706 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.40039440989494324,0.5996055603027344 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4026159644126892,0.5973840355873108 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.48.14 PM.png,ripe,overripe,0.0,0.4006088674068451,0.5993911027908325 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.51.58 PM.png,ripe,overripe,0.0,0.40046441555023193,0.5995355844497681 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.4161301553249359,0.5838698744773865 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.53.30 PM.png,ripe,overripe,0.0,0.4196575880050659,0.5803424119949341 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.53.46 PM.png,ripe,overripe,0.0,0.40495771169662476,0.5950422883033752 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4074162244796753,0.5925837755203247 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.40321463346481323,0.5967853665351868 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.55.27 PM.png,ripe,overripe,0.0,0.46910932660102844,0.5308906435966492 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.55.42 PM.png,ripe,overripe,0.0,0.4007209837436676,0.5992790460586548 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.56.16 PM.png,ripe,overripe,0.0,0.4074477553367615,0.5925522446632385 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4006917178630829,0.5993082523345947 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.41423696279525757,0.5857630372047424 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4122275114059448,0.5877724885940552 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.21 PM.png,ripe,overripe,0.0,0.4035252630710602,0.5964747071266174 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.40220341086387634,0.5977965593338013 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.44702309370040894,0.5529769062995911 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.01.27 PM.png,ripe,overripe,0.0,0.4116845428943634,0.588315486907959 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.04.54 PM.png,ripe,overripe,0.0,0.40060341358184814,0.5993965864181519 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.40403395891189575,0.5959660410881042 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,unripe,0.5945558547973633,0.4054441452026367,0.2886337339878082 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.4023350775241852,0.5976648926734924 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4537760615348816,0.5462239384651184 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.15 PM.png,ripe,overripe,0.0,0.411295622587204,0.5887043476104736 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.22 PM.png,ripe,overripe,0.0,0.4166676700115204,0.5833323001861572 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.51 PM.png,ripe,overripe,0.0,0.4003036618232727,0.5996963381767273 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4629598557949066,0.5370401740074158 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.40.02 PM.png,ripe,overripe,0.0,0.4022187888622284,0.5977811813354492 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.40.10 PM.png,ripe,overripe,0.0,0.411202609539032,0.588797390460968 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.481577605009079,0.5184223651885986 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.4144980311393738,0.5855019688606262 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.41 PM.png,ripe,overripe,0.0,0.40586814284324646,0.5941318869590759 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.49 PM.png,ripe,overripe,0.0,0.40239351987838745,0.5976064801216125 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4003485441207886,0.5996514558792114 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.02 PM.png,ripe,overripe,0.0,0.4600534439086914,0.5399465560913086 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,unripe,0.7590015530586243,0.24099847674369812,0.17440639436244965 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.4062114655971527,0.5937885046005249 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4498637616634369,0.5501362681388855 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.400638222694397,0.599361777305603 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.18 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.40260374546051025,0.5973962545394897 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.402023583650589,0.5979763865470886 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.48.21 PM.png,ripe,overripe,0.0,0.4163835644721985,0.5836164355278015 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.48.26 PM.png,ripe,overripe,0.0,0.43620607256889343,0.5637938976287842 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.49.00 PM.png,ripe,overripe,0.0,0.4029470980167389,0.5970528721809387 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.49.54 PM.png,ripe,overripe,0.0,0.41637665033340454,0.5836233496665955 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.4113241136074066,0.5886759161949158 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.51.24 PM.png,ripe,overripe,0.0,0.4092138707637787,0.5907861590385437 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.51.29 PM.png,ripe,overripe,0.0,0.4105728268623352,0.5894271731376648 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.41530755162239075,0.5846924781799316 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4107085168361664,0.5892914533615112 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.45 PM.png,ripe,unripe,0.6882190704345703,0.3117808997631073,0.3052977919578552 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.41935211420059204,0.580647885799408 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.40251049399375916,0.5974894762039185 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.55.13 PM.png,ripe,overripe,0.0,0.40076541900634766,0.5992345809936523 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4047336280345917,0.5952663421630859 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.16 PM.png,ripe,overripe,0.0,0.4022133946418762,0.5977866053581238 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.23 PM.png,ripe,overripe,0.0,0.41336682438850403,0.5866332054138184 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.48 PM.png,ripe,overripe,0.0,0.4003463089466095,0.5996537208557129 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.56 PM.png,ripe,overripe,0.0,0.40635472536087036,0.5936452746391296 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4191682040691376,0.58083176612854 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.31 PM.png,ripe,overripe,0.0,0.4000087380409241,0.5999912619590759 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.38 PM.png,ripe,overripe,0.0,0.40136852860450745,0.5986314415931702 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4164174795150757,0.5835825204849243 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.4105767011642456,0.5894232988357544 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.40162232518196106,0.5983776450157166 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,unripe,0.8399990200996399,0.1600009649991989,0.17886854708194733 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,unripe,0.8650002479553223,0.13499975204467773,0.2597161829471588 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.48 PM.png,ripe,overripe,0.0,0.4046390652656555,0.5953609347343445 +banana/test/unripe/1.jpg,unripe,ripe,0.0,0.6358645558357239,0.3641354441642761 +banana/test/unripe/10.jpg,unripe,ripe,0.0,0.5583236217498779,0.44167637825012207 +banana/test/unripe/100.jpg,unripe,ripe,0.2540648579597473,0.7459351420402527,0.0 +banana/test/unripe/101.jpg,unripe,ripe,0.410550981760025,0.5894490480422974,0.0 +banana/test/unripe/102.jpg,unripe,ripe,0.3723626136779785,0.6276373863220215,0.0 +banana/test/unripe/103.jpg,unripe,ripe,0.19117584824562073,0.8088241815567017,0.0 +banana/test/unripe/104.jpg,unripe,ripe,0.27399715781211853,0.7260028123855591,0.0 +banana/test/unripe/105.jpg,unripe,unripe,0.803205132484436,0.19679486751556396,0.10160256177186966 +banana/test/unripe/106.jpg,unripe,ripe,0.11356575042009354,0.8864342570304871,0.027460509911179543 +banana/test/unripe/107.jpg,unripe,ripe,0.39128240942955017,0.6087175607681274,0.0 +banana/test/unripe/108.jpg,unripe,ripe,0.0,0.5526010990142822,0.4473989009857178 +banana/test/unripe/109.jpg,unripe,ripe,0.07202459871768951,0.5009623765945435,0.49903762340545654 +banana/test/unripe/11.jpg,unripe,ripe,0.30912819504737854,0.6908717751502991,0.0 +banana/test/unripe/110.jpg,unripe,unripe,0.8314419984817505,0.1685580164194107,0.2835462987422943 +banana/test/unripe/111.jpg,unripe,unripe,0.5191391110420227,0.4808609187602997,0.010446788743138313 +banana/test/unripe/112.jpg,unripe,ripe,0.032210007309913635,0.8683537840843201,0.13164621591567993 +banana/test/unripe/113.jpg,unripe,ripe,0.19440896809101105,0.8055910468101501,0.08733713626861572 +banana/test/unripe/114.jpg,unripe,ripe,0.08211275935173035,0.7050282955169678,0.2949717044830322 +banana/test/unripe/115.jpg,unripe,ripe,0.22703589498996735,0.7729641199111938,0.0 +banana/test/unripe/116.jpg,unripe,ripe,0.07179973274469376,0.9084370732307434,0.09156293421983719 +banana/test/unripe/117.jpg,unripe,ripe,0.3408099114894867,0.6591901183128357,0.09865052253007889 +banana/test/unripe/118.jpg,unripe,unripe,0.8654598593711853,0.1345401406288147,0.014238168485462666 +banana/test/unripe/119.jpg,unripe,ripe,0.13326840102672577,0.866731584072113,0.04864395037293434 +banana/test/unripe/12.jpg,unripe,ripe,0.0,0.6271478533744812,0.3728521466255188 +banana/test/unripe/120.jpg,unripe,ripe,0.0,0.6339591145515442,0.3660408854484558 +banana/test/unripe/121.jpg,unripe,ripe,0.2346867471933365,0.7653132677078247,0.07688801735639572 +banana/test/unripe/122.jpg,unripe,unripe,0.5575279593467712,0.44247207045555115,0.11304662376642227 +banana/test/unripe/123.jpg,unripe,ripe,0.08211275935173035,0.7050282955169678,0.2949717044830322 +banana/test/unripe/124.jpg,unripe,ripe,0.30618253350257874,0.6938174366950989,0.11032669991254807 +banana/test/unripe/125.jpg,unripe,ripe,0.06762923300266266,0.9323707818984985,0.0 +banana/test/unripe/126.jpg,unripe,ripe,0.0,0.5275937914848328,0.47240620851516724 +banana/test/unripe/127.jpg,unripe,ripe,0.3753022849559784,0.6246976852416992,0.0 +banana/test/unripe/128.jpg,unripe,unripe,0.9692683219909668,0.030731678009033203,0.0 +banana/test/unripe/129.jpg,unripe,ripe,0.3208006024360657,0.6791993975639343,0.28766846656799316 +banana/test/unripe/13.jpg,unripe,ripe,0.36000001430511475,0.6399999856948853,0.0 +banana/test/unripe/130.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/131.jpg,unripe,ripe,0.19117584824562073,0.8088241815567017,0.0 +banana/test/unripe/132.jpg,unripe,ripe,0.35914167761802673,0.6408583521842957,0.0 +banana/test/unripe/133.jpg,unripe,overripe,0.0,0.4181878864765167,0.5818120837211609 +banana/test/unripe/134.jpg,unripe,ripe,0.21790535748004913,0.7820946574211121,0.0 +banana/test/unripe/135.jpg,unripe,ripe,0.0,0.6754948496818542,0.32450515031814575 +banana/test/unripe/136.jpg,unripe,ripe,0.39174410700798035,0.6082558631896973,0.0 +banana/test/unripe/137.jpg,unripe,unripe,0.5797886848449707,0.4202113151550293,0.06462782621383667 +banana/test/unripe/138.jpg,unripe,unripe,0.803205132484436,0.19679486751556396,0.10160256177186966 +banana/test/unripe/139.jpg,unripe,overripe,0.0,0.4192410707473755,0.5807589292526245 +banana/test/unripe/14.jpg,unripe,ripe,0.0,0.6795845627784729,0.3204154670238495 +banana/test/unripe/140.jpg,unripe,ripe,0.38181546330451965,0.6181845664978027,0.0 +banana/test/unripe/141.jpg,unripe,unripe,0.7117740511894226,0.288225919008255,0.03874172270298004 +banana/test/unripe/142.jpg,unripe,overripe,0.0,0.4192410707473755,0.5807589292526245 +banana/test/unripe/143.jpg,unripe,unripe,0.6265468001365662,0.3734532296657562,0.2572615146636963 +banana/test/unripe/144.jpg,unripe,ripe,0.27623945474624634,0.7237605452537537,0.0 +banana/test/unripe/145.jpg,unripe,overripe,0.0,0.41163232922554016,0.5883677005767822 +banana/test/unripe/146.jpg,unripe,ripe,0.38181546330451965,0.6181845664978027,0.0 +banana/test/unripe/147.jpg,unripe,unripe,0.6875622868537903,0.3124377131462097,0.0 +banana/test/unripe/148.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/149.jpg,unripe,unripe,0.7706589698791504,0.2293410450220108,0.0 +banana/test/unripe/15.jpg,unripe,unripe,0.6750101447105408,0.32498985528945923,0.0 +banana/test/unripe/150.jpg,unripe,unripe,0.7688244581222534,0.23117555677890778,0.38987085223197937 +banana/test/unripe/151.jpg,unripe,overripe,0.0,0.40023767948150635,0.5997623205184937 +banana/test/unripe/152.jpg,unripe,ripe,0.0,0.6382961869239807,0.3617038428783417 +banana/test/unripe/153.jpg,unripe,ripe,0.13404317200183868,0.8659568428993225,0.07151854783296585 +banana/test/unripe/154.jpg,unripe,ripe,0.0,0.6334759593009949,0.36652401089668274 +banana/test/unripe/155.jpg,unripe,unripe,0.5507725477218628,0.4492274522781372,0.3006802797317505 +banana/test/unripe/156.jpg,unripe,ripe,0.011689024046063423,0.7010434865951538,0.2989565134048462 +banana/test/unripe/157.jpg,unripe,ripe,0.0,0.5606083273887634,0.4393916726112366 +banana/test/unripe/158.jpg,unripe,ripe,0.05202769115567207,0.7666611075401306,0.2333388775587082 +banana/test/unripe/159.jpg,unripe,ripe,0.16664397716522217,0.8333560228347778,0.0 +banana/test/unripe/16.jpg,unripe,ripe,0.23027831315994263,0.7697216868400574,0.0 +banana/test/unripe/160.jpg,unripe,ripe,0.06414730846881866,0.9358527064323425,0.0 +banana/test/unripe/161.jpg,unripe,unripe,0.7706589698791504,0.2293410450220108,0.0 +banana/test/unripe/162.jpg,unripe,unripe,0.7688244581222534,0.23117555677890778,0.38987085223197937 +banana/test/unripe/163.jpg,unripe,ripe,0.0,0.6382961869239807,0.3617038428783417 +banana/test/unripe/164.jpg,unripe,ripe,0.13404317200183868,0.8659568428993225,0.07151854783296585 +banana/test/unripe/165.jpg,unripe,unripe,0.5507725477218628,0.4492274522781372,0.3006802797317505 +banana/test/unripe/166.jpg,unripe,ripe,0.011689024046063423,0.7010434865951538,0.2989565134048462 +banana/test/unripe/167.jpg,unripe,ripe,0.27474966645240784,0.7252503037452698,0.0 +banana/test/unripe/168.jpg,unripe,ripe,0.0,0.5606083273887634,0.4393916726112366 +banana/test/unripe/169.jpg,unripe,ripe,0.05202769115567207,0.7666611075401306,0.2333388775587082 +banana/test/unripe/17.jpg,unripe,ripe,0.1819055676460266,0.8180944323539734,0.0 +banana/test/unripe/170.jpg,unripe,ripe,0.16664397716522217,0.8333560228347778,0.0 +banana/test/unripe/171.jpg,unripe,ripe,0.24912264943122864,0.750877320766449,0.0 +banana/test/unripe/172.jpg,unripe,ripe,0.36429086327552795,0.6357091069221497,0.0 +banana/test/unripe/173.jpg,unripe,unripe,0.9514727592468262,0.04852722957730293,0.03016018681228161 +banana/test/unripe/174.jpg,unripe,ripe,0.2510722875595093,0.7489277124404907,0.0 +banana/test/unripe/175.jpg,unripe,unripe,0.9936967492103577,0.0063032410107553005,0.02944241277873516 +banana/test/unripe/176.jpg,unripe,ripe,0.23027144372463226,0.7581603527069092,0.24183964729309082 +banana/test/unripe/177.jpg,unripe,ripe,0.2664501667022705,0.7335498332977295,0.0 +banana/test/unripe/178.jpg,unripe,ripe,0.3595768213272095,0.6404231786727905,0.04823543131351471 +banana/test/unripe/179.jpg,unripe,ripe,0.46124956011772156,0.5387504696846008,0.0 +banana/test/unripe/18.jpg,unripe,ripe,0.12003646790981293,0.8799635171890259,0.0 +banana/test/unripe/180.jpg,unripe,ripe,0.03885054588317871,0.6073590517044067,0.3926409184932709 +banana/test/unripe/181.jpg,unripe,ripe,0.28180932998657227,0.7181906700134277,0.0 +banana/test/unripe/182.jpg,unripe,unripe,0.916376531124115,0.0836234912276268,0.20797055959701538 +banana/test/unripe/183.jpg,unripe,ripe,0.3664979636669159,0.6335020065307617,0.0 +banana/test/unripe/184.jpg,unripe,unripe,0.9265190958976746,0.07348092645406723,0.03920019790530205 +banana/test/unripe/185.jpg,unripe,ripe,0.1069841980934143,0.8930158019065857,0.0 +banana/test/unripe/186.jpg,unripe,ripe,0.1092090979218483,0.7564257979393005,0.24357418715953827 +banana/test/unripe/187.jpg,unripe,ripe,0.1784210354089737,0.8215789794921875,0.0 +banana/test/unripe/188.jpg,unripe,ripe,0.2336679846048355,0.7663320302963257,0.0 +banana/test/unripe/189.jpg,unripe,unripe,0.5191138386726379,0.48088616132736206,0.08432000130414963 +banana/test/unripe/19.jpg,unripe,overripe,0.0,0.4123533070087433,0.5876467227935791 +banana/test/unripe/190.jpg,unripe,unripe,0.9491356611251831,0.0508643202483654,0.07207336276769638 +banana/test/unripe/191.jpg,unripe,ripe,0.0,0.6621729135513306,0.33782705664634705 +banana/test/unripe/192.jpg,unripe,unripe,0.8408434391021729,0.15915657579898834,0.01759880594909191 +banana/test/unripe/193.jpg,unripe,ripe,0.15346628427505493,0.8465337157249451,0.0 +banana/test/unripe/194.jpg,unripe,ripe,0.0,0.5014575719833374,0.4985424280166626 +banana/test/unripe/195.jpg,unripe,ripe,0.1781393438577652,0.821860671043396,0.0 +banana/test/unripe/196.jpg,unripe,ripe,0.0,0.5573312044143677,0.4426687955856323 +banana/test/unripe/197.jpg,unripe,ripe,0.2879611551761627,0.7120388150215149,0.07886551320552826 +banana/test/unripe/198.jpg,unripe,ripe,0.3320983350276947,0.6679016947746277,0.0 +banana/test/unripe/199.jpg,unripe,overripe,0.0,0.4392305612564087,0.5607694387435913 +banana/test/unripe/2.jpg,unripe,ripe,0.33967164158821106,0.6603283882141113,0.0 +banana/test/unripe/20.jpg,unripe,ripe,0.17099973559379578,0.8290002942085266,0.0 +banana/test/unripe/200.jpg,unripe,unripe,0.9495400786399841,0.05045991390943527,0.13728152215480804 +banana/test/unripe/201.jpg,unripe,ripe,0.47401341795921326,0.5259865522384644,0.2341340035200119 +banana/test/unripe/202.jpg,unripe,ripe,0.05435812473297119,0.7425945401191711,0.25740548968315125 +banana/test/unripe/203.jpg,unripe,unripe,0.9604841470718384,0.03951586037874222,0.0 +banana/test/unripe/204.jpg,unripe,unripe,0.9256457686424255,0.07435421645641327,0.20120593905448914 +banana/test/unripe/205.jpg,unripe,ripe,0.190354585647583,0.809645414352417,0.04102335125207901 +banana/test/unripe/206.jpg,unripe,overripe,0.0,0.449664443731308,0.5503355860710144 +banana/test/unripe/207.jpg,unripe,ripe,0.0506606251001358,0.9000629186630249,0.0999370813369751 +banana/test/unripe/208.jpg,unripe,ripe,0.23312261700630188,0.6508814692497253,0.34911853075027466 +banana/test/unripe/209.jpg,unripe,ripe,0.0,0.5355250239372253,0.46447497606277466 +banana/test/unripe/21.jpg,unripe,ripe,0.2694321274757385,0.7305678725242615,0.0 +banana/test/unripe/210.jpg,unripe,ripe,0.4957944452762604,0.5042055249214172,0.03989071026444435 +banana/test/unripe/211.jpg,unripe,ripe,0.1455976963043213,0.8544023036956787,0.0964689701795578 +banana/test/unripe/212.jpg,unripe,ripe,0.46871668100357056,0.5312833189964294,0.16120605170726776 +banana/test/unripe/213.jpg,unripe,ripe,0.35644903779029846,0.6435509920120239,0.3122726380825043 +banana/test/unripe/214.jpg,unripe,unripe,0.5001989603042603,0.49980103969573975,0.3132651150226593 +banana/test/unripe/215.jpg,unripe,overripe,0.0,0.4724540114402771,0.5275459885597229 +banana/test/unripe/216.jpg,unripe,ripe,0.13723433017730713,0.7282894253730774,0.2717105746269226 +banana/test/unripe/217.jpg,unripe,ripe,0.13262909650802612,0.7461115717887878,0.25388842821121216 +banana/test/unripe/218.jpg,unripe,ripe,0.2316400557756424,0.7683599591255188,0.0 +banana/test/unripe/219.jpg,unripe,ripe,0.3344775140285492,0.6655225157737732,0.0 +banana/test/unripe/22.jpg,unripe,ripe,0.17615944147109985,0.8238405585289001,0.0 +banana/test/unripe/220.jpg,unripe,unripe,0.9442819952964783,0.05571798235177994,0.020696986466646194 +banana/test/unripe/221.jpg,unripe,unripe,0.9463150501251221,0.05368497222661972,0.025346694514155388 +banana/test/unripe/222.jpg,unripe,ripe,0.1978285014629364,0.802171528339386,0.0 +banana/test/unripe/223.jpg,unripe,ripe,0.0,0.8833997249603271,0.11660026758909225 +banana/test/unripe/224.jpg,unripe,ripe,0.23312261700630188,0.6508814692497253,0.34911853075027466 +banana/test/unripe/225.jpg,unripe,ripe,0.1092090979218483,0.7564257979393005,0.24357418715953827 +banana/test/unripe/226.jpg,unripe,ripe,0.15346628427505493,0.8465337157249451,0.0 +banana/test/unripe/227.jpg,unripe,overripe,0.0,0.4343537390232086,0.565646231174469 +banana/test/unripe/228.jpg,unripe,ripe,0.2910826504230499,0.7089173197746277,0.2024182677268982 +banana/test/unripe/229.jpg,unripe,ripe,0.49999380111694336,0.5000061988830566,0.0 +banana/test/unripe/23.jpg,unripe,ripe,0.17826314270496368,0.8217368721961975,0.0 +banana/test/unripe/230.jpg,unripe,ripe,0.1609143614768982,0.8390856385231018,0.029130009934306145 +banana/test/unripe/231.jpg,unripe,ripe,0.0,0.5111933350563049,0.48880666494369507 +banana/test/unripe/232.jpg,unripe,ripe,0.3320983350276947,0.6679016947746277,0.0 +banana/test/unripe/233.jpg,unripe,ripe,0.0,0.5111547112464905,0.4888452887535095 +banana/test/unripe/234.jpg,unripe,overripe,0.0,0.402335524559021,0.597664475440979 +banana/test/unripe/235.jpg,unripe,unripe,0.5328506827354431,0.4671493172645569,0.0 +banana/test/unripe/236.jpg,unripe,ripe,0.18300502002239227,0.5689057111740112,0.43109431862831116 +banana/test/unripe/237.jpg,unripe,unripe,0.504915177822113,0.4950847923755646,0.0 +banana/test/unripe/238.jpg,unripe,unripe,0.5424380302429199,0.4575619697570801,0.17120708525180817 +banana/test/unripe/239.jpg,unripe,overripe,0.0,0.449664443731308,0.5503355860710144 +banana/test/unripe/24.jpg,unripe,ripe,0.09727908670902252,0.6068675518035889,0.39313244819641113 +banana/test/unripe/240.jpg,unripe,ripe,0.29882490634918213,0.7011750936508179,0.0 +banana/test/unripe/241.jpg,unripe,unripe,0.7982535362243652,0.20174647867679596,0.2664501965045929 +banana/test/unripe/242.jpg,unripe,ripe,0.2532975375652313,0.6696004271507263,0.33039960265159607 +banana/test/unripe/243.jpg,unripe,ripe,0.05554822459816933,0.811663806438446,0.18833619356155396 +banana/test/unripe/244.jpg,unripe,ripe,0.18406756222248077,0.8159324526786804,0.11088031530380249 +banana/test/unripe/245.jpg,unripe,overripe,0.0,0.4700336754322052,0.5299663543701172 +banana/test/unripe/246.jpg,unripe,unripe,0.9686957597732544,0.031304240226745605,0.15591837465763092 +banana/test/unripe/247.jpg,unripe,overripe,0.0,0.4760121703147888,0.5239878296852112 +banana/test/unripe/248.jpg,unripe,ripe,0.0,0.9288811087608337,0.07111886888742447 +banana/test/unripe/249.jpg,unripe,ripe,0.0506606251001358,0.9000629186630249,0.0999370813369751 +banana/test/unripe/25.jpg,unripe,ripe,0.472179651260376,0.527820348739624,0.2150060087442398 +banana/test/unripe/250.jpg,unripe,unripe,0.916376531124115,0.0836234912276268,0.20797055959701538 +banana/test/unripe/251.jpg,unripe,overripe,0.0,0.45973193645477295,0.540268063545227 +banana/test/unripe/252.jpg,unripe,ripe,0.49093255400657654,0.5090674757957458,0.0040493193082511425 +banana/test/unripe/253.jpg,unripe,ripe,0.32533591985702515,0.6746640801429749,0.18337075412273407 +banana/test/unripe/254.jpg,unripe,ripe,0.01695108227431774,0.7189971208572388,0.28100287914276123 +banana/test/unripe/255.jpg,unripe,ripe,0.0,0.5318891406059265,0.4681108593940735 +banana/test/unripe/256.jpg,unripe,ripe,0.37106767296791077,0.6289322972297668,0.07601729035377502 +banana/test/unripe/257.jpg,unripe,ripe,0.2810545563697815,0.7189454436302185,0.0 +banana/test/unripe/258.jpg,unripe,ripe,0.27235227823257446,0.7276477217674255,0.0 +banana/test/unripe/259.jpg,unripe,ripe,0.05080292001366615,0.8476885557174683,0.15231142938137054 +banana/test/unripe/26.jpg,unripe,overripe,0.0,0.42015621066093445,0.5798438191413879 +banana/test/unripe/260.jpg,unripe,overripe,0.0,0.416292667388916,0.583707332611084 +banana/test/unripe/261.jpg,unripe,ripe,0.19014105200767517,0.8098589777946472,0.0 +banana/test/unripe/262.jpg,unripe,unripe,0.5740900635719299,0.42590993642807007,0.0642734095454216 +banana/test/unripe/263.jpg,unripe,ripe,0.17423053085803986,0.7971675992012024,0.2028323858976364 +banana/test/unripe/264.jpg,unripe,ripe,0.0,0.5590758323669434,0.44092416763305664 +banana/test/unripe/265.jpg,unripe,ripe,0.09018750488758087,0.9098125100135803,0.04809482768177986 +banana/test/unripe/266.jpg,unripe,ripe,0.41914695501327515,0.5808530449867249,0.3239709436893463 +banana/test/unripe/267.jpg,unripe,overripe,0.0,0.41706687211990356,0.5829331278800964 +banana/test/unripe/268.jpg,unripe,overripe,0.0,0.45973193645477295,0.540268063545227 +banana/test/unripe/269.jpg,unripe,ripe,0.2726276218891144,0.7273723483085632,0.0 +banana/test/unripe/27.jpg,unripe,ripe,0.0,0.6195615530014038,0.3804384171962738 +banana/test/unripe/270.jpg,unripe,ripe,0.05858363211154938,0.6085811853408813,0.39141878485679626 +banana/test/unripe/271.jpg,unripe,ripe,0.0,0.5775843262672424,0.42241570353507996 +banana/test/unripe/272.jpg,unripe,ripe,0.10826786607503891,0.8442574143409729,0.1557426154613495 +banana/test/unripe/273.jpg,unripe,unripe,0.900320291519165,0.09967972338199615,0.046735119074583054 +banana/test/unripe/274.jpg,unripe,ripe,0.0,0.5172346830368042,0.4827653169631958 +banana/test/unripe/275.jpg,unripe,overripe,0.0,0.4058885872364044,0.5941113829612732 +banana/test/unripe/276.jpg,unripe,ripe,0.28196480870246887,0.7180352210998535,0.009968146681785583 +banana/test/unripe/277.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/278.jpg,unripe,ripe,0.36587128043174744,0.634128749370575,0.0 +banana/test/unripe/279.jpg,unripe,ripe,0.0,0.6873548626899719,0.3126451373100281 +banana/test/unripe/28.jpg,unripe,ripe,0.238764688372612,0.7612352967262268,0.0 +banana/test/unripe/280.jpg,unripe,ripe,0.1918776035308838,0.7188829779624939,0.2811169922351837 +banana/test/unripe/281.jpg,unripe,overripe,0.0,0.4710013270378113,0.5289986729621887 +banana/test/unripe/282.jpg,unripe,ripe,0.0,0.5186662077903748,0.48133379220962524 +banana/test/unripe/283.jpg,unripe,ripe,0.2688201367855072,0.7311798334121704,0.17532621324062347 +banana/test/unripe/284.jpg,unripe,ripe,0.0,0.5709973573684692,0.42900267243385315 +banana/test/unripe/285.jpg,unripe,ripe,0.022618401795625687,0.7690849900245667,0.23091503977775574 +banana/test/unripe/286.jpg,unripe,ripe,0.4009052813053131,0.5990946888923645,0.08302363008260727 +banana/test/unripe/287.jpg,unripe,ripe,0.0,0.5775843262672424,0.42241570353507996 +banana/test/unripe/288.jpg,unripe,ripe,0.47401341795921326,0.5259865522384644,0.2341340035200119 +banana/test/unripe/289.jpg,unripe,ripe,0.1918776035308838,0.7188829779624939,0.2811169922351837 +banana/test/unripe/29.jpg,unripe,ripe,0.29024583101272583,0.7097541689872742,0.0 +banana/test/unripe/290.jpg,unripe,ripe,0.17423053085803986,0.7971675992012024,0.2028323858976364 +banana/test/unripe/291.jpg,unripe,ripe,0.36587128043174744,0.634128749370575,0.0 +banana/test/unripe/292.jpg,unripe,ripe,0.05091063678264618,0.9490893483161926,0.0 +banana/test/unripe/293.jpg,unripe,ripe,0.27016884088516235,0.7298311591148376,0.05167118459939957 +banana/test/unripe/294.jpg,unripe,ripe,0.0,0.5055115818977356,0.4944884479045868 +banana/test/unripe/295.jpg,unripe,ripe,0.21329061686992645,0.6414762139320374,0.35852378606796265 +banana/test/unripe/296.jpg,unripe,overripe,0.0,0.42230111360549927,0.5776988863945007 +banana/test/unripe/297.jpg,unripe,ripe,0.21766895055770874,0.7823310494422913,0.06475440412759781 +banana/test/unripe/298.jpg,unripe,unripe,0.7165770530700684,0.28342294692993164,0.2057463526725769 +banana/test/unripe/299.jpg,unripe,overripe,0.0,0.41163232922554016,0.5883677005767822 +banana/test/unripe/3.jpg,unripe,ripe,0.27904078364372253,0.7209591865539551,0.0 +banana/test/unripe/30.jpg,unripe,ripe,0.0,0.5862075090408325,0.4137924611568451 +banana/test/unripe/300.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/301.jpg,unripe,overripe,0.0,0.42230111360549927,0.5776988863945007 +banana/test/unripe/302.jpg,unripe,ripe,0.0,0.6887493133544922,0.3112506866455078 +banana/test/unripe/303.jpg,unripe,unripe,0.8648175597190857,0.1351824402809143,0.23789408802986145 +banana/test/unripe/304.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/305.jpg,unripe,ripe,0.13721106946468353,0.8627889156341553,0.0 +banana/test/unripe/306.jpg,unripe,ripe,0.004313443787395954,0.5478020310401917,0.45219799876213074 +banana/test/unripe/307.jpg,unripe,ripe,0.0,0.6975603699684143,0.3024396002292633 +banana/test/unripe/308.jpg,unripe,ripe,0.11657766252756119,0.883422315120697,0.09286066144704819 +banana/test/unripe/309.jpg,unripe,ripe,0.22048380970954895,0.7795161604881287,0.0 +banana/test/unripe/31.jpg,unripe,ripe,0.15100179612636566,0.8489981889724731,0.08904464542865753 +banana/test/unripe/310.jpg,unripe,unripe,0.8904908299446106,0.10950917750597,0.0 +banana/test/unripe/311.jpg,unripe,ripe,0.0,0.56282639503479,0.4371735751628876 +banana/test/unripe/312.jpg,unripe,ripe,0.21766895055770874,0.7823310494422913,0.06475440412759781 +banana/test/unripe/313.jpg,unripe,ripe,0.0,0.5055115818977356,0.4944884479045868 +banana/test/unripe/314.jpg,unripe,ripe,0.05091063678264618,0.9490893483161926,0.0 +banana/test/unripe/315.jpg,unripe,ripe,0.0,0.5186662077903748,0.48133379220962524 +banana/test/unripe/316.jpg,unripe,unripe,0.8953600525856018,0.10463997721672058,0.0 +banana/test/unripe/317.jpg,unripe,ripe,0.06670267134904861,0.933297336101532,0.01468850951641798 +banana/test/unripe/318.jpg,unripe,ripe,0.035729601979255676,0.7862626314163208,0.2137373685836792 +banana/test/unripe/319.jpg,unripe,overripe,0.0,0.404333233833313,0.595666766166687 +banana/test/unripe/32.jpg,unripe,ripe,0.0,0.5399177074432373,0.4600822925567627 +banana/test/unripe/320.jpg,unripe,ripe,0.20743311941623688,0.7483723759651184,0.251627653837204 +banana/test/unripe/321.jpg,unripe,ripe,0.0,0.7059524059295654,0.29404762387275696 +banana/test/unripe/322.jpg,unripe,ripe,0.06670267134904861,0.933297336101532,0.01468850951641798 +banana/test/unripe/323.jpg,unripe,ripe,0.13721106946468353,0.8627889156341553,0.0 +banana/test/unripe/324.jpg,unripe,ripe,0.0,0.6487730741500854,0.35122692584991455 +banana/test/unripe/325.jpg,unripe,ripe,0.0,0.5459854006767273,0.4540145993232727 +banana/test/unripe/326.jpg,unripe,unripe,0.7717524170875549,0.22824758291244507,0.17408940196037292 +banana/test/unripe/327.jpg,unripe,ripe,0.4603919982910156,0.5396080017089844,0.0 +banana/test/unripe/328.jpg,unripe,ripe,0.2645529806613922,0.7354470491409302,0.25005632638931274 +banana/test/unripe/329.jpg,unripe,ripe,0.22048380970954895,0.7795161604881287,0.0 +banana/test/unripe/33.jpg,unripe,ripe,0.2983088493347168,0.7016911506652832,0.09145495295524597 +banana/test/unripe/330.jpg,unripe,ripe,0.16628538072109222,0.8337146043777466,0.014498141594231129 +banana/test/unripe/331.jpg,unripe,ripe,0.2256045788526535,0.6939019560813904,0.3060980439186096 +banana/test/unripe/332.jpg,unripe,ripe,0.41861772537231445,0.5813822746276855,0.0009373315260745585 +banana/test/unripe/333.jpg,unripe,ripe,0.07301632314920425,0.9024714231491089,0.09752857685089111 +banana/test/unripe/334.jpg,unripe,ripe,0.3834002614021301,0.6165997385978699,0.0 +banana/test/unripe/335.jpg,unripe,ripe,0.3831455409526825,0.6168544888496399,0.0 +banana/test/unripe/336.jpg,unripe,ripe,0.49572649598121643,0.504273533821106,0.0 +banana/test/unripe/337.jpg,unripe,ripe,0.1430942267179489,0.6680036187171936,0.331996351480484 +banana/test/unripe/338.jpg,unripe,ripe,0.0,0.6005666851997375,0.39943331480026245 +banana/test/unripe/339.jpg,unripe,ripe,0.0,0.6887493133544922,0.3112506866455078 +banana/test/unripe/34.jpg,unripe,overripe,0.0,0.42033421993255615,0.5796657800674438 +banana/test/unripe/340.jpg,unripe,ripe,0.0,0.6479957699775696,0.3520042300224304 +banana/test/unripe/341.jpg,unripe,ripe,0.0,0.5709973573684692,0.42900267243385315 +banana/test/unripe/342.jpg,unripe,ripe,0.23956044018268585,0.7604395747184753,0.021245421841740608 +banana/test/unripe/343.jpg,unripe,ripe,0.0,0.693358302116394,0.30664166808128357 +banana/test/unripe/344.jpg,unripe,ripe,0.4963388442993164,0.5036611557006836,0.3081219792366028 +banana/test/unripe/345.jpg,unripe,ripe,0.05672227591276169,0.9087719321250916,0.09122806787490845 +banana/test/unripe/346.jpg,unripe,overripe,0.0,0.4810695946216583,0.5189303755760193 +banana/test/unripe/347.jpg,unripe,overripe,0.0,0.46037623286247253,0.5396237373352051 +banana/test/unripe/348.jpg,unripe,ripe,0.05138048157095909,0.8555342555046082,0.14446577429771423 +banana/test/unripe/349.jpg,unripe,ripe,0.30301612615585327,0.6969838738441467,0.0 +banana/test/unripe/35.jpg,unripe,unripe,0.5257328152656555,0.4742671549320221,0.23988844454288483 +banana/test/unripe/350.jpg,unripe,ripe,0.3397550880908966,0.660244882106781,0.0 +banana/test/unripe/351.jpg,unripe,ripe,0.16605237126350403,0.8339476585388184,0.13462218642234802 +banana/test/unripe/352.jpg,unripe,unripe,0.7136009335517883,0.28639906644821167,0.04170991852879524 +banana/test/unripe/353.jpg,unripe,ripe,0.12750178575515747,0.8724982142448425,0.0 +banana/test/unripe/354.jpg,unripe,ripe,0.12403503060340881,0.8593230843544006,0.14067691564559937 +banana/test/unripe/355.jpg,unripe,overripe,0.001049682847224176,0.47785684466362,0.5221431851387024 +banana/test/unripe/356.jpg,unripe,ripe,0.0,0.5340055823326111,0.46599438786506653 +banana/test/unripe/357.jpg,unripe,unripe,0.5740900635719299,0.42590993642807007,0.0642734095454216 +banana/test/unripe/358.jpg,unripe,ripe,0.18789666891098022,0.8121033310890198,0.0 +banana/test/unripe/359.jpg,unripe,unripe,0.9789761304855347,0.02102384716272354,0.06815633922815323 +banana/test/unripe/36.jpg,unripe,ripe,0.1882268637418747,0.8117731213569641,0.0 +banana/test/unripe/360.jpg,unripe,unripe,0.7456104159355164,0.25438958406448364,0.3163160979747772 +banana/test/unripe/361.jpg,unripe,ripe,0.13198316097259521,0.6581996083259583,0.34180036187171936 +banana/test/unripe/362.jpg,unripe,ripe,0.05554822459816933,0.811663806438446,0.18833619356155396 +banana/test/unripe/363.jpg,unripe,ripe,0.23956044018268585,0.7604395747184753,0.021245421841740608 +banana/test/unripe/364.jpg,unripe,ripe,0.3397550880908966,0.660244882106781,0.0 +banana/test/unripe/365.jpg,unripe,ripe,0.0,0.693358302116394,0.30664166808128357 +banana/test/unripe/366.jpg,unripe,ripe,0.0,0.6487730741500854,0.35122692584991455 +banana/test/unripe/367.jpg,unripe,ripe,0.0,0.5048636794090271,0.4951362907886505 +banana/test/unripe/368.jpg,unripe,ripe,0.12750178575515747,0.8724982142448425,0.0 +banana/test/unripe/369.jpg,unripe,unripe,0.7136009335517883,0.28639906644821167,0.04170991852879524 +banana/test/unripe/37.jpg,unripe,ripe,0.2370036542415619,0.7629963159561157,0.11401332169771194 +banana/test/unripe/370.jpg,unripe,overripe,0.0,0.4819594919681549,0.5180405378341675 +banana/test/unripe/371.jpg,unripe,overripe,0.001049682847224176,0.47785684466362,0.5221431851387024 +banana/test/unripe/372.jpg,unripe,unripe,0.9154831767082214,0.08451680839061737,0.18944084644317627 +banana/test/unripe/373.jpg,unripe,overripe,0.0,0.4226034879684448,0.5773965120315552 +banana/test/unripe/374.jpg,unripe,ripe,0.0,0.6447997093200684,0.35520026087760925 +banana/test/unripe/375.jpg,unripe,ripe,0.4976888597011566,0.5023111701011658,0.0 +banana/test/unripe/376.jpg,unripe,unripe,0.9789761304855347,0.02102384716272354,0.06815633922815323 +banana/test/unripe/377.jpg,unripe,ripe,0.311576247215271,0.688423752784729,0.0 +banana/test/unripe/378.jpg,unripe,unripe,0.7717524170875549,0.22824758291244507,0.17408940196037292 +banana/test/unripe/379.jpg,unripe,ripe,0.3520611822605133,0.6479388475418091,0.09856297075748444 +banana/test/unripe/38.jpg,unripe,ripe,0.0,0.6217189431190491,0.3782810866832733 +banana/test/unripe/380.jpg,unripe,ripe,0.0,0.5030698180198669,0.49693018198013306 +banana/test/unripe/381.jpg,unripe,ripe,0.062187083065509796,0.6575918793678284,0.342408150434494 +banana/test/unripe/382.jpg,unripe,overripe,0.0,0.4476684033870697,0.5523316264152527 +banana/test/unripe/383.jpg,unripe,overripe,0.0,0.48741188645362854,0.5125881433486938 +banana/test/unripe/384.jpg,unripe,ripe,0.16166193783283234,0.6606232523918152,0.3393767774105072 +banana/test/unripe/385.jpg,unripe,unripe,0.7818182110786438,0.2181818187236786,0.0 +banana/test/unripe/386.jpg,unripe,ripe,0.34957167506217957,0.6504283547401428,0.19132156670093536 +banana/test/unripe/387.jpg,unripe,ripe,0.04904285818338394,0.9509571194648743,0.003807900007814169 +banana/test/unripe/388.jpg,unripe,unripe,0.7939819097518921,0.2060181051492691,0.14755339920520782 +banana/test/unripe/389.jpg,unripe,ripe,0.03558528423309326,0.7668277025222778,0.23317229747772217 +banana/test/unripe/39.jpg,unripe,overripe,0.0,0.42544031143188477,0.5745596885681152 +banana/test/unripe/390.jpg,unripe,unripe,0.915412962436676,0.08458702266216278,0.14772145450115204 +banana/test/unripe/391.jpg,unripe,ripe,0.40413200855255127,0.5958679914474487,0.19159096479415894 +banana/test/unripe/392.jpg,unripe,ripe,0.0,0.8397820591926575,0.16021794080734253 +banana/test/unripe/393.jpg,unripe,ripe,0.0,0.5627022385597229,0.4372977316379547 +banana/test/unripe/394.jpg,unripe,ripe,0.0,0.6804366111755371,0.3195633888244629 +banana/test/unripe/395.jpg,unripe,ripe,0.0,0.7141633033752441,0.28583666682243347 +banana/test/unripe/396.jpg,unripe,overripe,0.0,0.4216409921646118,0.5783590078353882 +banana/test/unripe/397.jpg,unripe,ripe,0.0,0.6095388531684875,0.39046117663383484 +banana/test/unripe/398.jpg,unripe,unripe,0.5585853457450867,0.44141462445259094,0.0 +banana/test/unripe/399.jpg,unripe,overripe,0.0,0.40038806200027466,0.5996119379997253 +banana/test/unripe/4.jpg,unripe,ripe,0.36086389422416687,0.6391360759735107,0.0 +banana/test/unripe/40.jpg,unripe,unripe,0.6815970540046692,0.3184029459953308,0.0 +banana/test/unripe/400.jpg,unripe,ripe,0.0,0.5553000569343567,0.4446999430656433 +banana/test/unripe/41.jpg,unripe,ripe,0.0,0.6531587243080139,0.34684130549430847 +banana/test/unripe/42.jpg,unripe,ripe,0.08652738481760025,0.913472592830658,0.0 +banana/test/unripe/43.jpg,unripe,ripe,0.23927068710327148,0.7607293128967285,0.0 +banana/test/unripe/44.jpg,unripe,ripe,0.0,0.5578181147575378,0.44218191504478455 +banana/test/unripe/45.jpg,unripe,ripe,0.0,0.5712973475456238,0.4287026524543762 +banana/test/unripe/46.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/47.jpg,unripe,ripe,0.4873133897781372,0.5126866102218628,0.21425436437129974 +banana/test/unripe/48.jpg,unripe,ripe,0.2241072803735733,0.7758927345275879,0.017076123505830765 +banana/test/unripe/49.jpg,unripe,unripe,0.7751216888427734,0.22487829625606537,0.0 +banana/test/unripe/5.jpg,unripe,unripe,0.9796347618103027,0.020365232601761818,0.07538141310214996 +banana/test/unripe/50.jpg,unripe,ripe,0.0,0.7547183036804199,0.24528169631958008 +banana/test/unripe/51.jpg,unripe,ripe,0.2193397879600525,0.7806602120399475,0.0 +banana/test/unripe/52.jpg,unripe,ripe,0.25537028908729553,0.7446296811103821,0.0 +banana/test/unripe/53.jpg,unripe,ripe,0.01868448778986931,0.7649363279342651,0.23506368696689606 +banana/test/unripe/54.jpg,unripe,ripe,0.27473804354667664,0.725261926651001,0.0 +banana/test/unripe/55.jpg,unripe,ripe,0.15886865556240082,0.841131329536438,0.0 +banana/test/unripe/56.jpg,unripe,ripe,0.03114016354084015,0.7785758376121521,0.2214241623878479 +banana/test/unripe/57.jpg,unripe,ripe,0.24113181233406067,0.7588681578636169,0.0 +banana/test/unripe/58.jpg,unripe,ripe,0.13488896191120148,0.8651110529899597,0.0 +banana/test/unripe/59.jpg,unripe,overripe,0.0,0.49939602613449097,0.500603973865509 +banana/test/unripe/6.jpg,unripe,ripe,0.4156913161277771,0.5843086838722229,0.0 +banana/test/unripe/60.jpg,unripe,overripe,0.0,0.47354474663734436,0.526455283164978 +banana/test/unripe/61.jpg,unripe,overripe,0.0,0.4458690285682678,0.5541309714317322 +banana/test/unripe/62.jpg,unripe,ripe,0.0,0.6254998445510864,0.3745001554489136 +banana/test/unripe/63.jpg,unripe,ripe,0.012043465860188007,0.9879565238952637,0.006882989313453436 +banana/test/unripe/64.jpg,unripe,ripe,0.0,0.6910738348960876,0.30892613530158997 +banana/test/unripe/65.jpg,unripe,ripe,0.18842670321464539,0.811573326587677,0.0 +banana/test/unripe/66.jpg,unripe,ripe,0.25700828433036804,0.7429916858673096,0.0 +banana/test/unripe/67.jpg,unripe,unripe,0.6635995507240295,0.33640041947364807,0.12556804716587067 +banana/test/unripe/68.jpg,unripe,unripe,0.5794715285301208,0.42052847146987915,0.2856895923614502 +banana/test/unripe/69.jpg,unripe,ripe,0.2576649487018585,0.7423350214958191,0.038596492260694504 +banana/test/unripe/7.jpg,unripe,ripe,0.10781701654195786,0.8921830058097839,0.0749310851097107 +banana/test/unripe/70.jpg,unripe,unripe,0.9617946743965149,0.03820532187819481,0.21587540209293365 +banana/test/unripe/71.jpg,unripe,ripe,0.24302314221858978,0.756976842880249,0.0 +banana/test/unripe/72.jpg,unripe,ripe,0.2642914354801178,0.7357085943222046,0.0 +banana/test/unripe/73.jpg,unripe,ripe,0.02726702019572258,0.5439355969429016,0.4560644030570984 +banana/test/unripe/74.jpg,unripe,ripe,0.0,0.9824594259262085,0.01754060387611389 +banana/test/unripe/75.jpg,unripe,ripe,0.0,0.5262106657028198,0.4737893342971802 +banana/test/unripe/76.jpg,unripe,ripe,0.10666332393884659,0.8933366537094116,0.0 +banana/test/unripe/77.jpg,unripe,ripe,0.3950059413909912,0.6049940586090088,0.08746276795864105 +banana/test/unripe/78.jpg,unripe,ripe,0.2836199700832367,0.7163800597190857,0.03421182557940483 +banana/test/unripe/79.jpg,unripe,ripe,0.07875967025756836,0.8861175179481506,0.11388248950242996 +banana/test/unripe/8.jpg,unripe,ripe,0.0,0.6060280799865723,0.39397192001342773 +banana/test/unripe/80.jpg,unripe,ripe,0.14745305478572845,0.8525469303131104,0.0 +banana/test/unripe/81.jpg,unripe,ripe,0.10666332393884659,0.8933366537094116,0.0 +banana/test/unripe/82.jpg,unripe,ripe,0.07883959263563156,0.6067556738853455,0.39324429631233215 +banana/test/unripe/83.jpg,unripe,ripe,0.0,0.6662529706954956,0.3337470293045044 +banana/test/unripe/84.jpg,unripe,ripe,0.24302314221858978,0.756976842880249,0.0 +banana/test/unripe/85.jpg,unripe,ripe,0.07875967025756836,0.8861175179481506,0.11388248950242996 +banana/test/unripe/86.jpg,unripe,ripe,0.2017023265361786,0.798297643661499,0.11432239413261414 +banana/test/unripe/87.jpg,unripe,ripe,0.22843103110790253,0.7715689539909363,0.0 +banana/test/unripe/88.jpg,unripe,unripe,0.5507725477218628,0.4492274522781372,0.3006802797317505 +banana/test/unripe/89.jpg,unripe,ripe,0.0,0.6827380657196045,0.3172619044780731 +banana/test/unripe/9.jpg,unripe,ripe,0.2614806592464447,0.7385193705558777,0.0 +banana/test/unripe/90.jpg,unripe,overripe,0.0,0.4275541305541992,0.5724458694458008 +banana/test/unripe/91.jpg,unripe,unripe,0.9984765648841858,0.0015234416350722313,0.038400743156671524 +banana/test/unripe/92.jpg,unripe,ripe,0.29894185066223145,0.5364506840705872,0.46354931592941284 +banana/test/unripe/93.jpg,unripe,ripe,0.2421608418226242,0.7578391432762146,0.0 +banana/test/unripe/94.jpg,unripe,ripe,0.17768056690692902,0.8223194479942322,0.0 +banana/test/unripe/95.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/96.jpg,unripe,ripe,0.25607940554618835,0.743920624256134,0.0 +banana/test/unripe/97.jpg,unripe,ripe,0.2836199700832367,0.7163800597190857,0.03421182557940483 +banana/test/unripe/98.jpg,unripe,overripe,0.0,0.41444000601768494,0.5855599641799927 +banana/test/unripe/99.jpg,unripe,ripe,0.39128240942955017,0.6087175607681274,0.0 diff --git a/AgCloud/services/ripeness-baseline/eval/banana_argmax/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/banana_argmax/roc_curves.png new file mode 100644 index 000000000..9bc631052 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/banana_argmax/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/banana_test/metrics.json b/AgCloud/services/ripeness-baseline/eval/banana_test/metrics.json new file mode 100644 index 000000000..258384beb --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/banana_test/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.4279176201372998, + "report": { + "unripe": { + "precision": 0.890625, + "recall": 0.1425, + "f1-score": 0.24568965517241378, + "support": 400.0 + }, + "ripe": { + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 381.0 + }, + "overripe": { + "precision": 0.44919786096256686, + "recall": 0.9509433962264151, + "f1-score": 0.6101694915254238, + "support": 530.0 + }, + "accuracy": 0.4279176201372998, + "macro avg": { + "precision": 0.4466076203208556, + "recall": 0.3644811320754717, + "f1-score": 0.28528638223261255, + "support": 1311.0 + }, + "weighted avg": { + "precision": 0.45333704524039703, + "recall": 0.4279176201372998, + "f1-score": 0.32163668388820754, + "support": 1311.0 + } + }, + "confusion_matrix": [ + [ + 57, + 106, + 237 + ], + [ + 0, + 0, + 381 + ], + [ + 7, + 19, + 504 + ] + ], + "samples": 1311, + "prefix": "banana/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/banana_test/per_image.csv b/AgCloud/services/ripeness-baseline/eval/banana_test/per_image.csv new file mode 100644 index 000000000..07b29a2b5 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/banana_test/per_image.csv @@ -0,0 +1,1312 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +banana/test/overripe/Screen Shot 2018-06-12 at 8.47.41 PM.png,overripe,overripe,0.0,0.5180085897445679,0.48199138045310974 +banana/test/overripe/Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.422983855009079,0.5770161747932434 +banana/test/overripe/Screen Shot 2018-06-12 at 8.48.40 PM.png,overripe,overripe,0.0,0.423921138048172,0.5760788321495056 +banana/test/overripe/Screen Shot 2018-06-12 at 8.49.25 PM.png,overripe,overripe,0.0,0.7116640210151672,0.28833597898483276 +banana/test/overripe/Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,overripe,0.0,0.5562599301338196,0.4437400698661804 +banana/test/overripe/Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,overripe,0.0,0.6357175707817078,0.36428239941596985 +banana/test/overripe/Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4366108179092407,0.5633891820907593 +banana/test/overripe/Screen Shot 2018-06-12 at 8.52.16 PM.png,overripe,overripe,0.0,0.6122689247131348,0.38773104548454285 +banana/test/overripe/Screen Shot 2018-06-12 at 8.52.21 PM.png,overripe,overripe,0.0,0.40863141417503357,0.5913686156272888 +banana/test/overripe/Screen Shot 2018-06-12 at 8.53.09 PM.png,overripe,overripe,0.729612410068512,0.27038758993148804,0.06428125500679016 +banana/test/overripe/Screen Shot 2018-06-12 at 8.53.47 PM.png,overripe,overripe,0.65863436460495,0.34136560559272766,0.21442356705665588 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,overripe,0.0,0.49039360880851746,0.5096063613891602 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.07 PM.png,overripe,overripe,0.9623796939849854,0.03762033209204674,0.2919136583805084 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.4092310667037964,0.5907689332962036 +banana/test/overripe/Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,overripe,0.0,0.7216289639472961,0.27837100625038147 +banana/test/overripe/Screen Shot 2018-06-12 at 8.57.14 PM.png,overripe,overripe,0.0,0.4152335822582245,0.5847664475440979 +banana/test/overripe/Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,overripe,0.5725548267364502,0.4274452030658722,0.18132103979587555 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.38 PM.png,overripe,overripe,0.0,0.41843780875205994,0.5815621614456177 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.43 PM.png,overripe,overripe,0.0,0.40056419372558594,0.5994358062744141 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40106678009033203,0.598933219909668 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.02 PM.png,overripe,overripe,0.0,0.4011296033859253,0.5988703966140747 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.15 PM.png,overripe,overripe,0.0,0.45257753133773804,0.547422468662262 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6803687214851379,0.31963127851486206 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.44 PM.png,overripe,overripe,0.22994448244571686,0.6056786179542542,0.39432141184806824 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.10729925334453583,0.5256212949752808,0.47437870502471924 +banana/test/overripe/Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4748240113258362,0.5251759886741638 +banana/test/overripe/Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.4656502306461334,0.534349799156189 +banana/test/overripe/Screen Shot 2018-06-12 at 9.01.26 PM.png,overripe,overripe,0.0,0.40085864067077637,0.5991413593292236 +banana/test/overripe/Screen Shot 2018-06-12 at 9.03.34 PM.png,overripe,overripe,0.0,0.4899188280105591,0.5100811719894409 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,overripe,0.0,0.4079243242740631,0.5920756459236145 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.4159392714500427,0.5840607285499573 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.572858452796936,0.42714154720306396 +banana/test/overripe/Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.6519812345504761,0.34801873564720154 +banana/test/overripe/Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.41806748509407043,0.5819324851036072 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,overripe,0.0,0.5305242538452148,0.46947571635246277 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.29 PM.png,overripe,overripe,0.0446758009493351,0.48695650696754456,0.5130434632301331 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5766761898994446,0.4233238399028778 +banana/test/overripe/Screen Shot 2018-06-12 at 9.10.20 PM.png,overripe,overripe,0.0,0.4480549395084381,0.5519450306892395 +banana/test/overripe/Screen Shot 2018-06-12 at 9.11.00 PM.png,overripe,overripe,0.0,0.4974040389060974,0.5025959610939026 +banana/test/overripe/Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,overripe,0.0,0.652233362197876,0.347766637802124 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.40 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,unripe,0.577717661857605,0.422282338142395,0.0 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.2852266728878021,0.6214674711227417,0.3785325288772583 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.16 PM.png,overripe,overripe,0.16011843085289001,0.7705056071281433,0.2294943928718567 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.4532082974910736,0.546791672706604 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.34 PM.png,overripe,overripe,0.0,0.47079646587371826,0.5292035341262817 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.44 PM.png,overripe,overripe,0.0,0.5558438897132874,0.44415608048439026 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,overripe,0.0629357397556305,0.5778120756149292,0.4221879243850708 +banana/test/overripe/Screen Shot 2018-06-12 at 9.14.07 PM.png,overripe,overripe,0.0,0.4085470139980316,0.5914530158042908 +banana/test/overripe/Screen Shot 2018-06-12 at 9.14.22 PM.png,overripe,overripe,0.0,0.40129563212394714,0.5987043976783752 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.40392670035362244,0.5960732698440552 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.28 PM.png,overripe,overripe,0.0,0.4485962390899658,0.5514037609100342 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7279413342475891,0.2720586955547333 +banana/test/overripe/Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,overripe,0.0012857052497565746,0.8990954160690308,0.10090460628271103 +banana/test/overripe/Screen Shot 2018-06-12 at 9.18.57 PM.png,overripe,overripe,0.0,0.41857340931892395,0.5814266204833984 +banana/test/overripe/Screen Shot 2018-06-12 at 9.19.17 PM.png,overripe,overripe,0.0,0.48725929856300354,0.5127407312393188 +banana/test/overripe/Screen Shot 2018-06-12 at 9.21.25 PM.png,overripe,overripe,0.0,0.4005010724067688,0.5994989275932312 +banana/test/overripe/Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,overripe,0.0,0.6557254195213318,0.3442745506763458 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4021773040294647,0.5978227257728577 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.4071398377418518,0.5928601622581482 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.28 PM.png,overripe,overripe,0.011655289679765701,0.4221014678478241,0.5778985619544983 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.4729684889316559,0.5270314812660217 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.26 PM.png,overripe,overripe,0.0,0.7230361700057983,0.2769638001918793 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.718128502368927,0.2818715274333954 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.4174003601074219,0.5825996398925781 +banana/test/overripe/Screen Shot 2018-06-12 at 9.28.04 PM.png,overripe,overripe,0.0,0.49620938301086426,0.5037906169891357 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42254066467285156,0.5774593353271484 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.36276790499687195,0.6372321248054504 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,overripe,0.0,0.638027548789978,0.361972451210022 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.43536117672920227,0.5646387934684753 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.52.48 PM.png,overripe,overripe,0.0,0.4012342393398285,0.5987657308578491 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,overripe,0.0027726120315492153,0.9674152135848999,0.03258480504155159 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.6276884078979492,0.37231162190437317,0.19478917121887207 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.4163297712802887,0.5836701989173889 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.7902842164039612,0.2097157984972,0.21134407818317413 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.16 PM.png,overripe,overripe,0.0,0.42554154992103577,0.5744584798812866 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.37 PM.png,overripe,overripe,0.9421945810317993,0.057805418968200684,0.2791872024536133 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,overripe,0.0,0.5568508505821228,0.4431491494178772 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.59 PM.png,overripe,overripe,0.0,0.6886929869651794,0.31130701303482056 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.57.04 PM.png,overripe,overripe,0.0,0.7236341834068298,0.2763657867908478 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,overripe,0.5683773159980774,0.4316226840019226,0.18677106499671936 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.15 PM.png,overripe,overripe,0.0,0.4545516073703766,0.545448362827301 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6884653568267822,0.3115346431732178 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.32 PM.png,overripe,overripe,0.0,0.6857001781463623,0.3142998218536377 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.18360267579555511,0.535484254360199,0.46451571583747864 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4772810935974121,0.5227189064025879 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.00.49 PM.png,overripe,overripe,0.0,0.6748172044754028,0.32518282532691956 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.40687575936317444,0.5931242108345032 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.42860326170921326,0.5713967680931091 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,overripe,0.0,0.6289316415786743,0.3710683584213257 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,overripe,0.3926085829734802,0.6073914170265198,0.17350107431411743 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.41384345293045044,0.5861565470695496 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.05.02 PM.png,overripe,overripe,0.997406542301178,0.0025934644509106874,0.35866934061050415 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.06.13 PM.png,overripe,overripe,0.0,0.46435490250587463,0.5356451272964478 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.07.35 PM.png,overripe,overripe,0.0,0.9712498188018799,0.02875019609928131 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,overripe,0.0,0.5310501456260681,0.4689498543739319 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,overripe,0.0,0.7605984210968018,0.23940156400203705 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5818113684654236,0.4181886315345764 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.04 PM.png,overripe,overripe,0.0,0.6066378355026245,0.3933621644973755 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.42829108238220215,0.5717089176177979 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.4680885970592499,0.5319113731384277 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,overripe,0.0,0.652106761932373,0.34789323806762695 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.11.47 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.4000408351421356,0.5999591946601868 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,unripe,0.5687039494514465,0.43129608035087585,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.2979259192943573,0.6156362891197205,0.38436371088027954 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.40475165843963623,0.5952483415603638 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.40388223528862,0.5961177349090576 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.18.43 PM.png,overripe,overripe,0.0,0.41637328267097473,0.5836266875267029 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.18.50 PM.png,overripe,overripe,0.0,0.40041297674179077,0.5995870232582092 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.5030400156974792,0.49695998430252075 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4547254145145416,0.545274555683136 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.36 PM.png,overripe,overripe,0.0,0.5123593807220459,0.4876405894756317 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.21.05 PM.png,overripe,overripe,0.0,0.4006882309913635,0.5993117690086365 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.22.19 PM.png,overripe,overripe,0.0,0.4300176203250885,0.5699824094772339 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.22.50 PM.png,overripe,overripe,0.0,0.7398862242698669,0.26011377573013306 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,overripe,0.4015491306781769,0.5984508395195007,0.11349669098854065 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.11 PM.png,overripe,overripe,0.0,0.8352605104446411,0.16473950445652008 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.7189414501190186,0.28105854988098145 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,overripe,0.0,0.5706666707992554,0.42933332920074463 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.46 PM.png,overripe,overripe,0.0,0.6046900749206543,0.3953099548816681 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,overripe,0.0,0.7825623154640198,0.21743766963481903 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.36351263523101807,0.6364873647689819 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,overripe,0.0,0.48445233702659607,0.5155476927757263 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.07 PM.png,overripe,overripe,0.4715419113636017,0.5284581184387207,0.16793374717235565 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.6414921879768372,0.35850781202316284,0.17568239569664001 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.8531145453453064,0.146885484457016,0.32168999314308167 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.37 PM.png,overripe,overripe,0.0,0.5099373459815979,0.4900626242160797 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.43 PM.png,overripe,overripe,0.0,0.5813634991645813,0.4186364710330963 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.55.15 PM.png,overripe,overripe,0.0,0.5371758937835693,0.46282413601875305 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.56.11 PM.png,overripe,overripe,0.0,0.4051627814769745,0.5948371887207031 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.56.41 PM.png,overripe,overripe,0.01684659533202648,0.5251268148422241,0.4748731851577759 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.57.29 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,overripe,0.5884822607040405,0.41151776909828186,0.18995894491672516 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,overripe,0.2914331555366516,0.7085668444633484,0.10736473649740219 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6916402578353882,0.30835971236228943 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.00.31 PM.png,overripe,overripe,0.0,0.4514472484588623,0.5485527515411377 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.00.49 PM.png,overripe,overripe,0.0,0.6798491477966309,0.32015088200569153 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.4667853116989136,0.5332146883010864 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.02.52 PM.png,overripe,overripe,0.0,0.4039134681224823,0.5960865616798401 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.03.25 PM.png,overripe,overripe,0.0,0.5654733777046204,0.434526652097702 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,overripe,0.5143269896507263,0.4856730103492737,0.17088904976844788 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,overripe,0.0,0.40648153424263,0.5935184359550476 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,overripe,0.9976025223731995,0.002397472970187664,0.309544175863266 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,overripe,0.0,0.6619175672531128,0.3380824625492096 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.41651207208633423,0.5834879279136658 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.00 PM.png,overripe,overripe,0.0,0.4004811644554138,0.5995188355445862 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.12 PM.png,overripe,overripe,0.0,0.4365890622138977,0.5634109377861023 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.26 PM.png,overripe,overripe,0.0,0.4860347807407379,0.5139651894569397 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.43 PM.png,overripe,overripe,0.0,0.5635502934455872,0.43644973635673523 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.4284514784812927,0.5715485215187073 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.10.45 PM.png,overripe,unripe,0.38497835397720337,0.6150216460227966,0.0 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.05 PM.png,overripe,overripe,0.0,0.40108826756477356,0.5989117622375488 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.17 PM.png,overripe,overripe,0.0,0.43206173181533813,0.5679382681846619 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.27 PM.png,overripe,overripe,0.0,0.6774063110351562,0.32259368896484375 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.58 PM.png,overripe,overripe,0.0,0.4187590181827545,0.5812409520149231 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.12.17 PM.png,overripe,overripe,0.0,0.4312475919723511,0.5687524080276489 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.13.10 PM.png,overripe,unripe,0.5736991763114929,0.42630085349082947,0.0 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.4011547863483429,0.5988451838493347 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.34 PM.png,overripe,overripe,0.0,0.40323519706726074,0.5967648029327393 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,overripe,0.0,0.8403545618057251,0.15964540839195251 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.53 PM.png,overripe,overripe,0.0,0.6728172898292542,0.32718273997306824 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7350778579711914,0.2649221420288086 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.18.11 PM.png,overripe,overripe,0.0,0.8698397874832153,0.13016018271446228 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.18.50 PM.png,overripe,overripe,0.0,0.400338739156723,0.5996612906455994 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.08 PM.png,overripe,overripe,0.0,0.6214917898178101,0.37850821018218994 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.5026924014091492,0.4973076283931732 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.42 PM.png,overripe,overripe,0.5687336325645447,0.4312663674354553,0.34256094694137573 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.49 PM.png,overripe,overripe,0.0,0.40151548385620117,0.5984845161437988 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.20.36 PM.png,overripe,overripe,0.0,0.5144941210746765,0.4855058789253235 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,overripe,0.0,0.6563125252723694,0.343687504529953 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.25.09 PM.png,overripe,overripe,0.0,0.42020443081855774,0.5797955393791199 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.47327226400375366,0.5267277359962463 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.26.01 PM.png,overripe,overripe,0.0,0.8801390528678894,0.1198609471321106 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.26.13 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,overripe,0.0,0.6926509141921997,0.3073490858078003 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.31 PM.png,overripe,overripe,0.0,0.7743561863899231,0.2256438136100769 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.4138185977935791,0.5861814022064209 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.47.28 PM.png,overripe,overripe,0.0,0.6613686084747314,0.33863136172294617 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42400431632995605,0.575995683670044 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,overripe,0.0,0.7819136381149292,0.218086376786232 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.3612042963504791,0.6387957334518433 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.53.09 PM.png,overripe,overripe,0.7052476406097412,0.2947523295879364,0.05787266418337822 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.41741979122161865,0.5825802087783813 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.9509525299072266,0.049047473818063736,0.2962638735771179 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.58 PM.png,overripe,overripe,0.0,0.4311821758747101,0.5688177943229675 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.4267336130142212,0.5732663869857788 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,overripe,0.0,0.5548846125602722,0.4451153874397278 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,overripe,0.0,0.5194494128227234,0.4805505871772766 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,overripe,0.7023075819015503,0.2976924479007721,0.19857682287693024 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.58.38 PM.png,overripe,overripe,0.0,0.40999236702919006,0.5900076627731323 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.4725177586078644,0.5274822115898132 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4739494323730469,0.5260505676269531 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.27 PM.png,overripe,overripe,0.0,0.4701732099056244,0.529826819896698 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.32 PM.png,overripe,overripe,0.0,0.8104894757270813,0.1895105093717575 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.4277803301811218,0.5722196698188782 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.02.15 PM.png,overripe,overripe,0.0,0.6592307686805725,0.3407692313194275 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.02.32 PM.png,overripe,overripe,0.0,0.6495700478553772,0.3504299819469452 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.01 PM.png,overripe,overripe,0.0,0.40085509419441223,0.5991448760032654 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.12 PM.png,overripe,overripe,0.0,0.6612592935562134,0.3387407064437866 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.21 PM.png,overripe,overripe,0.2246299386024475,0.7753700613975525,0.1245737224817276 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,overripe,0.5080413222312927,0.4919586479663849,0.1670374721288681 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.4154772460460663,0.5845227241516113 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.05.32 PM.png,overripe,overripe,0.0,0.45961683988571167,0.5403831601142883 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.6561471223831177,0.3438528776168823 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.6296001076698303,0.3703998625278473 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.07.35 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.09.09 PM.png,overripe,overripe,0.0,0.45097243785858154,0.5490275621414185 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.4340859055519104,0.5659140944480896 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.40619754791259766,0.5938024520874023 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,overripe,0.0,0.49866417050361633,0.501335859298706 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.46182265877723694,0.5381773710250854 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.4017491936683655,0.5982508063316345 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7406160235404968,0.2593839764595032 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.17.44 PM.png,overripe,overripe,0.0,0.6056808233261108,0.39431917667388916 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.18.43 PM.png,overripe,overripe,0.0,0.4350062906742096,0.5649937391281128 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4577691853046417,0.5422308444976807 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.56 PM.png,overripe,overripe,0.0,0.5125678181648254,0.48743218183517456 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.22.37 PM.png,overripe,overripe,0.0,0.7400026917457581,0.25999730825424194 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.22.57 PM.png,overripe,overripe,0.0,0.40713825821876526,0.5928617119789124 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.23.24 PM.png,overripe,overripe,0.0,0.40198785066604614,0.5980121493339539 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.4049931466579437,0.5950068235397339 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.4000379741191864,0.599962055683136 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.47.28 PM.png,overripe,overripe,0.0,0.6871942281723022,0.31280577182769775 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.49.04 PM.png,overripe,overripe,0.0,0.5114953517913818,0.48850464820861816 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.49.30 PM.png,overripe,overripe,0.0,0.42236217856407166,0.5776378512382507 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.50.40 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.51.38 PM.png,overripe,overripe,0.0,0.4002057909965515,0.5997942090034485 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4277200996875763,0.5722798705101013 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.52.37 PM.png,overripe,overripe,0.0,0.4108024835586548,0.5891975164413452 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,overripe,0.011900430545210838,0.9785563945770264,0.02144363336265087 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.5579964518547058,0.4420035481452942,0.18188446760177612 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.996368408203125,0.003631588537245989,0.2828529477119446 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.55.08 PM.png,overripe,overripe,0.0,0.408454030752182,0.5915459990501404 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,overripe,0.0,0.6622987985610962,0.3377012312412262 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.21 PM.png,overripe,overripe,0.0,0.40810537338256836,0.5918946266174316 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.4295704960823059,0.5704295039176941 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,overripe,0.0,0.5551725625991821,0.44482743740081787 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,overripe,0.0,0.5185880661010742,0.4814119338989258 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,overripe,0.3773368000984192,0.6226631999015808,0.0814678966999054 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.32 PM.png,overripe,overripe,0.0,0.8631157279014587,0.13688428699970245 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.35 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.4278220534324646,0.5721779465675354 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.02.09 PM.png,overripe,overripe,0.0,0.6463564038276672,0.3536435663700104 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,overripe,0.0,0.6059812903404236,0.3940187096595764 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.03.30 PM.png,overripe,overripe,0.0,0.4062570631504059,0.5937429666519165 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.03.34 PM.png,overripe,overripe,0.0,0.5086967945098877,0.4913031756877899 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.04.01 PM.png,overripe,overripe,0.0,0.48034125566482544,0.5196587443351746 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.05.21 PM.png,overripe,overripe,0.0,0.40393736958503723,0.5960626602172852 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.6360745429992676,0.3639254570007324 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.41323572397232056,0.5867642760276794 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.08.00 PM.png,overripe,overripe,0.0,0.4005391001701355,0.5994608998298645 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.43489736318588257,0.5651026368141174 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.11.23 PM.png,overripe,overripe,0.0,0.4047800600528717,0.5952199101448059 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.12.27 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,unripe,0.5913803577423096,0.40861964225769043,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.16 PM.png,overripe,overripe,0.3572331368923187,0.6427668333053589,0.20945028960704803 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.4806104004383087,0.5193896293640137 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.34 PM.png,overripe,overripe,0.0,0.4663829803466797,0.5336170196533203 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.4145076274871826,0.5854923725128174 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7459256052970886,0.2540743947029114 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.4788902997970581,0.5211097002029419 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,overripe,0.0,0.9186676144599915,0.08133237808942795 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.5016115307807922,0.49838846921920776 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.20.01 PM.png,overripe,overripe,0.0,0.47734203934669495,0.5226579308509827 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.20.47 PM.png,overripe,overripe,0.0,0.41172102093696594,0.5882790088653564 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.23.15 PM.png,overripe,overripe,0.0,0.9497952461242676,0.05020472779870033 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.40133020281791687,0.5986697673797607 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.04 PM.png,overripe,overripe,0.0,0.4333333373069763,0.5666666626930237 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.34 PM.png,overripe,overripe,0.0,0.4664335548877716,0.533566415309906 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.4613356292247772,0.5386644005775452 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.4000140428543091,0.5999859571456909 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.4138440191745758,0.5861559510231018 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.47.41 PM.png,overripe,overripe,0.0,0.5277735590934753,0.47222641110420227 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,overripe,0.0,0.5693193674087524,0.43068060278892517 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.04 PM.png,overripe,overripe,0.0,0.6057265400886536,0.39427343010902405 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.47 PM.png,overripe,overripe,0.0,0.9750521183013916,0.02494790405035019 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.54 PM.png,overripe,overripe,0.0,0.7215200662612915,0.2784799039363861 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.30 PM.png,overripe,overripe,0.0,0.6327446103096008,0.3672553598880768 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4238884449005127,0.5761115550994873 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.56 PM.png,overripe,overripe,0.0,0.5161150097846985,0.4838849604129791 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,overripe,0.0,0.48262107372283936,0.5173789262771606 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.52.11 PM.png,overripe,overripe,0.0,0.4018452763557434,0.5981547236442566 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,overripe,0.01234220527112484,0.5128180384635925,0.48718199133872986 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.5342133641242981,0.4657866656780243,0.17622481286525726 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.55.08 PM.png,overripe,overripe,0.0,0.40762653946876526,0.5923734903335571 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.55.35 PM.png,overripe,overripe,0.0,0.6437617540359497,0.3562382459640503 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,overripe,0.0,0.7137935757637024,0.2862063944339752 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.4268249273300171,0.5731750726699829 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.57.46 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.58.07 PM.png,overripe,overripe,0.0,0.9886578917503357,0.011342094279825687 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.4008045792579651,0.5991954207420349 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.59.32 PM.png,overripe,overripe,0.0,0.6875770688056946,0.31242290139198303 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.00.11 PM.png,overripe,overripe,0.0,0.8064071536064148,0.1935928761959076 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.00.17 PM.png,overripe,overripe,0.0,0.4132850468158722,0.5867149233818054 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.4651840329170227,0.5348159670829773 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.4079340696334839,0.5920659303665161 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.26 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.4242115318775177,0.5757884383201599 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.02.03 PM.png,overripe,overripe,0.0,0.6609264016151428,0.3390735983848572 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.03.25 PM.png,overripe,overripe,0.07959207147359848,0.4966653287410736,0.5033347010612488 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.03.40 PM.png,overripe,overripe,0.6609894037246704,0.3390105664730072,0.39824196696281433 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.01 PM.png,overripe,overripe,0.0,0.49243393540382385,0.5075660347938538 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.08 PM.png,overripe,overripe,0.48776042461395264,0.5122395753860474,0.14185991883277893 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,overripe,0.8559536337852478,0.1440463364124298,0.31021803617477417 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.05.08 PM.png,overripe,overripe,0.6409002542495728,0.35909971594810486,0.2768278121948242 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.6610236763954163,0.33897632360458374 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.6065458655357361,0.39345410466194153 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.07.26 PM.png,overripe,overripe,0.0,0.5394606590270996,0.4605393409729004 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.09.16 PM.png,overripe,overripe,0.0,0.42116647958755493,0.5788335204124451 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,overripe,0.0,0.7619672417640686,0.23803278803825378 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.10.10 PM.png,overripe,overripe,0.0,0.5458154678344727,0.45418453216552734 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.10.45 PM.png,overripe,overripe,0.4275338649749756,0.5724661350250244,0.04108250513672829 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.05 PM.png,overripe,overripe,0.0,0.40329381823539734,0.596706211566925 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,overripe,0.0,0.6700390577316284,0.3299609124660492 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.43 PM.png,overripe,overripe,0.0,0.5707794427871704,0.4292205572128296 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.52 PM.png,overripe,overripe,0.0,0.5034315586090088,0.4965684413909912 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.11 PM.png,overripe,overripe,0.0,0.41264259815216064,0.5873574018478394 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.30651697516441345,0.62054842710495,0.37945157289505005 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.4837146997451782,0.5162853002548218 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.40526652336120605,0.594733476638794 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.54 PM.png,overripe,overripe,0.0,0.5107474327087402,0.4892525374889374 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.14.22 PM.png,overripe,overripe,0.0,0.40070074796676636,0.5992992520332336 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.40067949891090393,0.5993204712867737 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.16.28 PM.png,overripe,overripe,0.0,0.4745299816131592,0.5254700183868408 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.17.44 PM.png,overripe,overripe,0.0,0.6212266087532043,0.37877339124679565 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.18.11 PM.png,overripe,overripe,0.0,0.7976831197738647,0.20231688022613525 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.19.03 PM.png,overripe,overripe,0.0,0.40135952830314636,0.598640501499176 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.20.01 PM.png,overripe,overripe,0.0,0.4801580607891083,0.5198419690132141 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.47064274549484253,0.5293572545051575 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.22.57 PM.png,overripe,overripe,0.0,0.4058937430381775,0.5941062569618225 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.23.15 PM.png,overripe,overripe,0.0,0.9477465748786926,0.05225345119833946 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.40504133701324463,0.5949586629867554 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.28 PM.png,overripe,overripe,0.0,0.422005295753479,0.577994704246521 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.4721311330795288,0.5278688669204712 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,overripe,0.32478398084640503,0.675216019153595,0.1019367054104805 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,overripe,0.0,0.6858146786689758,0.31418535113334656 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.7296133041381836,0.2703866958618164 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.46 PM.png,overripe,overripe,0.0,0.6029106378555298,0.3970893621444702 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42363619804382324,0.5763638019561768 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.48.40 PM.png,overripe,overripe,0.0,0.42478179931640625,0.5752182006835938 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,overripe,0.0,0.7803327441215515,0.21966728568077087 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,overripe,0.0,0.5577042698860168,0.44229575991630554 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.04 PM.png,overripe,overripe,0.0,0.6127811074256897,0.3872188627719879 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.26 PM.png,overripe,overripe,0.0,0.7423098087310791,0.2576901912689209 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.54 PM.png,overripe,overripe,0.0,0.7286481261253357,0.2713518738746643 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.51.38 PM.png,overripe,overripe,0.0,0.4028184115886688,0.5971816182136536 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,overripe,0.0,0.5043723583221436,0.49562764167785645 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.37 PM.png,overripe,overripe,0.0,0.41437557339668274,0.5856244564056396 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.42 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,overripe,0.0,0.9939393997192383,0.0060606058686971664 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.6693846583366394,0.330615371465683,0.2072066217660904 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.41813144087791443,0.581868588924408 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.9122645258903503,0.08773548901081085,0.26031336188316345 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.41116032004356384,0.5888397097587585 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.55.47 PM.png,overripe,overripe,0.0,0.638323962688446,0.36167603731155396 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.56.21 PM.png,overripe,overripe,0.0,0.4089197814464569,0.5910802483558655 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,overripe,0.5889270305633545,0.4110729694366455,0.1820257008075714 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,overripe,0.3034626543521881,0.6965373754501343,0.11491396278142929 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40193912386894226,0.5980609059333801 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.0,0.4894222617149353,0.5105777382850647 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.4645213782787323,0.5354785919189453 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.47272172570228577,0.5272783041000366 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.00.31 PM.png,overripe,overripe,0.0,0.4520655572414398,0.5479344129562378 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.4082038402557373,0.5917961597442627 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.4247381091117859,0.5752618908882141 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.32 PM.png,overripe,overripe,0.0,0.646916389465332,0.3530835807323456 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,overripe,0.0,0.6281049251556396,0.37189507484436035 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.52 PM.png,overripe,overripe,0.0,0.40581968426704407,0.5941803455352783 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.03.30 PM.png,overripe,overripe,0.0,0.4112621545791626,0.5887378454208374 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.03.55 PM.png,overripe,overripe,0.0,0.6219345927238464,0.37806543707847595 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.05.02 PM.png,overripe,overripe,0.9626431465148926,0.03735683485865593,0.3440455198287964 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.5726621150970459,0.4273378551006317 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.06.21 PM.png,overripe,overripe,0.0,0.42410144209861755,0.5758985877037048 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,overripe,0.0,0.6618884801864624,0.3381114900112152 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.07.39 PM.png,overripe,overripe,0.0,0.5139515995979309,0.4860484302043915 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.08.22 PM.png,overripe,overripe,0.0,0.43503788113594055,0.5649621486663818 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,overripe,0.0,0.5305560827255249,0.4694439172744751 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.10 PM.png,overripe,overripe,0.0,0.5282238721847534,0.47177615761756897 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.4272027015686035,0.5727972984313965 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.41 PM.png,overripe,overripe,0.0,0.4405330717563629,0.5594669580459595 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.46970152854919434,0.5302984714508057 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.11.17 PM.png,overripe,overripe,0.0,0.42985010147094727,0.5701498985290527 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.11 PM.png,overripe,overripe,0.0,0.4182077646255493,0.5817922353744507 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.40 PM.png,overripe,overripe,0.0,0.4010657072067261,0.5989342927932739 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.49 PM.png,overripe,unripe,0.8449491858482361,0.1550508439540863,0.0 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.29050156474113464,0.6077450513839722,0.39225494861602783 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.4275326132774353,0.5724673867225647 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.45875588059425354,0.5412440896034241 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.4153866171836853,0.5846133828163147 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.466950386762619,0.5330496430397034 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.18.07 PM.png,overripe,overripe,0.0,0.8912680149078369,0.10873197764158249 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.5035802125930786,0.4964197874069214 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.19.42 PM.png,overripe,overripe,0.31821343302726746,0.6030164361000061,0.3969835638999939 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.4437985420227051,0.5562014579772949 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.4701445996761322,0.5298554301261902 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.22.37 PM.png,overripe,overripe,0.0,0.764580488204956,0.23541948199272156 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.23.24 PM.png,overripe,overripe,0.0,0.40583088994026184,0.5941690802574158 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4034782648086548,0.5965217351913452 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.25.12 PM.png,overripe,overripe,0.0,0.40597623586654663,0.5940237641334534 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,overripe,0.0,0.5721859931945801,0.4278140068054199 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.50.20 PM.png,overripe,overripe,0.0,0.41233327984809875,0.5876666903495789 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,overripe,0.0,0.6343944072723389,0.3656056225299835 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4368506669998169,0.5631493330001831 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.56 PM.png,overripe,overripe,0.0,0.5213042497634888,0.47869572043418884 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,overripe,0.0,0.4859375059604645,0.5140625238418579 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,overripe,0.0,0.48260697722435,0.5173929929733276 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,overripe,0.0,0.48607197403907776,0.5139279961585999 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.7725262641906738,0.22747370600700378,0.20452077686786652 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.37 PM.png,overripe,overripe,0.0,0.5106898546218872,0.4893101751804352 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.43 PM.png,overripe,overripe,0.0,0.6213203072547913,0.37867972254753113 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.40853962302207947,0.5914603471755981 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.55.21 PM.png,overripe,overripe,0.0,0.421293169260025,0.5787068605422974 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.55.28 PM.png,overripe,overripe,0.0,0.40061619877815247,0.5993838310241699 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,overripe,0.5518074631690979,0.4481925070285797,0.1793033480644226 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.59.44 PM.png,overripe,overripe,0.2609780728816986,0.6119973659515381,0.3880026340484619 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.4744454622268677,0.5255545377731323 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4752677083015442,0.5247322916984558 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.42761048674583435,0.5723894834518433 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.02.38 PM.png,overripe,overripe,0.09077959507703781,0.6676390171051025,0.33236098289489746 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.03.17 PM.png,overripe,overripe,0.22342145442962646,0.5150956511497498,0.48490437865257263 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.03.40 PM.png,overripe,overripe,0.3758748471736908,0.5550387501716614,0.4449612498283386 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.05.12 PM.png,overripe,overripe,0.0,0.4125745892524719,0.5874254107475281 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.05.52 PM.png,overripe,overripe,0.0,0.8479008674621582,0.152099147439003 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.06.13 PM.png,overripe,overripe,0.0,0.46558400988578796,0.5344159603118896 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.06.40 PM.png,overripe,overripe,0.0,0.41947484016418457,0.5805251598358154 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.07.04 PM.png,overripe,overripe,0.0,0.5942515134811401,0.40574848651885986 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,overripe,0.0,0.7605412602424622,0.23945875465869904 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5802062749862671,0.4197937250137329 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.10.27 PM.png,overripe,overripe,0.0,0.5028048157691956,0.49719518423080444 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.46691474318504333,0.533085286617279 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.11.52 PM.png,overripe,overripe,0.0,0.5271773338317871,0.4728226363658905 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.12.49 PM.png,overripe,unripe,0.5414751768112183,0.45852479338645935,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.13.10 PM.png,overripe,ripe,0.003242116654291749,0.9967578649520874,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.4048907160758972,0.5951092839241028 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.43199241161346436,0.5680075883865356 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.4145185649394989,0.5854814052581787 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,overripe,0.0,0.8464685082435608,0.1535315066576004 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.17.27 PM.png,overripe,overripe,0.0,0.4293381869792938,0.5706617832183838 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4541700482368469,0.5458299517631531 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.20 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.43190059065818787,0.5680993795394897 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.4725949466228485,0.5274050235748291 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,overripe,0.0,0.6571125388145447,0.3428874909877777 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.25.12 PM.png,overripe,overripe,0.0,0.4033161401748657,0.5966838598251343 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.25.18 PM.png,overripe,overripe,0.0,0.4097762107849121,0.5902237892150879 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.26.01 PM.png,overripe,overripe,0.0,0.8792394995689392,0.120760478079319 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.26.21 PM.png,overripe,overripe,0.0,0.5030471682548523,0.4969528317451477 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.22 PM.png,overripe,overripe,0.0,0.6925570964813232,0.30744290351867676 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.7158554792404175,0.2841445207595825 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,overripe,0.0,0.5709831714630127,0.4290168285369873 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.52 PM.png,overripe,overripe,0.0,0.4097576439380646,0.590242326259613 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.41648703813552856,0.5835129618644714 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.07 PM.png,overripe,overripe,0.0,0.5831494331359863,0.4168505370616913 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.46 PM.png,overripe,overripe,0.0,0.410361647605896,0.589638352394104 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.50.15 PM.png,overripe,overripe,0.0,0.4465591609477997,0.5534408688545227 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.52.21 PM.png,overripe,overripe,0.0,0.4087159037590027,0.5912840962409973 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,overripe,0.0,0.48406460881233215,0.5159353613853455 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.21 PM.png,overripe,overripe,0.0,0.42180338501930237,0.57819664478302 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.41 PM.png,overripe,overripe,0.0,0.6536629796028137,0.3463369905948639 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.53 PM.png,overripe,overripe,0.0,0.68889981508255,0.31110018491744995 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.56.06 PM.png,overripe,overripe,0.0,0.5656326413154602,0.4343673586845398 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.56.11 PM.png,overripe,overripe,0.0,0.40996548533439636,0.590034544467926 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,overripe,0.0,0.5219272971153259,0.4780726730823517 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.58.07 PM.png,overripe,overripe,0.0,0.9787823557853699,0.02121761441230774 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.4010540246963501,0.5989459753036499 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.685649573802948,0.314350426197052 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.08297812193632126,0.5196046829223633,0.4803953170776367 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.00.17 PM.png,overripe,overripe,0.0,0.414227694272995,0.5857723355293274 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.4660247564315796,0.5339752435684204 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.42613303661346436,0.5738669633865356 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.02.09 PM.png,overripe,overripe,0.0,0.649239718914032,0.35076025128364563 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,overripe,0.0,0.4078253209590912,0.5921746492385864 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,overripe,0.602094829082489,0.3979051411151886,0.3919844627380371 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,overripe,0.0,0.40145033597946167,0.5985496640205383 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.05.08 PM.png,overripe,overripe,0.5724209547042847,0.42757904529571533,0.3103591799736023 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.05.21 PM.png,overripe,overripe,0.0,0.40394917130470276,0.5960508584976196 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.06.05 PM.png,overripe,overripe,0.0,0.5620594024658203,0.4379405975341797 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.06.27 PM.png,overripe,overripe,0.0,0.4186296761035919,0.5813703536987305 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,overripe,0.0,0.6617900729179382,0.33820995688438416 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.08.59 PM.png,overripe,overripe,0.0,0.4171282649040222,0.5828717350959778 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.09 PM.png,overripe,overripe,0.0,0.4496324062347412,0.5503675937652588 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.22 PM.png,overripe,overripe,0.0,0.4301176965236664,0.569882333278656 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.29 PM.png,overripe,overripe,0.06198154762387276,0.48982343077659607,0.5101765394210815 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.576330840587616,0.42366915941238403 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.27 PM.png,overripe,overripe,0.0,0.5033921003341675,0.4966078996658325 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.32 PM.png,overripe,overripe,0.0,0.427558034658432,0.5724419355392456 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.4713280200958252,0.5286719799041748 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.11.00 PM.png,overripe,overripe,0.0,0.4976867139339447,0.5023132562637329 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.11.27 PM.png,overripe,overripe,0.0,0.6743411421775818,0.3256588280200958 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.40013471245765686,0.5998652577400208 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.45356303453445435,0.5464369654655457 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,overripe,0.088561050593853,0.5813860297203064,0.4186139702796936 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.4296588897705078,0.5703411102294922 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.4594477713108063,0.5405522584915161 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.4012683629989624,0.5987316370010376 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,overripe,0.0,0.8495321869850159,0.15046779811382294 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7316058874130249,0.2683940827846527 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.46613553166389465,0.533864438533783 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,overripe,0.0034683304838836193,0.8971059322357178,0.10289406031370163 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.18.07 PM.png,overripe,overripe,0.0,0.8816095590591431,0.11839045584201813 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.18.57 PM.png,overripe,overripe,0.0,0.4202258884906769,0.5797740817070007 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.19.08 PM.png,overripe,overripe,0.0,0.41767174005508423,0.5823282599449158 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.19.28 PM.png,overripe,overripe,0.0,0.4109136164188385,0.5890863537788391 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4562079608440399,0.5437920093536377 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.21.05 PM.png,overripe,overripe,0.0,0.4005288779735565,0.5994710922241211 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.44112396240234375,0.5588760375976562 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.4830312430858612,0.5169687271118164 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.22.50 PM.png,overripe,overripe,0.0,0.7423622012138367,0.25763779878616333 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4021754264831543,0.5978245735168457 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.18 PM.png,overripe,overripe,0.0,0.41176313161849976,0.5882368683815002 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.40668758749961853,0.5933123826980591 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.400073766708374,0.599926233291626 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,overripe,0.39959874749183655,0.6004012823104858,0.10363361984491348 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.27.11 PM.png,overripe,overripe,0.0,0.8019285202026367,0.19807149469852448 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,overripe,0.0,0.7002153992652893,0.2997846305370331 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.28.09 PM.png,overripe,overripe,0.9500781893730164,0.049921829253435135,0.11470714211463928 +banana/test/ripe/Screen Shot 2018-06-12 at 10.00.37 PM.png,ripe,overripe,0.0,0.4000312089920044,0.5999687910079956 +banana/test/ripe/Screen Shot 2018-06-12 at 10.01.07 PM.png,ripe,overripe,0.0,0.4139438271522522,0.5860561728477478 +banana/test/ripe/Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.40040484070777893,0.5995951890945435 +banana/test/ripe/Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.41472452878952026,0.5852754712104797 +banana/test/ripe/Screen Shot 2018-06-12 at 10.05.54 PM.png,ripe,overripe,0.0,0.9981600642204285,0.0018399464897811413 +banana/test/ripe/Screen Shot 2018-06-12 at 10.06.38 PM.png,ripe,overripe,0.0,0.4054895341396332,0.5945104956626892 +banana/test/ripe/Screen Shot 2018-06-12 at 10.07.21 PM.png,ripe,overripe,0.0,0.4002189636230469,0.5997810363769531 +banana/test/ripe/Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4006580710411072,0.5993419289588928 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.41147440671920776,0.5885255932807922 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.10 PM.png,ripe,overripe,0.0,0.4000700116157532,0.5999299883842468 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.15 PM.png,ripe,overripe,0.0,0.41085872054100037,0.589141309261322 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4139981269836426,0.5860018730163574 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4271842837333679,0.5728157162666321 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.33 PM.png,ripe,overripe,0.0,0.42782512307167053,0.5721749067306519 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.47 PM.png,ripe,overripe,0.0,0.41406604647636414,0.5859339833259583 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.58 PM.png,ripe,overripe,0.026932531967759132,0.4806954264640808,0.5193045735359192 +banana/test/ripe/Screen Shot 2018-06-12 at 9.40.26 PM.png,ripe,overripe,0.0,0.40822741389274597,0.5917725563049316 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.400695264339447,0.599304735660553 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.30 PM.png,ripe,overripe,0.0,0.4028119742870331,0.5971880555152893 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.4000893235206604,0.5999106764793396 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4002116620540619,0.5997883081436157 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.53 PM.png,ripe,overripe,0.0,0.40223127603530884,0.5977687239646912 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.59 PM.png,ripe,overripe,0.0,0.4069928824901581,0.5930070877075195 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.02 PM.png,ripe,overripe,0.0,0.4004940688610077,0.5995059609413147 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.15 PM.png,ripe,overripe,0.0,0.40818899869918823,0.5918110013008118 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.22 PM.png,ripe,overripe,0.0,0.4030654728412628,0.5969344973564148 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.34 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4016314148902893,0.5983685851097107 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.400256484746933,0.5997434854507446 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.55 PM.png,ripe,overripe,0.0,0.4279852509498596,0.5720147490501404 +banana/test/ripe/Screen Shot 2018-06-12 at 9.49.00 PM.png,ripe,overripe,0.0,0.4020763337612152,0.5979236364364624 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.4005866050720215,0.5994133949279785 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.44 PM.png,ripe,overripe,0.0,0.40139251947402954,0.5986074805259705 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.42801904678344727,0.5719809532165527 +banana/test/ripe/Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.4012305438518524,0.59876948595047 +banana/test/ripe/Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.4160788357257843,0.5839211940765381 +banana/test/ripe/Screen Shot 2018-06-12 at 9.54.35 PM.png,ripe,overripe,0.0,0.4004150629043579,0.5995849370956421 +banana/test/ripe/Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40353554487228394,0.5964644551277161 +banana/test/ripe/Screen Shot 2018-06-12 at 9.55.46 PM.png,ripe,overripe,0.0,0.4185090959072113,0.5814909338951111 +banana/test/ripe/Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.40345168113708496,0.596548318862915 +banana/test/ripe/Screen Shot 2018-06-12 at 9.56.03 PM.png,ripe,overripe,0.0,0.43586570024490356,0.5641342997550964 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.17 PM.png,ripe,overripe,0.0,0.40110769867897034,0.598892331123352 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.41618576645851135,0.583814263343811 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.31 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.58.36 PM.png,ripe,overripe,0.0,0.4396243393421173,0.5603756308555603 +banana/test/ripe/Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.40369537472724915,0.5963045954704285 +banana/test/ripe/Screen Shot 2018-06-12 at 9.59.48 PM.png,ripe,overripe,0.0,0.4015810489654541,0.5984189510345459 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.07 PM.png,ripe,overripe,0.0,0.4027537405490875,0.5972462296485901 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.12 PM.png,ripe,overripe,0.3775460720062256,0.4806671142578125,0.5193328857421875 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.4005703628063202,0.5994296669960022 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.41455382108688354,0.5854461789131165 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.13 PM.png,ripe,overripe,0.0,0.40020835399627686,0.5997916460037231 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4169490933418274,0.5830509066581726 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,overripe,0.8264207243919373,0.17357930541038513,0.33499813079833984 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.40381476283073425,0.5961852073669434 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4180341362953186,0.5819658637046814 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.4053783416748047,0.5946216583251953 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.38.29 PM.png,ripe,overripe,0.0,0.42406782507896423,0.5759321451187134 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.39.53 PM.png,ripe,overripe,0.0,0.40038973093032837,0.5996102690696716 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.40.43 PM.png,ripe,overripe,0.0,0.4005717933177948,0.5994281768798828 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.40019455552101135,0.599805474281311 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.42.29 PM.png,ripe,overripe,0.0,0.403168261051178,0.596831738948822 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.43.27 PM.png,ripe,overripe,0.0,0.4152435064315796,0.5847564935684204 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4017592966556549,0.5982407331466675 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.4116499423980713,0.5883500576019287 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.4048355519771576,0.59516441822052 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.4001210331916809,0.5998789668083191 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.48 PM.png,ripe,overripe,0.0,0.4138261079788208,0.5861738920211792 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.49.15 PM.png,ripe,overripe,0.0,0.4048892855644226,0.5951107144355774 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.49.37 PM.png,ripe,overripe,0.0,0.404979407787323,0.595020592212677 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.4267714023590088,0.5732285976409912 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.50.53 PM.png,ripe,overripe,0.0,0.40127241611480713,0.5987275838851929 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.55.19 PM.png,ripe,overripe,0.0,0.409139484167099,0.5908605456352234 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.40020042657852173,0.5997995734214783 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4131985306739807,0.5868014693260193 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.07 PM.png,ripe,overripe,0.0,0.4062332212924957,0.5937667489051819 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.40785467624664307,0.5921453237533569 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.4038906693458557,0.5961093306541443 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.4006115198135376,0.5993884801864624 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.12 PM.png,ripe,overripe,0.0,0.4053735136985779,0.5946264863014221 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.17 PM.png,ripe,overripe,0.0,0.4001081883907318,0.5998917818069458 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,overripe,0.9497051239013672,0.05029488727450371,0.2152465581893921 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.00.00 PM.png,ripe,overripe,0.0,0.4001714289188385,0.5998285412788391 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.4005454480648041,0.5994545221328735 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.07 PM.png,ripe,overripe,0.0,0.4072098731994629,0.5927901268005371 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4105253219604492,0.5894746780395508 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.52 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.40367868542671204,0.5963213443756104 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000624716281891,0.5999374985694885 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.36 PM.png,ripe,overripe,0.0,0.4005030393600464,0.5994969606399536 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.4101296067237854,0.5898703932762146 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.05.20 PM.png,ripe,overripe,0.0,0.44718149304389954,0.5528185367584229 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.24 PM.png,ripe,overripe,0.0,0.4069942831993103,0.5930057168006897 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.33 PM.png,ripe,overripe,0.9148285388946533,0.08517148345708847,0.322907030582428 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.59 PM.png,ripe,overripe,0.0,0.4041949510574341,0.5958050489425659 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.40789616107940674,0.5921038389205933 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.41.57 PM.png,ripe,overripe,0.0,0.44321322441101074,0.5567867755889893 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.4001104235649109,0.5998895764350891 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.4003518223762512,0.5996481776237488 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.48 PM.png,ripe,overripe,0.0,0.40582332015037537,0.5941766500473022 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.27 PM.png,ripe,overripe,0.0,0.40947920083999634,0.5905207991600037 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4014472961425781,0.5985527038574219 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.4002085328102112,0.5997914671897888 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.49.23 PM.png,ripe,overripe,0.0,0.40542060136795044,0.5945793986320496 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.49.45 PM.png,ripe,overripe,0.0,0.40298762917518616,0.5970124006271362 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.50.12 PM.png,ripe,overripe,0.0,0.41763949394226074,0.5823605060577393 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.50.44 PM.png,ripe,overripe,0.0,0.4015171527862549,0.5984828472137451 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.41270002722740173,0.5873000025749207 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.37 PM.png,ripe,overripe,0.0,0.45818421244621277,0.5418157577514648 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.45 PM.png,ripe,overripe,0.6204873323440552,0.3795126974582672,0.33108481764793396 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.54.02 PM.png,ripe,overripe,0.0,0.4002782702445984,0.5997217297554016 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40092021226882935,0.5990797877311707 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.55.19 PM.png,ripe,overripe,0.0,0.41022560000419617,0.5897744297981262 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.56.03 PM.png,ripe,overripe,0.0,0.43435338139533997,0.5656466484069824 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4002508223056793,0.5997492074966431 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.57.08 PM.png,ripe,overripe,0.0,0.41268590092658997,0.5873140692710876 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.59.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,overripe,0.755244255065918,0.24475575983524323,0.301241010427475 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.42434948682785034,0.5756505131721497 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.00.49 PM.png,ripe,overripe,0.0,0.4013291597366333,0.5986708402633667 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.01.27 PM.png,ripe,overripe,0.0,0.4010368883609772,0.5989631414413452 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.400322824716568,0.5996771454811096 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.413273423910141,0.5867265462875366 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.13 PM.png,ripe,overripe,0.0,0.40043187141418457,0.5995681285858154 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.35 PM.png,ripe,overripe,0.0,0.40040838718414307,0.5995916128158569 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.54 PM.png,ripe,overripe,0.0,0.996773898601532,0.0032261242158710957 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.06.59 PM.png,ripe,overripe,0.0,0.4045007526874542,0.5954992175102234 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.4057241380214691,0.5942758321762085 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.38.29 PM.png,ripe,overripe,0.0,0.4075471758842468,0.5924528241157532 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.00 PM.png,ripe,overripe,0.0,0.4001399278640747,0.5998600721359253 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.43681058287620544,0.5631893873214722 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4171135425567627,0.5828864574432373 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.26 PM.png,ripe,overripe,0.0,0.40522974729537964,0.5947702527046204 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.38 PM.png,ripe,overripe,0.0,0.40840309858322144,0.5915969014167786 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.49 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.41.18 PM.png,ripe,overripe,0.0,0.4006958305835724,0.5993041396141052 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.41.38 PM.png,ripe,overripe,0.5690962076187134,0.4309037923812866,0.0995408222079277 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.42.29 PM.png,ripe,overripe,0.0,0.4003923833370209,0.5996075868606567 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.40509286522865295,0.5949071049690247 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.44.49 PM.png,ripe,overripe,0.0,0.40209969878196716,0.5979003310203552 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.40055105090141296,0.5994489192962646 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.45.28 PM.png,ripe,overripe,0.0,0.40087732672691345,0.5991226434707642 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,overripe,0.9340673089027405,0.06593269109725952,0.22156643867492676 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.47.27 PM.png,ripe,overripe,0.0,0.4096314311027527,0.5903685688972473 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.48.04 PM.png,ripe,overripe,0.0,0.6274720430374146,0.37252795696258545 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.48.39 PM.png,ripe,overripe,0.0,0.41634273529052734,0.5836572647094727 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.49.23 PM.png,ripe,overripe,0.0,0.40530112385749817,0.5946988463401794 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.50.53 PM.png,ripe,overripe,0.0,0.4001989960670471,0.5998010039329529 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.51.44 PM.png,ripe,overripe,0.7888066172599792,0.21119339764118195,0.23615087568759918 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.51.58 PM.png,ripe,overripe,0.0,0.40021276473999023,0.5997872352600098 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.26 PM.png,ripe,overripe,0.0,0.40014350414276123,0.5998564958572388 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4103735089302063,0.5896264910697937 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.50 PM.png,ripe,overripe,0.0,0.4002915918827057,0.5997083783149719 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.4014979302883148,0.5985020399093628 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.53.22 PM.png,ripe,overripe,0.0,0.41162022948265076,0.5883797407150269 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.07 PM.png,ripe,overripe,0.0,0.4022936224937439,0.5977063775062561 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.18 PM.png,ripe,overripe,0.30633553862571716,0.6936644315719604,0.013877339661121368 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40098142623901367,0.5990185737609863 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.55.13 PM.png,ripe,overripe,0.0,0.40001848340034485,0.5999814867973328 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4014565050601959,0.5985435247421265 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.56.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.400156706571579,0.5998432636260986 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.57.38 PM.png,ripe,overripe,0.0,0.4001062214374542,0.5998938083648682 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.57.42 PM.png,ripe,overripe,0.0,0.4002130925655365,0.5997869372367859 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.58.49 PM.png,ripe,overripe,0.0,0.4337926506996155,0.5662073493003845 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,overripe,0.7497923970222473,0.2502076029777527,0.3026432991027832 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.00.07 PM.png,ripe,overripe,0.0,0.4021512269973755,0.5978487730026245 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.4004470705986023,0.5995529294013977 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.40435999631881714,0.5956400036811829 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.40006473660469055,0.5999352335929871 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,overripe,0.7987644672393799,0.20123550295829773,0.35494768619537354 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.07.21 PM.png,ripe,overripe,0.0,0.40009069442749023,0.5999093055725098 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.41968414187431335,0.5803158283233643 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.40597549080848694,0.5940245389938354 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.4001466929912567,0.5998533368110657 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4434317946434021,0.5565682053565979 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.43770283460617065,0.5622971653938293 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.41.57 PM.png,ripe,overripe,0.0,0.44230806827545166,0.5576919317245483 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.4114241302013397,0.5885758996009827 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,overripe,0.9305617213249207,0.06943826377391815,0.22133301198482513 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4102540612220764,0.5897459387779236 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.40 PM.png,ripe,overripe,0.0,0.4076647162437439,0.5923352837562561 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.55 PM.png,ripe,overripe,0.4978123903274536,0.5021876096725464,0.0848536491394043 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4014912545681,0.5985087752342224 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.46 PM.png,ripe,overripe,0.0,0.40003448724746704,0.599965512752533 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.48.04 PM.png,ripe,overripe,0.0,0.6210048198699951,0.3789951503276825 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.48.54 PM.png,ripe,overripe,0.0,0.4006514847278595,0.5993485450744629 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.40049758553504944,0.5995023846626282 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.51.36 PM.png,ripe,overripe,0.0,0.4018602669239044,0.5981397032737732 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4104996919631958,0.5895003080368042 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.30 PM.png,ripe,overripe,0.0,0.4139896035194397,0.5860103964805603 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.41 PM.png,ripe,overripe,0.0,0.41076308488845825,0.5892369151115417 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.46 PM.png,ripe,overripe,0.0,0.4045611023902893,0.5954388976097107 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40102341771125793,0.5989765524864197 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.55.27 PM.png,ripe,overripe,0.014927398413419724,0.48118311166763306,0.5188168883323669 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.55.33 PM.png,ripe,overripe,0.0,0.40025556087493896,0.599744439125061 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.56.23 PM.png,ripe,overripe,0.0,0.40862834453582764,0.5913716554641724 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.56.56 PM.png,ripe,overripe,0.0,0.4003240168094635,0.5996760129928589 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.57.42 PM.png,ripe,overripe,0.0,0.40003105998039246,0.5999689698219299 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.40730810165405273,0.5926918983459473 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,overripe,0.9698076248168945,0.030192390084266663,0.22056439518928528 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.21 PM.png,ripe,overripe,0.0,0.40062615275382996,0.5993738174438477 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.37 PM.png,ripe,overripe,0.0,0.40001925826072693,0.5999807119369507 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.4208853542804718,0.5791146755218506 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.01.52 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.01.58 PM.png,ripe,overripe,0.0,0.4050024747848511,0.5949975252151489 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.02.01 PM.png,ripe,overripe,0.0,0.4735628664493561,0.5264371633529663 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.02.36 PM.png,ripe,overripe,0.0,0.40058237314224243,0.5994176268577576 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4000648558139801,0.5999351143836975 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4030042588710785,0.5969957113265991 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.06.07 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4190528988838196,0.5809471011161804 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.38.22 PM.png,ripe,overripe,0.0,0.4092729389667511,0.5907270312309265 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4025892913341522,0.5974107384681702 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.39.58 PM.png,ripe,overripe,0.10192081332206726,0.5131067037582397,0.48689329624176025 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.22 PM.png,ripe,overripe,0.0,0.4000823199748993,0.5999176502227783 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.32 PM.png,ripe,overripe,0.0,0.4001457691192627,0.5998542308807373 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.40084365010261536,0.5991563200950623 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.38 PM.png,ripe,overripe,0.7023411989212036,0.2976588308811188,0.13630683720111847 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.40009814500808716,0.5999018549919128 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.40638113021850586,0.5936188697814941 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.32 PM.png,ripe,overripe,0.0,0.40571102499961853,0.5942889451980591 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.59 PM.png,ripe,overripe,0.0,0.40027859807014465,0.5997214317321777 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4007018208503723,0.5992981791496277 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.41151994466781616,0.5884800553321838 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.48.14 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.49.15 PM.png,ripe,overripe,0.0,0.40585869550704956,0.5941413044929504 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.49.45 PM.png,ripe,overripe,0.0,0.4026698172092438,0.5973302125930786 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.51.00 PM.png,ripe,overripe,0.0,0.46175727248191833,0.538242757320404 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.52.06 PM.png,ripe,overripe,0.40994882583618164,0.5302994847297668,0.46970051527023315 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.4011770188808441,0.5988230109214783 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.22 PM.png,ripe,overripe,0.0,0.41138944029808044,0.5886105298995972 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.41 PM.png,ripe,overripe,0.0,0.4070480763912201,0.5929519534111023 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.54.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.42 PM.png,ripe,overripe,0.0,0.4000239074230194,0.5999760627746582 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.46 PM.png,ripe,overripe,0.0,0.41300415992736816,0.5869958400726318 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4004939794540405,0.5995060205459595 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4012807607650757,0.5987192392349243 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.58.07 PM.png,ripe,overripe,0.0,0.40649229288101196,0.593507707118988 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.40811872482299805,0.591881275177002 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.59.02 PM.png,ripe,overripe,0.0,0.40014132857322693,0.5998586416244507 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.00 PM.png,ripe,overripe,0.0,0.4020969569683075,0.5979030728340149 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.4271935522556305,0.5728064179420471 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.49 PM.png,ripe,overripe,0.0,0.40204480290412903,0.5979552268981934 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4276435077190399,0.5723564624786377 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.02.01 PM.png,ripe,overripe,0.0,0.4637008309364319,0.5362991690635681 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4019751250743866,0.5980249047279358 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.40197595953941345,0.5980240106582642 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4200434684753418,0.5799565315246582 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.4056112766265869,0.5943887233734131 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.24 PM.png,ripe,overripe,0.0,0.4064786434173584,0.5935213565826416 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.38 PM.png,ripe,overripe,0.0,0.40731164813041687,0.5926883220672607 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.07.29 PM.png,ripe,overripe,0.0,0.41717466711997986,0.5828253626823425 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4023759067058563,0.5976240634918213 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.4139963388442993,0.5860036611557007 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.38.38 PM.png,ripe,overripe,0.0,0.43694809079170227,0.5630519390106201 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.00 PM.png,ripe,overripe,0.0,0.40191230177879333,0.5980876684188843 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.33 PM.png,ripe,overripe,0.0,0.42805027961730957,0.5719497203826904 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.47 PM.png,ripe,overripe,0.0,0.4152317941188812,0.5847682356834412 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.40.43 PM.png,ripe,overripe,0.0,0.401864618062973,0.5981354117393494 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.40.49 PM.png,ripe,overripe,0.0,0.40170618891716003,0.5982938408851624 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4699949622154236,0.5300050377845764 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.4023744761943817,0.5976254940032959 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.42.03 PM.png,ripe,overripe,0.7334650158882141,0.2665349841117859,0.24821482598781586 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.4028699994087219,0.5971300005912781 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4019871652126312,0.5980128645896912 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.43.53 PM.png,ripe,overripe,0.0,0.4040296971797943,0.5959702730178833 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.44.06 PM.png,ripe,overripe,0.0,0.4114595651626587,0.5885404348373413 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.45.02 PM.png,ripe,overripe,0.0,0.40212687849998474,0.5978731513023376 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.40717819333076477,0.5928218364715576 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.40581855177879333,0.5941814184188843 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.40 PM.png,ripe,overripe,0.0,0.4087916314601898,0.5912083387374878 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.40167108178138733,0.5983289480209351 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.40194594860076904,0.598054051399231 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.48.54 PM.png,ripe,overripe,0.0,0.40209999680519104,0.5979000329971313 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.50.38 PM.png,ripe,overripe,0.0,0.4023190140724182,0.5976809859275818 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.42962443828582764,0.5703755617141724 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.4167494773864746,0.5832505226135254 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4016675651073456,0.5983324646949768 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.00.12 PM.png,ripe,overripe,0.2552977502346039,0.4629673361778259,0.5370326638221741 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4285447895526886,0.571455180644989 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.4003638029098511,0.5996361970901489 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.4043201208114624,0.5956798791885376 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4000973105430603,0.5999026894569397 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.04.54 PM.png,ripe,overripe,0.0,0.40031155943870544,0.5996884107589722 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.05.20 PM.png,ripe,overripe,0.0,0.4445528984069824,0.5554471015930176 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.42059940099716187,0.5794005990028381 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.38.51 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.40.38 PM.png,ripe,overripe,0.0,0.4084869921207428,0.5915130376815796 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.41.30 PM.png,ripe,overripe,0.0,0.40277695655822754,0.5972230434417725 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.40011945366859436,0.599880576133728 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.42.18 PM.png,ripe,overripe,0.0,0.40040501952171326,0.5995949506759644 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.43.27 PM.png,ripe,overripe,0.0,0.4146549701690674,0.5853450298309326 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.44.19 PM.png,ripe,overripe,0.0,0.4199175238609314,0.5800824761390686 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4001437723636627,0.5998561978340149 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.45.34 PM.png,ripe,overripe,0.0,0.400233656167984,0.5997663140296936 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.4001203775405884,0.5998796224594116 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4015525281429291,0.5984474420547485 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.48.14 PM.png,ripe,overripe,0.0,0.40046370029449463,0.5995362997055054 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.51.58 PM.png,ripe,overripe,0.0,0.40013375878334045,0.5998662114143372 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.4125412106513977,0.5874587893486023 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.53.30 PM.png,ripe,overripe,0.0,0.4163532555103302,0.5836467146873474 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.53.46 PM.png,ripe,overripe,0.0,0.40497374534606934,0.5950262546539307 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4016713798046112,0.5983286499977112 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.40228211879730225,0.5977178812026978 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.55.27 PM.png,ripe,overripe,0.0,0.4422408640384674,0.557759165763855 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.55.42 PM.png,ripe,overripe,0.0,0.40007245540618896,0.599927544593811 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.56.16 PM.png,ripe,overripe,0.0,0.40187370777130127,0.5981262922286987 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4004635810852051,0.5995364189147949 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.41428685188293457,0.5857131481170654 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4071093201637268,0.5928906798362732 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.21 PM.png,ripe,overripe,0.0,0.402984082698822,0.597015917301178 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.40065816044807434,0.599341869354248 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.42612653970718384,0.5738734602928162 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.01.27 PM.png,ripe,overripe,0.0,0.40079957246780396,0.599200427532196 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.04.54 PM.png,ripe,overripe,0.0,0.4003519117832184,0.5996480584144592 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.4003278911113739,0.5996721386909485 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,overripe,0.9469749927520752,0.053025007247924805,0.3173115849494934 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.403739333152771,0.596260666847229 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4006580412387848,0.5993419885635376 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.15 PM.png,ripe,overripe,0.0,0.41085872054100037,0.589141309261322 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.22 PM.png,ripe,overripe,0.0,0.40788936614990234,0.5921106338500977 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.51 PM.png,ripe,overripe,0.0,0.40019556879997253,0.5998044610023499 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4148893654346466,0.585110604763031 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.40.02 PM.png,ripe,overripe,0.0,0.40134191513061523,0.5986580848693848 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.40.10 PM.png,ripe,overripe,0.0,0.40904998779296875,0.5909500122070312 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4752107560634613,0.5247892141342163 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.40363287925720215,0.5963671207427979 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.41 PM.png,ripe,overripe,0.0,0.4050742983818054,0.5949257016181946 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.49 PM.png,ripe,overripe,0.0,0.40208959579467773,0.5979104042053223 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.40026310086250305,0.5997368693351746 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.02 PM.png,ripe,overripe,0.0,0.4099971055984497,0.5900028944015503 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,overripe,0.7611841559410095,0.23881584405899048,0.17524206638336182 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.4057081639766693,0.5942918658256531 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4048203229904175,0.5951796770095825 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.4002339243888855,0.5997660756111145 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.18 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4016987085342407,0.5983012914657593 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.40027621388435364,0.599723756313324 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.48.21 PM.png,ripe,overripe,0.0,0.40012499690055847,0.5998749732971191 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.48.26 PM.png,ripe,overripe,0.0,0.42949768900871277,0.5705023407936096 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.49.00 PM.png,ripe,overripe,0.0,0.4020785093307495,0.5979214906692505 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.49.54 PM.png,ripe,overripe,0.0,0.41515421867370605,0.584845781326294 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.4005866050720215,0.5994133949279785 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.51.24 PM.png,ripe,overripe,0.0,0.40122008323669434,0.5987799167633057 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.51.29 PM.png,ripe,overripe,0.0,0.40287014842033386,0.5971298217773438 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.41145849227905273,0.5885415077209473 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4105552136898041,0.5894448161125183 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.45 PM.png,ripe,overripe,0.7431451678276062,0.2568548023700714,0.29027077555656433 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.41608723998069763,0.5839127898216248 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.40229859948158264,0.5977014303207397 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.55.13 PM.png,ripe,overripe,0.0,0.4000558853149414,0.5999441146850586 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4033763110637665,0.5966237187385559 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.16 PM.png,ripe,overripe,0.0,0.40154746174812317,0.5984525680541992 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.23 PM.png,ripe,overripe,0.0,0.40837010741233826,0.5916298627853394 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.48 PM.png,ripe,overripe,0.0,0.40008264780044556,0.5999173521995544 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.56 PM.png,ripe,overripe,0.0,0.40029966831207275,0.5997003316879272 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4159791171550751,0.5840208530426025 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.31 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.38 PM.png,ripe,overripe,0.0,0.4001748859882355,0.5998250842094421 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4069969952106476,0.5930030345916748 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.4037032425403595,0.5962967276573181 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.40048009157180786,0.5995199084281921 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,overripe,0.8693798184394836,0.13062018156051636,0.19094973802566528 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,overripe,0.8647128343582153,0.13528719544410706,0.2601509988307953 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.48 PM.png,ripe,overripe,0.0,0.40158116817474365,0.5984188318252563 +banana/test/unripe/1.jpg,unripe,ripe,0.11851349472999573,0.8814864754676819,0.0 +banana/test/unripe/10.jpg,unripe,overripe,0.0,0.5952472686767578,0.4047527313232422 +banana/test/unripe/100.jpg,unripe,unripe,0.33858436346054077,0.6614156365394592,0.0 +banana/test/unripe/101.jpg,unripe,unripe,0.41697612404823303,0.5830238461494446,0.0 +banana/test/unripe/102.jpg,unripe,ripe,0.25916948914527893,0.7408304810523987,0.0 +banana/test/unripe/103.jpg,unripe,ripe,0.16772247850894928,0.8322775363922119,0.0 +banana/test/unripe/104.jpg,unripe,unripe,0.2757834792137146,0.7242165207862854,0.0 +banana/test/unripe/105.jpg,unripe,overripe,0.01698598824441433,0.7073110342025757,0.2926889657974243 +banana/test/unripe/106.jpg,unripe,ripe,0.14398625493049622,0.8560137748718262,0.0 +banana/test/unripe/107.jpg,unripe,unripe,0.36091139912605286,0.6390885710716248,0.0 +banana/test/unripe/108.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/109.jpg,unripe,overripe,0.0,0.43712297081947327,0.5628770589828491 +banana/test/unripe/11.jpg,unripe,unripe,0.30912819504737854,0.6908717751502991,0.0 +banana/test/unripe/110.jpg,unripe,overripe,0.9575156569480896,0.042484331876039505,0.21493826806545258 +banana/test/unripe/111.jpg,unripe,overripe,0.12610910832881927,0.8738908767700195,0.05449650064110756 +banana/test/unripe/112.jpg,unripe,overripe,0.09839513152837753,0.9016048908233643,0.034319210797548294 +banana/test/unripe/113.jpg,unripe,overripe,0.18795405328273773,0.8120459318161011,0.012759787030518055 +banana/test/unripe/114.jpg,unripe,overripe,0.0,0.48706677556037903,0.5129331946372986 +banana/test/unripe/115.jpg,unripe,ripe,0.23475784063339233,0.7652421593666077,0.0 +banana/test/unripe/116.jpg,unripe,overripe,0.08312845230102539,0.9168715476989746,0.06419143825769424 +banana/test/unripe/117.jpg,unripe,overripe,0.40816569328308105,0.591834306716919,0.06701670587062836 +banana/test/unripe/118.jpg,unripe,unripe,0.3002995252609253,0.6997004747390747,0.0 +banana/test/unripe/119.jpg,unripe,overripe,0.14381644129753113,0.8561835885047913,0.030505774542689323 +banana/test/unripe/12.jpg,unripe,overripe,0.0021421615965664387,0.7887884378433228,0.21121157705783844 +banana/test/unripe/120.jpg,unripe,overripe,0.0,0.5582762360572815,0.4417237639427185 +banana/test/unripe/121.jpg,unripe,overripe,0.1433267593383789,0.8423076868057251,0.1576923131942749 +banana/test/unripe/122.jpg,unripe,overripe,0.21325166523456573,0.7867483496665955,0.0733976736664772 +banana/test/unripe/123.jpg,unripe,overripe,0.0,0.48706677556037903,0.5129331946372986 +banana/test/unripe/124.jpg,unripe,overripe,0.5249345898628235,0.4750653803348541,0.1283368021249771 +banana/test/unripe/125.jpg,unripe,ripe,0.07967209815979004,0.92032790184021,0.0 +banana/test/unripe/126.jpg,unripe,overripe,0.0,0.5871457457542419,0.41285425424575806 +banana/test/unripe/127.jpg,unripe,unripe,0.34404057264328003,0.65595942735672,0.0 +banana/test/unripe/128.jpg,unripe,overripe,0.9620997309684753,0.03790026158094406,0.0 +banana/test/unripe/129.jpg,unripe,overripe,0.0,0.9384536743164062,0.061546340584754944 +banana/test/unripe/13.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/130.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/131.jpg,unripe,ripe,0.16772247850894928,0.8322775363922119,0.0 +banana/test/unripe/132.jpg,unripe,unripe,0.3226715624332428,0.6773284673690796,0.0 +banana/test/unripe/133.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/134.jpg,unripe,ripe,0.21786165237426758,0.7821383476257324,0.0 +banana/test/unripe/135.jpg,unripe,overripe,0.0,0.7759930491447449,0.22400698065757751 +banana/test/unripe/136.jpg,unripe,unripe,0.3121367394924164,0.6878632307052612,0.0 +banana/test/unripe/137.jpg,unripe,overripe,0.0,0.9665772914886475,0.033422697335481644 +banana/test/unripe/138.jpg,unripe,overripe,0.01698598824441433,0.7073110342025757,0.2926889657974243 +banana/test/unripe/139.jpg,unripe,overripe,0.0,0.4364761412143707,0.5635238289833069 +banana/test/unripe/14.jpg,unripe,overripe,0.11812412738800049,0.8560697436332703,0.14393024146556854 +banana/test/unripe/140.jpg,unripe,unripe,0.3602502942085266,0.6397497057914734,0.0 +banana/test/unripe/141.jpg,unripe,unripe,0.35891103744506836,0.6410889625549316,0.0 +banana/test/unripe/142.jpg,unripe,overripe,0.0,0.4364761412143707,0.5635238289833069 +banana/test/unripe/143.jpg,unripe,ripe,0.19765886664390564,0.8023411631584167,0.0 +banana/test/unripe/144.jpg,unripe,ripe,0.24106132984161377,0.7589386701583862,0.0 +banana/test/unripe/145.jpg,unripe,overripe,0.0,0.4062001407146454,0.593799889087677 +banana/test/unripe/146.jpg,unripe,unripe,0.3602502942085266,0.6397497057914734,0.0 +banana/test/unripe/147.jpg,unripe,overripe,0.0,0.4934723973274231,0.5065276026725769 +banana/test/unripe/148.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/149.jpg,unripe,unripe,0.600759744644165,0.39924025535583496,0.0 +banana/test/unripe/15.jpg,unripe,unripe,0.6893436908721924,0.3106563091278076,0.0 +banana/test/unripe/150.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/151.jpg,unripe,overripe,0.0,0.40017297863960266,0.599826991558075 +banana/test/unripe/152.jpg,unripe,overripe,0.0,0.5375438332557678,0.4624561369419098 +banana/test/unripe/153.jpg,unripe,overripe,0.07403173297643661,0.9259682893753052,0.03421288728713989 +banana/test/unripe/154.jpg,unripe,overripe,0.0,0.45005959272384644,0.5499404072761536 +banana/test/unripe/155.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/156.jpg,unripe,overripe,0.07760400325059891,0.8421392440795898,0.15786077082157135 +banana/test/unripe/157.jpg,unripe,unripe,0.6070672273635864,0.3929327726364136,0.0 +banana/test/unripe/158.jpg,unripe,overripe,0.0,0.40934914350509644,0.5906508564949036 +banana/test/unripe/159.jpg,unripe,ripe,0.1436639279127121,0.8563360571861267,0.0 +banana/test/unripe/16.jpg,unripe,ripe,0.18313778936862946,0.8168622255325317,0.0 +banana/test/unripe/160.jpg,unripe,ripe,0.06396495550870895,0.9360350370407104,0.0 +banana/test/unripe/161.jpg,unripe,unripe,0.600759744644165,0.39924025535583496,0.0 +banana/test/unripe/162.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/163.jpg,unripe,overripe,0.0,0.5375438332557678,0.4624561369419098 +banana/test/unripe/164.jpg,unripe,overripe,0.07403173297643661,0.9259682893753052,0.03421288728713989 +banana/test/unripe/165.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/166.jpg,unripe,overripe,0.07760400325059891,0.8421392440795898,0.15786077082157135 +banana/test/unripe/167.jpg,unripe,unripe,0.27474966645240784,0.7252503037452698,0.0 +banana/test/unripe/168.jpg,unripe,unripe,0.6070672273635864,0.3929327726364136,0.0 +banana/test/unripe/169.jpg,unripe,overripe,0.0,0.40934914350509644,0.5906508564949036 +banana/test/unripe/17.jpg,unripe,ripe,0.18119095265865326,0.8188090324401855,0.0 +banana/test/unripe/170.jpg,unripe,ripe,0.1436639279127121,0.8563360571861267,0.0 +banana/test/unripe/171.jpg,unripe,ripe,0.24864156544208527,0.7513584494590759,0.0 +banana/test/unripe/172.jpg,unripe,ripe,0.21455588936805725,0.7854441404342651,0.0 +banana/test/unripe/173.jpg,unripe,overripe,0.07330873608589172,0.9266912937164307,0.05223452299833298 +banana/test/unripe/174.jpg,unripe,ripe,0.25171077251434326,0.7482892274856567,0.0 +banana/test/unripe/175.jpg,unripe,overripe,0.40214303135871887,0.5978569984436035,0.14203323423862457 +banana/test/unripe/176.jpg,unripe,overripe,0.0,0.6286426186561584,0.37135735154151917 +banana/test/unripe/177.jpg,unripe,unripe,0.26514002680778503,0.7348600029945374,0.0 +banana/test/unripe/178.jpg,unripe,unripe,0.28433048725128174,0.7156695127487183,0.0 +banana/test/unripe/179.jpg,unripe,overripe,0.0,0.5152263641357422,0.4847736656665802 +banana/test/unripe/18.jpg,unripe,ripe,0.12568987905979156,0.8743101358413696,0.0 +banana/test/unripe/180.jpg,unripe,overripe,0.0,0.497323602437973,0.5026764273643494 +banana/test/unripe/181.jpg,unripe,overripe,0.0,0.800000011920929,0.20000000298023224 +banana/test/unripe/182.jpg,unripe,overripe,0.8363176584243774,0.16368235647678375,0.23420271277427673 +banana/test/unripe/183.jpg,unripe,unripe,0.3558293879032135,0.6441705822944641,0.0 +banana/test/unripe/184.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/185.jpg,unripe,ripe,0.1165744736790657,0.8834255337715149,0.0 +banana/test/unripe/186.jpg,unripe,overripe,0.051014069467782974,0.7958828210830688,0.20411717891693115 +banana/test/unripe/187.jpg,unripe,ripe,0.1876244693994522,0.812375545501709,0.0 +banana/test/unripe/188.jpg,unripe,ripe,0.2339361011981964,0.766063928604126,0.0 +banana/test/unripe/189.jpg,unripe,overripe,0.6943153142929077,0.3056846857070923,0.2738012969493866 +banana/test/unripe/19.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/190.jpg,unripe,overripe,0.8095073699951172,0.1904926598072052,0.12059924751520157 +banana/test/unripe/191.jpg,unripe,overripe,0.0,0.7752522826194763,0.22474773228168488 +banana/test/unripe/192.jpg,unripe,overripe,0.10097037255764008,0.8990296125411987,0.0430520735681057 +banana/test/unripe/193.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/194.jpg,unripe,overripe,0.0,0.40121138095855713,0.5987886190414429 +banana/test/unripe/195.jpg,unripe,ripe,0.17931538820266724,0.8206846117973328,0.0 +banana/test/unripe/196.jpg,unripe,overripe,0.0,0.5478070378303528,0.4521929621696472 +banana/test/unripe/197.jpg,unripe,overripe,0.3495568633079529,0.6504431366920471,0.0610695481300354 +banana/test/unripe/198.jpg,unripe,unripe,0.27107226848602295,0.728927731513977,0.0 +banana/test/unripe/199.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/2.jpg,unripe,unripe,0.3073500692844391,0.6926499605178833,0.0 +banana/test/unripe/20.jpg,unripe,ripe,0.17099973559379578,0.8290002942085266,0.0 +banana/test/unripe/200.jpg,unripe,overripe,0.7045116424560547,0.2954883277416229,0.27041199803352356 +banana/test/unripe/201.jpg,unripe,overripe,0.5017203688621521,0.4982796311378479,0.2875199317932129 +banana/test/unripe/202.jpg,unripe,overripe,0.0,0.40644121170043945,0.5935587882995605 +banana/test/unripe/203.jpg,unripe,unripe,0.8070899248123169,0.1929100900888443,0.0 +banana/test/unripe/204.jpg,unripe,overripe,0.8855337500572205,0.11446626484394073,0.1932181715965271 +banana/test/unripe/205.jpg,unripe,unripe,0.2948881685733795,0.7051118016242981,0.0 +banana/test/unripe/206.jpg,unripe,overripe,0.0,0.4691907465457916,0.5308092832565308 +banana/test/unripe/207.jpg,unripe,overripe,0.04859159514307976,0.8971679210662842,0.10283205658197403 +banana/test/unripe/208.jpg,unripe,overripe,0.0,0.6682730913162231,0.33172690868377686 +banana/test/unripe/209.jpg,unripe,overripe,0.0,0.4485952854156494,0.5514047145843506 +banana/test/unripe/21.jpg,unripe,ripe,0.23293288052082062,0.7670671343803406,0.0 +banana/test/unripe/210.jpg,unripe,ripe,0.23178952932357788,0.7682104706764221,0.0 +banana/test/unripe/211.jpg,unripe,overripe,0.0,0.80140620470047,0.19859379529953003 +banana/test/unripe/212.jpg,unripe,overripe,0.8185083866119385,0.18149159848690033,0.2333897352218628 +banana/test/unripe/213.jpg,unripe,overripe,0.0,0.587817907333374,0.41218212246894836 +banana/test/unripe/214.jpg,unripe,overripe,0.0,0.618564248085022,0.381435751914978 +banana/test/unripe/215.jpg,unripe,overripe,0.0,0.4957271218299866,0.5042728781700134 +banana/test/unripe/216.jpg,unripe,overripe,0.7313368320465088,0.2686631381511688,0.02760208398103714 +banana/test/unripe/217.jpg,unripe,overripe,0.0,0.4146396517753601,0.5853603482246399 +banana/test/unripe/218.jpg,unripe,ripe,0.22904686629772186,0.7709531188011169,0.0 +banana/test/unripe/219.jpg,unripe,ripe,0.19540490210056305,0.8045951128005981,0.0 +banana/test/unripe/22.jpg,unripe,ripe,0.19545376300811768,0.8045462369918823,0.0 +banana/test/unripe/220.jpg,unripe,overripe,0.9484840035438538,0.051515985280275345,0.021649019792675972 +banana/test/unripe/221.jpg,unripe,ripe,0.0994543731212616,0.9005456566810608,0.0 +banana/test/unripe/222.jpg,unripe,ripe,0.1669519990682602,0.8330479860305786,0.0 +banana/test/unripe/223.jpg,unripe,overripe,0.0,0.9444648027420044,0.05553518980741501 +banana/test/unripe/224.jpg,unripe,overripe,0.0,0.6682730913162231,0.33172690868377686 +banana/test/unripe/225.jpg,unripe,overripe,0.051014069467782974,0.7958828210830688,0.20411717891693115 +banana/test/unripe/226.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/227.jpg,unripe,overripe,0.0,0.41324812173843384,0.5867518782615662 +banana/test/unripe/228.jpg,unripe,overripe,0.0,0.4362873136997223,0.5637127161026001 +banana/test/unripe/229.jpg,unripe,unripe,0.43228214979171753,0.5677178502082825,0.0 +banana/test/unripe/23.jpg,unripe,ripe,0.18783578276634216,0.8121642470359802,0.0 +banana/test/unripe/230.jpg,unripe,overripe,0.1608658730983734,0.839134156703949,0.031155778095126152 +banana/test/unripe/231.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/232.jpg,unripe,unripe,0.27107226848602295,0.728927731513977,0.0 +banana/test/unripe/233.jpg,unripe,overripe,0.0,0.5441314578056335,0.45586854219436646 +banana/test/unripe/234.jpg,unripe,overripe,0.0,0.4004732668399811,0.5995267629623413 +banana/test/unripe/235.jpg,unripe,unripe,0.32008546590805054,0.6799145340919495,0.0 +banana/test/unripe/236.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/237.jpg,unripe,overripe,0.9543884992599487,0.04561149701476097,0.10489620268344879 +banana/test/unripe/238.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/239.jpg,unripe,overripe,0.0,0.4691907465457916,0.5308092832565308 +banana/test/unripe/24.jpg,unripe,overripe,0.016780728474259377,0.5578687191009521,0.44213128089904785 +banana/test/unripe/240.jpg,unripe,unripe,0.30254316329956055,0.6974568367004395,0.0 +banana/test/unripe/241.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/242.jpg,unripe,unripe,0.5557692050933838,0.4442307651042938,0.0 +banana/test/unripe/243.jpg,unripe,overripe,0.05047917738556862,0.8087958097457886,0.1912042200565338 +banana/test/unripe/244.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/245.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/246.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/247.jpg,unripe,overripe,0.0,0.46911218762397766,0.5308878421783447 +banana/test/unripe/248.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/249.jpg,unripe,overripe,0.04859159514307976,0.8971679210662842,0.10283205658197403 +banana/test/unripe/25.jpg,unripe,overripe,0.0,0.671453595161438,0.328546404838562 +banana/test/unripe/250.jpg,unripe,overripe,0.8363176584243774,0.16368235647678375,0.23420271277427673 +banana/test/unripe/251.jpg,unripe,overripe,0.0,0.45251351594924927,0.5474864840507507 +banana/test/unripe/252.jpg,unripe,unripe,0.8138721585273743,0.18612782657146454,0.0 +banana/test/unripe/253.jpg,unripe,overripe,0.0,0.46130266785621643,0.5386973023414612 +banana/test/unripe/254.jpg,unripe,overripe,0.025481324642896652,0.7640907168388367,0.23590926826000214 +banana/test/unripe/255.jpg,unripe,overripe,0.0,0.4427388906478882,0.5572611093521118 +banana/test/unripe/256.jpg,unripe,overripe,0.0,0.8838931918144226,0.11610680818557739 +banana/test/unripe/257.jpg,unripe,ripe,0.24935102462768555,0.7506489753723145,0.0 +banana/test/unripe/258.jpg,unripe,unripe,0.27235227823257446,0.7276477217674255,0.0 +banana/test/unripe/259.jpg,unripe,ripe,0.23542696237564087,0.7645730376243591,0.0 +banana/test/unripe/26.jpg,unripe,overripe,0.0,0.4202655553817749,0.5797344446182251 +banana/test/unripe/260.jpg,unripe,overripe,0.0,0.4172379672527313,0.5827620029449463 +banana/test/unripe/261.jpg,unripe,ripe,0.19547511637210846,0.8045248985290527,0.0 +banana/test/unripe/262.jpg,unripe,overripe,0.593195378780365,0.4068046510219574,0.17153753340244293 +banana/test/unripe/263.jpg,unripe,overripe,0.0859709307551384,0.8628450036048889,0.13715499639511108 +banana/test/unripe/264.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/265.jpg,unripe,overripe,0.08506689965724945,0.9149330854415894,0.05054846778512001 +banana/test/unripe/266.jpg,unripe,overripe,0.0,0.4592592716217041,0.5407407283782959 +banana/test/unripe/267.jpg,unripe,overripe,0.0,0.4238473176956177,0.5761526823043823 +banana/test/unripe/268.jpg,unripe,overripe,0.0,0.45251351594924927,0.5474864840507507 +banana/test/unripe/269.jpg,unripe,unripe,0.2738255560398102,0.7261744737625122,0.0 +banana/test/unripe/27.jpg,unripe,overripe,0.0,0.579688549041748,0.42031148076057434 +banana/test/unripe/270.jpg,unripe,overripe,0.0,0.4765167236328125,0.5234832763671875 +banana/test/unripe/271.jpg,unripe,overripe,0.0,0.6273543834686279,0.37264561653137207 +banana/test/unripe/272.jpg,unripe,overripe,0.03437212109565735,0.9488649368286133,0.05113504081964493 +banana/test/unripe/273.jpg,unripe,ripe,0.1844145655632019,0.8155854344367981,0.0 +banana/test/unripe/274.jpg,unripe,overripe,0.0,0.430692583322525,0.5693074464797974 +banana/test/unripe/275.jpg,unripe,overripe,0.0,0.40594035387039185,0.5940596461296082 +banana/test/unripe/276.jpg,unripe,unripe,0.29146310687065125,0.7085368633270264,0.0 +banana/test/unripe/277.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/278.jpg,unripe,ripe,0.24238619208335876,0.7576138377189636,0.0 +banana/test/unripe/279.jpg,unripe,overripe,0.0,0.407696008682251,0.592303991317749 +banana/test/unripe/28.jpg,unripe,ripe,0.2376963347196579,0.7623036503791809,0.0 +banana/test/unripe/280.jpg,unripe,overripe,0.19028575718402863,0.7310406565666199,0.26895931363105774 +banana/test/unripe/281.jpg,unripe,overripe,0.0,0.4683305025100708,0.5316694974899292 +banana/test/unripe/282.jpg,unripe,overripe,0.0,0.5598151683807373,0.4401848614215851 +banana/test/unripe/283.jpg,unripe,overripe,0.07427055388689041,0.913409948348999,0.08659003674983978 +banana/test/unripe/284.jpg,unripe,overripe,0.0,0.57662433385849,0.4233756959438324 +banana/test/unripe/285.jpg,unripe,overripe,0.09968637675046921,0.9003136157989502,0.04875142127275467 +banana/test/unripe/286.jpg,unripe,ripe,0.2356974333524704,0.7643025517463684,0.0 +banana/test/unripe/287.jpg,unripe,overripe,0.0,0.6273543834686279,0.37264561653137207 +banana/test/unripe/288.jpg,unripe,overripe,0.5017203688621521,0.4982796311378479,0.2875199317932129 +banana/test/unripe/289.jpg,unripe,overripe,0.19028575718402863,0.7310406565666199,0.26895931363105774 +banana/test/unripe/29.jpg,unripe,unripe,0.2802675664424896,0.7197324633598328,0.0 +banana/test/unripe/290.jpg,unripe,overripe,0.0859709307551384,0.8628450036048889,0.13715499639511108 +banana/test/unripe/291.jpg,unripe,ripe,0.24238619208335876,0.7576138377189636,0.0 +banana/test/unripe/292.jpg,unripe,ripe,0.18870636820793152,0.8112936615943909,0.0 +banana/test/unripe/293.jpg,unripe,overripe,0.12270887196063995,0.8772911429405212,0.05614035204052925 +banana/test/unripe/294.jpg,unripe,overripe,0.0,0.47225549817085266,0.5277445316314697 +banana/test/unripe/295.jpg,unripe,overripe,0.0,0.5501803159713745,0.4498196840286255 +banana/test/unripe/296.jpg,unripe,overripe,0.0,0.41478562355041504,0.585214376449585 +banana/test/unripe/297.jpg,unripe,overripe,0.20829081535339355,0.7917091846466064,0.022035302594304085 +banana/test/unripe/298.jpg,unripe,overripe,0.38002052903175354,0.6199794411659241,0.15054456889629364 +banana/test/unripe/299.jpg,unripe,overripe,0.0,0.4062001407146454,0.593799889087677 +banana/test/unripe/3.jpg,unripe,unripe,0.2789868712425232,0.7210131287574768,0.0 +banana/test/unripe/30.jpg,unripe,overripe,0.0,0.5876860022544861,0.4123139977455139 +banana/test/unripe/300.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/301.jpg,unripe,overripe,0.0,0.41478562355041504,0.585214376449585 +banana/test/unripe/302.jpg,unripe,overripe,0.0,0.4238095283508301,0.5761904716491699 +banana/test/unripe/303.jpg,unripe,overripe,0.9272355437278748,0.07276446372270584,0.22092309594154358 +banana/test/unripe/304.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/305.jpg,unripe,ripe,0.09708444029092789,0.9029155373573303,0.0 +banana/test/unripe/306.jpg,unripe,overripe,0.0,0.432235449552536,0.5677645206451416 +banana/test/unripe/307.jpg,unripe,overripe,0.04345858842134476,0.8686643242835999,0.13133566081523895 +banana/test/unripe/308.jpg,unripe,overripe,0.11067527532577515,0.8893247246742249,0.09720101952552795 +banana/test/unripe/309.jpg,unripe,ripe,0.22581180930137634,0.774188220500946,0.0 +banana/test/unripe/31.jpg,unripe,ripe,0.2132963389158249,0.7867036461830139,0.0 +banana/test/unripe/310.jpg,unripe,unripe,0.9859107136726379,0.01408926397562027,0.0 +banana/test/unripe/311.jpg,unripe,overripe,0.0,0.5425031185150146,0.45749691128730774 +banana/test/unripe/312.jpg,unripe,overripe,0.20829081535339355,0.7917091846466064,0.022035302594304085 +banana/test/unripe/313.jpg,unripe,overripe,0.0,0.47225549817085266,0.5277445316314697 +banana/test/unripe/314.jpg,unripe,ripe,0.18870636820793152,0.8112936615943909,0.0 +banana/test/unripe/315.jpg,unripe,overripe,0.0,0.5598151683807373,0.4401848614215851 +banana/test/unripe/316.jpg,unripe,ripe,0.22376485168933868,0.7762351632118225,0.0 +banana/test/unripe/317.jpg,unripe,overripe,0.06575498729944229,0.9342449903488159,0.013468013145029545 +banana/test/unripe/318.jpg,unripe,ripe,0.08998210728168488,0.9100179076194763,0.0 +banana/test/unripe/319.jpg,unripe,overripe,0.0,0.4089820384979248,0.5910179615020752 +banana/test/unripe/32.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/320.jpg,unripe,ripe,0.2124481350183487,0.7875518798828125,0.0 +banana/test/unripe/321.jpg,unripe,overripe,0.0,0.40957045555114746,0.5904295444488525 +banana/test/unripe/322.jpg,unripe,overripe,0.06575498729944229,0.9342449903488159,0.013468013145029545 +banana/test/unripe/323.jpg,unripe,ripe,0.09708444029092789,0.9029155373573303,0.0 +banana/test/unripe/324.jpg,unripe,overripe,0.07460922747850418,0.9253907799720764,0.019733333960175514 +banana/test/unripe/325.jpg,unripe,overripe,0.0,0.5097288489341736,0.49027112126350403 +banana/test/unripe/326.jpg,unripe,ripe,0.020168645307421684,0.979831337928772,0.0 +banana/test/unripe/327.jpg,unripe,unripe,0.43799570202827454,0.5620042681694031,0.0 +banana/test/unripe/328.jpg,unripe,overripe,0.0,0.7668282389640808,0.23317177593708038 +banana/test/unripe/329.jpg,unripe,ripe,0.22581180930137634,0.774188220500946,0.0 +banana/test/unripe/33.jpg,unripe,overripe,0.4441128075122833,0.5558872222900391,0.09405610710382462 +banana/test/unripe/330.jpg,unripe,unripe,0.956748902797699,0.04325108975172043,0.0 +banana/test/unripe/331.jpg,unripe,overripe,0.28487274050712585,0.5696524977684021,0.4303475022315979 +banana/test/unripe/332.jpg,unripe,ripe,0.138188436627388,0.8618115782737732,0.0 +banana/test/unripe/333.jpg,unripe,overripe,0.0766623243689537,0.9087928533554077,0.09120713174343109 +banana/test/unripe/334.jpg,unripe,unripe,0.3661538362503052,0.6338461637496948,0.0 +banana/test/unripe/335.jpg,unripe,ripe,0.20887574553489685,0.7911242842674255,0.0 +banana/test/unripe/336.jpg,unripe,unripe,0.49572649598121643,0.504273533821106,0.0 +banana/test/unripe/337.jpg,unripe,overripe,0.0,0.5040107369422913,0.49598926305770874 +banana/test/unripe/338.jpg,unripe,overripe,0.0,0.4465608596801758,0.5534391403198242 +banana/test/unripe/339.jpg,unripe,overripe,0.0,0.4238095283508301,0.5761904716491699 +banana/test/unripe/34.jpg,unripe,overripe,0.0,0.4038291573524475,0.5961708426475525 +banana/test/unripe/340.jpg,unripe,overripe,0.0,0.4594414532184601,0.5405585169792175 +banana/test/unripe/341.jpg,unripe,overripe,0.0,0.57662433385849,0.4233756959438324 +banana/test/unripe/342.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/343.jpg,unripe,unripe,0.8755641579627991,0.12443581968545914,0.0 +banana/test/unripe/344.jpg,unripe,overripe,0.0,0.4350877106189728,0.5649122595787048 +banana/test/unripe/345.jpg,unripe,overripe,0.037369806319475174,0.9448613524436951,0.055138662457466125 +banana/test/unripe/346.jpg,unripe,overripe,0.0,0.5,0.5 +banana/test/unripe/347.jpg,unripe,overripe,0.0,0.43288642168045044,0.5671135783195496 +banana/test/unripe/348.jpg,unripe,overripe,0.0,0.8365880846977234,0.16341190040111542 +banana/test/unripe/349.jpg,unripe,ripe,0.1580294519662857,0.8419705629348755,0.0 +banana/test/unripe/35.jpg,unripe,overripe,0.6118519306182861,0.3881480395793915,0.22839611768722534 +banana/test/unripe/350.jpg,unripe,unripe,0.30200669169425964,0.6979933381080627,0.0 +banana/test/unripe/351.jpg,unripe,overripe,0.0,0.6665284037590027,0.3334715962409973 +banana/test/unripe/352.jpg,unripe,unripe,0.6013625860214233,0.39863744378089905,0.0 +banana/test/unripe/353.jpg,unripe,ripe,0.13375745713710785,0.866242527961731,0.0 +banana/test/unripe/354.jpg,unripe,ripe,0.21373973786830902,0.7862602472305298,0.0 +banana/test/unripe/355.jpg,unripe,overripe,0.0,0.405193567276001,0.594806432723999 +banana/test/unripe/356.jpg,unripe,unripe,0.358450323343277,0.6415497064590454,0.0 +banana/test/unripe/357.jpg,unripe,overripe,0.593195378780365,0.4068046510219574,0.17153753340244293 +banana/test/unripe/358.jpg,unripe,ripe,0.19921405613422394,0.8007859587669373,0.0 +banana/test/unripe/359.jpg,unripe,overripe,0.8698968291282654,0.1301031857728958,0.04778892546892166 +banana/test/unripe/36.jpg,unripe,ripe,0.1949339509010315,0.8050660490989685,0.0 +banana/test/unripe/360.jpg,unripe,overripe,0.7057510614395142,0.29424890875816345,0.3146226406097412 +banana/test/unripe/361.jpg,unripe,overripe,0.2364698052406311,0.6345364451408386,0.36546358466148376 +banana/test/unripe/362.jpg,unripe,overripe,0.05047917738556862,0.8087958097457886,0.1912042200565338 +banana/test/unripe/363.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/364.jpg,unripe,unripe,0.30200669169425964,0.6979933381080627,0.0 +banana/test/unripe/365.jpg,unripe,unripe,0.8755641579627991,0.12443581968545914,0.0 +banana/test/unripe/366.jpg,unripe,overripe,0.07460922747850418,0.9253907799720764,0.019733333960175514 +banana/test/unripe/367.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/368.jpg,unripe,ripe,0.13375745713710785,0.866242527961731,0.0 +banana/test/unripe/369.jpg,unripe,unripe,0.6013625860214233,0.39863744378089905,0.0 +banana/test/unripe/37.jpg,unripe,overripe,0.0,0.4493827223777771,0.5506172776222229 +banana/test/unripe/370.jpg,unripe,overripe,0.0,0.5359715819358826,0.46402841806411743 +banana/test/unripe/371.jpg,unripe,overripe,0.0,0.405193567276001,0.594806432723999 +banana/test/unripe/372.jpg,unripe,overripe,0.06308994442224503,0.48603665828704834,0.5139633417129517 +banana/test/unripe/373.jpg,unripe,overripe,0.0,0.40058326721191406,0.5994167327880859 +banana/test/unripe/374.jpg,unripe,overripe,0.0,0.6553191542625427,0.3446808457374573 +banana/test/unripe/375.jpg,unripe,unripe,0.49014586210250854,0.5098541378974915,0.0 +banana/test/unripe/376.jpg,unripe,overripe,0.8698968291282654,0.1301031857728958,0.04778892546892166 +banana/test/unripe/377.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/378.jpg,unripe,ripe,0.020168645307421684,0.979831337928772,0.0 +banana/test/unripe/379.jpg,unripe,overripe,0.5769528746604919,0.42304712533950806,0.06495170295238495 +banana/test/unripe/38.jpg,unripe,overripe,0.0,0.5492117404937744,0.450788289308548 +banana/test/unripe/380.jpg,unripe,overripe,0.0,0.5066666603088379,0.4933333396911621 +banana/test/unripe/381.jpg,unripe,overripe,0.0,0.6236457228660583,0.37635427713394165 +banana/test/unripe/382.jpg,unripe,overripe,0.0,0.4053042232990265,0.5946957468986511 +banana/test/unripe/383.jpg,unripe,overripe,0.0,0.4774111807346344,0.522588849067688 +banana/test/unripe/384.jpg,unripe,overripe,0.0,0.4493827223777771,0.5506172776222229 +banana/test/unripe/385.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/386.jpg,unripe,overripe,0.06822855770587921,0.8222222328186035,0.17777778208255768 +banana/test/unripe/387.jpg,unripe,ripe,0.22185535728931427,0.7781446576118469,0.0 +banana/test/unripe/388.jpg,unripe,overripe,0.9132457971572876,0.0867542028427124,0.02819274738430977 +banana/test/unripe/389.jpg,unripe,overripe,0.03209414705634117,0.7546099424362183,0.24539007246494293 +banana/test/unripe/39.jpg,unripe,overripe,0.0,0.41464436054229736,0.5853556394577026 +banana/test/unripe/390.jpg,unripe,overripe,0.915206789970398,0.08479321748018265,0.1481768786907196 +banana/test/unripe/391.jpg,unripe,unripe,0.33535081148147583,0.6646491885185242,0.0 +banana/test/unripe/392.jpg,unripe,overripe,0.0,0.8973180055618286,0.10268199443817139 +banana/test/unripe/393.jpg,unripe,overripe,0.0,0.4007619023323059,0.5992380976676941 +banana/test/unripe/394.jpg,unripe,overripe,0.0,0.6389610171318054,0.3610389530658722 +banana/test/unripe/395.jpg,unripe,overripe,0.0,0.7350465655326843,0.2649534344673157 +banana/test/unripe/396.jpg,unripe,overripe,0.0,0.4167163074016571,0.5832836627960205 +banana/test/unripe/397.jpg,unripe,overripe,0.0,0.7493714690208435,0.2506285309791565 +banana/test/unripe/398.jpg,unripe,overripe,0.22533021867275238,0.7746697664260864,0.0747474730014801 +banana/test/unripe/399.jpg,unripe,overripe,0.0,0.4001990556716919,0.5998009443283081 +banana/test/unripe/4.jpg,unripe,overripe,0.5947144627571106,0.405285507440567,0.03967280313372612 +banana/test/unripe/40.jpg,unripe,unripe,0.6570414304733276,0.34295856952667236,0.0 +banana/test/unripe/400.jpg,unripe,overripe,0.0,0.5167825818061829,0.48321741819381714 +banana/test/unripe/41.jpg,unripe,overripe,0.0,0.6697494983673096,0.33025047183036804 +banana/test/unripe/42.jpg,unripe,ripe,0.08986382931470871,0.9101361632347107,0.0 +banana/test/unripe/43.jpg,unripe,overripe,0.08513225615024567,0.8584980368614197,0.14150197803974152 +banana/test/unripe/44.jpg,unripe,overripe,0.0,0.5150402784347534,0.48495975136756897 +banana/test/unripe/45.jpg,unripe,overripe,0.0,0.542089581489563,0.4579104483127594 +banana/test/unripe/46.jpg,unripe,overripe,0.0,0.4277777671813965,0.5722222328186035 +banana/test/unripe/47.jpg,unripe,overripe,0.5021380186080933,0.4978620111942291,0.24998702108860016 +banana/test/unripe/48.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/49.jpg,unripe,unripe,0.807218611240387,0.19278137385845184,0.0 +banana/test/unripe/5.jpg,unripe,overripe,0.9715474843978882,0.02845248579978943,0.04391995072364807 +banana/test/unripe/50.jpg,unripe,overripe,0.0,0.8518673181533813,0.14813268184661865 +banana/test/unripe/51.jpg,unripe,ripe,0.2519397437572479,0.7480602264404297,0.0 +banana/test/unripe/52.jpg,unripe,unripe,0.26180484890937805,0.7381951808929443,0.0 +banana/test/unripe/53.jpg,unripe,ripe,0.1300944685935974,0.8699055314064026,0.0 +banana/test/unripe/54.jpg,unripe,unripe,0.2753332853317261,0.7246667146682739,0.0 +banana/test/unripe/55.jpg,unripe,ripe,0.10385211557149887,0.8961479067802429,0.0 +banana/test/unripe/56.jpg,unripe,ripe,0.17390958964824677,0.826090395450592,0.0 +banana/test/unripe/57.jpg,unripe,ripe,0.23288694024085999,0.7671130895614624,0.0 +banana/test/unripe/58.jpg,unripe,ripe,0.1080457866191864,0.891954243183136,0.0 +banana/test/unripe/59.jpg,unripe,overripe,0.0,0.4565645158290863,0.5434355139732361 +banana/test/unripe/6.jpg,unripe,unripe,0.41578975319862366,0.5842102766036987,0.0 +banana/test/unripe/60.jpg,unripe,overripe,0.0,0.4449104070663452,0.5550895929336548 +banana/test/unripe/61.jpg,unripe,overripe,0.0,0.44126689434051514,0.5587331056594849 +banana/test/unripe/62.jpg,unripe,overripe,0.0,0.6136986017227173,0.3863013684749603 +banana/test/unripe/63.jpg,unripe,ripe,0.23574599623680115,0.7642540335655212,0.0 +banana/test/unripe/64.jpg,unripe,overripe,0.0,0.5904426574707031,0.4095573425292969 +banana/test/unripe/65.jpg,unripe,ripe,0.2186000496149063,0.7813999652862549,0.0 +banana/test/unripe/66.jpg,unripe,ripe,0.24327614903450012,0.7567238807678223,0.0 +banana/test/unripe/67.jpg,unripe,overripe,0.3813640773296356,0.618635892868042,0.07815820723772049 +banana/test/unripe/68.jpg,unripe,overripe,0.7153485417366028,0.2846514880657196,0.2734442353248596 +banana/test/unripe/69.jpg,unripe,overripe,0.0,0.540056049823761,0.4599439799785614 +banana/test/unripe/7.jpg,unripe,overripe,0.127019003033638,0.8729810118675232,0.0316464863717556 +banana/test/unripe/70.jpg,unripe,overripe,0.11492491513490677,0.4703851342201233,0.5296148657798767 +banana/test/unripe/71.jpg,unripe,ripe,0.24332894384860992,0.7566710710525513,0.0 +banana/test/unripe/72.jpg,unripe,unripe,0.26402828097343445,0.7359716892242432,0.0 +banana/test/unripe/73.jpg,unripe,overripe,0.0,0.4239651560783386,0.5760348439216614 +banana/test/unripe/74.jpg,unripe,ripe,0.0,1.0,0.0 +banana/test/unripe/75.jpg,unripe,overripe,0.0,0.7383270859718323,0.2616729140281677 +banana/test/unripe/76.jpg,unripe,ripe,0.1048535630106926,0.8951464295387268,0.0 +banana/test/unripe/77.jpg,unripe,overripe,0.19714151322841644,0.8028584718704224,0.07706689834594727 +banana/test/unripe/78.jpg,unripe,overripe,0.02286776714026928,0.7776435017585754,0.22235649824142456 +banana/test/unripe/79.jpg,unripe,overripe,0.07121928781270981,0.8870835304260254,0.11291644722223282 +banana/test/unripe/8.jpg,unripe,overripe,0.0,0.5724256038665771,0.42757439613342285 +banana/test/unripe/80.jpg,unripe,ripe,0.18095238506793976,0.8190476298332214,0.0 +banana/test/unripe/81.jpg,unripe,ripe,0.1048535630106926,0.8951464295387268,0.0 +banana/test/unripe/82.jpg,unripe,overripe,0.0,0.40401336550712585,0.5959866046905518 +banana/test/unripe/83.jpg,unripe,overripe,0.08912215381860733,0.6899985671043396,0.3100014328956604 +banana/test/unripe/84.jpg,unripe,ripe,0.24332894384860992,0.7566710710525513,0.0 +banana/test/unripe/85.jpg,unripe,overripe,0.07121928781270981,0.8870835304260254,0.11291644722223282 +banana/test/unripe/86.jpg,unripe,overripe,0.16249947249889374,0.8375005125999451,0.04065309092402458 +banana/test/unripe/87.jpg,unripe,ripe,0.25134098529815674,0.7486590147018433,0.0 +banana/test/unripe/88.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/89.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/9.jpg,unripe,ripe,0.2607308328151703,0.7392691373825073,0.0 +banana/test/unripe/90.jpg,unripe,overripe,0.0,0.4082893431186676,0.5917106866836548 +banana/test/unripe/91.jpg,unripe,overripe,0.44772982597351074,0.5522701740264893,0.05203251913189888 +banana/test/unripe/92.jpg,unripe,overripe,0.0,0.4239664375782013,0.5760335326194763 +banana/test/unripe/93.jpg,unripe,ripe,0.23816286027431488,0.7618371248245239,0.0 +banana/test/unripe/94.jpg,unripe,ripe,0.17399445176124573,0.8260055780410767,0.0 +banana/test/unripe/95.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/96.jpg,unripe,ripe,0.25641027092933655,0.7435897588729858,0.0 +banana/test/unripe/97.jpg,unripe,overripe,0.02286776714026928,0.7776435017585754,0.22235649824142456 +banana/test/unripe/98.jpg,unripe,overripe,0.0,0.4052811563014984,0.594718873500824 +banana/test/unripe/99.jpg,unripe,unripe,0.36091139912605286,0.6390885710716248,0.0 diff --git a/AgCloud/services/ripeness-baseline/eval/banana_test/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/banana_test/roc_curves.png new file mode 100644 index 000000000..14be548a6 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/banana_test/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/banana_tuned/metrics.json b/AgCloud/services/ripeness-baseline/eval/banana_tuned/metrics.json new file mode 100644 index 000000000..b8592e177 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/banana_tuned/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.4927536231884058, + "report": { + "unripe": { + "precision": 1.0, + "recall": 0.315, + "f1-score": 0.4790874524714829, + "support": 400.0 + }, + "ripe": { + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 381.0 + }, + "overripe": { + "precision": 0.45694200351493847, + "recall": 0.9811320754716981, + "f1-score": 0.6235011990407674, + "support": 530.0 + }, + "accuracy": 0.4927536231884058, + "macro avg": { + "precision": 0.4856473345049795, + "recall": 0.43204402515723267, + "f1-score": 0.3675295505040834, + "support": 1311.0 + }, + "weighted avg": { + "precision": 0.4898392539000133, + "recall": 0.4927536231884058, + "f1-score": 0.39823845650663603, + "support": 1311.0 + } + }, + "confusion_matrix": [ + [ + 126, + 37, + 237 + ], + [ + 0, + 0, + 381 + ], + [ + 0, + 10, + 520 + ] + ], + "samples": 1311, + "prefix": "banana/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/banana_tuned/per_image.csv b/AgCloud/services/ripeness-baseline/eval/banana_tuned/per_image.csv new file mode 100644 index 000000000..babbeb96f --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/banana_tuned/per_image.csv @@ -0,0 +1,1312 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +banana/test/overripe/Screen Shot 2018-06-12 at 8.47.41 PM.png,overripe,overripe,0.0,0.5190207362174988,0.4809792935848236 +banana/test/overripe/Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.41980308294296265,0.5801969170570374 +banana/test/overripe/Screen Shot 2018-06-12 at 8.48.40 PM.png,overripe,overripe,0.0,0.42711177468299866,0.572888195514679 +banana/test/overripe/Screen Shot 2018-06-12 at 8.49.25 PM.png,overripe,overripe,0.0,0.6948841214179993,0.30511584877967834 +banana/test/overripe/Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,overripe,0.0,0.56043940782547,0.43956059217453003 +banana/test/overripe/Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,overripe,0.0,0.5550108551979065,0.4449891746044159 +banana/test/overripe/Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.43755561113357544,0.5624443888664246 +banana/test/overripe/Screen Shot 2018-06-12 at 8.52.16 PM.png,overripe,overripe,0.0,0.576696515083313,0.4233034551143646 +banana/test/overripe/Screen Shot 2018-06-12 at 8.52.21 PM.png,overripe,overripe,0.0,0.416103333234787,0.5838966369628906 +banana/test/overripe/Screen Shot 2018-06-12 at 8.53.09 PM.png,overripe,overripe,0.0,0.8583524227142334,0.1416475623846054 +banana/test/overripe/Screen Shot 2018-06-12 at 8.53.47 PM.png,overripe,overripe,0.0,0.48068898916244507,0.5193110108375549 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,overripe,0.0,0.46114015579223633,0.5388598442077637 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.07 PM.png,overripe,overripe,0.0,0.4556906223297119,0.5443093776702881 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.0,0.43497222661972046,0.5650277733802795 +banana/test/overripe/Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.40954867005348206,0.5904513597488403 +banana/test/overripe/Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,overripe,0.0,0.5129450559616089,0.4870549142360687 +banana/test/overripe/Screen Shot 2018-06-12 at 8.57.14 PM.png,overripe,overripe,0.0,0.4124445915222168,0.5875554084777832 +banana/test/overripe/Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,overripe,0.0,0.5830256342887878,0.41697439551353455 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.38 PM.png,overripe,overripe,0.0,0.4274294376373291,0.5725705623626709 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.43 PM.png,overripe,overripe,0.0,0.40541163086891174,0.5945883393287659 +banana/test/overripe/Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40300723910331726,0.5969927906990051 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.02 PM.png,overripe,overripe,0.0,0.4026658236980438,0.5973341464996338 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.15 PM.png,overripe,overripe,0.0,0.42170533537864685,0.5782946348190308 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6545853614807129,0.3454146385192871 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.44 PM.png,overripe,overripe,0.0,0.4464573264122009,0.5535426735877991 +banana/test/overripe/Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.0,0.37968316674232483,0.6203168630599976 +banana/test/overripe/Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4923015534877777,0.5076984167098999 +banana/test/overripe/Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.46601876616477966,0.533981204032898 +banana/test/overripe/Screen Shot 2018-06-12 at 9.01.26 PM.png,overripe,overripe,0.0,0.4005715250968933,0.5994284749031067 +banana/test/overripe/Screen Shot 2018-06-12 at 9.03.34 PM.png,overripe,overripe,0.0,0.46653082966804504,0.5334691405296326 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,overripe,0.0,0.4217844307422638,0.5782155990600586 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.4286319315433502,0.5713680982589722 +banana/test/overripe/Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,overripe,0.0,0.40317684412002563,0.5968231558799744 +banana/test/overripe/Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.6705124378204346,0.32948756217956543 +banana/test/overripe/Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.5652235746383667,0.4347763955593109 +banana/test/overripe/Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.42500630021095276,0.5749937295913696 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,overripe,0.0,0.521776556968689,0.47822344303131104 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.29 PM.png,overripe,overripe,0.0,0.4028625190258026,0.597137451171875 +banana/test/overripe/Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5606018304824829,0.4393981695175171 +banana/test/overripe/Screen Shot 2018-06-12 at 9.10.20 PM.png,overripe,overripe,0.0,0.44684770703315735,0.553152322769165 +banana/test/overripe/Screen Shot 2018-06-12 at 9.11.00 PM.png,overripe,overripe,0.0,0.5006198287010193,0.4993801414966583 +banana/test/overripe/Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,overripe,0.0,0.6648975014686584,0.33510252833366394 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.40 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.0,0.4661552906036377,0.5338447093963623 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.16 PM.png,overripe,overripe,0.0,0.6577422022819519,0.3422578275203705 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.4382789731025696,0.5617210268974304 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.34 PM.png,overripe,overripe,0.0,0.48572543263435364,0.514274537563324 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.44 PM.png,overripe,overripe,0.0,0.5455908179283142,0.4544092118740082 +banana/test/overripe/Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,overripe,0.0,0.4506637454032898,0.5493362545967102 +banana/test/overripe/Screen Shot 2018-06-12 at 9.14.07 PM.png,overripe,overripe,0.0,0.41742148995399475,0.5825784802436829 +banana/test/overripe/Screen Shot 2018-06-12 at 9.14.22 PM.png,overripe,overripe,0.0,0.4019027054309845,0.5980973243713379 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.41072356700897217,0.5892764329910278 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.28 PM.png,overripe,overripe,0.0,0.4363642930984497,0.5636357069015503 +banana/test/overripe/Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7059701681137085,0.2940298616886139 +banana/test/overripe/Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,overripe,0.085574209690094,0.8207557797431946,0.17924422025680542 +banana/test/overripe/Screen Shot 2018-06-12 at 9.18.57 PM.png,overripe,overripe,0.0,0.43988874554634094,0.5601112842559814 +banana/test/overripe/Screen Shot 2018-06-12 at 9.19.17 PM.png,overripe,overripe,0.0,0.33684054017066956,0.6631594896316528 +banana/test/overripe/Screen Shot 2018-06-12 at 9.21.25 PM.png,overripe,overripe,0.0,0.40207400918006897,0.5979259610176086 +banana/test/overripe/Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,overripe,0.0,0.652916431427002,0.34708356857299805 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.402952641248703,0.5970473289489746 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.40915563702583313,0.5908443331718445 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.28 PM.png,overripe,overripe,0.0,0.4000914692878723,0.5999085307121277 +banana/test/overripe/Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.763676643371582,0.23632337152957916 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.26 PM.png,overripe,overripe,0.0,0.7054208517074585,0.2945791780948639 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.7508425116539001,0.24915747344493866 +banana/test/overripe/Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.42007726430892944,0.5799227356910706 +banana/test/overripe/Screen Shot 2018-06-12 at 9.28.04 PM.png,overripe,overripe,0.0,0.5179276466369629,0.4820723533630371 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.41956889629364014,0.5804311037063599 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,overripe,0.0,0.9979836344718933,0.002016383223235607 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.2897368371486664,0.7102631330490112 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,overripe,0.0,0.5510762333869934,0.4489237666130066 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4321342408657074,0.567865788936615 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.52.48 PM.png,overripe,overripe,0.0,0.4112994372844696,0.5887005925178528 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,overripe,0.0,0.6512255072593689,0.3487745225429535 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.0,0.4344812333583832,0.5655187964439392 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.41593801975250244,0.5840619802474976 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.0,0.4318799078464508,0.5681200623512268 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.16 PM.png,overripe,overripe,0.0,0.4298971891403198,0.5701028108596802 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.37 PM.png,overripe,overripe,0.0,0.469759464263916,0.530240535736084 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,overripe,0.0,0.5151658058166504,0.4848341941833496 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.56.59 PM.png,overripe,overripe,0.0,0.5848426818847656,0.4151573181152344 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.57.04 PM.png,overripe,overripe,0.0,0.6593568325042725,0.34064316749572754 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,overripe,0.0,0.6485406756401062,0.3514593243598938 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.15 PM.png,overripe,overripe,0.0,0.4212583601474762,0.5787416100502014 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6841452717781067,0.3158547282218933 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.32 PM.png,overripe,overripe,0.0,0.6662626266479492,0.3337373733520508 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.0,0.3708841800689697,0.6291158199310303 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4895118772983551,0.5104881525039673 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.00.49 PM.png,overripe,overripe,0.0,0.6713870167732239,0.3286129832267761 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.41076400876045227,0.5892359614372253 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.4392685890197754,0.5607314109802246 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,overripe,0.0,0.5570738911628723,0.4429260790348053 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,overripe,0.0,0.5735056400299072,0.4264943301677704 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.42236343026161194,0.5776365995407104 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.05.02 PM.png,overripe,overripe,0.0,0.42424145340919495,0.5757585763931274 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.06.13 PM.png,overripe,overripe,0.0,0.4361393451690674,0.5638606548309326 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.07.35 PM.png,overripe,overripe,0.0,0.8836938142776489,0.11630621552467346 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,overripe,0.0,0.5221313238143921,0.4778686761856079 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,overripe,0.0,0.7140040993690491,0.2859959006309509 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5595277547836304,0.440472275018692 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.04 PM.png,overripe,overripe,0.0,0.6111322641372681,0.3888677656650543 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.42606937885284424,0.5739306211471558 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.46065232157707214,0.5393477082252502 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,overripe,0.0,0.6630242466926575,0.3369757831096649 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.11.47 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.4000948667526245,0.5999051332473755 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.0,0.46137022972106934,0.5386297702789307 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.40653595328330994,0.5934640765190125 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.40747830271720886,0.5925216674804688 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.18.43 PM.png,overripe,overripe,0.0,0.4129756689071655,0.5870243310928345 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.18.50 PM.png,overripe,overripe,0.0,0.4010092318058014,0.598990797996521 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.4796682894229889,0.5203317403793335 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,overripe,0.0,0.6628487706184387,0.33715125918388367 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4689416289329529,0.5310583710670471 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.20.36 PM.png,overripe,overripe,0.0,0.4631175100803375,0.5368824601173401 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.21.05 PM.png,overripe,overripe,0.0,0.4009423851966858,0.5990576148033142 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.22.19 PM.png,overripe,overripe,0.0,0.42409637570381165,0.575903594493866 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.22.50 PM.png,overripe,overripe,0.0,0.7331185340881348,0.26688146591186523 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,overripe,0.01689780130982399,0.6431063413619995,0.3568936884403229 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.11 PM.png,overripe,overripe,0.0,0.7637640833854675,0.23623591661453247 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.7482868432998657,0.2517131567001343 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,overripe,0.0,0.5353091359138489,0.4646908640861511 +banana/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 9.27.46 PM.png,overripe,overripe,0.0,0.5570825338363647,0.44291749596595764 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,overripe,0.0,0.7497320175170898,0.25026795268058777 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.2926582396030426,0.7073417901992798 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,overripe,0.0,0.4547353982925415,0.5452646017074585 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.07 PM.png,overripe,overripe,0.0,0.4582670331001282,0.5417329668998718 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.0,0.43220841884613037,0.5677915811538696 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.0,0.4143693745136261,0.5856306552886963 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.37 PM.png,overripe,overripe,0.0,0.46460139751434326,0.5353986024856567 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.54.43 PM.png,overripe,overripe,0.0,0.40505802631378174,0.5949419736862183 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.55.15 PM.png,overripe,overripe,0.0,0.5304825901985168,0.46951740980148315 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.56.11 PM.png,overripe,overripe,0.0,0.41768479347229004,0.58231520652771 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.56.41 PM.png,overripe,overripe,0.10476932674646378,0.5359510183334351,0.46404898166656494 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.33597061038017273,0.6640293598175049 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.57.29 PM.png,overripe,overripe,0.0,0.40119048953056335,0.598809540271759 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,overripe,0.0,0.649676501750946,0.35032346844673157 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,overripe,0.0,0.7253084778785706,0.27469155192375183 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6791341304779053,0.3208658695220947 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.00.31 PM.png,overripe,overripe,0.0,0.45877620577812195,0.5412238240242004 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.00.49 PM.png,overripe,overripe,0.0,0.6700701713562012,0.32992979884147644 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.4672205150127411,0.5327794551849365 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.02.52 PM.png,overripe,overripe,0.0,0.40510234236717224,0.5948976874351501 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.03.25 PM.png,overripe,overripe,0.0,0.4406673312187195,0.5593326687812805 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,overripe,0.0,0.5731421113014221,0.42685791850090027 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,overripe,0.0,0.4068227708339691,0.5931772589683533 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,overripe,0.0,0.4448603093624115,0.5551396608352661 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,overripe,0.0,0.62083899974823,0.37916097044944763 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.4233849048614502,0.5766150951385498 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.00 PM.png,overripe,overripe,0.0,0.4004933536052704,0.599506676197052 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.12 PM.png,overripe,overripe,0.0,0.400892049074173,0.5991079807281494 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.26 PM.png,overripe,overripe,0.0,0.49909910559654236,0.50090092420578 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.08.43 PM.png,overripe,overripe,0.0,0.518804669380188,0.481195330619812 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.42632776498794556,0.5736722350120544 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.10.45 PM.png,overripe,overripe,0.0,0.9568039774894714,0.04319600388407707 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.05 PM.png,overripe,overripe,0.0,0.4077688157558441,0.5922311544418335 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.17 PM.png,overripe,overripe,0.0,0.4377222955226898,0.5622777342796326 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.27 PM.png,overripe,overripe,0.0,0.6318313479423523,0.3681686222553253 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.11.58 PM.png,overripe,overripe,0.0,0.44946715235710144,0.550532877445221 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.12.17 PM.png,overripe,overripe,0.0,0.40841391682624817,0.5915861129760742 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.13.10 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.4016973674297333,0.5983026027679443 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.34 PM.png,overripe,overripe,0.0,0.40535232424736023,0.5946477055549622 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,overripe,0.0,0.7811575531959534,0.21884244680404663 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.53 PM.png,overripe,overripe,0.0,0.5202947854995728,0.47970521450042725 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.6973763108253479,0.3026236891746521 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.18.11 PM.png,overripe,overripe,0.0,0.40376901626586914,0.5962309837341309 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.18.50 PM.png,overripe,overripe,0.0,0.4006788730621338,0.5993211269378662 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.08 PM.png,overripe,overripe,0.0,0.488278329372406,0.511721670627594 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.4788985550403595,0.5211014151573181 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.42 PM.png,overripe,overripe,0.0,0.45282718539237976,0.5471728444099426 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.19.49 PM.png,overripe,overripe,0.0,0.40314769744873047,0.5968523025512695 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.20.36 PM.png,overripe,overripe,0.0,0.4600125849246979,0.5399873852729797 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,overripe,0.0,0.6486755013465881,0.3513244688510895 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.25.09 PM.png,overripe,overripe,0.0,0.41948023438453674,0.5805197358131409 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.7557742595672607,0.24422572553157806 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.26.01 PM.png,overripe,overripe,0.0,0.8599386215209961,0.1400613933801651 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.26.13 PM.png,overripe,overripe,0.0,0.9975994825363159,0.002400505356490612 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,overripe,0.0,0.6510022282600403,0.3489977717399597 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.31 PM.png,overripe,overripe,0.0,0.8072313070297241,0.1927686631679535 +banana/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.41703471541404724,0.5829652547836304 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.47.28 PM.png,overripe,overripe,0.0,0.5650008916854858,0.43499910831451416 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.42039796710014343,0.5796020030975342 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,overripe,0.0,0.7498447299003601,0.2501552700996399 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.49.59 PM.png,overripe,overripe,0.0,0.28532370924949646,0.7146762609481812 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.53.09 PM.png,overripe,overripe,0.0,0.8651785850524902,0.13482142984867096 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.4189527928829193,0.5810471773147583 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.0,0.4128168225288391,0.5871831774711609 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.54.58 PM.png,overripe,overripe,0.0,0.4340837597846985,0.5659162402153015 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.4404272437095642,0.5595727562904358 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,overripe,0.0,0.5159614682197571,0.4840385615825653 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.3623117208480835,0.6376882791519165 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,overripe,0.0,0.5555597543716431,0.4444402754306793 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.58.13 PM.png,overripe,overripe,0.0,0.6493342518806458,0.35066577792167664 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.58.38 PM.png,overripe,overripe,0.0,0.4232991635799408,0.5767008066177368 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.4685072600841522,0.5314927697181702 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.4887147545814514,0.5112852454185486 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.27 PM.png,overripe,overripe,0.0,0.4828006327152252,0.5171993970870972 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.32 PM.png,overripe,overripe,0.0,0.7110203504562378,0.2889796495437622 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.43989530205726624,0.5601047277450562 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.02.15 PM.png,overripe,overripe,0.0,0.6148994565010071,0.3851005434989929 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.02.32 PM.png,overripe,overripe,0.0,0.5965939164161682,0.4034060835838318 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.01 PM.png,overripe,overripe,0.0,0.40172651410102844,0.598273515701294 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.12 PM.png,overripe,overripe,0.0,0.5981754064559937,0.40182456374168396 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.21 PM.png,overripe,overripe,0.0,0.42530032992362976,0.5746996998786926 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.03.46 PM.png,overripe,overripe,0.0,0.5976547002792358,0.40234529972076416 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.04.41 PM.png,overripe,overripe,0.0,0.4216116666793823,0.5783883333206177 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.05.32 PM.png,overripe,overripe,0.0,0.46507397294044495,0.5349260568618774 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.6693885326385498,0.3306114971637726 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.49980756640434265,0.500192403793335 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.07.35 PM.png,overripe,overripe,0.0,0.7880710959434509,0.21192888915538788 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.09.09 PM.png,overripe,overripe,0.0,0.4478395879268646,0.552160382270813 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.4262748062610626,0.5737252235412598 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.40684229135513306,0.5931577086448669 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,overripe,0.0,0.4377287030220032,0.5622712969779968 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.47159963846206665,0.5284003615379333 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.16.20 PM.png,overripe,overripe,0.0,0.4094066321849823,0.5905933976173401 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.6995007991790771,0.30049923062324524 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.17.44 PM.png,overripe,overripe,0.0,0.6045452952384949,0.3954547345638275 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.18.43 PM.png,overripe,overripe,0.0,0.4124089479446411,0.5875910520553589 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,overripe,0.0,0.6417688131332397,0.35823118686676025 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.47297653555870056,0.5270234942436218 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.20.56 PM.png,overripe,overripe,0.0,0.4617638885974884,0.5382360816001892 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.22.37 PM.png,overripe,overripe,0.0,0.6852208971977234,0.3147790729999542 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.22.57 PM.png,overripe,overripe,0.0,0.4068717658519745,0.5931282639503479 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.23.24 PM.png,overripe,overripe,0.0,0.40228888392448425,0.5977111458778381 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.40527480840682983,0.5947251915931702 +banana/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.4000946879386902,0.5999053120613098 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.47.28 PM.png,overripe,overripe,0.0,0.5897075533866882,0.41029247641563416 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.49.04 PM.png,overripe,overripe,0.0,0.5138072371482849,0.4861927330493927 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.49.30 PM.png,overripe,overripe,0.0,0.4220615029335022,0.5779384970664978 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.50.40 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.51.38 PM.png,overripe,overripe,0.0,0.38949134945869446,0.6105086207389832 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.42728105187416077,0.5727189183235168 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.52.37 PM.png,overripe,overripe,0.0,0.43152087926864624,0.5684791207313538 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,overripe,0.0,0.7394899129867554,0.260510116815567 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.0,0.4244990944862366,0.5755009055137634 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.0,0.41406261920928955,0.5859373807907104 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.55.08 PM.png,overripe,overripe,0.0,0.4086166322231293,0.5913833975791931 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,overripe,0.0,0.5606938600540161,0.4393061697483063 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.21 PM.png,overripe,overripe,0.0,0.42195242643356323,0.5780475735664368 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.44221705198287964,0.5577829480171204 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.56.54 PM.png,overripe,overripe,0.0,0.5214791297912598,0.47852087020874023 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.57.22 PM.png,overripe,overripe,0.0,0.3240487277507782,0.6759512424468994 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,overripe,0.0,0.5598072409629822,0.4401927590370178 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,overripe,0.0,0.7257846593856812,0.27421534061431885 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.32 PM.png,overripe,overripe,0.0,0.7985507249832153,0.20144927501678467 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.35 PM.png,overripe,overripe,0.0,0.13207373023033142,0.8679262399673462 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.44058138132095337,0.5594186186790466 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.02.09 PM.png,overripe,overripe,0.0,0.5875465869903564,0.41245341300964355 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,overripe,0.0,0.5524405241012573,0.4475594460964203 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.03.30 PM.png,overripe,overripe,0.0,0.4058566391468048,0.5941433906555176 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.03.34 PM.png,overripe,overripe,0.0,0.4695705473423004,0.5304294228553772 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.04.01 PM.png,overripe,overripe,0.0,0.4645787179470062,0.5354212522506714 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.05.21 PM.png,overripe,overripe,0.0,0.4019378125667572,0.5980621576309204 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.5324752330780029,0.4675247371196747 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.07.46 PM.png,overripe,overripe,0.0,0.4212151765823364,0.5787848234176636 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.08.00 PM.png,overripe,overripe,0.0,0.4005418121814728,0.5994582176208496 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.4270947277545929,0.5729053020477295 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.11.23 PM.png,overripe,overripe,0.0,0.4058290421962738,0.5941709280014038 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.12.27 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.12.45 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.16 PM.png,overripe,overripe,0.0,0.6348609924316406,0.3651390075683594 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.4366362988948822,0.5633636713027954 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.13.34 PM.png,overripe,overripe,0.0,0.48263970017433167,0.517360270023346 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.414336621761322,0.585663378238678 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.6970834136009216,0.30291658639907837 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.48094749450683594,0.5190525054931641 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,overripe,0.07358856499195099,0.8077471852302551,0.19225279986858368 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.4759728014469147,0.5240271687507629 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.20.01 PM.png,overripe,overripe,0.0,0.47088396549224854,0.5291160345077515 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.20.47 PM.png,overripe,overripe,0.0,0.4088868498802185,0.5911131501197815 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.23.15 PM.png,overripe,overripe,0.0,0.8999719619750977,0.10002803057432175 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4021891951560974,0.5978108048439026 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.04 PM.png,overripe,overripe,0.0,0.5734297037124634,0.426570326089859 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.34 PM.png,overripe,overripe,0.0,0.4451480805873871,0.5548518896102905 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.4649895131587982,0.5350104570388794 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.4000472128391266,0.5999528169631958 +banana/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.4175601601600647,0.5824398398399353 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.47.41 PM.png,overripe,overripe,0.0,0.5251906514167786,0.47480934858322144 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,overripe,0.0,0.5720648765563965,0.42793509364128113 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.04 PM.png,overripe,overripe,0.0,0.5340560078620911,0.46594399213790894 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.47 PM.png,overripe,overripe,0.0,0.9892989993095398,0.010701008141040802 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.50.54 PM.png,overripe,overripe,0.0,0.7092341780662537,0.29076582193374634 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.30 PM.png,overripe,overripe,0.0,0.5900334715843201,0.40996652841567993 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4237128794193268,0.5762870907783508 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.51.56 PM.png,overripe,overripe,0.0,0.5390769243240356,0.46092307567596436 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,overripe,0.0,0.44336044788360596,0.556639552116394 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.52.11 PM.png,overripe,overripe,0.0,0.4050554633140564,0.5949445366859436 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,overripe,0.0,0.45825985074043274,0.5417401790618896 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.0,0.4311744272708893,0.5688256025314331 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.55.08 PM.png,overripe,overripe,0.0,0.40816017985343933,0.5918397903442383 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.55.35 PM.png,overripe,overripe,0.0,0.6615965366363525,0.33840346336364746 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.56.01 PM.png,overripe,overripe,0.0,0.4959656894207001,0.5040343403816223 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.56.30 PM.png,overripe,overripe,0.0,0.44275516271591187,0.5572448372840881 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.57.46 PM.png,overripe,overripe,0.0,0.8227895498275757,0.1772104650735855 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.58.07 PM.png,overripe,overripe,0.0,0.8293110132217407,0.17068897187709808 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40243566036224365,0.5975643396377563 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 8.59.32 PM.png,overripe,overripe,0.0,0.6587255001068115,0.34127452969551086 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.00.11 PM.png,overripe,overripe,0.0,0.8004805445671082,0.19951945543289185 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.00.17 PM.png,overripe,overripe,0.0,0.41973549127578735,0.5802645087242126 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.46598735451698303,0.5340126752853394 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.4106704294681549,0.5893295407295227 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.26 PM.png,overripe,overripe,0.0,0.4005770981311798,0.5994229316711426 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.43654271960258484,0.5634573101997375 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.02.03 PM.png,overripe,overripe,0.0,0.6204928159713745,0.3795071840286255 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.03.25 PM.png,overripe,overripe,0.0,0.4460490643978119,0.5539509057998657 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.03.40 PM.png,overripe,overripe,0.0,0.4357312321662903,0.5642687678337097 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.01 PM.png,overripe,overripe,0.0,0.46440890431404114,0.5355911254882812 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.08 PM.png,overripe,overripe,0.0,0.6278841495513916,0.372115820646286 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,overripe,0.0,0.4494374096393585,0.5505626201629639 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.05.08 PM.png,overripe,overripe,0.0,0.48866868019104004,0.51133131980896 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.6680976748466492,0.3319023549556732 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.06.33 PM.png,overripe,overripe,0.0,0.5094115734100342,0.4905884265899658 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.07.26 PM.png,overripe,overripe,0.0,0.5541261434555054,0.44587385654449463 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.09.16 PM.png,overripe,overripe,0.0,0.4305081367492676,0.5694918632507324 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,overripe,0.0,0.7137255072593689,0.2862745225429535 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.10.10 PM.png,overripe,overripe,0.0,0.5250982642173767,0.4749017655849457 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.10.45 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.05 PM.png,overripe,overripe,0.0,0.4037339985370636,0.596265971660614 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.35 PM.png,overripe,overripe,0.0,0.6838871240615845,0.3161128759384155 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.43 PM.png,overripe,overripe,0.0,0.5778014063835144,0.4221985638141632 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.11.52 PM.png,overripe,overripe,0.0,0.5083375573158264,0.4916624128818512 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.11 PM.png,overripe,overripe,0.0,0.4122283458709717,0.5877716541290283 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.0,0.4621380865573883,0.5378618836402893 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.42715734243392944,0.5728426575660706 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.4062332808971405,0.5937667489051819 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.13.54 PM.png,overripe,overripe,0.0,0.419460266828537,0.5805397033691406 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.14.22 PM.png,overripe,overripe,0.0,0.40103578567504883,0.5989642143249512 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.40108081698417664,0.5989192128181458 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.16.28 PM.png,overripe,overripe,0.0,0.41859254240989685,0.5814074277877808 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.17.44 PM.png,overripe,overripe,0.0,0.6251206994056702,0.37487927079200745 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.18.11 PM.png,overripe,overripe,0.0,0.38393017649650574,0.6160698533058167 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.19.03 PM.png,overripe,overripe,0.0,0.43764856457710266,0.562351405620575 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.20.01 PM.png,overripe,overripe,0.0,0.47358444333076477,0.5264155268669128 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.4606512486934662,0.5393487215042114 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.22.57 PM.png,overripe,overripe,0.0,0.40577560663223267,0.5942243933677673 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.23.15 PM.png,overripe,overripe,0.0,0.8985614776611328,0.10143852233886719 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.4051816165447235,0.5948184132575989 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.28 PM.png,overripe,overripe,0.0,0.40009549260139465,0.5999045372009277 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.25.41 PM.png,overripe,overripe,0.0,0.4355555474758148,0.5644444227218628 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,overripe,0.04070419445633888,0.6479013562202454,0.35209861397743225 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,overripe,0.0,0.666878879070282,0.33312109112739563 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.771630048751831,0.22836996614933014 +banana/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 9.27.46 PM.png,overripe,overripe,0.0,0.5529600977897644,0.447039932012558 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.47.51 PM.png,overripe,overripe,0.0,0.4219214916229248,0.5780785083770752 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.48.40 PM.png,overripe,overripe,0.0,0.4293177127838135,0.5706822872161865 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.49.20 PM.png,overripe,overripe,0.0,0.7503296136856079,0.24967040121555328 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.49.41 PM.png,overripe,overripe,0.0,0.561509907245636,0.438490092754364 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.04 PM.png,overripe,overripe,0.0,0.5401669144630432,0.4598331153392792 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.26 PM.png,overripe,overripe,0.0,0.6883344650268555,0.31166550517082214 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.50.54 PM.png,overripe,overripe,0.0,0.709503173828125,0.2904968559741974 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.51.38 PM.png,overripe,overripe,0.0,0.4033472239971161,0.5966527462005615 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,overripe,0.0,0.6940000057220459,0.3059999942779541 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.37 PM.png,overripe,overripe,0.0,0.4311803877353668,0.5688195824623108 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.52.42 PM.png,overripe,overripe,0.0,0.4031233787536621,0.5968766212463379 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.53.29 PM.png,overripe,overripe,0.0,0.6430167555809021,0.3569832444190979 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.53.54 PM.png,overripe,overripe,0.0,0.43221741914749146,0.5677825808525085 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.14 PM.png,overripe,overripe,0.0,0.4198237955570221,0.5801762342453003 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.30 PM.png,overripe,overripe,0.0,0.4167361855506897,0.5832638144493103 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.4117199182510376,0.5882800817489624 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.55.47 PM.png,overripe,overripe,0.0,0.5869631171226501,0.41303688287734985 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.56.21 PM.png,overripe,overripe,0.0,0.41177815198898315,0.5882218480110168 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,overripe,0.0,0.5823426842689514,0.4176573157310486 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.58.18 PM.png,overripe,overripe,0.0,0.5874334573745728,0.41256657242774963 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40446481108665466,0.5955352187156677 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.0,0.384358286857605,0.615641713142395 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.47086867690086365,0.5291313529014587 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.49370577931404114,0.5062941908836365 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.00.31 PM.png,overripe,overripe,0.0,0.46044012904167175,0.5395598411560059 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.01.21 PM.png,overripe,overripe,0.0,0.4134400486946106,0.5865599513053894 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.44160351157188416,0.5583964586257935 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.32 PM.png,overripe,overripe,0.0,0.6092451810836792,0.3907548189163208 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.44 PM.png,overripe,overripe,0.0,0.5663164258003235,0.4336836040019989 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.02.52 PM.png,overripe,overripe,0.0,0.4086061716079712,0.5913938283920288 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.03.30 PM.png,overripe,overripe,0.0,0.4100880026817322,0.5899119973182678 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.03.55 PM.png,overripe,overripe,0.0,0.5195397734642029,0.4804602563381195 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,overripe,0.0,0.4045831263065338,0.5954169034957886 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.05.02 PM.png,overripe,overripe,0.0,0.4261198043823242,0.5738801956176758 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.05.37 PM.png,overripe,overripe,0.0,0.67098468542099,0.3290153443813324 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.06.21 PM.png,overripe,overripe,0.0,0.43298545479774475,0.5670145153999329 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,overripe,0.0,0.6160199642181396,0.38398003578186035 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.07.39 PM.png,overripe,overripe,0.0,0.5414669513702393,0.45853304862976074 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.08.22 PM.png,overripe,overripe,0.0,0.41679054498672485,0.5832094550132751 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.09.05 PM.png,overripe,overripe,0.0,0.5224829316139221,0.47751709818840027 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.10 PM.png,overripe,overripe,0.0,0.531227171421051,0.4687727987766266 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.37 PM.png,overripe,overripe,0.0,0.42744481563568115,0.5725551843643188 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.41 PM.png,overripe,overripe,0.0,0.44075512886047363,0.5592448711395264 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.46437275409698486,0.5356272459030151 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.11.17 PM.png,overripe,overripe,0.0,0.4430682957172394,0.5569316744804382 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.11 PM.png,overripe,overripe,0.0,0.4140622019767761,0.5859377980232239 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.40 PM.png,overripe,overripe,0.0,0.40144628286361694,0.5985537171363831 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.49 PM.png,overripe,ripe,0.0,1.0,0.0 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.12.57 PM.png,overripe,overripe,0.0,0.4570924639701843,0.5429075360298157 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.4440954625606537,0.5559045672416687 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.47292712330818176,0.5270728468894958 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.41750943660736084,0.5824905633926392 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.4709013104438782,0.5290986895561218 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.18.07 PM.png,overripe,overripe,0.0,0.45301204919815063,0.5469879508018494 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.19.35 PM.png,overripe,overripe,0.0,0.4853736460208893,0.5146263241767883 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.19.42 PM.png,overripe,overripe,0.0,0.4546276032924652,0.5453723669052124 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.4124287962913513,0.5875712037086487 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.46242812275886536,0.5375718474388123 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.22.37 PM.png,overripe,overripe,0.0,0.7148457169532776,0.2851543128490448 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.23.24 PM.png,overripe,overripe,0.0,0.40914666652679443,0.5908533334732056 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4046136736869812,0.5953863263130188 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.25.12 PM.png,overripe,overripe,0.0,0.40959057211875916,0.5904093980789185 +banana/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,overripe,0.0,0.5431870222091675,0.4568129777908325 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,overripe,0.0,0.9964075684547424,0.0035924336407333612 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.50.20 PM.png,overripe,overripe,0.0,0.41448596119880676,0.5855140089988708 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.00 PM.png,overripe,overripe,0.0,0.5571404099464417,0.44285959005355835 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.46 PM.png,overripe,overripe,0.0,0.4328310489654541,0.5671689510345459 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.51.56 PM.png,overripe,overripe,0.0,0.5373833775520325,0.46261662244796753 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.52.01 PM.png,overripe,overripe,0.0,0.6578875184059143,0.3421124815940857 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,overripe,0.0,0.46328234672546387,0.5367176532745361 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.00 PM.png,overripe,overripe,0.0,0.45493966341018677,0.5450603365898132 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.23 PM.png,overripe,overripe,0.0,0.43235042691230774,0.5676496028900146 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.37 PM.png,overripe,overripe,0.0,0.47001367807388306,0.5299863219261169 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.43 PM.png,overripe,overripe,0.0,0.40558093786239624,0.5944190621376038 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.54.48 PM.png,overripe,overripe,0.0,0.409347265958786,0.5906527042388916 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.55.21 PM.png,overripe,overripe,0.0,0.450257271528244,0.5497427582740784 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.55.28 PM.png,overripe,overripe,0.0,0.4009576737880707,0.5990422964096069 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.57.54 PM.png,overripe,overripe,0.0,0.5941936373710632,0.40580639243125916 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.59.44 PM.png,overripe,overripe,0.0,0.4446737766265869,0.5553262233734131 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 8.59.57 PM.png,overripe,overripe,0.0,0.47153210639953613,0.5284678936004639 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.00.22 PM.png,overripe,overripe,0.0,0.48705166578292847,0.5129483342170715 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.01.56 PM.png,overripe,overripe,0.0,0.4401245713233948,0.5598754286766052 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.02.38 PM.png,overripe,overripe,0.0,0.5864802002906799,0.41351979970932007 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.03.17 PM.png,overripe,overripe,0.0,0.4142550826072693,0.5857449173927307 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.03.40 PM.png,overripe,overripe,0.0,0.43034353852272034,0.569656491279602 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.05.12 PM.png,overripe,overripe,0.0,0.42269766330718994,0.5773023366928101 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.05.52 PM.png,overripe,overripe,0.0,0.8441019654273987,0.15589801967144012 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.06.13 PM.png,overripe,overripe,0.0,0.4455825984477997,0.5544174313545227 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.06.40 PM.png,overripe,overripe,0.0,0.41819676756858826,0.5818032026290894 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.07.04 PM.png,overripe,overripe,0.0,0.5823701024055481,0.4176298975944519 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.09.43 PM.png,overripe,overripe,0.0,0.7220033407211304,0.277996689081192 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5597981810569763,0.4402018189430237 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.10.27 PM.png,overripe,overripe,0.0,0.4905471205711365,0.5094528794288635 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.46153879165649414,0.5384612083435059 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.11.52 PM.png,overripe,overripe,0.0,0.5081029534339905,0.49189701676368713 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.12.49 PM.png,overripe,overripe,0.0,0.7385621070861816,0.26143792271614075 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.13.10 PM.png,overripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.13.39 PM.png,overripe,overripe,0.0,0.4075345993041992,0.5924654006958008 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.44041985273361206,0.5595801472663879 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.14.43 PM.png,overripe,overripe,0.0,0.4152606129646301,0.5847393870353699 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,overripe,0.0,0.7848396301269531,0.21516035497188568 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.17.27 PM.png,overripe,overripe,0.0,0.4366793632507324,0.5633206367492676 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.07 PM.png,overripe,overripe,0.0,0.6539896726608276,0.34601032733917236 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.4711407721042633,0.5288591980934143 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.20.20 PM.png,overripe,overripe,0.0,0.9575220942497253,0.04247787594795227 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.4122196435928345,0.5877803564071655 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.45711973309516907,0.5428802371025085 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.22.32 PM.png,overripe,overripe,0.0,0.6491385102272034,0.35086148977279663 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.25.12 PM.png,overripe,overripe,0.0,0.4073278605937958,0.5926721692085266 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.25.18 PM.png,overripe,overripe,0.0,0.41178905963897705,0.588210940361023 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.26.01 PM.png,overripe,overripe,0.0,0.8567703366279602,0.1432296484708786 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.26.21 PM.png,overripe,overripe,0.0,0.42250069975852966,0.577499270439148 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.22 PM.png,overripe,overripe,0.0,0.6382330060005188,0.3617669641971588 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.35 PM.png,overripe,overripe,0.0,0.7533740997314453,0.2466258853673935 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.41 PM.png,overripe,overripe,0.0,0.531108558177948,0.4688914716243744 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.52 PM.png,overripe,overripe,0.0,0.41116082668304443,0.5888391733169556 +banana/test/overripe/translation_Screen Shot 2018-06-12 at 9.27.56 PM.png,overripe,overripe,0.0,0.4199279546737671,0.5800720453262329 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.07 PM.png,overripe,overripe,0.0,0.5966823101043701,0.4033176898956299 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.18 PM.png,overripe,overripe,0.0,0.9961385130882263,0.003861496690660715 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.48.46 PM.png,overripe,overripe,0.0,0.41620194911956787,0.5837980508804321 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.50.15 PM.png,overripe,overripe,0.0,0.44873976707458496,0.551260232925415 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.52.21 PM.png,overripe,overripe,0.0,0.41604146361351013,0.5839585065841675 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.53.42 PM.png,overripe,overripe,0.0,0.4643203914165497,0.5356796383857727 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.21 PM.png,overripe,overripe,0.0,0.4510710537433624,0.54892897605896 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.41 PM.png,overripe,overripe,0.0,0.6172770857810974,0.382722944021225 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.55.53 PM.png,overripe,overripe,0.0,0.613049328327179,0.38695067167282104 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.56.06 PM.png,overripe,overripe,0.0,0.5936073064804077,0.4063926935195923 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.56.11 PM.png,overripe,overripe,0.0,0.4220080077648163,0.5779919624328613 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.57.34 PM.png,overripe,overripe,0.0,0.5650535821914673,0.4349464476108551 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.58.07 PM.png,overripe,overripe,0.0,0.8006658554077148,0.19933414459228516 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.58.57 PM.png,overripe,overripe,0.0,0.40304842591285706,0.5969516038894653 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.59.23 PM.png,overripe,overripe,0.0,0.6651078462600708,0.3348921835422516 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 8.59.51 PM.png,overripe,overripe,0.0,0.3795820474624634,0.6204179525375366 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.00.17 PM.png,overripe,overripe,0.0,0.42052707076072693,0.5794728994369507 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.01.10 PM.png,overripe,overripe,0.0,0.46582600474357605,0.5341739654541016 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.01.51 PM.png,overripe,overripe,0.0,0.4404166340827942,0.5595833659172058 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.02.09 PM.png,overripe,overripe,0.0,0.6047645807266235,0.39523541927337646 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.15 PM.png,overripe,overripe,0.0,0.42282506823539734,0.5771749019622803 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.19 PM.png,overripe,overripe,0.0,0.45058828592300415,0.5494117140769958 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.04.47 PM.png,overripe,overripe,0.0,0.4028780162334442,0.5971219539642334 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.05.08 PM.png,overripe,overripe,0.0,0.45936715602874756,0.5406328439712524 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.05.21 PM.png,overripe,overripe,0.0,0.4024459421634674,0.5975540280342102 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.06.05 PM.png,overripe,overripe,0.0,0.5552839636802673,0.44471603631973267 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.06.27 PM.png,overripe,overripe,0.0,0.42154309153556824,0.5784569382667542 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.07.08 PM.png,overripe,overripe,0.0,0.6201404929161072,0.3798595070838928 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.08.59 PM.png,overripe,overripe,0.0,0.4556134045124054,0.544386625289917 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.09 PM.png,overripe,overripe,0.0,0.44604772329330444,0.5539522767066956 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.22 PM.png,overripe,overripe,0.0,0.43209269642829895,0.5679073333740234 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.29 PM.png,overripe,overripe,0.0,0.40288370847702026,0.5971162915229797 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.09.55 PM.png,overripe,overripe,0.0,0.5616564750671387,0.43834352493286133 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.27 PM.png,overripe,overripe,0.0,0.49032875895500183,0.5096712112426758 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.32 PM.png,overripe,overripe,0.0,0.42534542083740234,0.5746545791625977 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.10.55 PM.png,overripe,overripe,0.0,0.46286243200302124,0.5371375679969788 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.11.00 PM.png,overripe,overripe,0.0,0.5005115866661072,0.49948838353157043 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.11.27 PM.png,overripe,overripe,0.0,0.6311663389205933,0.36883363127708435 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.12.32 PM.png,overripe,overripe,0.0,0.4001311659812927,0.5998688340187073 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.13.21 PM.png,overripe,overripe,0.0,0.4393240213394165,0.5606759786605835 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.13.48 PM.png,overripe,overripe,0.0,0.45079687237739563,0.5492031574249268 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.01 PM.png,overripe,overripe,0.0,0.43455827236175537,0.5654417276382446 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.28 PM.png,overripe,overripe,0.0,0.4724476933479309,0.5275523066520691 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.14.48 PM.png,overripe,overripe,0.0,0.4018910229206085,0.5981089472770691 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.16.47 PM.png,overripe,overripe,0.0,0.7794567346572876,0.2205432951450348 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.16.57 PM.png,overripe,overripe,0.0,0.7054411172866821,0.2945588529109955 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.17.19 PM.png,overripe,overripe,0.0,0.46953603625297546,0.5304639339447021 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.17.57 PM.png,overripe,overripe,0.09396454691886902,0.8444444537162781,0.15555556118488312 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.18.07 PM.png,overripe,overripe,0.0,0.47569721937179565,0.5243027806282043 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.18.57 PM.png,overripe,overripe,0.0,0.44101905822753906,0.5589809417724609 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.19.08 PM.png,overripe,overripe,0.0,0.4868828356266022,0.5131171941757202 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.19.28 PM.png,overripe,overripe,0.0,0.3442631959915161,0.6557368040084839 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.20.14 PM.png,overripe,overripe,0.0,0.47138655185699463,0.5286134481430054 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.21.05 PM.png,overripe,overripe,0.0,0.40060174465179443,0.5993982553482056 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.21.14 PM.png,overripe,overripe,0.0,0.41125914454460144,0.5887408256530762 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.22.13 PM.png,overripe,overripe,0.0,0.4592039883136749,0.5407960414886475 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.22.50 PM.png,overripe,overripe,0.0,0.7377047538757324,0.2622952461242676 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.00 PM.png,overripe,overripe,0.0,0.4029286205768585,0.5970713496208191 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.18 PM.png,overripe,overripe,0.0,0.4099743962287903,0.5900256037712097 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.23 PM.png,overripe,overripe,0.0,0.40916284918785095,0.5908371210098267 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.25.54 PM.png,overripe,overripe,0.0,0.40018898248672485,0.5998110175132751 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.26.27 PM.png,overripe,overripe,0.0617702417075634,0.6458821296691895,0.35411787033081055 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.27.11 PM.png,overripe,overripe,0.0,0.7735167741775513,0.22648325562477112 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.27.15 PM.png,overripe,overripe,0.0,0.6683064699172974,0.33169350028038025 +banana/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 9.28.09 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 10.00.37 PM.png,ripe,overripe,0.0,0.40001577138900757,0.5999842286109924 +banana/test/ripe/Screen Shot 2018-06-12 at 10.01.07 PM.png,ripe,overripe,0.0,0.4137611985206604,0.5862388014793396 +banana/test/ripe/Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.40017133951187134,0.5998286604881287 +banana/test/ripe/Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.40217816829681396,0.597821831703186 +banana/test/ripe/Screen Shot 2018-06-12 at 10.05.54 PM.png,ripe,overripe,0.0,0.503227710723877,0.49677225947380066 +banana/test/ripe/Screen Shot 2018-06-12 at 10.06.38 PM.png,ripe,overripe,0.0,0.4051584303379059,0.5948415994644165 +banana/test/ripe/Screen Shot 2018-06-12 at 10.07.21 PM.png,ripe,overripe,0.0,0.40021154284477234,0.5997884273529053 +banana/test/ripe/Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4004342257976532,0.5995658040046692 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.40284299850463867,0.5971570014953613 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.10 PM.png,ripe,overripe,0.0,0.40008464455604553,0.5999153852462769 +banana/test/ripe/Screen Shot 2018-06-12 at 9.38.15 PM.png,ripe,overripe,0.0,0.40357813239097595,0.5964218378067017 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.406267374753952,0.5937325954437256 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4268682301044464,0.573131799697876 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.33 PM.png,ripe,overripe,0.0,0.4021894037723541,0.5978106260299683 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.47 PM.png,ripe,overripe,0.0,0.41406431794166565,0.5859357118606567 +banana/test/ripe/Screen Shot 2018-06-12 at 9.39.58 PM.png,ripe,overripe,0.0,0.4003910422325134,0.5996089577674866 +banana/test/ripe/Screen Shot 2018-06-12 at 9.40.26 PM.png,ripe,overripe,0.0,0.40081986784935,0.5991801023483276 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.4005763530731201,0.5994236469268799 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.30 PM.png,ripe,overripe,0.0,0.4023776650428772,0.5976223349571228 +banana/test/ripe/Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.40004509687423706,0.5999549031257629 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.40110424160957336,0.598895788192749 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.53 PM.png,ripe,overripe,0.0,0.40206512808799744,0.5979348421096802 +banana/test/ripe/Screen Shot 2018-06-12 at 9.43.59 PM.png,ripe,overripe,0.0,0.40231823921203613,0.5976817607879639 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.02 PM.png,ripe,overripe,0.0,0.4006114602088928,0.5993885397911072 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.15 PM.png,ripe,overripe,0.0,0.4078090488910675,0.5921909213066101 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.22 PM.png,ripe,overripe,0.0,0.4028102159500122,0.5971897840499878 +banana/test/ripe/Screen Shot 2018-06-12 at 9.45.34 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.40105006098747253,0.5989499688148499 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.4000996947288513,0.5999003052711487 +banana/test/ripe/Screen Shot 2018-06-12 at 9.47.55 PM.png,ripe,overripe,0.0,0.42692163586616516,0.5730783343315125 +banana/test/ripe/Screen Shot 2018-06-12 at 9.49.00 PM.png,ripe,overripe,0.0,0.4020845293998718,0.5979154706001282 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.40032583475112915,0.5996741652488708 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.44 PM.png,ripe,overripe,0.0,0.40241116285324097,0.597588837146759 +banana/test/ripe/Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.42760056257247925,0.5723994374275208 +banana/test/ripe/Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.4003199636936188,0.5996800065040588 +banana/test/ripe/Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.4021573066711426,0.5978426933288574 +banana/test/ripe/Screen Shot 2018-06-12 at 9.54.35 PM.png,ripe,overripe,0.0,0.40045052766799927,0.5995494723320007 +banana/test/ripe/Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40310707688331604,0.5968929529190063 +banana/test/ripe/Screen Shot 2018-06-12 at 9.55.46 PM.png,ripe,overripe,0.0,0.4137071371078491,0.5862928628921509 +banana/test/ripe/Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.40231314301490784,0.5976868271827698 +banana/test/ripe/Screen Shot 2018-06-12 at 9.56.03 PM.png,ripe,overripe,0.0,0.40290728211402893,0.5970926880836487 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.17 PM.png,ripe,overripe,0.0,0.4001804292201996,0.5998196005821228 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.41498908400535583,0.5850108861923218 +banana/test/ripe/Screen Shot 2018-06-12 at 9.57.31 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/Screen Shot 2018-06-12 at 9.58.36 PM.png,ripe,overripe,0.0,0.4394181966781616,0.5605818033218384 +banana/test/ripe/Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.4009401202201843,0.5990598797798157 +banana/test/ripe/Screen Shot 2018-06-12 at 9.59.48 PM.png,ripe,overripe,0.0,0.4015248119831085,0.5984751582145691 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.07 PM.png,ripe,overripe,0.0,0.4031350314617157,0.5968649387359619 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.12 PM.png,ripe,overripe,0.0,0.40426772832870483,0.5957322716712952 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.40048184990882874,0.5995181798934937 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.40169090032577515,0.5983090996742249 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.13 PM.png,ripe,overripe,0.0,0.40077248215675354,0.5992275476455688 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.41486233472824097,0.585137665271759 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,overripe,0.0,0.4001055359840393,0.5998944640159607 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.4045589566230774,0.5954410433769226 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.40082553029060364,0.5991744995117188 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.40074822306632996,0.5992517471313477 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.38.29 PM.png,ripe,overripe,0.0,0.42404070496559143,0.5759592652320862 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.39.53 PM.png,ripe,overripe,0.0,0.4001491069793701,0.5998508930206299 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.40.43 PM.png,ripe,overripe,0.0,0.40050044655799866,0.599499523639679 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.4000452756881714,0.5999547243118286 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.42.29 PM.png,ripe,overripe,0.0,0.40263718366622925,0.5973628163337708 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.43.27 PM.png,ripe,overripe,0.0,0.4105256497859955,0.5894743204116821 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4016863703727722,0.5983136296272278 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.4097648859024048,0.5902351140975952 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.40476319193840027,0.5952367782592773 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.40009188652038574,0.5999081134796143 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.46.48 PM.png,ripe,overripe,0.0,0.4074811041355133,0.5925188660621643 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.49.15 PM.png,ripe,overripe,0.0,0.40109652280807495,0.598903477191925 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.49.37 PM.png,ripe,overripe,0.0,0.40205445885658264,0.597945511341095 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.426172137260437,0.573827862739563 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.50.53 PM.png,ripe,overripe,0.0,0.40115514397621155,0.5988448262214661 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.55.19 PM.png,ripe,overripe,0.0,0.4025651216506958,0.5974348783493042 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4000404179096222,0.5999596118927002 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.41403928399086,0.5859606862068176 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.07 PM.png,ripe,overripe,0.0,0.4010542631149292,0.5989457368850708 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4072134792804718,0.5927865505218506 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.40076297521591187,0.5992370247840881 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.40012890100479126,0.5998710989952087 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.12 PM.png,ripe,overripe,0.0,0.4059765934944153,0.5940234065055847 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.17 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,overripe,0.0,0.40565964579582214,0.5943403244018555 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.00.00 PM.png,ripe,overripe,0.0,0.4001830816268921,0.5998169183731079 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.4004173278808594,0.5995826721191406 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.07 PM.png,ripe,overripe,0.0,0.40694460272789,0.5930553674697876 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.40112191438674927,0.5988780856132507 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.01.52 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.4006412625312805,0.5993587374687195 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.40004217624664307,0.5999578237533569 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.02.36 PM.png,ripe,overripe,0.0,0.4005014896392822,0.5994985103607178 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.4016706645488739,0.5983293056488037 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.05.20 PM.png,ripe,overripe,0.0,0.4175286293029785,0.5824713706970215 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.24 PM.png,ripe,overripe,0.0,0.4025256037712097,0.5974743962287903 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.33 PM.png,ripe,overripe,0.0,0.40638095140457153,0.5936190485954285 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.06.59 PM.png,ripe,overripe,0.0,0.400058388710022,0.599941611289978 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.4075324833393097,0.5924675464630127 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.41.57 PM.png,ripe,overripe,0.0,0.4294571578502655,0.5705428123474121 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4006046950817108,0.5993952751159668 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.40013524889945984,0.5998647212982178 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.40017861127853394,0.5998213887214661 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.46.48 PM.png,ripe,overripe,0.0,0.40009936690330505,0.5999006628990173 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.27 PM.png,ripe,overripe,0.0,0.41007524728775024,0.5899247527122498 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4007580578327179,0.5992419719696045 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.40005019307136536,0.5999497771263123 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.49.23 PM.png,ripe,overripe,0.0,0.40549236536026,0.59450763463974 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.49.45 PM.png,ripe,overripe,0.0,0.4017292857170105,0.5982707142829895 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.50.12 PM.png,ripe,overripe,0.0,0.4010520875453949,0.5989479422569275 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.50.44 PM.png,ripe,overripe,0.0,0.4023985266685486,0.5976014733314514 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.4012118875980377,0.5987881422042847 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.37 PM.png,ripe,overripe,0.0,0.40767526626586914,0.5923247337341309 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.52.45 PM.png,ripe,overripe,0.0,0.4104578495025635,0.5895421504974365 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.54.02 PM.png,ripe,overripe,0.0,0.4001550078392029,0.5998449921607971 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4001179039478302,0.5998821258544922 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.55.19 PM.png,ripe,overripe,0.0,0.40260612964630127,0.5973938703536987 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.56.03 PM.png,ripe,overripe,0.0,0.4000100791454315,0.5999899506568909 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4000909626483917,0.5999090671539307 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.57.08 PM.png,ripe,overripe,0.0,0.4154648184776306,0.5845351815223694 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.59.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,overripe,0.0,0.40206587314605713,0.5979341268539429 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.42423516511917114,0.5757648348808289 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.00.49 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.01.27 PM.png,ripe,overripe,0.0,0.4003828763961792,0.5996171236038208 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.4001411199569702,0.5998588800430298 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.07 PM.png,ripe,overripe,0.0,0.40208959579467773,0.5979104042053223 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.13 PM.png,ripe,overripe,0.0,0.40080639719963074,0.5991936326026917 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.35 PM.png,ripe,overripe,0.0,0.4003045856952667,0.5996953845024109 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.05.54 PM.png,ripe,overripe,0.0,0.471702516078949,0.528297483921051 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.06.59 PM.png,ripe,overripe,0.0,0.40007469058036804,0.5999252796173096 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.40561503171920776,0.5943849682807922 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.38.29 PM.png,ripe,overripe,0.0,0.4072902202606201,0.5927097797393799 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.00 PM.png,ripe,overripe,0.0,0.4002033472061157,0.5997966527938843 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.40015938878059387,0.5998405814170837 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.4169665277004242,0.5830335021018982 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.26 PM.png,ripe,overripe,0.0,0.40022581815719604,0.599774181842804 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.38 PM.png,ripe,overripe,0.0,0.4042184352874756,0.5957815647125244 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.40.49 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.41.18 PM.png,ripe,overripe,0.0,0.4007248282432556,0.5992751717567444 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.41.38 PM.png,ripe,overripe,0.0,0.4161908030509949,0.5838091969490051 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.42.29 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.40044569969177246,0.5995543003082275 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.44.49 PM.png,ripe,overripe,0.0,0.4012131989002228,0.5987867712974548 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4004206359386444,0.599579393863678 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.45.28 PM.png,ripe,overripe,0.0,0.40070807933807373,0.5992919206619263 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,overripe,0.0,0.43373438715934753,0.5662655830383301 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.47.27 PM.png,ripe,overripe,0.0,0.41036340594291687,0.5896365642547607 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.48.04 PM.png,ripe,overripe,0.0,0.4007483124732971,0.5992516875267029 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.48.39 PM.png,ripe,overripe,0.0,0.4005746841430664,0.5994253158569336 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.49.23 PM.png,ripe,overripe,0.0,0.4053896963596344,0.594610333442688 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.50.53 PM.png,ripe,overripe,0.0,0.4001190662384033,0.5998809337615967 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.51.44 PM.png,ripe,overripe,0.0,0.4004523456096649,0.5995476245880127 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.51.58 PM.png,ripe,overripe,0.0,0.4000793695449829,0.5999206304550171 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.26 PM.png,ripe,overripe,0.0,0.4001457691192627,0.5998542308807373 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4102632999420166,0.5897367000579834 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.52.50 PM.png,ripe,overripe,0.0,0.4002382159233093,0.5997617840766907 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.400312602519989,0.599687397480011 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.53.22 PM.png,ripe,overripe,0.0,0.40987464785575867,0.590125322341919 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.07 PM.png,ripe,overripe,0.0,0.4007641673088074,0.5992358326911926 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.18 PM.png,ripe,overripe,0.06597807258367538,0.6171181797981262,0.3828818202018738 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40013834834098816,0.5998616814613342 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.55.13 PM.png,ripe,overripe,0.0,0.4000285267829895,0.5999714732170105 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.40046507120132446,0.5995349287986755 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.56.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4000946283340454,0.5999053716659546 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.57.38 PM.png,ripe,overripe,0.0,0.4001621901988983,0.5998377799987793 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.57.42 PM.png,ripe,overripe,0.0,0.4001575708389282,0.5998424291610718 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.58.49 PM.png,ripe,overripe,0.0,0.43394020199775696,0.5660597681999207 +banana/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,overripe,0.0,0.4020269513130188,0.5979730486869812 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.00.07 PM.png,ripe,overripe,0.0,0.4025065302848816,0.5974934697151184 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.40014171600341797,0.599858283996582 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.4014604985713959,0.5985394716262817 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4000653326511383,0.5999346375465393 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,overripe,0.0,0.4000946283340454,0.5999053716659546 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.07.21 PM.png,ripe,overripe,0.0,0.40007835626602173,0.5999216437339783 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.40057745575904846,0.5994225740432739 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.4055301249027252,0.5944699048995972 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.38.04 PM.png,ripe,overripe,0.0,0.4000999927520752,0.5999000072479248 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4001786410808563,0.5998213291168213 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.41.57 PM.png,ripe,overripe,0.0,0.42968666553497314,0.5703133344650269 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.4099133312702179,0.5900866985321045 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,overripe,0.0,0.43495556712150574,0.5650444626808167 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4000703990459442,0.5999295711517334 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.40 PM.png,ripe,overripe,0.0,0.4076027274131775,0.5923972725868225 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.46.55 PM.png,ripe,overripe,0.0,0.42912155389785767,0.5708784461021423 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4008009731769562,0.5991990566253662 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.47.46 PM.png,ripe,overripe,0.0,0.40003660321235657,0.599963366985321 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.48.04 PM.png,ripe,overripe,0.0,0.40069037675857544,0.5993096232414246 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.48.54 PM.png,ripe,overripe,0.0,0.40167462825775146,0.5983253717422485 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.40033480525016785,0.5996652245521545 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.51.36 PM.png,ripe,overripe,0.0,0.40356895327568054,0.5964310169219971 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4104151129722595,0.5895848870277405 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.30 PM.png,ripe,overripe,0.0,0.41550976037979126,0.5844902396202087 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.41 PM.png,ripe,overripe,0.0,0.41291895508766174,0.5870810747146606 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.53.46 PM.png,ripe,overripe,0.0,0.4044325053691864,0.595567524433136 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.4001374840736389,0.5998625159263611 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.55.27 PM.png,ripe,overripe,0.0,0.4082065522670746,0.591793417930603 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.55.33 PM.png,ripe,overripe,0.0,0.4000779092311859,0.5999220609664917 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.56.23 PM.png,ripe,overripe,0.0,0.402100533246994,0.5978994369506836 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.56.56 PM.png,ripe,overripe,0.0,0.40066638588905334,0.5993335843086243 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.57.42 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4075164198875427,0.5924835801124573 +banana/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,overripe,0.0,0.4053225517272949,0.5946774482727051 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.21 PM.png,ripe,overripe,0.0,0.40061280131340027,0.5993872284889221 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.37 PM.png,ripe,overripe,0.0,0.40001940727233887,0.5999805927276611 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.4196944236755371,0.5803055763244629 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.01.52 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.01.58 PM.png,ripe,overripe,0.0,0.40525901317596436,0.5947409868240356 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.02.01 PM.png,ripe,overripe,0.0,0.4143305718898773,0.5856694579124451 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.02.36 PM.png,ripe,overripe,0.0,0.40060877799987793,0.5993912220001221 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.40006548166275024,0.5999345183372498 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4000431001186371,0.5999568700790405 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.06.07 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.4004741311073303,0.5995258688926697 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.38.22 PM.png,ripe,overripe,0.0,0.4017881453037262,0.5982118248939514 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.39.22 PM.png,ripe,overripe,0.0,0.40211278200149536,0.5978872179985046 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.39.58 PM.png,ripe,overripe,0.0,0.40017685294151306,0.5998231172561646 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.22 PM.png,ripe,overripe,0.0,0.40002766251564026,0.5999723076820374 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.40.32 PM.png,ripe,overripe,0.0,0.40014973282814026,0.5998502969741821 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.4004271626472473,0.5995728373527527 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.38 PM.png,ripe,overripe,0.0,0.4164261519908905,0.5835738778114319 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.4000493586063385,0.5999506115913391 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.40050452947616577,0.5994954705238342 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.32 PM.png,ripe,overripe,0.0,0.40543970465660095,0.5945602655410767 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.48 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.43.59 PM.png,ripe,overripe,0.0,0.4002636969089508,0.5997362732887268 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4003359377384186,0.5996640920639038 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.46.07 PM.png,ripe,overripe,0.0,0.40816760063171387,0.5918323993682861 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.48.14 PM.png,ripe,overripe,0.0,0.4004130959510803,0.5995869040489197 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.49.15 PM.png,ripe,overripe,0.0,0.4008656442165375,0.5991343855857849 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.49.45 PM.png,ripe,overripe,0.0,0.40177032351493835,0.5982296466827393 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.51.00 PM.png,ripe,overripe,0.0,0.44647181034088135,0.5535281896591187 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.52.06 PM.png,ripe,overripe,0.0,0.4006986618041992,0.5993013381958008 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.03 PM.png,ripe,overripe,0.0,0.40028610825538635,0.599713921546936 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.22 PM.png,ripe,overripe,0.0,0.40994784235954285,0.5900521874427795 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.53.41 PM.png,ripe,overripe,0.0,0.4119848608970642,0.5880151391029358 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.54.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.02 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.42 PM.png,ripe,overripe,0.0,0.40002450346946716,0.5999754667282104 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.46 PM.png,ripe,overripe,0.0,0.4109625220298767,0.5890374779701233 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.400481253862381,0.5995187163352966 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.40138912200927734,0.5986108779907227 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.58.07 PM.png,ripe,overripe,0.0,0.4011700749397278,0.5988299250602722 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.4073731601238251,0.5926268696784973 +banana/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 9.59.02 PM.png,ripe,overripe,0.0,0.4001477360725403,0.5998522639274597 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.00 PM.png,ripe,overripe,0.0,0.40232837200164795,0.597671627998352 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.4267595708370209,0.5732404589653015 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.00.49 PM.png,ripe,overripe,0.0,0.40207764506340027,0.5979223847389221 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.40373170375823975,0.5962682962417603 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.02.01 PM.png,ripe,overripe,0.0,0.41809821128845215,0.5819017887115479 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.4017581641674042,0.5982418656349182 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.4021693766117096,0.597830593585968 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.05.41 PM.png,ripe,overripe,0.0,0.4169991612434387,0.5830008387565613 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.4066186249256134,0.5933813452720642 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.24 PM.png,ripe,overripe,0.0,0.4046179950237274,0.5953819751739502 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.06.38 PM.png,ripe,overripe,0.0,0.40758955478668213,0.5924104452133179 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.07.29 PM.png,ripe,overripe,0.0,0.41729721426963806,0.5827028155326843 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.4024200439453125,0.5975799560546875 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 10.08.01 PM.png,ripe,overripe,0.0,0.412956565618515,0.5870434641838074 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.38.38 PM.png,ripe,overripe,0.0,0.41345709562301636,0.5865429043769836 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.00 PM.png,ripe,overripe,0.0,0.40254122018814087,0.5974587798118591 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.33 PM.png,ripe,overripe,0.0,0.4041823148727417,0.5958176851272583 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.39.47 PM.png,ripe,overripe,0.0,0.4158986806869507,0.5841013193130493 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.40.43 PM.png,ripe,overripe,0.0,0.40239956974983215,0.5976004600524902 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.40.49 PM.png,ripe,overripe,0.0,0.4020630717277527,0.5979369282722473 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4025110602378845,0.5974889397621155 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.41.26 PM.png,ripe,overripe,0.0,0.4024946689605713,0.5975053310394287 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.42.03 PM.png,ripe,overripe,0.0,0.41891810297966003,0.5810818672180176 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.40274396538734436,0.597256064414978 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.43.39 PM.png,ripe,overripe,0.0,0.40329185128211975,0.5967081189155579 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.43.53 PM.png,ripe,overripe,0.0,0.4039858281612396,0.596014142036438 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.44.06 PM.png,ripe,overripe,0.0,0.4119851887226105,0.5880147814750671 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.45.02 PM.png,ripe,overripe,0.0,0.4025084972381592,0.5974915027618408 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.40764710307121277,0.5923529267311096 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.40198591351509094,0.5980141162872314 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.40 PM.png,ripe,overripe,0.0,0.4091266095638275,0.5908734202384949 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.40196359157562256,0.5980364084243774 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.47.22 PM.png,ripe,overripe,0.0,0.40200161933898926,0.5979983806610107 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.48.54 PM.png,ripe,overripe,0.0,0.40441974997520447,0.5955802202224731 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.50.38 PM.png,ripe,overripe,0.0,0.40281349420547485,0.5971865057945251 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.50.48 PM.png,ripe,overripe,0.0,0.42928922176361084,0.5707107782363892 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.4041205942630768,0.5958793759346008 +banana/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.40186572074890137,0.5981342792510986 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.00.12 PM.png,ripe,overripe,0.0,0.40505945682525635,0.5949405431747437 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.01.37 PM.png,ripe,overripe,0.0,0.4017530381679535,0.5982469916343689 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.01.46 PM.png,ripe,overripe,0.0,0.40018606185913086,0.5998139381408691 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.02.19 PM.png,ripe,overripe,0.0,0.4004511535167694,0.599548876285553 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.04.49 PM.png,ripe,overripe,0.0,0.40009805560112,0.5999019145965576 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.04.54 PM.png,ripe,overripe,0.0,0.40023937821388245,0.5997606515884399 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.05.20 PM.png,ripe,overripe,0.0,0.4173445403575897,0.5826554894447327 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 10.07.52 PM.png,ripe,overripe,0.0,0.40093642473220825,0.5990635752677917 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.38.51 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.40.38 PM.png,ripe,overripe,0.0,0.40462028980255127,0.5953797101974487 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.41.30 PM.png,ripe,overripe,0.0,0.4023813009262085,0.5976186990737915 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.41.43 PM.png,ripe,overripe,0.0,0.4000752866268158,0.5999246835708618 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.42.18 PM.png,ripe,overripe,0.0,0.40012988448143005,0.5998700857162476 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.43.27 PM.png,ripe,overripe,0.0,0.41099151968955994,0.5890084505081177 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.44.19 PM.png,ripe,overripe,0.0,0.41969534754753113,0.5803046226501465 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4000992178916931,0.5999007821083069 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.45.34 PM.png,ripe,overripe,0.0,0.40005359053611755,0.5999464392662048 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.46.24 PM.png,ripe,overripe,0.0,0.4000685513019562,0.5999314785003662 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4008170962333679,0.5991829037666321 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.48.14 PM.png,ripe,overripe,0.0,0.4002113342285156,0.5997886657714844 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.51.58 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.4024154245853424,0.5975845456123352 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.53.30 PM.png,ripe,overripe,0.0,0.4174172878265381,0.5825827121734619 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.53.46 PM.png,ripe,overripe,0.0,0.4047594368457794,0.595240592956543 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.54.43 PM.png,ripe,overripe,0.0,0.40217897295951843,0.597821056842804 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.40215376019477844,0.597846269607544 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.55.27 PM.png,ripe,overripe,0.0,0.40768393874168396,0.5923160314559937 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.55.42 PM.png,ripe,overripe,0.0,0.4000590443611145,0.5999409556388855 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.56.16 PM.png,ripe,overripe,0.0,0.4034040868282318,0.5965959429740906 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.56.33 PM.png,ripe,overripe,0.0,0.4002333879470825,0.5997666120529175 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4151339530944824,0.5848660469055176 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.40734198689460754,0.5926579833030701 +banana/test/ripe/translation_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.21 PM.png,ripe,overripe,0.0,0.40305575728416443,0.5969442129135132 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.30 PM.png,ripe,overripe,0.0,0.4006196856498718,0.5993803143501282 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.00.42 PM.png,ripe,overripe,0.0,0.42489439249038696,0.575105607509613 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.01.27 PM.png,ripe,overripe,0.0,0.40015125274658203,0.599848747253418 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.02.24 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.04.54 PM.png,ripe,overripe,0.0,0.4002692997455597,0.5997307300567627 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.04.59 PM.png,ripe,overripe,0.0,0.4002191424369812,0.5997808575630188 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.06.12 PM.png,ripe,overripe,0.0,0.4003983438014984,0.5996016263961792 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.06.19 PM.png,ripe,overripe,0.0,0.40433865785598755,0.5956613421440125 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 10.07.46 PM.png,ripe,overripe,0.0,0.400434285402298,0.5995656847953796 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.15 PM.png,ripe,overripe,0.0,0.40357810258865356,0.5964218974113464 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.22 PM.png,ripe,overripe,0.0,0.40422943234443665,0.5957705974578857 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.38.51 PM.png,ripe,overripe,0.0,0.4000566005706787,0.5999433994293213 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.39.13 PM.png,ripe,overripe,0.0,0.4062674641609192,0.5937325358390808 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.40.02 PM.png,ripe,overripe,0.0,0.40135589241981506,0.5986440777778625 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.40.10 PM.png,ripe,overripe,0.0,0.40019193291664124,0.5998080968856812 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.41.03 PM.png,ripe,overripe,0.0,0.4007888734340668,0.5992110967636108 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.35 PM.png,ripe,overripe,0.0,0.4007224440574646,0.5992775559425354 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.41 PM.png,ripe,overripe,0.0,0.40487492084503174,0.5951250791549683 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.42.49 PM.png,ripe,overripe,0.0,0.4021640717983246,0.5978359580039978 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.44.55 PM.png,ripe,overripe,0.0,0.4001590609550476,0.5998409390449524 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.02 PM.png,ripe,overripe,0.0,0.4083845615386963,0.5916154384613037 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.12 PM.png,ripe,overripe,0.0,0.44220998883247375,0.5577899813652039 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.19 PM.png,ripe,overripe,0.0,0.4056133031845093,0.5943866968154907 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.30 PM.png,ripe,overripe,0.0,0.4001263380050659,0.5998736619949341 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.46.43 PM.png,ripe,overripe,0.0,0.40005946159362793,0.5999405384063721 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.18 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.39 PM.png,ripe,overripe,0.0,0.4010499119758606,0.5989500880241394 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.47.51 PM.png,ripe,overripe,0.0,0.40009963512420654,0.5999003648757935 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.48.21 PM.png,ripe,overripe,0.0,0.40004533529281616,0.5999546647071838 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.48.26 PM.png,ripe,overripe,0.0,0.42643094062805176,0.5735690593719482 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.49.00 PM.png,ripe,overripe,0.0,0.4020310044288635,0.5979689955711365 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.49.54 PM.png,ripe,overripe,0.0,0.4149446487426758,0.5850553512573242 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.50.04 PM.png,ripe,overripe,0.0,0.4003256559371948,0.5996743440628052 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.51.24 PM.png,ripe,overripe,0.0,0.4021562933921814,0.5978437066078186 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.51.29 PM.png,ripe,overripe,0.0,0.4027984142303467,0.5972015857696533 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.21 PM.png,ripe,overripe,0.0,0.40294891595840454,0.5970510840415955 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.34 PM.png,ripe,overripe,0.0,0.4104839563369751,0.5895160436630249 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.52.45 PM.png,ripe,overripe,0.0,0.4108235836029053,0.5891764163970947 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.53.51 PM.png,ripe,overripe,0.0,0.4021630585193634,0.597836971282959 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.54.56 PM.png,ripe,overripe,0.0,0.4022073447704315,0.5977926850318909 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.55.13 PM.png,ripe,overripe,0.0,0.40001657605171204,0.5999834537506104 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.55.53 PM.png,ripe,overripe,0.0,0.4022936224937439,0.5977063775062561 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.16 PM.png,ripe,overripe,0.0,0.4002748429775238,0.5997251272201538 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.23 PM.png,ripe,overripe,0.0,0.4044758975505829,0.5955240726470947 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.48 PM.png,ripe,overripe,0.0,0.4000835716724396,0.599916398525238 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.56.56 PM.png,ripe,overripe,0.0,0.40074285864830017,0.5992571711540222 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.25 PM.png,ripe,overripe,0.0,0.4169984459877014,0.5830015540122986 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.31 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.57.38 PM.png,ripe,overripe,0.0,0.40019601583480835,0.5998039841651917 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.58.16 PM.png,ripe,overripe,0.0,0.40715041756629944,0.592849612236023 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.58.56 PM.png,ripe,overripe,0.0,0.4009407162666321,0.5990592837333679 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.07 PM.png,ripe,overripe,0.0,0.40018805861473083,0.5998119711875916 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.35 PM.png,ripe,overripe,0.0,0.40616628527641296,0.5938336849212646 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.41 PM.png,ripe,overripe,0.0,0.4098283350467682,0.5901716351509094 +banana/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 9.59.48 PM.png,ripe,overripe,0.0,0.40152496099472046,0.5984750390052795 +banana/test/unripe/1.jpg,unripe,overripe,0.010567075572907925,0.8272535800933838,0.1727464199066162 +banana/test/unripe/10.jpg,unripe,overripe,0.0,0.4888888895511627,0.5111111402511597 +banana/test/unripe/100.jpg,unripe,unripe,0.2655267119407654,0.7344732880592346,0.0 +banana/test/unripe/101.jpg,unripe,unripe,0.40162965655326843,0.598370373249054,0.0 +banana/test/unripe/102.jpg,unripe,unripe,0.2683210074901581,0.7316789627075195,0.0 +banana/test/unripe/103.jpg,unripe,overripe,0.16475652158260345,0.8352434635162354,0.0 +banana/test/unripe/104.jpg,unripe,unripe,0.27399715781211853,0.7260028123855591,0.0 +banana/test/unripe/105.jpg,unripe,overripe,0.01698598824441433,0.7073110342025757,0.2926889657974243 +banana/test/unripe/106.jpg,unripe,overripe,0.12427154183387756,0.8757284879684448,0.01575138419866562 +banana/test/unripe/107.jpg,unripe,unripe,0.3024612069129944,0.6975387930870056,0.0 +banana/test/unripe/108.jpg,unripe,overripe,0.0,0.6211298704147339,0.3788700997829437 +banana/test/unripe/109.jpg,unripe,overripe,0.0,0.42819520831108093,0.5718047618865967 +banana/test/unripe/11.jpg,unripe,unripe,0.30912819504737854,0.6908717751502991,0.0 +banana/test/unripe/110.jpg,unripe,overripe,0.0,0.41003045439720154,0.5899695754051208 +banana/test/unripe/111.jpg,unripe,unripe,0.1892145723104477,0.8107854127883911,0.0 +banana/test/unripe/112.jpg,unripe,overripe,0.02045512944459915,0.8876076936721802,0.11239233613014221 +banana/test/unripe/113.jpg,unripe,unripe,0.23482529819011688,0.7651746869087219,0.0 +banana/test/unripe/114.jpg,unripe,overripe,0.0,0.5223981738090515,0.4776018559932709 +banana/test/unripe/115.jpg,unripe,unripe,0.2486145943403244,0.7513853907585144,0.0 +banana/test/unripe/116.jpg,unripe,overripe,0.08942171186208725,0.9105783104896545,0.05469401925802231 +banana/test/unripe/117.jpg,unripe,overripe,0.05047721788287163,0.8976612687110901,0.1023387461900711 +banana/test/unripe/118.jpg,unripe,unripe,0.3072366714477539,0.6927633285522461,0.0 +banana/test/unripe/119.jpg,unripe,overripe,0.1392562985420227,0.8607437014579773,0.03691481798887253 +banana/test/unripe/12.jpg,unripe,overripe,0.0,0.7484447956085205,0.2515552043914795 +banana/test/unripe/120.jpg,unripe,overripe,0.0,0.568229615688324,0.431770384311676 +banana/test/unripe/121.jpg,unripe,overripe,0.17842499911785126,0.8215749859809875,0.06327395886182785 +banana/test/unripe/122.jpg,unripe,overripe,0.0,0.61409991979599,0.38590008020401 +banana/test/unripe/123.jpg,unripe,overripe,0.0,0.5223981738090515,0.4776018559932709 +banana/test/unripe/124.jpg,unripe,overripe,0.203399196267128,0.7966008186340332,0.004923016764223576 +banana/test/unripe/125.jpg,unripe,ripe,0.07866686582565308,0.9213331341743469,0.0 +banana/test/unripe/126.jpg,unripe,overripe,0.0,0.5592802166938782,0.44071975350379944 +banana/test/unripe/127.jpg,unripe,unripe,0.3570950925350189,0.6429049372673035,0.0 +banana/test/unripe/128.jpg,unripe,unripe,0.24252907931804657,0.7574709057807922,0.0 +banana/test/unripe/129.jpg,unripe,unripe,0.20827266573905945,0.7917273640632629,0.0 +banana/test/unripe/13.jpg,unripe,unripe,0.3138461410999298,0.6861538290977478,0.0 +banana/test/unripe/130.jpg,unripe,overripe,0.004703576676547527,0.6208068132400513,0.3791932165622711 +banana/test/unripe/131.jpg,unripe,overripe,0.16475652158260345,0.8352434635162354,0.0 +banana/test/unripe/132.jpg,unripe,unripe,0.36032965779304504,0.6396703720092773,0.0 +banana/test/unripe/133.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/134.jpg,unripe,unripe,0.22052955627441406,0.7794704437255859,0.0 +banana/test/unripe/135.jpg,unripe,overripe,0.06161519140005112,0.9019502401351929,0.09804974496364594 +banana/test/unripe/136.jpg,unripe,unripe,0.3957971930503845,0.6042028069496155,0.0 +banana/test/unripe/137.jpg,unripe,unripe,0.3524216413497925,0.6475783586502075,0.0 +banana/test/unripe/138.jpg,unripe,overripe,0.01698598824441433,0.7073110342025757,0.2926889657974243 +banana/test/unripe/139.jpg,unripe,overripe,0.0,0.4345071315765381,0.5654928684234619 +banana/test/unripe/14.jpg,unripe,overripe,0.0,0.6951653957366943,0.30483460426330566 +banana/test/unripe/140.jpg,unripe,unripe,0.3615051507949829,0.6384948492050171,0.0 +banana/test/unripe/141.jpg,unripe,overripe,0.27551165223121643,0.724488377571106,0.008766564540565014 +banana/test/unripe/142.jpg,unripe,overripe,0.0,0.4345071315765381,0.5654928684234619 +banana/test/unripe/143.jpg,unripe,unripe,0.21406371891498566,0.7859362959861755,0.0 +banana/test/unripe/144.jpg,unripe,unripe,0.24013753235340118,0.75986248254776,0.0 +banana/test/unripe/145.jpg,unripe,overripe,0.0,0.41205069422721863,0.587949275970459 +banana/test/unripe/146.jpg,unripe,unripe,0.3615051507949829,0.6384948492050171,0.0 +banana/test/unripe/147.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/148.jpg,unripe,overripe,0.004703576676547527,0.6208068132400513,0.3791932165622711 +banana/test/unripe/149.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/15.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/150.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/151.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/152.jpg,unripe,unripe,0.16041822731494904,0.8395817875862122,0.0 +banana/test/unripe/153.jpg,unripe,overripe,0.06034813076257706,0.9384207725524902,0.061579227447509766 +banana/test/unripe/154.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/155.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/156.jpg,unripe,overripe,0.05791496858000755,0.7975926995277405,0.20240730047225952 +banana/test/unripe/157.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/158.jpg,unripe,overripe,0.0,0.4332895278930664,0.5667104721069336 +banana/test/unripe/159.jpg,unripe,unripe,0.14362889528274536,0.8563711047172546,0.0 +banana/test/unripe/16.jpg,unripe,unripe,0.23479454219341278,0.765205442905426,0.0 +banana/test/unripe/160.jpg,unripe,ripe,0.06357325613498688,0.9364267587661743,0.0 +banana/test/unripe/161.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/162.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/163.jpg,unripe,unripe,0.16041822731494904,0.8395817875862122,0.0 +banana/test/unripe/164.jpg,unripe,overripe,0.06034813076257706,0.9384207725524902,0.061579227447509766 +banana/test/unripe/165.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/166.jpg,unripe,overripe,0.05791496858000755,0.7975926995277405,0.20240730047225952 +banana/test/unripe/167.jpg,unripe,unripe,0.27474966645240784,0.7252503037452698,0.0 +banana/test/unripe/168.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/169.jpg,unripe,overripe,0.0,0.4332895278930664,0.5667104721069336 +banana/test/unripe/17.jpg,unripe,unripe,0.18782629072666168,0.8121737241744995,0.0 +banana/test/unripe/170.jpg,unripe,unripe,0.14362889528274536,0.8563711047172546,0.0 +banana/test/unripe/171.jpg,unripe,unripe,0.2305772304534912,0.7694227695465088,0.0 +banana/test/unripe/172.jpg,unripe,unripe,0.212583988904953,0.7874159812927246,0.0 +banana/test/unripe/173.jpg,unripe,overripe,0.06777908653020859,0.9093322157859802,0.09066776931285858 +banana/test/unripe/174.jpg,unripe,unripe,0.25212398171424866,0.747875988483429,0.0 +banana/test/unripe/175.jpg,unripe,overripe,0.0,0.43292179703712463,0.567078173160553 +banana/test/unripe/176.jpg,unripe,overripe,0.0,0.6205971837043762,0.3794028162956238 +banana/test/unripe/177.jpg,unripe,unripe,0.26761600375175476,0.7323840260505676,0.0 +banana/test/unripe/178.jpg,unripe,overripe,0.2800717353820801,0.7199282646179199,0.024006908759474754 +banana/test/unripe/179.jpg,unripe,unripe,0.3268786668777466,0.6731213331222534,0.0 +banana/test/unripe/18.jpg,unripe,unripe,0.1252054125070572,0.874794602394104,0.0 +banana/test/unripe/180.jpg,unripe,overripe,0.0,0.47735369205474854,0.5226463079452515 +banana/test/unripe/181.jpg,unripe,unripe,0.28002822399139404,0.719971776008606,0.0 +banana/test/unripe/182.jpg,unripe,overripe,0.0,0.5504146218299866,0.4495854079723358 +banana/test/unripe/183.jpg,unripe,unripe,0.3609153926372528,0.6390845775604248,0.0 +banana/test/unripe/184.jpg,unripe,overripe,0.0,0.27308642864227295,0.726913571357727 +banana/test/unripe/185.jpg,unripe,unripe,0.11332949250936508,0.8866705298423767,0.0 +banana/test/unripe/186.jpg,unripe,overripe,0.041280459612607956,0.7574878931045532,0.2425120770931244 +banana/test/unripe/187.jpg,unripe,unripe,0.18666194379329681,0.813338041305542,0.0 +banana/test/unripe/188.jpg,unripe,unripe,0.23594951629638672,0.7640504837036133,0.0 +banana/test/unripe/189.jpg,unripe,unripe,0.32199665904045105,0.6780033707618713,0.0 +banana/test/unripe/19.jpg,unripe,overripe,0.0,0.40907028317451477,0.5909296870231628 +banana/test/unripe/190.jpg,unripe,overripe,0.348239004611969,0.651760995388031,0.19908884167671204 +banana/test/unripe/191.jpg,unripe,overripe,0.0,0.7213281989097595,0.2786717712879181 +banana/test/unripe/192.jpg,unripe,unripe,0.28421831130981445,0.7157816886901855,0.0 +banana/test/unripe/193.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/194.jpg,unripe,overripe,0.0,0.4160403609275818,0.5839596390724182 +banana/test/unripe/195.jpg,unripe,unripe,0.18063423037528992,0.8193657398223877,0.0 +banana/test/unripe/196.jpg,unripe,overripe,0.0,0.6143019795417786,0.38569802045822144 +banana/test/unripe/197.jpg,unripe,overripe,0.1837267428636551,0.8162732720375061,0.0 +banana/test/unripe/198.jpg,unripe,overripe,0.22605352103710175,0.7739464640617371,0.008564363233745098 +banana/test/unripe/199.jpg,unripe,overripe,0.0,0.40186741948127747,0.5981326103210449 +banana/test/unripe/2.jpg,unripe,unripe,0.31392616033554077,0.6860738396644592,0.0 +banana/test/unripe/20.jpg,unripe,unripe,0.17040516436100006,0.8295948505401611,0.0 +banana/test/unripe/200.jpg,unripe,overripe,0.14560994505882263,0.854390025138855,0.08484848588705063 +banana/test/unripe/201.jpg,unripe,overripe,0.017595956102013588,0.6956878900527954,0.3043121099472046 +banana/test/unripe/202.jpg,unripe,overripe,0.0,0.4975706934928894,0.5024293065071106 +banana/test/unripe/203.jpg,unripe,overripe,0.13168370723724365,0.8382284641265869,0.16177156567573547 +banana/test/unripe/204.jpg,unripe,overripe,0.0,0.4656853973865509,0.5343146324157715 +banana/test/unripe/205.jpg,unripe,unripe,0.27269652485847473,0.7273034453392029,0.0 +banana/test/unripe/206.jpg,unripe,overripe,0.0,0.4081977307796478,0.5918022394180298 +banana/test/unripe/207.jpg,unripe,overripe,0.04895251989364624,0.8967402577400208,0.10325972735881805 +banana/test/unripe/208.jpg,unripe,overripe,0.0,0.6129420399665833,0.38705793023109436 +banana/test/unripe/209.jpg,unripe,overripe,0.0,0.6960944533348083,0.30390554666519165 +banana/test/unripe/21.jpg,unripe,unripe,0.23387251794338226,0.7661274671554565,0.0 +banana/test/unripe/210.jpg,unripe,overripe,0.2153741866350174,0.7846258282661438,0.03641912341117859 +banana/test/unripe/211.jpg,unripe,unripe,0.20140686631202698,0.7985931038856506,0.0 +banana/test/unripe/212.jpg,unripe,unripe,0.311082124710083,0.688917875289917,0.0 +banana/test/unripe/213.jpg,unripe,overripe,0.0,0.5737767219543457,0.4262232780456543 +banana/test/unripe/214.jpg,unripe,overripe,0.0,0.564656674861908,0.43534329533576965 +banana/test/unripe/215.jpg,unripe,overripe,0.0,0.40318581461906433,0.5968142151832581 +banana/test/unripe/216.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/217.jpg,unripe,overripe,0.0,0.5497584342956543,0.4502415359020233 +banana/test/unripe/218.jpg,unripe,unripe,0.22907985746860504,0.7709201574325562,0.0 +banana/test/unripe/219.jpg,unripe,unripe,0.19046944379806519,0.8095305562019348,0.0 +banana/test/unripe/22.jpg,unripe,unripe,0.19422373175621033,0.8057762980461121,0.0 +banana/test/unripe/220.jpg,unripe,overripe,0.0691191628575325,0.9114335179328918,0.08856646716594696 +banana/test/unripe/221.jpg,unripe,unripe,0.4737188220024109,0.5262811779975891,0.0 +banana/test/unripe/222.jpg,unripe,unripe,0.1720374971628189,0.8279625177383423,0.0 +banana/test/unripe/223.jpg,unripe,overripe,0.0,0.9490973949432373,0.0509025976061821 +banana/test/unripe/224.jpg,unripe,overripe,0.0,0.6129420399665833,0.38705793023109436 +banana/test/unripe/225.jpg,unripe,overripe,0.041280459612607956,0.7574878931045532,0.2425120770931244 +banana/test/unripe/226.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/227.jpg,unripe,overripe,0.0,0.41389134526252747,0.5861086845397949 +banana/test/unripe/228.jpg,unripe,overripe,0.0,0.4540715515613556,0.545928418636322 +banana/test/unripe/229.jpg,unripe,unripe,0.3057272434234619,0.6942727565765381,0.0 +banana/test/unripe/23.jpg,unripe,unripe,0.18347479403018951,0.8165252208709717,0.0 +banana/test/unripe/230.jpg,unripe,overripe,0.18422876298427582,0.815771222114563,0.016404885798692703 +banana/test/unripe/231.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/232.jpg,unripe,overripe,0.22605352103710175,0.7739464640617371,0.008564363233745098 +banana/test/unripe/233.jpg,unripe,overripe,0.0,0.5189635157585144,0.481036514043808 +banana/test/unripe/234.jpg,unripe,overripe,0.0,0.40033960342407227,0.5996603965759277 +banana/test/unripe/235.jpg,unripe,unripe,0.32049599289894104,0.6795039772987366,0.0 +banana/test/unripe/236.jpg,unripe,overripe,0.0,0.4001862108707428,0.5998137593269348 +banana/test/unripe/237.jpg,unripe,unripe,0.19080016016960144,0.809199869632721,0.0 +banana/test/unripe/238.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/239.jpg,unripe,overripe,0.0,0.4081977307796478,0.5918022394180298 +banana/test/unripe/24.jpg,unripe,overripe,0.03671346604824066,0.5796058177947998,0.4203941822052002 +banana/test/unripe/240.jpg,unripe,unripe,0.24574898183345795,0.7542510032653809,0.0 +banana/test/unripe/241.jpg,unripe,overripe,0.0,0.4224942624568939,0.5775057077407837 +banana/test/unripe/242.jpg,unripe,overripe,0.0,0.599346399307251,0.400653600692749 +banana/test/unripe/243.jpg,unripe,overripe,0.03880923613905907,0.8061468005180359,0.19385316967964172 +banana/test/unripe/244.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/245.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/246.jpg,unripe,overripe,0.09422402083873749,0.8464912176132202,0.1535087674856186 +banana/test/unripe/247.jpg,unripe,overripe,0.0,0.46841078996658325,0.5315892100334167 +banana/test/unripe/248.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/249.jpg,unripe,overripe,0.04895251989364624,0.8967402577400208,0.10325972735881805 +banana/test/unripe/25.jpg,unripe,overripe,0.0,0.6705909967422485,0.32940900325775146 +banana/test/unripe/250.jpg,unripe,overripe,0.0,0.5504146218299866,0.4495854079723358 +banana/test/unripe/251.jpg,unripe,overripe,0.0,0.4599769413471222,0.5400230884552002 +banana/test/unripe/252.jpg,unripe,unripe,0.27632999420166016,0.7236700057983398,0.0 +banana/test/unripe/253.jpg,unripe,overripe,0.18545351922512054,0.8145464658737183,0.0230836383998394 +banana/test/unripe/254.jpg,unripe,overripe,0.017102090641856194,0.7238872051239014,0.27611279487609863 +banana/test/unripe/255.jpg,unripe,overripe,0.0,0.477450430393219,0.522549569606781 +banana/test/unripe/256.jpg,unripe,overripe,0.0,0.8287110328674316,0.17128898203372955 +banana/test/unripe/257.jpg,unripe,unripe,0.25278863310813904,0.7472113370895386,0.0 +banana/test/unripe/258.jpg,unripe,unripe,0.27235227823257446,0.7276477217674255,0.0 +banana/test/unripe/259.jpg,unripe,overripe,0.0510951466858387,0.7902809381484985,0.20971906185150146 +banana/test/unripe/26.jpg,unripe,overripe,0.0,0.4662393033504486,0.533760666847229 +banana/test/unripe/260.jpg,unripe,overripe,0.0,0.4122416079044342,0.5877583622932434 +banana/test/unripe/261.jpg,unripe,unripe,0.1914602369070053,0.8085397481918335,0.0 +banana/test/unripe/262.jpg,unripe,unripe,0.3476392924785614,0.652360737323761,0.0 +banana/test/unripe/263.jpg,unripe,overripe,0.04404762014746666,0.7650793790817261,0.23492063581943512 +banana/test/unripe/264.jpg,unripe,overripe,0.0,0.4114750623703003,0.5885249376296997 +banana/test/unripe/265.jpg,unripe,overripe,0.0849359855055809,0.9150640368461609,0.05156177282333374 +banana/test/unripe/266.jpg,unripe,overripe,0.0,0.4246913492679596,0.575308620929718 +banana/test/unripe/267.jpg,unripe,overripe,0.0,0.400383859872818,0.5996161699295044 +banana/test/unripe/268.jpg,unripe,overripe,0.0,0.4599769413471222,0.5400230884552002 +banana/test/unripe/269.jpg,unripe,unripe,0.27490442991256714,0.7250955700874329,0.0 +banana/test/unripe/27.jpg,unripe,overripe,0.0,0.5693944096565247,0.43060556054115295 +banana/test/unripe/270.jpg,unripe,overripe,0.0,0.4051169455051422,0.5948830246925354 +banana/test/unripe/271.jpg,unripe,overripe,0.0,0.593269407749176,0.406730592250824 +banana/test/unripe/272.jpg,unripe,overripe,0.060498058795928955,0.933277428150177,0.0667225569486618 +banana/test/unripe/273.jpg,unripe,unripe,0.17898686230182648,0.8210131525993347,0.0 +banana/test/unripe/274.jpg,unripe,overripe,0.0,0.42932450771331787,0.5706754922866821 +banana/test/unripe/275.jpg,unripe,overripe,0.0,0.4085965156555176,0.5914034843444824 +banana/test/unripe/276.jpg,unripe,unripe,0.24809731543064117,0.75190269947052,0.0 +banana/test/unripe/277.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/278.jpg,unripe,unripe,0.306523859500885,0.693476140499115,0.0 +banana/test/unripe/279.jpg,unripe,overripe,0.0,0.5178247690200806,0.48217523097991943 +banana/test/unripe/28.jpg,unripe,unripe,0.23791487514972687,0.7620851397514343,0.0 +banana/test/unripe/280.jpg,unripe,overripe,0.0,0.46352481842041016,0.5364751815795898 +banana/test/unripe/281.jpg,unripe,overripe,0.0,0.46840915083885193,0.5315908193588257 +banana/test/unripe/282.jpg,unripe,overripe,0.0,0.5853102803230286,0.41468971967697144 +banana/test/unripe/283.jpg,unripe,overripe,0.08783463388681412,0.9121653437614441,0.04787077754735947 +banana/test/unripe/284.jpg,unripe,overripe,0.0,0.6173912882804871,0.38260868191719055 +banana/test/unripe/285.jpg,unripe,overripe,0.09255142509937286,0.9074485898017883,0.050510238856077194 +banana/test/unripe/286.jpg,unripe,overripe,0.2124839872121811,0.7875159978866577,0.016421807929873466 +banana/test/unripe/287.jpg,unripe,overripe,0.0,0.593269407749176,0.406730592250824 +banana/test/unripe/288.jpg,unripe,overripe,0.017595956102013588,0.6956878900527954,0.3043121099472046 +banana/test/unripe/289.jpg,unripe,overripe,0.0,0.46352481842041016,0.5364751815795898 +banana/test/unripe/29.jpg,unripe,unripe,0.2737400531768799,0.7262599468231201,0.0 +banana/test/unripe/290.jpg,unripe,overripe,0.04404762014746666,0.7650793790817261,0.23492063581943512 +banana/test/unripe/291.jpg,unripe,unripe,0.306523859500885,0.693476140499115,0.0 +banana/test/unripe/292.jpg,unripe,unripe,0.21250776946544647,0.7874922156333923,0.0 +banana/test/unripe/293.jpg,unripe,overripe,0.2736797034740448,0.7263202667236328,0.04706426337361336 +banana/test/unripe/294.jpg,unripe,overripe,0.0,0.48513340950012207,0.5148665904998779 +banana/test/unripe/295.jpg,unripe,overripe,0.0,0.410793662071228,0.589206337928772 +banana/test/unripe/296.jpg,unripe,overripe,0.0,0.41417577862739563,0.585824191570282 +banana/test/unripe/297.jpg,unripe,overripe,0.1652795523405075,0.8347204327583313,0.0304588470607996 +banana/test/unripe/298.jpg,unripe,overripe,0.0,0.3190154731273651,0.6809845566749573 +banana/test/unripe/299.jpg,unripe,overripe,0.0,0.41205069422721863,0.587949275970459 +banana/test/unripe/3.jpg,unripe,unripe,0.2790624499320984,0.7209375500679016,0.0 +banana/test/unripe/30.jpg,unripe,overripe,0.0,0.5946194529533386,0.4053805470466614 +banana/test/unripe/300.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/301.jpg,unripe,overripe,0.0,0.41417577862739563,0.585824191570282 +banana/test/unripe/302.jpg,unripe,unripe,0.2945569157600403,0.7054430842399597,0.0 +banana/test/unripe/303.jpg,unripe,overripe,0.0,0.40811029076576233,0.5918896794319153 +banana/test/unripe/304.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/305.jpg,unripe,ripe,0.09690377116203308,0.9030962586402893,0.0 +banana/test/unripe/306.jpg,unripe,overripe,0.0,0.4268256425857544,0.5731743574142456 +banana/test/unripe/307.jpg,unripe,overripe,0.1270330250263214,0.8729669451713562,0.02431643009185791 +banana/test/unripe/308.jpg,unripe,overripe,0.13674618303775787,0.8632538318634033,0.06109175458550453 +banana/test/unripe/309.jpg,unripe,unripe,0.21980716288089752,0.7801928520202637,0.0 +banana/test/unripe/31.jpg,unripe,unripe,0.21450957655906677,0.7854904532432556,0.0 +banana/test/unripe/310.jpg,unripe,unripe,0.2546294331550598,0.7453705668449402,0.0 +banana/test/unripe/311.jpg,unripe,overripe,0.0,0.6651515364646912,0.3348484933376312 +banana/test/unripe/312.jpg,unripe,overripe,0.1652795523405075,0.8347204327583313,0.0304588470607996 +banana/test/unripe/313.jpg,unripe,overripe,0.0,0.48513340950012207,0.5148665904998779 +banana/test/unripe/314.jpg,unripe,unripe,0.21250776946544647,0.7874922156333923,0.0 +banana/test/unripe/315.jpg,unripe,overripe,0.0,0.5853102803230286,0.41468971967697144 +banana/test/unripe/316.jpg,unripe,unripe,0.21906639635562897,0.7809336185455322,0.0 +banana/test/unripe/317.jpg,unripe,overripe,0.06623509526252747,0.9337649345397949,0.010335036553442478 +banana/test/unripe/318.jpg,unripe,ripe,0.08340875059366226,0.916591227054596,0.0 +banana/test/unripe/319.jpg,unripe,overripe,0.0,0.40570804476737976,0.5942919254302979 +banana/test/unripe/32.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/320.jpg,unripe,overripe,0.1927945464849472,0.8072054386138916,0.09367088973522186 +banana/test/unripe/321.jpg,unripe,overripe,0.0,0.41144078969955444,0.5885592103004456 +banana/test/unripe/322.jpg,unripe,overripe,0.06623509526252747,0.9337649345397949,0.010335036553442478 +banana/test/unripe/323.jpg,unripe,ripe,0.09690377116203308,0.9030962586402893,0.0 +banana/test/unripe/324.jpg,unripe,overripe,0.03606715798377991,0.8471442461013794,0.1528557389974594 +banana/test/unripe/325.jpg,unripe,overripe,0.0,0.4903954863548279,0.5096045136451721 +banana/test/unripe/326.jpg,unripe,overripe,0.1887485682964325,0.8112514615058899,0.06558872014284134 +banana/test/unripe/327.jpg,unripe,unripe,0.20435471832752228,0.7956452965736389,0.0 +banana/test/unripe/328.jpg,unripe,overripe,0.01958335004746914,0.8031197190284729,0.1968802809715271 +banana/test/unripe/329.jpg,unripe,unripe,0.21980716288089752,0.7801928520202637,0.0 +banana/test/unripe/33.jpg,unripe,overripe,0.16914106905460358,0.8308589458465576,0.047035958617925644 +banana/test/unripe/330.jpg,unripe,overripe,0.14635108411312103,0.8536489009857178,0.0017094017239287496 +banana/test/unripe/331.jpg,unripe,overripe,0.0,0.491601824760437,0.508398175239563 +banana/test/unripe/332.jpg,unripe,unripe,0.18423785269260406,0.8157621622085571,0.0 +banana/test/unripe/333.jpg,unripe,overripe,0.0841563418507576,0.915843665599823,0.07781939208507538 +banana/test/unripe/334.jpg,unripe,unripe,0.3615909218788147,0.6384090781211853,0.0 +banana/test/unripe/335.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/336.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/337.jpg,unripe,overripe,0.0,0.5626559853553772,0.4373440146446228 +banana/test/unripe/338.jpg,unripe,overripe,0.0,0.525356113910675,0.47464388608932495 +banana/test/unripe/339.jpg,unripe,unripe,0.2945569157600403,0.7054430842399597,0.0 +banana/test/unripe/34.jpg,unripe,overripe,0.0,0.40407416224479675,0.5959258079528809 +banana/test/unripe/340.jpg,unripe,overripe,0.0,0.4456702172756195,0.5543297529220581 +banana/test/unripe/341.jpg,unripe,overripe,0.0,0.6173912882804871,0.38260868191719055 +banana/test/unripe/342.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/343.jpg,unripe,unripe,0.34701865911483765,0.6529813408851624,0.0 +banana/test/unripe/344.jpg,unripe,overripe,0.0,0.4374403655529022,0.5625596046447754 +banana/test/unripe/345.jpg,unripe,overripe,0.0584152415394783,0.9270281195640564,0.0729718953371048 +banana/test/unripe/346.jpg,unripe,overripe,0.0,0.4095405042171478,0.5904595255851746 +banana/test/unripe/347.jpg,unripe,overripe,0.0,0.4485221207141876,0.55147784948349 +banana/test/unripe/348.jpg,unripe,overripe,0.13992559909820557,0.8600744009017944,0.0 +banana/test/unripe/349.jpg,unripe,unripe,0.15143704414367676,0.8485629558563232,0.0 +banana/test/unripe/35.jpg,unripe,overripe,0.0,0.6803610920906067,0.3196389079093933 +banana/test/unripe/350.jpg,unripe,unripe,0.30551671981811523,0.6944832801818848,0.0 +banana/test/unripe/351.jpg,unripe,overripe,0.0,0.6721028089523315,0.32789719104766846 +banana/test/unripe/352.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/353.jpg,unripe,unripe,0.13517801463603973,0.8648219704627991,0.0 +banana/test/unripe/354.jpg,unripe,unripe,0.21788348257541656,0.7821165323257446,0.0 +banana/test/unripe/355.jpg,unripe,overripe,0.0,0.41126248240470886,0.5887374877929688 +banana/test/unripe/356.jpg,unripe,overripe,0.0,0.5567777752876282,0.4432222247123718 +banana/test/unripe/357.jpg,unripe,unripe,0.3476392924785614,0.652360737323761,0.0 +banana/test/unripe/358.jpg,unripe,unripe,0.19937273859977722,0.8006272315979004,0.0 +banana/test/unripe/359.jpg,unripe,overripe,0.07270319014787674,0.9067650437355042,0.09323493391275406 +banana/test/unripe/36.jpg,unripe,unripe,0.19442878663539886,0.8055711984634399,0.0 +banana/test/unripe/360.jpg,unripe,overripe,0.0,0.41022011637687683,0.5897798538208008 +banana/test/unripe/361.jpg,unripe,overripe,0.0,0.4228925108909607,0.5771074891090393 +banana/test/unripe/362.jpg,unripe,overripe,0.03880923613905907,0.8061468005180359,0.19385316967964172 +banana/test/unripe/363.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/364.jpg,unripe,unripe,0.30551671981811523,0.6944832801818848,0.0 +banana/test/unripe/365.jpg,unripe,unripe,0.34701865911483765,0.6529813408851624,0.0 +banana/test/unripe/366.jpg,unripe,overripe,0.03606715798377991,0.8471442461013794,0.1528557389974594 +banana/test/unripe/367.jpg,unripe,overripe,0.0,0.4100912809371948,0.5899087190628052 +banana/test/unripe/368.jpg,unripe,unripe,0.13517801463603973,0.8648219704627991,0.0 +banana/test/unripe/369.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/37.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/370.jpg,unripe,overripe,0.0,0.5661218166351318,0.43387818336486816 +banana/test/unripe/371.jpg,unripe,overripe,0.0,0.41126248240470886,0.5887374877929688 +banana/test/unripe/372.jpg,unripe,overripe,0.0,0.42768076062202454,0.5723192095756531 +banana/test/unripe/373.jpg,unripe,overripe,0.0,0.4002741277217865,0.5997259020805359 +banana/test/unripe/374.jpg,unripe,overripe,0.0,0.6403522491455078,0.3596477508544922 +banana/test/unripe/375.jpg,unripe,unripe,0.4793522357940674,0.5206477642059326,0.0 +banana/test/unripe/376.jpg,unripe,overripe,0.07270319014787674,0.9067650437355042,0.09323493391275406 +banana/test/unripe/377.jpg,unripe,unripe,0.2137744277715683,0.7862255573272705,0.0 +banana/test/unripe/378.jpg,unripe,overripe,0.1887485682964325,0.8112514615058899,0.06558872014284134 +banana/test/unripe/379.jpg,unripe,overripe,0.11097387224435806,0.8890261054039001,0.0809817910194397 +banana/test/unripe/38.jpg,unripe,overripe,0.0,0.46940740942955017,0.5305926203727722 +banana/test/unripe/380.jpg,unripe,overripe,0.0,0.5652977228164673,0.4347022473812103 +banana/test/unripe/381.jpg,unripe,overripe,0.0,0.6122082471847534,0.3877917528152466 +banana/test/unripe/382.jpg,unripe,overripe,0.0,0.43425893783569336,0.5657410621643066 +banana/test/unripe/383.jpg,unripe,overripe,0.0,0.47783640027046204,0.5221635699272156 +banana/test/unripe/384.jpg,unripe,overripe,0.14098002016544342,0.7819691300392151,0.21803084015846252 +banana/test/unripe/385.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/386.jpg,unripe,overripe,0.0,0.5064189434051514,0.493581086397171 +banana/test/unripe/387.jpg,unripe,overripe,0.21762900054454803,0.7823709845542908,0.0 +banana/test/unripe/388.jpg,unripe,unripe,0.2505240738391876,0.7494759559631348,0.0 +banana/test/unripe/389.jpg,unripe,overripe,0.02717781625688076,0.7362287640571594,0.26377126574516296 +banana/test/unripe/39.jpg,unripe,overripe,0.0,0.4121074676513672,0.5878925323486328 +banana/test/unripe/390.jpg,unripe,overripe,0.0,0.42701399326324463,0.5729860067367554 +banana/test/unripe/391.jpg,unripe,overripe,0.0,0.6891566514968872,0.3108433783054352 +banana/test/unripe/392.jpg,unripe,overripe,0.0,0.955960214138031,0.04403981566429138 +banana/test/unripe/393.jpg,unripe,overripe,0.0,0.4792253375053406,0.5207746624946594 +banana/test/unripe/394.jpg,unripe,overripe,0.0,0.6511052250862122,0.34889477491378784 +banana/test/unripe/395.jpg,unripe,overripe,0.051568761467933655,0.9101651906967163,0.08983481675386429 +banana/test/unripe/396.jpg,unripe,overripe,0.0,0.4150037467479706,0.584996223449707 +banana/test/unripe/397.jpg,unripe,overripe,0.0,0.773571789264679,0.22642821073532104 +banana/test/unripe/398.jpg,unripe,unripe,0.3309306204319,0.6690694093704224,0.0 +banana/test/unripe/399.jpg,unripe,overripe,0.0,0.4002090394496918,0.5997909307479858 +banana/test/unripe/4.jpg,unripe,unripe,0.20671246945858002,0.7932875156402588,0.0 +banana/test/unripe/40.jpg,unripe,overripe,0.3259464204311371,0.6740536093711853,0.01123595517128706 +banana/test/unripe/400.jpg,unripe,overripe,0.0,0.4053281247615814,0.5946718454360962 +banana/test/unripe/41.jpg,unripe,overripe,0.0,0.642585039138794,0.35741496086120605 +banana/test/unripe/42.jpg,unripe,ripe,0.0894274190068245,0.9105725884437561,0.0 +banana/test/unripe/43.jpg,unripe,overripe,0.15676464140415192,0.8432353734970093,0.06160794943571091 +banana/test/unripe/44.jpg,unripe,overripe,0.0,0.5622232556343079,0.43777674436569214 +banana/test/unripe/45.jpg,unripe,overripe,0.0,0.5713248252868652,0.42867520451545715 +banana/test/unripe/46.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/47.jpg,unripe,overripe,0.0,0.5046659708023071,0.49533402919769287 +banana/test/unripe/48.jpg,unripe,overripe,0.14100931584835052,0.8589906692504883,0.10424628108739853 +banana/test/unripe/49.jpg,unripe,unripe,0.5467646718025208,0.45323535799980164,0.0 +banana/test/unripe/5.jpg,unripe,overripe,0.07055406272411346,0.9294459223747253,0.06304501742124557 +banana/test/unripe/50.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/51.jpg,unripe,unripe,0.22388215363025665,0.7761178612709045,0.0 +banana/test/unripe/52.jpg,unripe,unripe,0.2584488093852997,0.7415511608123779,0.0 +banana/test/unripe/53.jpg,unripe,overripe,0.12629161775112152,0.8737083673477173,0.027477994561195374 +banana/test/unripe/54.jpg,unripe,unripe,0.2753565013408661,0.7246435284614563,0.0 +banana/test/unripe/55.jpg,unripe,ripe,0.11102695763111115,0.88897305727005,0.0 +banana/test/unripe/56.jpg,unripe,overripe,0.0,0.6575028896331787,0.3424971401691437 +banana/test/unripe/57.jpg,unripe,unripe,0.23406916856765747,0.7659308314323425,0.0 +banana/test/unripe/58.jpg,unripe,ripe,0.10959841310977936,0.8904016017913818,0.0 +banana/test/unripe/59.jpg,unripe,overripe,0.0,0.4091874957084656,0.5908125042915344 +banana/test/unripe/6.jpg,unripe,unripe,0.41194820404052734,0.5880517959594727,0.0 +banana/test/unripe/60.jpg,unripe,overripe,0.0,0.4723740518093109,0.5276259779930115 +banana/test/unripe/61.jpg,unripe,overripe,0.0,0.4385194182395935,0.5614805817604065 +banana/test/unripe/62.jpg,unripe,overripe,0.0,0.6127858757972717,0.38721412420272827 +banana/test/unripe/63.jpg,unripe,unripe,0.2349892258644104,0.7650107741355896,0.0 +banana/test/unripe/64.jpg,unripe,overripe,0.0,0.4093896746635437,0.5906103253364563 +banana/test/unripe/65.jpg,unripe,overripe,0.2201867550611496,0.7798132300376892,0.0 +banana/test/unripe/66.jpg,unripe,unripe,0.25935423374176025,0.7406457662582397,0.0 +banana/test/unripe/67.jpg,unripe,overripe,0.21240898966789246,0.7875909805297852,0.10480109602212906 +banana/test/unripe/68.jpg,unripe,overripe,0.0,0.4566914141178131,0.5433086156845093 +banana/test/unripe/69.jpg,unripe,unripe,0.3017440140247345,0.6982559561729431,0.0 +banana/test/unripe/7.jpg,unripe,overripe,0.11427725851535797,0.8857227563858032,0.05853768065571785 +banana/test/unripe/70.jpg,unripe,unripe,0.29912522435188293,0.7008747458457947,0.0 +banana/test/unripe/71.jpg,unripe,unripe,0.23870299756526947,0.7612969875335693,0.0 +banana/test/unripe/72.jpg,unripe,unripe,0.29973652958869934,0.700263500213623,0.0 +banana/test/unripe/73.jpg,unripe,overripe,0.0,0.4619097113609314,0.5380902886390686 +banana/test/unripe/74.jpg,unripe,unripe,0.19054298102855682,0.809457004070282,0.0 +banana/test/unripe/75.jpg,unripe,overripe,0.0,0.516616940498352,0.48338308930397034 +banana/test/unripe/76.jpg,unripe,ripe,0.10822513699531555,0.8917748332023621,0.0 +banana/test/unripe/77.jpg,unripe,overripe,0.0,0.5859410166740417,0.41405895352363586 +banana/test/unripe/78.jpg,unripe,unripe,0.3186589479446411,0.6813410520553589,0.0 +banana/test/unripe/79.jpg,unripe,unripe,0.2101515233516693,0.7898484468460083,0.0 +banana/test/unripe/8.jpg,unripe,overripe,0.0,0.6063492298126221,0.3936507999897003 +banana/test/unripe/80.jpg,unripe,unripe,0.1720716804265976,0.8279283046722412,0.0 +banana/test/unripe/81.jpg,unripe,ripe,0.10822513699531555,0.8917748332023621,0.0 +banana/test/unripe/82.jpg,unripe,overripe,0.0,0.4171725809574127,0.5828274488449097 +banana/test/unripe/83.jpg,unripe,overripe,0.0,0.4055757522583008,0.5944242477416992 +banana/test/unripe/84.jpg,unripe,unripe,0.23870299756526947,0.7612969875335693,0.0 +banana/test/unripe/85.jpg,unripe,unripe,0.2101515233516693,0.7898484468460083,0.0 +banana/test/unripe/86.jpg,unripe,overripe,0.11886955797672272,0.8811304569244385,0.09054726362228394 +banana/test/unripe/87.jpg,unripe,unripe,0.24426253139972687,0.7557374835014343,0.0 +banana/test/unripe/88.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +banana/test/unripe/89.jpg,unripe,overripe,0.0,0.6959501504898071,0.30404984951019287 +banana/test/unripe/9.jpg,unripe,unripe,0.2609561085700989,0.7390438914299011,0.0 +banana/test/unripe/90.jpg,unripe,overripe,0.0,0.4175189435482025,0.5824810862541199 +banana/test/unripe/91.jpg,unripe,unripe,0.6248812675476074,0.3751187026500702,0.0 +banana/test/unripe/92.jpg,unripe,overripe,0.0,0.42737382650375366,0.5726261734962463 +banana/test/unripe/93.jpg,unripe,unripe,0.23883606493473053,0.7611639499664307,0.0 +banana/test/unripe/94.jpg,unripe,unripe,0.19001485407352448,0.8099851608276367,0.0 +banana/test/unripe/95.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +banana/test/unripe/96.jpg,unripe,unripe,0.2570036053657532,0.7429963946342468,0.0 +banana/test/unripe/97.jpg,unripe,unripe,0.3186589479446411,0.6813410520553589,0.0 +banana/test/unripe/98.jpg,unripe,overripe,0.0,0.40645113587379456,0.5935488343238831 +banana/test/unripe/99.jpg,unripe,unripe,0.3024612069129944,0.6975387930870056,0.0 diff --git a/AgCloud/services/ripeness-baseline/eval/banana_tuned/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/banana_tuned/roc_curves.png new file mode 100644 index 000000000..e66f9d094 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/banana_tuned/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/orange_argmax/metrics.json b/AgCloud/services/ripeness-baseline/eval/orange_argmax/metrics.json new file mode 100644 index 000000000..8fa7265a5 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/orange_argmax/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.410933081998115, + "report": { + "unripe": { + "precision": 0.5588235294117647, + "recall": 0.2814814814814815, + "f1-score": 0.37438423645320196, + "support": 270.0 + }, + "ripe": { + "precision": 0.39094650205761317, + "recall": 0.7345360824742269, + "f1-score": 0.5102954341987467, + "support": 388.0 + }, + "overripe": { + "precision": 0.3826530612244898, + "recall": 0.18610421836228289, + "f1-score": 0.25041736227045075, + "support": 403.0 + }, + "accuracy": 0.410933081998115, + "macro avg": { + "precision": 0.44414103089795587, + "recall": 0.4007072607726638, + "f1-score": 0.3783656776407998, + "support": 1061.0 + }, + "weighted avg": { + "precision": 0.4305172284759658, + "recall": 0.410933081998115, + "f1-score": 0.3769995940683034, + "support": 1061.0 + } + }, + "confusion_matrix": [ + [ + 76, + 171, + 23 + ], + [ + 5, + 285, + 98 + ], + [ + 55, + 273, + 75 + ] + ], + "samples": 1061, + "prefix": "orange/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/orange_argmax/per_image.csv b/AgCloud/services/ripeness-baseline/eval/orange_argmax/per_image.csv new file mode 100644 index 000000000..10b008a2d --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/orange_argmax/per_image.csv @@ -0,0 +1,1062 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +orange/test/overripe/Screen Shot 2018-06-12 at 11.18.34 PM.png,overripe,unripe,0.5813559889793396,0.4186440110206604,0.0 +orange/test/overripe/Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,unripe,0.9807354807853699,0.01926453970372677,0.2364673763513565 +orange/test/overripe/Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,unripe,0.9621754884719849,0.03782452642917633,0.26775965094566345 +orange/test/overripe/Screen Shot 2018-06-12 at 11.19.56 PM.png,overripe,overripe,0.0,0.4263884127140045,0.5736115574836731 +orange/test/overripe/Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,ripe,0.0,0.6619503498077393,0.33804967999458313 +orange/test/overripe/Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,ripe,0.32817143201828003,0.67182856798172,0.2953425943851471 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,unripe,0.502648115158081,0.49735188484191895,0.15113776922225952 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.4949825704097748,0.5050173997879028 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,ripe,0.0,0.858117401599884,0.14188256859779358 +orange/test/overripe/Screen Shot 2018-06-12 at 11.22.32 PM.png,overripe,unripe,0.9591161608695984,0.040883831679821014,0.0 +orange/test/overripe/Screen Shot 2018-06-12 at 11.23.03 PM.png,overripe,unripe,0.6969578266143799,0.3030421733856201,0.26323819160461426 +orange/test/overripe/Screen Shot 2018-06-12 at 11.23.33 PM.png,overripe,overripe,0.0,0.49141886830329895,0.5085811018943787 +orange/test/overripe/Screen Shot 2018-06-12 at 11.24.08 PM.png,overripe,ripe,0.39959079027175903,0.600409209728241,0.11207570880651474 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.20 PM.png,overripe,unripe,0.8290469646453857,0.17095303535461426,0.11556115001440048 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.472676157951355,0.527323842048645 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,unripe,0.6194191575050354,0.3805808126926422,0.2621327042579651 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.07 PM.png,overripe,ripe,0.21391628682613373,0.7860836982727051,0.10147877782583237 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,ripe,0.0,0.7548203468322754,0.24517963826656342 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.18 PM.png,overripe,ripe,0.35202234983444214,0.6479776501655579,0.0582931749522686 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.24 PM.png,overripe,ripe,0.274689257144928,0.7081780433654785,0.2918219268321991 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,ripe,0.3506443202495575,0.6493556499481201,0.025308780372142792 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4180251359939575,0.5819748640060425 +orange/test/overripe/Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,ripe,0.0,0.6343122720718384,0.36568769812583923 +orange/test/overripe/Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.4754934012889862,0.5245066285133362 +orange/test/overripe/Screen Shot 2018-06-12 at 11.28.21 PM.png,overripe,overripe,0.0,0.4760371446609497,0.5239628553390503 +orange/test/overripe/Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,ripe,0.0,0.5705550312995911,0.42944493889808655 +orange/test/overripe/Screen Shot 2018-06-12 at 11.29.44 PM.png,overripe,ripe,0.0,0.7267901301383972,0.2732098698616028 +orange/test/overripe/Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.47287118434906006,0.5271288156509399 +orange/test/overripe/Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,ripe,0.0,0.5278342366218567,0.4721657335758209 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.09 PM.png,overripe,overripe,0.0,0.47256824374198914,0.5274317264556885 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,ripe,0.0,0.6383212804794312,0.36167868971824646 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.21 PM.png,overripe,ripe,0.0,0.536528468132019,0.46347153186798096 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,ripe,0.0,0.7324671745300293,0.2675328254699707 +orange/test/overripe/Screen Shot 2018-06-12 at 11.33.12 PM.png,overripe,overripe,0.0,0.41255369782447815,0.5874463319778442 +orange/test/overripe/Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,ripe,0.040523022413253784,0.8078719973564148,0.1921280324459076 +orange/test/overripe/Screen Shot 2018-06-12 at 11.37.00 PM.png,overripe,ripe,0.0,0.6049668788909912,0.3950331509113312 +orange/test/overripe/Screen Shot 2018-06-12 at 11.37.52 PM.png,overripe,ripe,0.15329048037528992,0.8467094898223877,0.0018609330290928483 +orange/test/overripe/Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,ripe,0.0,0.5364204049110413,0.46357959508895874 +orange/test/overripe/Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,ripe,0.0,0.5203392505645752,0.4796607494354248 +orange/test/overripe/Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,ripe,0.0,0.5790743827819824,0.4209256172180176 +orange/test/overripe/Screen Shot 2018-06-12 at 11.41.35 PM.png,overripe,unripe,0.9723436236381531,0.027656368911266327,0.16256484389305115 +orange/test/overripe/Screen Shot 2018-06-12 at 11.42.38 PM.png,overripe,ripe,0.0,0.6507203578948975,0.34927964210510254 +orange/test/overripe/Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.46143028140068054,0.5385696887969971 +orange/test/overripe/Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,ripe,0.0,0.7188377976417542,0.28116217255592346 +orange/test/overripe/Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,ripe,0.0,0.6302172541618347,0.36978277564048767 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,unripe,0.5476340055465698,0.4523659944534302,0.38992825150489807 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,unripe,0.7918091416358948,0.20819082856178284,0.30429503321647644 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.20.52 PM.png,overripe,ripe,0.0,0.8589234352111816,0.14107654988765717 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,unripe,0.5713017582893372,0.42869821190834045,0.13089488446712494 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,ripe,0.0,0.5055472254753113,0.4944527745246887 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,ripe,0.0,0.7585963010787964,0.2414036989212036 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.29 PM.png,overripe,ripe,0.27037256956100464,0.7296274304389954,0.06957662850618362 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,ripe,0.0,0.6632696390151978,0.33673036098480225 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.16 PM.png,overripe,ripe,0.3126402795314789,0.6313470005989075,0.36865296959877014 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.47797611355781555,0.5220238566398621 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.55 PM.png,overripe,ripe,0.49875566363334656,0.5012443661689758,0.28493547439575195 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.07 PM.png,overripe,ripe,0.09116190671920776,0.896507740020752,0.10349228233098984 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,ripe,0.0,0.7381293177604675,0.26187068223953247 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4193204343318939,0.5806795358657837 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.44 PM.png,overripe,ripe,0.0,0.7926531434059143,0.2073468565940857 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,ripe,0.0,0.5778408646583557,0.4221591353416443 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.29.58 PM.png,overripe,ripe,0.0,0.9112523794174194,0.08874763548374176 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,ripe,0.0,0.7354021072387695,0.2645978629589081 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.30.41 PM.png,overripe,ripe,0.0,0.5630908608436584,0.43690916895866394 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,ripe,0.0,0.5025820732116699,0.4974179267883301 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.32.04 PM.png,overripe,overripe,0.0,0.4872611165046692,0.5127388834953308 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,ripe,0.0,0.5820140242576599,0.4179860055446625 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.34.13 PM.png,overripe,ripe,0.0,0.5029651522636414,0.49703484773635864 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.36.19 PM.png,overripe,ripe,0.0,0.6706015467643738,0.32939842343330383 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,ripe,0.35625454783439636,0.643745481967926,0.07872973382472992 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,ripe,0.06145981326699257,0.8133766651153564,0.18662336468696594 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.40.51 PM.png,overripe,unripe,0.6481132507324219,0.3518867492675781,0.15584227442741394 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.430654913187027,0.5693450570106506 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.43.49 PM.png,overripe,ripe,0.0,0.6686554551124573,0.3313445746898651 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,ripe,0.07333070784807205,0.6004956364631653,0.3995043933391571 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.45.17 PM.png,overripe,ripe,0.0,0.5301396250724792,0.46986037492752075 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.45.42 PM.png,overripe,ripe,0.323691189289093,0.6317269206047058,0.3682730793952942 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,ripe,0.4909811019897461,0.5090188980102539,0.17926804721355438 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,ripe,0.39193421602249146,0.5802605152130127,0.4197394847869873 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,ripe,0.18565113842487335,0.5377812385559082,0.4622187614440918 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.19.37 PM.png,overripe,ripe,0.0,0.7437195181846619,0.25628048181533813 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,unripe,0.8490487337112427,0.15095125138759613,0.027311978861689568 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.20.18 PM.png,overripe,ripe,0.0,0.5269141793251038,0.47308585047721863 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.17 PM.png,overripe,ripe,0.0,0.7847110629081726,0.2152889519929886 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,unripe,0.5851068496704102,0.41489318013191223,0.12363962084054947 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,ripe,0.34572845697402954,0.6542715430259705,0.27659958600997925 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.22.21 PM.png,overripe,ripe,0.022289132699370384,0.6230165958404541,0.3769834041595459 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.22.47 PM.png,overripe,ripe,0.0,0.574116051197052,0.4258839786052704 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.23.40 PM.png,overripe,ripe,0.0,0.5282679796218872,0.4717319905757904 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,ripe,0.0,0.7599790692329407,0.2400209605693817 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.25.16 PM.png,overripe,ripe,0.24029438197612762,0.6087165474891663,0.39128348231315613 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,unripe,0.6151899099349976,0.38481009006500244,0.26983070373535156 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,ripe,0.0,0.7347964644432068,0.2652035355567932 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,ripe,0.0,0.5168538093566895,0.48314619064331055 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.28.21 PM.png,overripe,overripe,0.0,0.47407305240631104,0.525926947593689 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.14 PM.png,overripe,ripe,0.0,0.8337898254394531,0.1662101447582245 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.21 PM.png,overripe,overripe,0.0,0.4486353397369385,0.5513646602630615 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.36 PM.png,overripe,ripe,0.0,0.5268393754959106,0.47316059470176697 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.30.11 PM.png,overripe,ripe,0.41443467140197754,0.5855653285980225,0.24893797934055328 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,ripe,0.30791911482810974,0.6920808553695679,0.19972680509090424 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.31.17 PM.png,overripe,ripe,0.0,0.7697551250457764,0.23024487495422363 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.31.24 PM.png,overripe,overripe,0.0,0.4542027711868286,0.5457972288131714 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,ripe,0.0,0.6391510963439941,0.36084890365600586 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.33.00 PM.png,overripe,overripe,0.0,0.47073110938072205,0.5292688608169556 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.33.23 PM.png,overripe,overripe,0.0,0.4057970345020294,0.594202995300293 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.06 PM.png,overripe,overripe,0.0,0.42225685715675354,0.5777431130409241 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,ripe,0.04310297966003418,0.9013168811798096,0.09868312627077103 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.53 PM.png,overripe,ripe,0.07663267105817795,0.9233673214912415,0.0 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,ripe,0.041373152285814285,0.8113475441932678,0.18865248560905457 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,ripe,0.04056641086935997,0.8174728155136108,0.18252718448638916 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,ripe,0.0,0.5467059016227722,0.4532940983772278 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,ripe,0.0,0.5250239968299866,0.4749760031700134 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,ripe,0.0,0.7599119544029236,0.24008801579475403 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,ripe,0.0,0.5360477566719055,0.46395227313041687 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,ripe,0.0,0.5420335531234741,0.4579664170742035 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.40.42 PM.png,overripe,ripe,0.0,0.5269377827644348,0.4730622172355652 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,ripe,0.16163982450962067,0.543298065662384,0.45670193433761597 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.41.44 PM.png,overripe,unripe,0.9417368769645691,0.05826309695839882,0.1013261005282402 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.42.05 PM.png,overripe,ripe,0.0,0.6521108150482178,0.3478891849517822 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.43.36 PM.png,overripe,ripe,0.0,0.6302101612091064,0.36978986859321594 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.43.54 PM.png,overripe,unripe,0.5904564261436462,0.40954354405403137,0.32399433851242065 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,ripe,0.038272175937891006,0.620588481426239,0.379411518573761 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.45082345604896545,0.5491765141487122 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.29 PM.png,overripe,ripe,0.19852136075496674,0.613620936870575,0.38637906312942505 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,ripe,0.0,0.7081316113471985,0.2918684184551239 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,ripe,0.0,0.5833781361579895,0.4166218340396881 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,ripe,0.0,0.6496687531471252,0.35033121705055237 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.47 PM.png,overripe,overripe,0.0,0.47152286767959595,0.528477132320404 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.46.26 PM.png,overripe,ripe,0.0,0.5404882431030273,0.45951178669929504 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.46.56 PM.png,overripe,unripe,0.7384567856788635,0.26154324412345886,0.27904796600341797 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,unripe,0.893688976764679,0.10631104558706284,0.25139930844306946 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,unripe,0.6229315996170044,0.3770684003829956,0.33571740984916687 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.19.37 PM.png,overripe,ripe,0.0,0.7385879158973694,0.261412113904953 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,ripe,0.0,0.6710619926452637,0.32893800735473633 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.40 PM.png,overripe,ripe,0.0,0.5233137607574463,0.4766862392425537 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,ripe,0.0,0.6683982610702515,0.33160173892974854 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,ripe,0.0,0.514373242855072,0.4856267273426056 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.35 PM.png,overripe,ripe,0.0,0.6596781015396118,0.34032192826271057 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,ripe,0.33303704857826233,0.6669629216194153,0.2281758189201355 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.19 PM.png,overripe,ripe,0.0,0.54155033826828,0.45844966173171997 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,ripe,0.0,0.5521043539047241,0.4478956162929535 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.54 PM.png,overripe,ripe,0.0,0.9931792616844177,0.006820726208388805 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,ripe,0.0,0.7633267045021057,0.2366732954978943 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,ripe,0.0,0.6774919033050537,0.3225080966949463 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.26.18 PM.png,overripe,ripe,0.04510095342993736,0.9448808431625366,0.05511915683746338 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,ripe,0.1773671805858612,0.8226328492164612,0.02731568180024624 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,ripe,0.0,0.6347293257713318,0.3652706444263458 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.27.38 PM.png,overripe,ripe,0.0,0.5105742812156677,0.4894257187843323 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,ripe,0.0,0.5339621305465698,0.4660378396511078 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.29.10 PM.png,overripe,ripe,0.0,0.660286545753479,0.339713454246521 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.29.26 PM.png,overripe,overripe,0.0,0.40743306279182434,0.592566967010498 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.4803808033466339,0.5196192264556885 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.30.48 PM.png,overripe,ripe,0.0,0.7501447200775146,0.24985525012016296 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.17 PM.png,overripe,ripe,0.0,0.7707810401916504,0.2292189598083496 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,ripe,0.0,0.5560489892959595,0.4439510107040405 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.44 PM.png,overripe,overripe,0.0,0.43574774265289307,0.5642522573471069 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,ripe,0.0,0.6806624531745911,0.31933754682540894 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.33.12 PM.png,overripe,overripe,0.0,0.4086771309375763,0.5913228392601013 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.34.07 PM.png,overripe,ripe,0.0,0.5920838117599487,0.40791618824005127 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.36.19 PM.png,overripe,ripe,0.0,0.7034083604812622,0.2965916395187378 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,ripe,0.0769498199224472,0.8589127659797668,0.14108723402023315 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.07 PM.png,overripe,overripe,0.0,0.4010140895843506,0.5989859104156494 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,ripe,0.03435973823070526,0.8096157908439636,0.19038422405719757 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,ripe,0.3068019151687622,0.683927059173584,0.316072940826416 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.38.19 PM.png,overripe,ripe,0.0,0.54302978515625,0.45697021484375 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,ripe,0.29247021675109863,0.5683040618896484,0.4316959083080292 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.04 PM.png,overripe,ripe,0.01020267978310585,0.6607130765914917,0.3392869234085083 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,ripe,0.11739564687013626,0.5339057445526123,0.4660942256450653 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.44 PM.png,overripe,unripe,0.8886565566062927,0.11134346574544907,0.10550668835639954 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,ripe,0.0,0.5949288606643677,0.4050711691379547 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.00 PM.png,overripe,ripe,0.0,0.5723161697387695,0.4276838004589081 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,ripe,0.0,0.8378027677536011,0.16219723224639893 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,ripe,0.0,0.9275993704795837,0.07240065187215805 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.43.03 PM.png,overripe,overripe,0.0,0.43021586537361145,0.5697841644287109 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.45054468512535095,0.5494552850723267 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.44.24 PM.png,overripe,ripe,0.3993045389652252,0.6006954312324524,0.32911184430122375 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.45.21 PM.png,overripe,ripe,0.4088394343852997,0.5911605358123779,0.3148910701274872 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.10 PM.png,overripe,ripe,0.0,0.5197015404701233,0.4802984893321991 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,ripe,0.49274352192878723,0.5072565078735352,0.1756661981344223 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.56 PM.png,overripe,unripe,0.6913628578186035,0.3086371123790741,0.287773996591568 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,ripe,0.3856875002384186,0.577261745929718,0.422738254070282 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,unripe,0.9099199175834656,0.09008006751537323,0.2480642944574356 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,ripe,0.15658849477767944,0.5312182307243347,0.4687817692756653 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,ripe,0.0,0.5132151246070862,0.4867849051952362 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.21.48 PM.png,overripe,ripe,0.1314806342124939,0.764303982257843,0.2356959879398346 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.22.47 PM.png,overripe,ripe,0.0,0.5778688192367554,0.42213118076324463 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.03 PM.png,overripe,unripe,0.8055973052978516,0.19440269470214844,0.23476499319076538 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.09 PM.png,overripe,unripe,0.762491762638092,0.23750823736190796,0.2178964465856552 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,ripe,0.0,0.6423583626747131,0.35764166712760925 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,ripe,0.0,0.552264928817749,0.447735071182251 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.17 PM.png,overripe,ripe,0.4912368059158325,0.5087631940841675,0.2535504996776581 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.24 PM.png,overripe,ripe,0.24575822055339813,0.7542417645454407,0.15319037437438965 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.37 PM.png,overripe,unripe,0.7543804049491882,0.24561958014965057,0.2608474791049957 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,ripe,0.2821162939071655,0.6892344951629639,0.31076547503471375 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.25.20 PM.png,overripe,unripe,0.9377288818359375,0.062271103262901306,0.13059929013252258 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,ripe,0.0,0.7423222064971924,0.25767782330513 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.26.49 PM.png,overripe,unripe,0.9917876720428467,0.008212314918637276,0.16649983823299408 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.27.26 PM.png,overripe,ripe,0.0,0.5261155366897583,0.4738844633102417 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,ripe,0.0,0.5313825011253357,0.4686175286769867 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.28.50 PM.png,overripe,overripe,0.0,0.4757324159145355,0.5242675542831421 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.29.03 PM.png,overripe,ripe,0.4341100752353668,0.5658899545669556,0.20558325946331024 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,ripe,0.0,0.5832070112228394,0.41679298877716064 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.30.35 PM.png,overripe,unripe,0.96696937084198,0.03303065523505211,0.21357038617134094 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.30.41 PM.png,overripe,ripe,0.0,0.5695290565490723,0.43047091364860535 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,ripe,0.0,0.5067123174667358,0.49328768253326416 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,ripe,0.0,0.5580410957336426,0.4419589340686798 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.16 PM.png,overripe,ripe,0.0,0.5208292007446289,0.4791707992553711 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.43 PM.png,overripe,ripe,0.0,0.5104200839996338,0.4895798861980438 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.55 PM.png,overripe,ripe,0.0,0.5018216967582703,0.4981783330440521 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,ripe,0.37104886770248413,0.6289511322975159,0.07444357126951218 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,ripe,0.27952125668525696,0.7204787135124207,0.07176702469587326 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,ripe,0.03664872795343399,0.814325749874115,0.1856742650270462 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.37.41 PM.png,overripe,unripe,0.7174473404884338,0.28255268931388855,0.0 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,ripe,0.0,0.7596529722213745,0.2403470128774643 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,ripe,0.0,0.5343858599662781,0.4656141698360443 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.41.17 PM.png,overripe,ripe,0.4181813597679138,0.5818186402320862,0.3825734257698059 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.41.48 PM.png,overripe,unripe,0.837356448173523,0.16264356672763824,0.10594653338193893 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.05 PM.png,overripe,ripe,0.0,0.6525604724884033,0.3474395275115967 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,ripe,0.0,0.8439850211143494,0.15601497888565063 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,ripe,0.35781344771385193,0.5428924560546875,0.4571075737476349 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.43.26 PM.png,overripe,overripe,0.0,0.44235700368881226,0.5576429963111877 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,ripe,0.08052556961774826,0.6349429488182068,0.3650570511817932 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.45.17 PM.png,overripe,ripe,0.0,0.538906455039978,0.4610935151576996 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.45.21 PM.png,overripe,ripe,0.36861708760261536,0.6313828825950623,0.31625258922576904 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.18.28 PM.png,overripe,overripe,0.0,0.44421979784965515,0.5557801723480225 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,ripe,0.0,0.8491753339767456,0.1508246809244156 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,unripe,0.7920124530792236,0.20798757672309875,0.03876941278576851 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.22.36 PM.png,overripe,unripe,0.7406169772148132,0.2593829929828644,0.08989925682544708 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,ripe,0.0,0.7653696537017822,0.23463036119937897 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.24 PM.png,overripe,ripe,0.281095951795578,0.7189040780067444,0.14808332920074463 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.51 PM.png,overripe,ripe,0.0,0.6589640974998474,0.3410359025001526 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,unripe,0.5958028435707092,0.40419715642929077,0.27900230884552 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.26.02 PM.png,overripe,ripe,0.12850719690322876,0.7516918778419495,0.24830815196037292 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.44386765360832214,0.5561323165893555 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.12 PM.png,overripe,ripe,0.0,0.5840409398078918,0.41595906019210815 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.30 PM.png,overripe,ripe,0.30260273814201355,0.6770003437995911,0.32299962639808655 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.28.00 PM.png,overripe,overripe,0.0,0.493613064289093,0.506386935710907 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.29.58 PM.png,overripe,ripe,0.0,0.9147711396217346,0.08522886037826538 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.30.11 PM.png,overripe,unripe,0.6234738230705261,0.3765261769294739,0.22792409360408783 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.31.01 PM.png,overripe,ripe,0.0,0.6927554607391357,0.30724453926086426 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,ripe,0.0,0.5025057196617126,0.49749428033828735 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.04 PM.png,overripe,overripe,0.0,0.4879741966724396,0.5120258331298828 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,ripe,0.0,0.7014654874801636,0.2985345423221588 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.50 PM.png,overripe,overripe,0.0,0.4052884876728058,0.5947115421295166 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.34.07 PM.png,overripe,ripe,0.0,0.506717324256897,0.493282675743103 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.35.51 PM.png,overripe,overripe,0.0,0.4562586843967438,0.5437412858009338 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.14 PM.png,overripe,overripe,0.0,0.4042861759662628,0.5957137942314148 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.35 PM.png,overripe,overripe,0.0,0.42574355006217957,0.5742564797401428 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,ripe,0.0650629997253418,0.8389634490013123,0.16103656589984894 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,ripe,0.34114646911621094,0.6588535308837891,0.3087787330150604 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,ripe,0.0,0.5336262583732605,0.4663737714290619 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,ripe,0.0,0.7610726356506348,0.23892736434936523 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,ripe,0.49376481771469116,0.5062351822853088,0.39885804057121277 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.40.42 PM.png,overripe,ripe,0.0,0.5218489170074463,0.4781510531902313 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,ripe,0.31662678718566895,0.5787749290466309,0.42122507095336914 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,ripe,0.03994043171405792,0.8755258321762085,0.1244741901755333 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,ripe,0.0,0.9305760860443115,0.06942390650510788 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,ripe,0.0,0.5157008767127991,0.4842991232872009 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.12 PM.png,overripe,unripe,0.5441847443580627,0.45581525564193726,0.27316975593566895 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,ripe,0.0,0.5806959867477417,0.4193040132522583 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.47 PM.png,overripe,overripe,0.0,0.47116684913635254,0.5288331508636475 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.57 PM.png,overripe,overripe,0.0,0.47569018602371216,0.5243098139762878 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,unripe,0.5063207149505615,0.4936792850494385,0.17585553228855133 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.18.41 PM.png,overripe,ripe,0.0,0.514185905456543,0.48581409454345703 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.08 PM.png,overripe,unripe,0.5915644764900208,0.40843555331230164,0.35281890630722046 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,ripe,0.0,0.7803023457527161,0.21969766914844513 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.56 PM.png,overripe,overripe,0.0,0.42812904715538025,0.5718709230422974 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,unripe,0.7949773073196411,0.2050226926803589,0.04410770907998085 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.21.17 PM.png,overripe,ripe,0.0,0.7941156625747681,0.20588430762290955 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.21.48 PM.png,overripe,ripe,0.0,0.5910632610321045,0.4089367091655731 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.22.41 PM.png,overripe,ripe,0.0,0.8113616108894348,0.18863838911056519 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,ripe,0.0,0.5950024724006653,0.4049975574016571 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,ripe,0.18418759107589722,0.7160047292709351,0.28399524092674255 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.17 PM.png,overripe,ripe,0.28890758752822876,0.687011182308197,0.3129887878894806 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.29 PM.png,overripe,ripe,0.32350215315818787,0.6764978766441345,0.05849553644657135 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,ripe,0.0,0.6405335664749146,0.35946640372276306 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.25.38 PM.png,overripe,overripe,0.0,0.40219059586524963,0.597809374332428 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.25.55 PM.png,overripe,ripe,0.43065470457077026,0.5693452954292297,0.3037247657775879 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.02 PM.png,overripe,ripe,0.18829679489135742,0.7708991765975952,0.22910082340240479 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.24 PM.png,overripe,ripe,0.2671969532966614,0.7086554169654846,0.291344553232193 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.419709712266922,0.5802903175354004 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,ripe,0.0,0.6348029375076294,0.3651970624923706 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.27.12 PM.png,overripe,ripe,0.0,0.5787205100059509,0.42127951979637146 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.28.17 PM.png,overripe,ripe,0.0,0.5093488097190857,0.4906511902809143 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,ripe,0.0,0.555169403553009,0.44483059644699097 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.29.44 PM.png,overripe,ripe,0.0,0.7268278002738953,0.27317219972610474 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.29.51 PM.png,overripe,ripe,0.0,0.6149546504020691,0.3850453495979309 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,unripe,0.8035372495651245,0.1964627355337143,0.16793328523635864 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,ripe,0.0,0.52947598695755,0.47052401304244995 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,ripe,0.0,0.6380183696746826,0.36198166012763977 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.41 PM.png,overripe,overripe,0.0,0.4089646339416504,0.5910353660583496 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.55 PM.png,overripe,ripe,0.0,0.9003026485443115,0.09969733655452728 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.33.43 PM.png,overripe,overripe,0.0,0.4769115746021271,0.5230883955955505 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.33.50 PM.png,overripe,overripe,0.0,0.4352206885814667,0.5647792816162109 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,ripe,0.30056217312812805,0.6994378566741943,0.09189045429229736 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,ripe,0.4039210081100464,0.5960789918899536,0.026400087401270866 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.19 PM.png,overripe,overripe,0.0,0.40350818634033203,0.596491813659668 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,ripe,0.04609706997871399,0.8151684403419495,0.18483155965805054 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.52 PM.png,overripe,ripe,0.14656765758991241,0.8534323573112488,0.0034540353808552027 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.58 PM.png,overripe,overripe,0.0,0.4070862829685211,0.5929136872291565 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.38.19 PM.png,overripe,ripe,0.0,0.5511941313743591,0.44880586862564087 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,ripe,0.0,0.5215831995010376,0.47841677069664 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.39.19 PM.png,overripe,unripe,0.559790849685669,0.44020918011665344,0.37312304973602295 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,ripe,0.0,0.578179121017456,0.42182090878486633 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,ripe,0.0,0.5982706546783447,0.4017293155193329 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,ripe,0.0,0.9280030727386475,0.07199694961309433 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.43.03 PM.png,overripe,overripe,0.0,0.4266124665737152,0.5733875036239624 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.44.24 PM.png,overripe,unripe,0.9214498400688171,0.07855017483234406,0.2679489552974701 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.4750445485115051,0.5249554514884949 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,ripe,0.0,0.5702788829803467,0.4297211468219757 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,ripe,0.0,0.6297933459281921,0.3702066242694855 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.18.28 PM.png,overripe,overripe,0.0,0.41149893403053284,0.5885010957717896 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,ripe,0.0,0.8542953133583069,0.14570467174053192 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.20.18 PM.png,overripe,ripe,0.0,0.5436177253723145,0.45638230443000793 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.20.30 PM.png,overripe,overripe,0.0,0.41757434606552124,0.5824256539344788 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,unripe,0.5857101678848267,0.41428983211517334,0.20020686089992523 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,ripe,0.0,0.838029682636261,0.1619703322649002 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.22.05 PM.png,overripe,overripe,0.0,0.4193146526813507,0.5806853771209717 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.22.32 PM.png,overripe,unripe,0.8956418633460999,0.10435812175273895,0.0 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,ripe,0.0,0.6752251982688904,0.32477477192878723 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,ripe,0.12285357713699341,0.5727700591087341,0.4272299110889435 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,ripe,0.0,0.6609114408493042,0.3390885293483734 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,ripe,0.26060009002685547,0.6816350817680359,0.3183648884296417 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.47457292675971985,0.5254271030426025 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,ripe,0.47013425827026367,0.5298657417297363,0.3003605604171753 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,ripe,0.0,0.7323866486549377,0.26761332154273987 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4191393554210663,0.5808606743812561 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.44 PM.png,overripe,ripe,0.0,0.794874906539917,0.205125093460083 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.27.46 PM.png,overripe,ripe,0.31795522570610046,0.5398896932601929,0.46011030673980713 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,ripe,0.0,0.7242827415466309,0.27571725845336914 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,unripe,0.5516011118888855,0.4483988881111145,0.17210331559181213 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.460245281457901,0.5397546887397766 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.09 PM.png,overripe,overripe,0.0,0.485073059797287,0.5149269104003906 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.21 PM.png,overripe,ripe,0.0,0.5222699642181396,0.47773003578186035 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.41 PM.png,overripe,ripe,0.0,0.5607610940933228,0.43923887610435486 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.33.00 PM.png,overripe,ripe,0.2566332221031189,0.7433667778968811,0.0 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.33.16 PM.png,overripe,overripe,0.0,0.49080196022987366,0.5091980695724487 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.34.00 PM.png,overripe,unripe,0.7608029842376709,0.2391970455646515,0.25070327520370483 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,ripe,0.3863961696624756,0.6136038303375244,0.10450488328933716 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.35 PM.png,overripe,overripe,0.0,0.429340124130249,0.570659875869751 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.53 PM.png,overripe,overripe,0.0,0.4310610294342041,0.5689389705657959 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,ripe,0.27214181423187256,0.6698411107063293,0.33015885949134827 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.37.36 PM.png,overripe,ripe,0.0,0.5461934208869934,0.4538065493106842 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,ripe,0.11518914252519608,0.561570942401886,0.4384290277957916 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,ripe,0.44536688923835754,0.5546331405639648,0.4066866934299469 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.30 PM.png,overripe,unripe,0.7265792489051819,0.2734207808971405,0.17099231481552124 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.36 PM.png,overripe,ripe,0.0,0.8552976250648499,0.14470236003398895 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,ripe,0.2608071267604828,0.5606556534767151,0.4393443167209625 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.41.17 PM.png,overripe,ripe,0.3820761442184448,0.6179238557815552,0.3677489459514618 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.42.38 PM.png,overripe,ripe,0.0,0.6341219544410706,0.36587801575660706 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,ripe,0.0,0.9269531965255737,0.07304681837558746 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,ripe,0.2505177855491638,0.6438321471214294,0.35616785287857056 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.4424832761287689,0.5575166940689087 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.39 PM.png,overripe,ripe,0.4610324800014496,0.538967490196228,0.3290189504623413 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,ripe,0.0,0.5316239595413208,0.4683760106563568 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.12 PM.png,overripe,ripe,0.30266261100769043,0.6943630576133728,0.3056369423866272 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.42 PM.png,overripe,ripe,0.38708725571632385,0.6129127740859985,0.35048869252204895 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.57 PM.png,overripe,ripe,0.0,0.5620419979095459,0.4379580020904541 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,unripe,0.5400806069374084,0.45991936326026917,0.17627818882465363 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.18.41 PM.png,overripe,ripe,0.0,0.5147162079811096,0.48528382182121277 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,unripe,0.7725955247879028,0.22740446031093597,0.3433808982372284 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,unripe,0.9639445543289185,0.03605544939637184,0.2675095796585083 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,unripe,0.5409854650497437,0.45901450514793396,0.38567662239074707 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.47 PM.png,overripe,overripe,0.0,0.41471734642982483,0.5852826833724976 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,ripe,0.0,0.6614300608634949,0.3385699391365051 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,ripe,0.32817143201828003,0.67182856798172,0.2953425943851471 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,ripe,0.3992922604084015,0.6007077693939209,0.18292704224586487 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,ripe,0.03858492523431778,0.6282339692115784,0.37176603078842163 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,ripe,0.0,0.8583651781082153,0.14163479208946228 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.05 PM.png,overripe,overripe,0.0,0.42030563950538635,0.5796943306922913 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.12 PM.png,overripe,ripe,0.0,0.582658588886261,0.4173413813114166 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.41 PM.png,overripe,ripe,0.0,0.8175500631332397,0.18244992196559906 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.24 PM.png,overripe,overripe,0.0,0.48650720715522766,0.5134928226470947 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,ripe,0.0,0.6735998392105103,0.32640016078948975 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,ripe,0.0,0.5939438343048096,0.40605616569519043 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.54 PM.png,overripe,ripe,0.0,0.9975428581237793,0.002457140479236841 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,ripe,0.0,0.7541245222091675,0.24587547779083252 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,ripe,0.19096258282661438,0.7159315347671509,0.2840684652328491 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,ripe,0.21103535592556,0.6627529263496399,0.3372470736503601 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.47273725271224976,0.5272627472877502 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,ripe,0.3497335612773895,0.6502664089202881,0.025225728750228882 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,ripe,0.0,0.7331722378730774,0.2668277621269226 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.47284677624702454,0.5271532535552979 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.31.44 PM.png,overripe,overripe,0.0,0.4321405291557312,0.5678594708442688 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,ripe,0.04051603376865387,0.7362380623817444,0.2637619078159332 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.32.33 PM.png,overripe,unripe,0.5365820527076721,0.4634179472923279,0.26602703332901 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.33.23 PM.png,overripe,overripe,0.0,0.44902750849723816,0.5509724617004395 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.35.51 PM.png,overripe,overripe,0.0,0.45659908652305603,0.5434008836746216 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.36.06 PM.png,overripe,ripe,0.0,0.5452300906181335,0.45476987957954407 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.36.24 PM.png,overripe,ripe,0.4327605664730072,0.5672394037246704,0.01578608900308609 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,ripe,0.05080558732151985,0.8142307996749878,0.1857692003250122 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,ripe,0.0,0.5203588008880615,0.47964122891426086 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.38.34 PM.png,overripe,overripe,0.0,0.4375983476638794,0.5624016523361206 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.11 PM.png,overripe,ripe,0.49666327238082886,0.5033367276191711,0.26440179347991943 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.30 PM.png,overripe,unripe,0.7165663242340088,0.2834336757659912,0.17243744432926178 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.51 PM.png,overripe,unripe,0.8641238212585449,0.13587619364261627,0.12342660874128342 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.41.22 PM.png,overripe,ripe,0.3296321630477905,0.6703678369522095,0.28759875893592834 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,ripe,0.0,0.598730206489563,0.4012698233127594 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.41058799624443054,0.5894119739532471 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,ripe,0.0,0.9288826584815979,0.0711173266172409 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.44.34 PM.png,overripe,overripe,0.0,0.4270998537540436,0.5729001760482788 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,ripe,0.0,0.718131959438324,0.28186801075935364 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.46.10 PM.png,overripe,overripe,0.0,0.4586178958415985,0.5413820743560791 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.46413466334342957,0.5358653664588928 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4659823477268219,0.5340176820755005 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.54 PM.png,ripe,overripe,0.0,0.42136433720588684,0.5786356925964355 +orange/test/ripe/Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.4226387143135071,0.5773612856864929 +orange/test/ripe/Screen Shot 2018-06-12 at 11.52.51 PM.png,ripe,overripe,0.0,0.4001951515674591,0.5998048186302185 +orange/test/ripe/Screen Shot 2018-06-12 at 11.53.17 PM.png,ripe,ripe,0.0,0.5593268871307373,0.4406730830669403 +orange/test/ripe/Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,ripe,0.0,0.5095239877700806,0.4904760420322418 +orange/test/ripe/Screen Shot 2018-06-12 at 11.54.03 PM.png,ripe,ripe,0.0,0.5944058299064636,0.4055941700935364 +orange/test/ripe/Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.43943503499031067,0.5605649352073669 +orange/test/ripe/Screen Shot 2018-06-12 at 11.55.23 PM.png,ripe,ripe,0.0,0.5785889029502869,0.42141109704971313 +orange/test/ripe/Screen Shot 2018-06-12 at 11.55.28 PM.png,ripe,unripe,0.8826243877410889,0.11737558990716934,0.2744981050491333 +orange/test/ripe/Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,ripe,0.0,0.5311582088470459,0.4688418209552765 +orange/test/ripe/Screen Shot 2018-06-12 at 11.57.52 PM.png,ripe,ripe,0.0,0.5069611668586731,0.4930388331413269 +orange/test/ripe/Screen Shot 2018-06-12 at 11.59.28 PM.png,ripe,ripe,0.0,0.5711702704429626,0.42882975935935974 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,ripe,0.0,0.7394142150878906,0.26058581471443176 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.43 AM.png,ripe,overripe,0.0,0.47487103939056396,0.525128960609436 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.4019729495048523,0.5980270504951477 +orange/test/ripe/Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,ripe,0.0,0.7088084816932678,0.2911914885044098 +orange/test/ripe/Screen Shot 2018-06-13 at 12.01.58 AM.png,ripe,ripe,0.0,0.5001865029335022,0.4998134672641754 +orange/test/ripe/Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,ripe,0.0,0.8440609574317932,0.1559390276670456 +orange/test/ripe/Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,ripe,0.0,0.5375992655754089,0.46240073442459106 +orange/test/ripe/Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.44409143924713135,0.5559085607528687 +orange/test/ripe/Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,ripe,0.0,0.6382271647453308,0.3617728650569916 +orange/test/ripe/Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,ripe,0.4003983736038208,0.5996016263961792,0.14444924890995026 +orange/test/ripe/Screen Shot 2018-06-13 at 12.06.43 AM.png,ripe,ripe,0.0,0.7256479263305664,0.274352103471756 +orange/test/ripe/Screen Shot 2018-06-13 at 12.07.05 AM.png,ripe,ripe,0.0,0.5788896083831787,0.4211103916168213 +orange/test/ripe/Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.43463650345802307,0.5653635263442993 +orange/test/ripe/Screen Shot 2018-06-13 at 12.08.17 AM.png,ripe,ripe,0.0,0.5175557732582092,0.4824441969394684 +orange/test/ripe/Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,ripe,0.0,0.7059669494628906,0.2940330505371094 +orange/test/ripe/Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,ripe,0.0,0.6203939914703369,0.3796060085296631 +orange/test/ripe/Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,ripe,0.0,0.5954688191413879,0.40453118085861206 +orange/test/ripe/Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,ripe,0.0,0.6335792541503906,0.36642077565193176 +orange/test/ripe/Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4430519938468933,0.5569480061531067 +orange/test/ripe/Screen Shot 2018-06-13 at 12.11.57 AM.png,ripe,ripe,0.0,0.6051974892616272,0.3948025405406952 +orange/test/ripe/Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.466745525598526,0.5332544445991516 +orange/test/ripe/Screen Shot 2018-06-13 at 12.14.03 AM.png,ripe,ripe,0.0,0.5609116554260254,0.4390883147716522 +orange/test/ripe/Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,ripe,0.3778100609779358,0.6221899390220642,0.3727862536907196 +orange/test/ripe/Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,unripe,0.8976861834526062,0.1023138239979744,0.3321131467819214 +orange/test/ripe/Screen Shot 2018-06-13 at 12.17.37 AM.png,ripe,ripe,0.0,0.5187209844589233,0.48127901554107666 +orange/test/ripe/Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4932303726673126,0.506769597530365 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,ripe,0.0,0.5547468066215515,0.4452532231807709 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.34 AM.png,ripe,ripe,0.16448640823364258,0.5638999938964844,0.4361000061035156 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.40 AM.png,ripe,ripe,0.11627037078142166,0.6589677929878235,0.3410321772098541 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,ripe,0.0,0.5744829177856445,0.4255170524120331 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.50.47 PM.png,ripe,overripe,0.0,0.4599936604499817,0.5400063395500183 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,ripe,0.0,0.5130348801612854,0.4869650900363922 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.08 PM.png,ripe,ripe,0.0,0.5831298828125,0.4168701171875 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.13 PM.png,ripe,ripe,0.0,0.6430251002311707,0.35697489976882935 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.424530565738678,0.575469434261322 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,ripe,0.0,0.5855536460876465,0.4144463539123535 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,ripe,0.0,0.633290708065033,0.36670926213264465 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.4788552522659302,0.5211447477340698 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.55.42 PM.png,ripe,overripe,0.0,0.41139766573905945,0.5886023044586182 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.11 PM.png,ripe,ripe,0.0,0.6831781268119812,0.3168218731880188 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,ripe,0.0,0.5870171189308167,0.41298285126686096 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.28 PM.png,ripe,ripe,0.0,0.6149587631225586,0.385041207075119 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.47733938694000244,0.5226606130599976 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.00.35 AM.png,ripe,ripe,0.0,0.6348615884780884,0.3651384115219116 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.00.43 AM.png,ripe,ripe,0.0,0.5541118383407593,0.4458881616592407 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,ripe,0.0,0.6885117292404175,0.31148824095726013 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.4777176082134247,0.5222823619842529 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,ripe,0.0,0.8323961496353149,0.16760383546352386 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.4410093128681183,0.5589907169342041 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,ripe,0.0,0.5562711954116821,0.44372880458831787 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,ripe,0.3290291428565979,0.6709708571434021,0.15979427099227905 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,ripe,0.0,0.5115753412246704,0.4884246587753296 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.42871609330177307,0.5712839365005493 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,ripe,0.0,0.6350864171981812,0.36491355299949646 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,ripe,0.0,0.6006070971488953,0.39939290285110474 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.09.43 AM.png,ripe,overripe,0.0,0.4833071827888489,0.5166928172111511 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,ripe,0.0,0.6257081627845764,0.3742918372154236 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.13.44 AM.png,ripe,ripe,0.0,0.5828467011451721,0.4171532988548279 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.14.43 AM.png,ripe,ripe,0.21437351405620575,0.6457313299179077,0.3542686998844147 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.15.39 AM.png,ripe,ripe,0.3176012337207794,0.6537373661994934,0.346262663602829 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.16.16 AM.png,ripe,ripe,0.20125189423561096,0.5613824129104614,0.43861761689186096 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.49508875608444214,0.5049112439155579 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.19.08 AM.png,ripe,ripe,0.0,0.5725721120834351,0.42742788791656494 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.06 AM.png,ripe,ripe,0.0,0.6829736828804016,0.3170263171195984 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,ripe,0.001162982196547091,0.7614369988441467,0.23856301605701447 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,ripe,0.3815053403377533,0.6184946298599243,0.3768773376941681 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,ripe,0.0,0.6628262400627136,0.337173730134964 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.19 PM.png,ripe,overripe,0.0,0.4981762170791626,0.5018237829208374 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,ripe,0.0,0.5510354042053223,0.44896459579467773 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.54 PM.png,ripe,ripe,0.0,0.5698212385177612,0.4301787316799164 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.51.08 PM.png,ripe,ripe,0.0,0.556279718875885,0.4437202513217926 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.51.47 PM.png,ripe,ripe,0.0,0.6347745060920715,0.36522552371025085 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.4292322099208832,0.5707678198814392 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,ripe,0.0,0.5979531407356262,0.4020468294620514 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.55 PM.png,ripe,ripe,0.0,0.5256267189979553,0.4743732810020447 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,ripe,0.0,0.627651035785675,0.37234899401664734 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.54.20 PM.png,ripe,ripe,0.0,0.678777277469635,0.321222722530365 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4178600013256073,0.5821400284767151 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,ripe,0.0,0.5883920192718506,0.4116079807281494 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.24 PM.png,ripe,ripe,0.0,0.5594457387924194,0.4405542314052582 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.28 PM.png,ripe,ripe,0.0,0.6926392912864685,0.3073607087135315 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,ripe,0.0,0.6634659767150879,0.3365340232849121 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.40223228931427,0.59776771068573 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.4815382659435272,0.5184617042541504 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.02.45 AM.png,ripe,ripe,0.0,0.5410522818565369,0.45894771814346313 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.03.44 AM.png,ripe,ripe,0.0,0.5210930705070496,0.47890692949295044 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,ripe,0.0,0.6716862320899963,0.32831376791000366 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.26 AM.png,ripe,ripe,0.0,0.5618066787719727,0.43819329142570496 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.39 AM.png,ripe,ripe,0.0,0.5209789872169495,0.47902101278305054 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,ripe,0.0,0.5155691504478455,0.48443087935447693 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,ripe,0.0,0.596464216709137,0.40353575348854065 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.06.15 AM.png,ripe,ripe,0.0,0.6853552460670471,0.3146447241306305 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.07.32 AM.png,ripe,ripe,0.0,0.6943247318267822,0.3056752383708954 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,ripe,0.0,0.6102549433708191,0.3897450268268585 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.49440798163414,0.5055919885635376 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,ripe,0.0,0.622377872467041,0.377622127532959 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.27 AM.png,ripe,ripe,0.0,0.5987610220909119,0.4012390077114105 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,ripe,0.0,0.9261002540588379,0.0738997608423233 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4480312764644623,0.5519686937332153 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.12.04 AM.png,ripe,ripe,0.0,0.7234818935394287,0.2765181362628937 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,ripe,0.0,0.5832264423370361,0.4167735278606415 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.13.51 AM.png,ripe,ripe,0.25249722599983215,0.7475027441978455,0.1534716784954071 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,ripe,0.0,0.5286296606063843,0.47137030959129333 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,unripe,0.5041658282279968,0.4958341419696808,0.40778908133506775 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.16.45 AM.png,ripe,ripe,0.0,0.5848846435546875,0.4151153564453125 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.4064089059829712,0.5935910940170288 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.19 AM.png,ripe,ripe,0.0,0.5060253739356995,0.4939746558666229 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.49870797991752625,0.5012920498847961 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,ripe,0.0,0.5647732019424438,0.43522676825523376 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,ripe,0.004382044076919556,0.6401737928390503,0.3598262071609497 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,ripe,0.0,0.6138001680374146,0.38619980216026306 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,ripe,0.0,0.5342581272125244,0.4657418727874756 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.52.40 PM.png,ripe,ripe,0.0,0.5407763719558716,0.45922359824180603 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,ripe,0.0,0.5781450271606445,0.4218549430370331 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,ripe,0.0,0.7818456888198853,0.21815432608127594 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,ripe,0.0,0.750489354133606,0.24951066076755524 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.57.37 PM.png,ripe,ripe,0.0,0.5522410273551941,0.4477589428424835 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.58.02 PM.png,ripe,ripe,0.0,0.5601279139518738,0.4398721158504486 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,ripe,0.0,0.6118299961090088,0.3881699740886688 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,ripe,0.0,0.5799661874771118,0.4200338125228882 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,ripe,0.0,0.554574728012085,0.44542527198791504 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,ripe,0.0,0.5106723308563232,0.48932769894599915 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,ripe,0.0,0.655523955821991,0.34447601437568665 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.00.02 AM.png,ripe,ripe,0.0,0.7426027655601501,0.25739726424217224 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,ripe,0.0,0.7754730582237244,0.22452692687511444 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.01.49 AM.png,ripe,ripe,0.0,0.5596505403518677,0.4403494596481323 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.02.41 AM.png,ripe,ripe,0.0,0.5999639630317688,0.4000360369682312 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,ripe,0.0,0.5431419610977173,0.4568580389022827 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,ripe,0.0,0.5055831074714661,0.4944169223308563 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,ripe,0.0,0.5066828727722168,0.4933171272277832 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.07.05 AM.png,ripe,ripe,0.0,0.7366968393325806,0.26330316066741943 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,ripe,0.0,0.8680079579353333,0.13199205696582794 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.08.58 AM.png,ripe,ripe,0.0,0.6751948595046997,0.3248051106929779 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.09.32 AM.png,ripe,ripe,0.0,0.6393738985061646,0.36062610149383545 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.04 AM.png,ripe,overripe,0.0,0.4110395014286041,0.5889604687690735 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,ripe,0.0,0.5816770195960999,0.41832295060157776 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.45 AM.png,ripe,ripe,0.0,0.6809642314910889,0.31903573870658875 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,ripe,0.0,0.9217618703842163,0.07823815941810608 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.11.28 AM.png,ripe,ripe,0.0,0.5628854036331177,0.4371145963668823 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.13.24 AM.png,ripe,overripe,0.0,0.4960310459136963,0.5039689540863037 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,ripe,0.0,0.5200886130332947,0.47991135716438293 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.15.01 AM.png,ripe,unripe,0.8343860507011414,0.16561394929885864,0.28309619426727295 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,ripe,0.180746391415596,0.5531648397445679,0.44683513045310974 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.16.54 AM.png,ripe,ripe,0.0,0.5538573265075684,0.44614264369010925 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.4295566976070404,0.5704432725906372 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,ripe,0.0,0.6567298769950867,0.34327009320259094 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.19 PM.png,ripe,overripe,0.0,0.4994567036628723,0.5005432963371277 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.33 PM.png,ripe,ripe,0.0,0.5176238417625427,0.4823761284351349 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.47 PM.png,ripe,ripe,0.0,0.5638852119445801,0.4361147880554199 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,ripe,0.0,0.5844510197639465,0.4155489504337311 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,ripe,0.0,0.5253274440765381,0.4746725857257843 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,ripe,0.0,0.5852555632591248,0.41474443674087524 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.03 PM.png,ripe,ripe,0.0,0.5294825434684753,0.47051742672920227 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.10 PM.png,ripe,ripe,0.0,0.5939609408378601,0.4060390889644623 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,ripe,0.0,0.6006720662117004,0.39932793378829956 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.35 PM.png,ripe,ripe,0.0,0.5585842728614807,0.4414157271385193 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,ripe,0.0,0.7907571196556091,0.20924289524555206 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4847947061061859,0.5152052640914917 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.02 PM.png,ripe,ripe,0.0,0.5004739165306091,0.49952611327171326 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,ripe,0.0,0.7585898041725159,0.24141018092632294 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.43 PM.png,ripe,ripe,0.0,0.5818751454353333,0.41812488436698914 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.58.43 PM.png,ripe,ripe,0.0,0.5250118374824524,0.47498819231987 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.4705030918121338,0.5294969081878662 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.11 PM.png,ripe,ripe,0.0,0.6608549356460571,0.3391450345516205 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,ripe,0.0,0.5844619870185852,0.4155379831790924 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.33 PM.png,ripe,ripe,0.0,0.5455157160758972,0.45448431372642517 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.49678337574005127,0.5032166242599487 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,ripe,0.0,0.6624698638916016,0.3375301659107208 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.40224504470825195,0.597754955291748 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,ripe,0.0,0.6664441823959351,0.33355584740638733 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,ripe,0.0,0.812529444694519,0.18747054040431976 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.02.41 AM.png,ripe,ripe,0.0,0.6027979850769043,0.3972020149230957 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.03.44 AM.png,ripe,ripe,0.0,0.5144721269607544,0.4855278730392456 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,ripe,0.0,0.5829916000366211,0.4170083701610565 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,ripe,0.2075539231300354,0.7924460768699646,0.16418780386447906 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.52 AM.png,ripe,ripe,0.0,0.6452182531356812,0.35478174686431885 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.4956985116004944,0.5043014883995056 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,ripe,0.0,0.5500611662864685,0.4499388635158539 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.07.39 AM.png,ripe,ripe,0.0,0.542056143283844,0.4579438865184784 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.4678647816181183,0.5321351885795593 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,ripe,0.0,0.6118584871292114,0.3881415128707886 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4971246123313904,0.5028753876686096 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,ripe,0.0,0.5861825942993164,0.413817435503006 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.10.45 AM.png,ripe,ripe,0.0,0.6860311627388,0.31396883726119995 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.11.02 AM.png,ripe,ripe,0.0,0.8070300817489624,0.19296994805335999 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4586566686630249,0.5413433313369751 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,ripe,0.0,0.5798652172088623,0.4201347529888153 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,ripe,0.0,0.5246070623397827,0.4753929376602173 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,ripe,0.2926439642906189,0.6061418056488037,0.3938581943511963 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.15.39 AM.png,ripe,ripe,0.11885170638561249,0.6301043629646301,0.3698956370353699 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.4587531089782715,0.5412468910217285 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.40620943903923035,0.5937905311584473 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.37 AM.png,ripe,ripe,0.0,0.5310667157173157,0.4689333140850067 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,ripe,0.0,0.5038840174674988,0.4961159825325012 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.18.02 AM.png,ripe,overripe,0.0,0.46047529578208923,0.5395247340202332 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4907777011394501,0.5092223286628723 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,ripe,0.0,0.5052798390388489,0.4947201907634735 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.53.12 PM.png,ripe,ripe,0.0,0.5851929187774658,0.4148070812225342 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,ripe,0.0,0.6256178021430969,0.3743821978569031 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,ripe,0.0,0.5997973680496216,0.4002026617527008 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,ripe,0.0,0.7935361266136169,0.20646385848522186 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.48159143328666687,0.5184085369110107 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,ripe,0.0,0.6219744682312012,0.37802550196647644 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,ripe,0.07345996052026749,0.6084184646606445,0.39158153533935547 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.24 PM.png,ripe,ripe,0.0,0.5644549131393433,0.43554508686065674 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.50 PM.png,ripe,overripe,0.0,0.4213649332523346,0.578635036945343 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,ripe,0.0,0.5198027491569519,0.4801972806453705 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,ripe,0.0,0.8026439547538757,0.19735604524612427 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,ripe,0.051716018468141556,0.5869549512863159,0.41304507851600647 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,ripe,0.0,0.6120885610580444,0.38791146874427795 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.02.53 AM.png,ripe,ripe,0.0,0.5956048965454102,0.40439507365226746 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,ripe,0.0,0.5946854948997498,0.40531450510025024 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.03.55 AM.png,ripe,ripe,0.0,0.6347578763961792,0.3652421236038208 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.04.34 AM.png,ripe,ripe,0.0,0.5702792406082153,0.42972075939178467 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,ripe,0.0,0.5460956692695618,0.45390430092811584 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.05.13 AM.png,ripe,overripe,0.0,0.4587169587612152,0.5412830114364624 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.05.27 AM.png,ripe,ripe,0.0,0.6007448434829712,0.3992551863193512 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.06.01 AM.png,ripe,overripe,0.0,0.48220720887184143,0.5177927613258362 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.06.28 AM.png,ripe,ripe,0.0,0.6526662111282349,0.3473338186740875 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.07.32 AM.png,ripe,ripe,0.0,0.7392997145652771,0.2607002854347229 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.07.46 AM.png,ripe,ripe,0.0,0.6470613479614258,0.35293862223625183 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.17 AM.png,ripe,ripe,0.0,0.5148753523826599,0.4851246476173401 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,ripe,0.0,0.5078655481338501,0.4921344220638275 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,ripe,0.0,0.8340276479721069,0.16597235202789307 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.10.27 AM.png,ripe,ripe,0.0,0.6095162034034729,0.3904837965965271 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.11.02 AM.png,ripe,ripe,0.0,0.8068479299545288,0.1931520700454712 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.11.57 AM.png,ripe,ripe,0.0,0.6925153732299805,0.3074846565723419 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,ripe,0.0,0.6055364608764648,0.39446353912353516 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,ripe,0.32625874876976013,0.611991822719574,0.38800814747810364 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,ripe,0.24521280825138092,0.568941593170166,0.431058406829834 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.16.08 AM.png,ripe,ripe,0.0,0.5609357357025146,0.43906423449516296 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,unripe,0.6455812454223633,0.3544187545776367,0.3840622007846832 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.4260769784450531,0.5739230513572693 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.17.51 AM.png,ripe,overripe,0.0,0.40298059582710266,0.597019374370575 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,ripe,0.0,0.5486857891082764,0.45131418108940125 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.23 AM.png,ripe,ripe,0.0,0.6096305251121521,0.3903694450855255 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.40 AM.png,ripe,ripe,0.05196743085980415,0.6208103895187378,0.3791896104812622 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.52 AM.png,ripe,ripe,0.0,0.5369626879692078,0.4630373418331146 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.19.17 AM.png,ripe,ripe,0.0,0.5723521113395691,0.4276478886604309 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,ripe,0.10757909715175629,0.6491133570671082,0.35088661313056946 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,ripe,0.0,0.7580013275146484,0.24199867248535156 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.25 AM.png,ripe,ripe,0.0,0.5190420150756836,0.4809579849243164 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,ripe,0.4007954001426697,0.5992045998573303,0.37634167075157166 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.4870355427265167,0.5129644274711609 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,ripe,0.0,0.6465319991111755,0.35346800088882446 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,ripe,0.0,0.648524284362793,0.35147568583488464 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,ripe,0.0,0.5110086798667908,0.48899132013320923 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.44527220726013184,0.5547277927398682 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.4716262221336365,0.5283737778663635 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.4251120984554291,0.5748879313468933 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.422443151473999,0.577556848526001 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.35 PM.png,ripe,ripe,0.0,0.5021678805351257,0.49783211946487427 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.43 PM.png,ripe,overripe,0.0,0.4129643440246582,0.5870356559753418 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,ripe,0.0,0.5307515263557434,0.4692484438419342 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.57.37 PM.png,ripe,overripe,0.0,0.49501514434814453,0.5049848556518555 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.58.18 PM.png,ripe,ripe,0.4337151348590851,0.5662848353385925,0.19602926075458527 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.45800572633743286,0.5419942736625671 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.4880301058292389,0.5119699239730835 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,ripe,0.0,0.5900573134422302,0.4099426567554474 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.54 PM.png,ripe,overripe,0.0,0.47479191422462463,0.525208055973053 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,ripe,0.0,0.7178704142570496,0.28212958574295044 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,ripe,0.3290982246398926,0.6622205972671509,0.3377794325351715 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.47625458240509033,0.5237454175949097 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,ripe,0.0,0.844268262386322,0.15573173761367798 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.41979697346687317,0.5802029967308044 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.4402255713939667,0.5597744584083557 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.05.13 AM.png,ripe,overripe,0.0,0.45885756611824036,0.5411424040794373 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.06.28 AM.png,ripe,ripe,0.0,0.5003489851951599,0.4996510148048401 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.07.39 AM.png,ripe,ripe,0.0,0.5785987973213196,0.4214012324810028 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,ripe,0.0,0.6807767748832703,0.31922322511672974 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,ripe,0.0,0.5117189884185791,0.4882810115814209 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,ripe,0.0,0.7062271237373352,0.2937729060649872 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,ripe,0.0,0.7778206467628479,0.2221793234348297 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.46662455797195435,0.5333754420280457 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,ripe,0.0,0.6208786368370056,0.3791213929653168 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.32 AM.png,ripe,ripe,0.0,0.5589628219604492,0.4410371780395508 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4838511347770691,0.5161488652229309 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.47822195291519165,0.5217780470848083 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.53 AM.png,ripe,ripe,0.0,0.8458054661750793,0.15419450402259827 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,ripe,0.0,0.9223780632019043,0.07762190699577332 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,ripe,0.0,0.5792731046676636,0.4207268953323364 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,ripe,0.37058037519454956,0.6277244091033936,0.37227559089660645 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4944813549518585,0.5055186748504639 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.17.55 AM.png,ripe,ripe,0.0,0.5915713310241699,0.4084286391735077 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,ripe,0.2827131748199463,0.6924627423286438,0.3075372278690338 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.19.43 AM.png,ripe,overripe,0.0,0.4607832431793213,0.5392167568206787 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,ripe,0.0,0.7652620077133179,0.23473802208900452 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,ripe,0.1580057591199875,0.5826349854469299,0.41736501455307007 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,ripe,0.0,0.5280618667602539,0.4719381332397461 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4414285719394684,0.558571457862854 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.03 PM.png,ripe,ripe,0.0,0.5712190270423889,0.4287809729576111 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.21 PM.png,ripe,ripe,0.0,0.6490809917449951,0.3509190082550049 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,ripe,0.0,0.5786306858062744,0.4213693141937256 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.53.22 PM.png,ripe,ripe,0.0,0.580143928527832,0.41985610127449036 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,ripe,0.0,0.6306790113449097,0.36932098865509033 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.54.10 PM.png,ripe,ripe,0.0,0.5453731417655945,0.4546268582344055 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,ripe,0.0,0.6292576789855957,0.3707423508167267 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.23 PM.png,ripe,ripe,0.0,0.6824740767478943,0.3175259530544281 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.48895394802093506,0.5110460519790649 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.58 PM.png,ripe,overripe,0.0,0.44124653935432434,0.5587534308433533 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,ripe,0.0,0.7561632394790649,0.24383676052093506 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,ripe,0.0,0.6098242402076721,0.3901757597923279 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,ripe,0.0,0.6611282825469971,0.33887168765068054 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,ripe,0.0,0.7922947406768799,0.20770524442195892 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,ripe,0.11815739423036575,0.5984800457954407,0.40151992440223694 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.01.36 AM.png,ripe,ripe,0.0,0.5267284512519836,0.47327157855033875 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.02.05 AM.png,ripe,ripe,0.0,0.6740936040878296,0.325906366109848 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.02.20 AM.png,ripe,ripe,0.0,0.5783435702323914,0.42165639996528625 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.03.17 AM.png,ripe,ripe,0.0,0.5497636795043945,0.45023632049560547 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,ripe,0.0,0.5813592672348022,0.41864073276519775 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,ripe,0.2969892621040344,0.7030107378959656,0.16977451741695404 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.06.43 AM.png,ripe,ripe,0.0,0.7048049569129944,0.295195072889328 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.07.46 AM.png,ripe,ripe,0.0,0.6139770746231079,0.3860229551792145 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,ripe,0.0,0.8469187021255493,0.1530812829732895 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,ripe,0.0,0.5585293769836426,0.4414706230163574 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.09.37 AM.png,ripe,overripe,0.0,0.45751988887786865,0.5424801111221313 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.10 AM.png,ripe,ripe,0.0,0.8020986914634705,0.19790129363536835 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,ripe,0.0,0.6471920013427734,0.35280802845954895 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.38 AM.png,ripe,overripe,0.0,0.4089628756046295,0.5910370945930481 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.11.28 AM.png,ripe,ripe,0.0,0.5834590792655945,0.4165409207344055 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.14.03 AM.png,ripe,ripe,0.0,0.5448690056800842,0.45513099431991577 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,ripe,0.09043771028518677,0.5410146117210388,0.4589853882789612 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.4301937520503998,0.5698062777519226 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,ripe,0.0,0.5920456647872925,0.4079543650150299 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.02 AM.png,ripe,overripe,0.0,0.4355875849723816,0.5644124150276184 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.46078741550445557,0.5392125844955444 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,ripe,0.0433468259871006,0.5805929899215698,0.4194069802761078 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.40200909972190857,0.5979909300804138 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4644017219543457,0.5355982780456543 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.21 PM.png,ripe,ripe,0.0,0.5452283620834351,0.45477163791656494 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.40 PM.png,ripe,overripe,0.0,0.47400882840156555,0.5259912014007568 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,ripe,0.0,0.519321620464325,0.48067837953567505 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.12 PM.png,ripe,overripe,0.0,0.4761006832122803,0.5238993167877197 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,ripe,0.0,0.5091862082481384,0.4908137619495392 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,ripe,0.0,0.5251373648643494,0.47486260533332825 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.54.35 PM.png,ripe,ripe,0.0,0.6266632676124573,0.3733367323875427 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.47034284472465515,0.5296571254730225 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.4236515760421753,0.5763484239578247 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.56.16 PM.png,ripe,ripe,0.0,0.8209228515625,0.1790771484375 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,ripe,0.06758951395750046,0.605705201625824,0.39429476857185364 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,ripe,0.0,0.5839123129844666,0.41608771681785583 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.59.54 PM.png,ripe,overripe,0.0,0.4731218218803406,0.5268781781196594 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,ripe,0.0,0.7392125725746155,0.26078739762306213 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.00.35 AM.png,ripe,ripe,0.0,0.540454626083374,0.45954540371894836 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,ripe,0.0,0.7087780833244324,0.29122188687324524 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.01.58 AM.png,ripe,overripe,0.0,0.4995957314968109,0.5004042983055115 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.17 AM.png,ripe,overripe,0.0,0.4067595303058624,0.59324049949646 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.4181251525878906,0.5818748474121094 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.55 AM.png,ripe,ripe,0.0,0.6578959822654724,0.3421040177345276 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.04.12 AM.png,ripe,overripe,0.0,0.4849308729171753,0.5150691270828247 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.04.26 AM.png,ripe,ripe,0.0,0.5109916925430298,0.4890083372592926 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,ripe,0.0,0.6382520794868469,0.3617479205131531 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.05.46 AM.png,ripe,ripe,0.0,0.6418250203132629,0.35817500948905945 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.06.01 AM.png,ripe,ripe,0.0,0.5019298195838928,0.4980701804161072 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.46784093976020813,0.5321590304374695 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,ripe,0.0,0.510124146938324,0.489875853061676 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.4650581479072571,0.5349418520927429 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.58 AM.png,ripe,ripe,0.0,0.7412697076797485,0.25873029232025146 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.43 AM.png,ripe,overripe,0.0,0.46992382407188416,0.5300762057304382 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.48306936025619507,0.5169306397438049 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.54 AM.png,ripe,ripe,0.0,0.5795968174934387,0.4204031825065613 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,ripe,0.0,0.6334545612335205,0.3665454387664795 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.53 AM.png,ripe,ripe,0.0,0.8503202199935913,0.1496797800064087 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,ripe,0.0,0.9245283007621765,0.07547169923782349 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.13.35 AM.png,ripe,ripe,0.0,0.63761967420578,0.36238032579421997 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,ripe,0.3797437250614166,0.6202563047409058,0.372307151556015 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.16.22 AM.png,ripe,ripe,0.0,0.6524733901023865,0.3475266098976135 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.40724247694015503,0.592757523059845 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.4173884987831116,0.5826115012168884 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.55 AM.png,ripe,ripe,0.0,0.5908302068710327,0.4091697931289673 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.18.23 AM.png,ripe,ripe,0.0,0.6540241241455078,0.3459758758544922 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,ripe,0.06664883345365524,0.6275117993354797,0.37248820066452026 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.19.08 AM.png,ripe,ripe,0.0,0.6193873882293701,0.3806126117706299 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.20.06 AM.png,ripe,ripe,0.0,0.6814011931419373,0.31859880685806274 +orange/test/unripe/1.jpg,unripe,ripe,0.3748931586742401,0.6251068115234375,0.04998597130179405 +orange/test/unripe/10.jpg,unripe,unripe,0.738859236240387,0.2611407935619354,0.0 +orange/test/unripe/100.jpg,unripe,unripe,0.9628605842590332,0.037139423191547394,0.04837622493505478 +orange/test/unripe/101.jpg,unripe,ripe,0.0,0.8545306921005249,0.1454692780971527 +orange/test/unripe/102.jpg,unripe,ripe,0.0,0.5817474126815796,0.4182525873184204 +orange/test/unripe/103.jpg,unripe,ripe,0.16085980832576752,0.7325323224067688,0.2674676775932312 +orange/test/unripe/104.jpg,unripe,ripe,0.0,0.5267026424407959,0.4732973575592041 +orange/test/unripe/105.jpg,unripe,ripe,0.3586871325969696,0.641312837600708,0.0 +orange/test/unripe/106.jpg,unripe,ripe,0.0,0.5173439383506775,0.4826560318470001 +orange/test/unripe/107.jpg,unripe,unripe,0.7367273569107056,0.26327264308929443,0.0 +orange/test/unripe/108.jpg,unripe,overripe,0.0,0.4487036168575287,0.5512963533401489 +orange/test/unripe/109.jpg,unripe,overripe,0.0,0.4433996379375458,0.5566003322601318 +orange/test/unripe/11.jpg,unripe,unripe,0.6834486722946167,0.3165513277053833,0.10166113078594208 +orange/test/unripe/110.jpg,unripe,ripe,0.28104084730148315,0.7189591526985168,0.0 +orange/test/unripe/111.jpg,unripe,ripe,0.3417400121688843,0.6582599878311157,0.18355166912078857 +orange/test/unripe/112.jpg,unripe,unripe,0.6199229955673218,0.3800770044326782,0.41996967792510986 +orange/test/unripe/113.jpg,unripe,unripe,0.5861092209815979,0.4138908088207245,0.03911300003528595 +orange/test/unripe/114.jpg,unripe,ripe,0.2951847016811371,0.7048152685165405,0.0 +orange/test/unripe/115.jpg,unripe,overripe,0.0,0.44073039293289185,0.5592696070671082 +orange/test/unripe/116.jpg,unripe,unripe,0.8463802933692932,0.1536197066307068,0.011530205607414246 +orange/test/unripe/117.jpg,unripe,unripe,0.5396875143051147,0.46031245589256287,0.020864639431238174 +orange/test/unripe/118.jpg,unripe,ripe,0.291843056678772,0.708156943321228,0.0 +orange/test/unripe/119.jpg,unripe,unripe,0.7201562523841858,0.2798437476158142,0.2359408289194107 +orange/test/unripe/12.jpg,unripe,overripe,0.0,0.4737035036087036,0.5262964963912964 +orange/test/unripe/120.jpg,unripe,ripe,0.3799397647380829,0.6200602650642395,0.0 +orange/test/unripe/121.jpg,unripe,ripe,0.17841123044490814,0.8215887546539307,0.030990008264780045 +orange/test/unripe/122.jpg,unripe,ripe,0.0,0.5822721719741821,0.41772782802581787 +orange/test/unripe/123.jpg,unripe,ripe,0.24150046706199646,0.7584995627403259,0.0 +orange/test/unripe/124.jpg,unripe,ripe,0.3213460147380829,0.6786540150642395,0.18326857686042786 +orange/test/unripe/125.jpg,unripe,ripe,0.2650356590747833,0.7349643707275391,0.0 +orange/test/unripe/127.jpg,unripe,ripe,0.23634977638721466,0.5176549553871155,0.4823450744152069 +orange/test/unripe/128.jpg,unripe,ripe,0.31484708189964294,0.6851528882980347,0.07617787271738052 +orange/test/unripe/13.jpg,unripe,ripe,0.05059828981757164,0.6384692788124084,0.36153072118759155 +orange/test/unripe/131.jpg,unripe,ripe,0.22023402154445648,0.7797659635543823,0.0 +orange/test/unripe/132.jpg,unripe,overripe,0.0,0.40110084414482117,0.5988991856575012 +orange/test/unripe/133.jpg,unripe,unripe,0.6166917085647583,0.3833082914352417,0.007533276919275522 +orange/test/unripe/134.jpg,unripe,ripe,0.0,0.6854900121688843,0.31450995802879333 +orange/test/unripe/135.jpg,unripe,ripe,0.0,0.6011002063751221,0.39889979362487793 +orange/test/unripe/137.jpg,unripe,unripe,0.9767425656318665,0.0232574213296175,0.0 +orange/test/unripe/139.jpg,unripe,ripe,0.027009524405002594,0.9642283320426941,0.035771694034338 +orange/test/unripe/14.jpg,unripe,ripe,0.1996557116508484,0.7173352241516113,0.28266477584838867 +orange/test/unripe/140.jpg,unripe,ripe,0.1521495133638382,0.847850501537323,0.0 +orange/test/unripe/141.jpg,unripe,ripe,0.22023402154445648,0.7797659635543823,0.0 +orange/test/unripe/142.jpg,unripe,unripe,0.5501556396484375,0.4498443305492401,0.17674170434474945 +orange/test/unripe/143.jpg,unripe,ripe,0.4596283733844757,0.5403716564178467,0.0 +orange/test/unripe/144.jpg,unripe,overripe,0.0,0.43169647455215454,0.5683035254478455 +orange/test/unripe/145.jpg,unripe,ripe,0.0,0.5218198895454407,0.4781801402568817 +orange/test/unripe/146.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/147.jpg,unripe,unripe,0.7817845344543457,0.2182154506444931,0.16100408136844635 +orange/test/unripe/148.jpg,unripe,ripe,0.360322505235672,0.6396774649620056,0.21884389221668243 +orange/test/unripe/149.jpg,unripe,ripe,0.30668631196022034,0.6159207224845886,0.3840792775154114 +orange/test/unripe/15.jpg,unripe,ripe,0.14608119428157806,0.8539187908172607,0.0 +orange/test/unripe/151.jpg,unripe,overripe,0.30877065658569336,0.41199377179145813,0.5880062580108643 +orange/test/unripe/152.jpg,unripe,unripe,0.5510884523391724,0.44891154766082764,0.31841155886650085 +orange/test/unripe/153.jpg,unripe,overripe,0.0,0.44958677887916565,0.5504132509231567 +orange/test/unripe/154.jpg,unripe,ripe,0.16552746295928955,0.8344725370407104,0.0 +orange/test/unripe/155.jpg,unripe,ripe,0.0,0.5869362950325012,0.41306373476982117 +orange/test/unripe/156.jpg,unripe,ripe,0.17436397075653076,0.8256360292434692,0.0 +orange/test/unripe/157.jpg,unripe,ripe,0.4848403036594391,0.5151597261428833,0.0 +orange/test/unripe/159.jpg,unripe,unripe,0.7631447911262512,0.23685520887374878,0.005847953259944916 +orange/test/unripe/16.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/160.jpg,unripe,unripe,0.5596864223480225,0.44031357765197754,0.0 +orange/test/unripe/161.jpg,unripe,ripe,0.4227452576160431,0.5772547721862793,0.0 +orange/test/unripe/163.jpg,unripe,ripe,0.15153075754642487,0.7583156228065491,0.24168436229228973 +orange/test/unripe/164.jpg,unripe,ripe,0.4848403036594391,0.5151597261428833,0.0 +orange/test/unripe/165.jpg,unripe,ripe,0.4426043629646301,0.5573956370353699,0.11441612988710403 +orange/test/unripe/166.jpg,unripe,unripe,0.5299648642539978,0.4700351059436798,0.0 +orange/test/unripe/167.jpg,unripe,ripe,0.2910665273666382,0.7089334726333618,0.1736883670091629 +orange/test/unripe/168.jpg,unripe,ripe,0.3212207853794098,0.6787792444229126,0.055236753076314926 +orange/test/unripe/169.jpg,unripe,ripe,0.2311326265335083,0.7688673734664917,0.17063568532466888 +orange/test/unripe/17.jpg,unripe,unripe,0.949138343334198,0.050861675292253494,0.10811436921358109 +orange/test/unripe/170.jpg,unripe,unripe,0.5530006289482117,0.44699937105178833,0.20286639034748077 +orange/test/unripe/171.jpg,unripe,ripe,0.41514766216278076,0.5848523378372192,0.1598656326532364 +orange/test/unripe/172.jpg,unripe,ripe,0.30668631196022034,0.6159207224845886,0.3840792775154114 +orange/test/unripe/174.jpg,unripe,ripe,0.12839412689208984,0.6014742255210876,0.39852580428123474 +orange/test/unripe/175.jpg,unripe,ripe,0.17810457944869995,0.8218954205513,0.13089798390865326 +orange/test/unripe/176.jpg,unripe,ripe,0.0,0.5817474126815796,0.4182525873184204 +orange/test/unripe/177.jpg,unripe,ripe,0.3689880073070526,0.6310120224952698,0.1037164106965065 +orange/test/unripe/178.jpg,unripe,ripe,0.12017671763896942,0.8798232674598694,0.11261261254549026 +orange/test/unripe/179.jpg,unripe,unripe,0.5065592527389526,0.493440717458725,0.029200103133916855 +orange/test/unripe/18.jpg,unripe,ripe,0.2478731870651245,0.7521268129348755,0.06059431657195091 +orange/test/unripe/180.jpg,unripe,ripe,0.2875452935695648,0.7124546766281128,0.0 +orange/test/unripe/181.jpg,unripe,ripe,0.2813742458820343,0.7186257839202881,0.0026832588482648134 +orange/test/unripe/183.jpg,unripe,ripe,0.17145149409770966,0.8285484910011292,0.15854372084140778 +orange/test/unripe/184.jpg,unripe,unripe,0.7194470167160034,0.2805529832839966,0.3180637061595917 +orange/test/unripe/185.jpg,unripe,ripe,0.0429808646440506,0.9570191502571106,0.0 +orange/test/unripe/186.jpg,unripe,unripe,0.698052167892456,0.30194783210754395,0.0 +orange/test/unripe/187.jpg,unripe,ripe,0.10745999217033386,0.8925399780273438,0.0 +orange/test/unripe/188.jpg,unripe,overripe,0.0,0.4892183542251587,0.5107816457748413 +orange/test/unripe/189.jpg,unripe,unripe,0.9272769689559937,0.07272303104400635,0.15804323554039001 +orange/test/unripe/19.jpg,unripe,ripe,0.1098562628030777,0.8901437520980835,0.0 +orange/test/unripe/190.jpg,unripe,ripe,0.09346949309110641,0.7941175103187561,0.2058825045824051 +orange/test/unripe/191.jpg,unripe,unripe,0.9898765087127686,0.010123515501618385,0.0 +orange/test/unripe/193.jpg,unripe,ripe,0.07306012511253357,0.5848680734634399,0.41513192653656006 +orange/test/unripe/194.jpg,unripe,ripe,0.1855761706829071,0.8144237995147705,0.11847514659166336 +orange/test/unripe/195.jpg,unripe,ripe,0.22337238490581512,0.7766276001930237,0.08034837245941162 +orange/test/unripe/196.jpg,unripe,ripe,0.2898508310317993,0.6385747194290161,0.3614252507686615 +orange/test/unripe/197.jpg,unripe,ripe,0.0,0.507483720779419,0.49251630902290344 +orange/test/unripe/198.jpg,unripe,ripe,0.0,0.6187492609024048,0.3812507390975952 +orange/test/unripe/199.jpg,unripe,unripe,0.6489712595939636,0.3510287404060364,0.11153674125671387 +orange/test/unripe/2.jpg,unripe,ripe,0.0,0.593306839466095,0.4066931903362274 +orange/test/unripe/20.jpg,unripe,unripe,0.9243123531341553,0.07568767666816711,0.0 +orange/test/unripe/202.jpg,unripe,unripe,0.6489712595939636,0.3510287404060364,0.11153674125671387 +orange/test/unripe/205.jpg,unripe,ripe,0.26847410202026367,0.7315258979797363,0.25645244121551514 +orange/test/unripe/207.jpg,unripe,ripe,0.0,0.8399953246116638,0.16000469028949738 +orange/test/unripe/209.jpg,unripe,unripe,0.8189653754234314,0.1810346394777298,0.3243288993835449 +orange/test/unripe/21.jpg,unripe,unripe,0.949138343334198,0.050861675292253494,0.10811436921358109 +orange/test/unripe/211.jpg,unripe,ripe,0.0,0.5427170991897583,0.4572829008102417 +orange/test/unripe/212.jpg,unripe,ripe,0.11544046550989151,0.7568893432617188,0.24311068654060364 +orange/test/unripe/213.jpg,unripe,ripe,0.11805644631385803,0.8819435834884644,0.010628019459545612 +orange/test/unripe/214.jpg,unripe,ripe,0.0,0.6191462874412537,0.38085371255874634 +orange/test/unripe/215.jpg,unripe,ripe,0.0,0.7133182883262634,0.2866816818714142 +orange/test/unripe/216.jpg,unripe,unripe,0.5455660820007324,0.4544339179992676,0.0 +orange/test/unripe/217.jpg,unripe,ripe,0.22035349905490875,0.7796465158462524,0.0 +orange/test/unripe/218.jpg,unripe,overripe,0.0,0.48920634388923645,0.5107936263084412 +orange/test/unripe/22.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/220.jpg,unripe,unripe,0.6287900805473328,0.3712099492549896,0.0 +orange/test/unripe/221.jpg,unripe,overripe,0.0,0.4999470114707947,0.5000529885292053 +orange/test/unripe/222.jpg,unripe,ripe,0.2655867636203766,0.734413206577301,0.016708917915821075 +orange/test/unripe/223.jpg,unripe,unripe,0.5360047221183777,0.4639953076839447,5.319351112120785e-05 +orange/test/unripe/225.jpg,unripe,ripe,0.24748799204826355,0.7525120377540588,0.0 +orange/test/unripe/226.jpg,unripe,ripe,0.48280519247055054,0.5171948075294495,0.0 +orange/test/unripe/23.jpg,unripe,ripe,0.2478731870651245,0.7521268129348755,0.06059431657195091 +orange/test/unripe/231.jpg,unripe,overripe,0.0,0.4683437645435333,0.5316562652587891 +orange/test/unripe/233.jpg,unripe,unripe,0.718343198299408,0.28165680170059204,0.23040293157100677 +orange/test/unripe/234.jpg,unripe,ripe,0.07501019537448883,0.7371774315834045,0.26282253861427307 +orange/test/unripe/235.jpg,unripe,ripe,0.09520731121301651,0.6048179864883423,0.3951820135116577 +orange/test/unripe/236.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/24.jpg,unripe,unripe,0.9243123531341553,0.07568767666816711,0.0 +orange/test/unripe/240.jpg,unripe,ripe,0.0,0.6872783303260803,0.3127216398715973 +orange/test/unripe/241.jpg,unripe,ripe,0.29348987340927124,0.7065101265907288,0.055231500416994095 +orange/test/unripe/242.jpg,unripe,unripe,0.5815086364746094,0.4184913635253906,0.24148519337177277 +orange/test/unripe/244.jpg,unripe,overripe,0.0,0.4683437645435333,0.5316562652587891 +orange/test/unripe/245.jpg,unripe,ripe,0.0,0.5823341608047485,0.41766583919525146 +orange/test/unripe/247.jpg,unripe,ripe,0.3206275403499603,0.6793724894523621,0.06894736737012863 +orange/test/unripe/249.jpg,unripe,ripe,0.12722913920879364,0.8727708458900452,0.0 +orange/test/unripe/25.jpg,unripe,ripe,0.1098562628030777,0.8901437520980835,0.0 +orange/test/unripe/253.jpg,unripe,ripe,0.28169867396354675,0.7183012962341309,0.1697627604007721 +orange/test/unripe/256.jpg,unripe,ripe,0.12010978162288666,0.8687534928321838,0.13124649226665497 +orange/test/unripe/257.jpg,unripe,ripe,0.35245394706726074,0.6475460529327393,0.33508315682411194 +orange/test/unripe/258.jpg,unripe,ripe,0.4845869541168213,0.5154130458831787,0.055931806564331055 +orange/test/unripe/26.jpg,unripe,ripe,0.2332906574010849,0.7667093276977539,0.0 +orange/test/unripe/261.jpg,unripe,ripe,0.18284986913204193,0.8171501159667969,0.10521630197763443 +orange/test/unripe/263.jpg,unripe,unripe,0.6207801103591919,0.3792198896408081,0.0 +orange/test/unripe/265.jpg,unripe,ripe,0.0,0.6802405714988708,0.31975939869880676 +orange/test/unripe/266.jpg,unripe,ripe,0.0,0.5883004665374756,0.4116995632648468 +orange/test/unripe/269.jpg,unripe,ripe,0.434142529964447,0.565857470035553,0.1876637041568756 +orange/test/unripe/27.jpg,unripe,unripe,0.803989827632904,0.19601018726825714,0.021907012909650803 +orange/test/unripe/271.jpg,unripe,ripe,0.3865990936756134,0.613400936126709,0.0 +orange/test/unripe/272.jpg,unripe,unripe,0.757853090763092,0.24214692413806915,0.32156723737716675 +orange/test/unripe/273.jpg,unripe,unripe,0.5584110021591187,0.44158899784088135,0.0 +orange/test/unripe/277.jpg,unripe,overripe,0.0,0.4694444537162781,0.5305555462837219 +orange/test/unripe/279.jpg,unripe,ripe,0.0,0.6986827254295349,0.3013173043727875 +orange/test/unripe/28.jpg,unripe,ripe,0.00999646820127964,0.8337924480438232,0.16620753705501556 +orange/test/unripe/280.jpg,unripe,unripe,0.981664776802063,0.01833520084619522,0.203262060880661 +orange/test/unripe/282.jpg,unripe,ripe,0.4845869541168213,0.5154130458831787,0.055931806564331055 +orange/test/unripe/283.jpg,unripe,ripe,0.12722913920879364,0.8727708458900452,0.0 +orange/test/unripe/286.jpg,unripe,unripe,0.981664776802063,0.01833520084619522,0.203262060880661 +orange/test/unripe/287.jpg,unripe,unripe,0.7198342084884644,0.28016579151153564,0.0 +orange/test/unripe/29.jpg,unripe,ripe,0.45264747738838196,0.5473524928092957,0.013337735086679459 +orange/test/unripe/290.jpg,unripe,ripe,0.35347267985343933,0.6465273499488831,0.0 +orange/test/unripe/291.jpg,unripe,ripe,0.13900667428970337,0.792066752910614,0.207933247089386 +orange/test/unripe/294.jpg,unripe,ripe,0.2926882207393646,0.707311749458313,0.0 +orange/test/unripe/295.jpg,unripe,ripe,0.24327681958675385,0.756723165512085,0.04353518784046173 +orange/test/unripe/297.jpg,unripe,ripe,0.33161473274230957,0.6425826549530029,0.35741737484931946 +orange/test/unripe/298.jpg,unripe,unripe,0.6207801103591919,0.3792198896408081,0.0 +orange/test/unripe/3.jpg,unripe,unripe,0.5088728666305542,0.4911271631717682,0.08792270720005035 +orange/test/unripe/30.jpg,unripe,unripe,0.5198462605476379,0.48015376925468445,0.012276073917746544 +orange/test/unripe/300.jpg,unripe,ripe,0.3559521436691284,0.6440478563308716,0.09276773780584335 +orange/test/unripe/303.jpg,unripe,unripe,0.9224913120269775,0.07750869542360306,0.0942629799246788 +orange/test/unripe/305.jpg,unripe,overripe,0.0,0.44814974069595337,0.5518502593040466 +orange/test/unripe/308.jpg,unripe,ripe,0.3559521436691284,0.6440478563308716,0.09276773780584335 +orange/test/unripe/309.jpg,unripe,overripe,0.0,0.41440197825431824,0.5855979919433594 +orange/test/unripe/31.jpg,unripe,ripe,0.0,0.9569571614265442,0.04304281249642372 +orange/test/unripe/311.jpg,unripe,unripe,0.6427077054977417,0.3572922945022583,0.26315295696258545 +orange/test/unripe/312.jpg,unripe,ripe,0.0,0.5247516632080078,0.4752483069896698 +orange/test/unripe/317.jpg,unripe,ripe,0.21749918162822723,0.7825008034706116,0.11993980407714844 +orange/test/unripe/32.jpg,unripe,ripe,0.372394323348999,0.627605676651001,0.33311334252357483 +orange/test/unripe/325.jpg,unripe,ripe,0.21749918162822723,0.7825008034706116,0.11993980407714844 +orange/test/unripe/327.jpg,unripe,unripe,0.517699122428894,0.48230090737342834,0.01593111827969551 +orange/test/unripe/33.jpg,unripe,ripe,0.18162426352500916,0.8183757066726685,0.0 +orange/test/unripe/336.jpg,unripe,overripe,0.0,0.4924432337284088,0.5075567960739136 +orange/test/unripe/339.jpg,unripe,ripe,0.17955079674720764,0.82044917345047,0.16653375327587128 +orange/test/unripe/34.jpg,unripe,ripe,0.3417400121688843,0.6582599878311157,0.18355166912078857 +orange/test/unripe/343.jpg,unripe,unripe,0.8093888759613037,0.19061113893985748,0.20671352744102478 +orange/test/unripe/35.jpg,unripe,ripe,0.0,0.5499009490013123,0.45009908080101013 +orange/test/unripe/353.jpg,unripe,ripe,0.20473916828632355,0.7952608466148376,0.06470389664173126 +orange/test/unripe/358.jpg,unripe,unripe,0.6785345077514648,0.32146552205085754,0.13678160309791565 +orange/test/unripe/359.jpg,unripe,ripe,0.17955079674720764,0.82044917345047,0.16653375327587128 +orange/test/unripe/36.jpg,unripe,ripe,0.25758957862854004,0.74241042137146,0.0 +orange/test/unripe/366.jpg,unripe,unripe,0.6362611651420593,0.3637388348579407,0.0 +orange/test/unripe/368.jpg,unripe,ripe,0.17955079674720764,0.82044917345047,0.16653375327587128 +orange/test/unripe/37.jpg,unripe,unripe,0.8830865621566772,0.11691341549158096,0.031104544177651405 +orange/test/unripe/372.jpg,unripe,ripe,0.11472741514444351,0.7848113179206848,0.21518869698047638 +orange/test/unripe/373.jpg,unripe,ripe,0.11678042262792587,0.7045031189918518,0.2954968810081482 +orange/test/unripe/377.jpg,unripe,ripe,0.0,0.5006887316703796,0.49931126832962036 +orange/test/unripe/379.jpg,unripe,unripe,0.7660435438156128,0.233956441283226,0.2872883975505829 +orange/test/unripe/38.jpg,unripe,overripe,0.0,0.4905155599117279,0.5094844698905945 +orange/test/unripe/383.jpg,unripe,ripe,0.0,0.5006887316703796,0.49931126832962036 +orange/test/unripe/384.jpg,unripe,ripe,0.11678042262792587,0.7045031189918518,0.2954968810081482 +orange/test/unripe/385.jpg,unripe,unripe,0.5584240555763245,0.44157594442367554,0.32386383414268494 +orange/test/unripe/387.jpg,unripe,unripe,0.7660435438156128,0.233956441283226,0.2872883975505829 +orange/test/unripe/39.jpg,unripe,ripe,0.0,0.8875652551651001,0.11243472248315811 +orange/test/unripe/398.jpg,unripe,ripe,0.44014835357666016,0.5598516464233398,0.08781751990318298 +orange/test/unripe/4.jpg,unripe,ripe,0.13499002158641815,0.8650099635124207,0.0 +orange/test/unripe/40.jpg,unripe,ripe,0.25448688864707947,0.7455131411552429,0.0 +orange/test/unripe/41.jpg,unripe,ripe,0.472189724445343,0.527810275554657,0.2726341485977173 +orange/test/unripe/42.jpg,unripe,ripe,0.3576212227344513,0.6423787474632263,0.0 +orange/test/unripe/43.jpg,unripe,ripe,0.2589099705219269,0.7410899996757507,0.0 +orange/test/unripe/44.jpg,unripe,unripe,0.8830865621566772,0.11691341549158096,0.031104544177651405 +orange/test/unripe/45.jpg,unripe,ripe,0.0,0.6639645099639893,0.33603546023368835 +orange/test/unripe/46.jpg,unripe,overripe,0.0,0.4808032512664795,0.5191967487335205 +orange/test/unripe/47.jpg,unripe,ripe,0.4018332362174988,0.5981667637825012,0.016835927963256836 +orange/test/unripe/48.jpg,unripe,unripe,0.6259051561355591,0.3740948438644409,0.0641535073518753 +orange/test/unripe/49.jpg,unripe,ripe,0.21515953540802002,0.78484046459198,0.15999004244804382 +orange/test/unripe/5.jpg,unripe,unripe,0.8656493425369263,0.13435065746307373,0.02589450404047966 +orange/test/unripe/50.jpg,unripe,ripe,0.17668688297271729,0.8233131170272827,0.0 +orange/test/unripe/51.jpg,unripe,overripe,0.0,0.4765731692314148,0.5234268307685852 +orange/test/unripe/52.jpg,unripe,ripe,0.37356865406036377,0.6264313459396362,0.08711595833301544 +orange/test/unripe/53.jpg,unripe,unripe,0.9900831580162048,0.009916824288666248,0.247427299618721 +orange/test/unripe/54.jpg,unripe,ripe,0.4659968912601471,0.5340031385421753,0.0 +orange/test/unripe/55.jpg,unripe,overripe,0.0,0.46518903970718384,0.5348109602928162 +orange/test/unripe/56.jpg,unripe,ripe,0.25448688864707947,0.7455131411552429,0.0 +orange/test/unripe/57.jpg,unripe,ripe,0.0,0.5484283566474915,0.45157164335250854 +orange/test/unripe/58.jpg,unripe,ripe,0.0039874413050711155,0.6953741312026978,0.30462583899497986 +orange/test/unripe/59.jpg,unripe,unripe,0.6170695424079895,0.3829304277896881,0.14724281430244446 +orange/test/unripe/6.jpg,unripe,ripe,0.20220732688903809,0.7977926731109619,0.18106797337532043 +orange/test/unripe/60.jpg,unripe,ripe,0.11675435304641724,0.7805026769638062,0.21949733793735504 +orange/test/unripe/61.jpg,unripe,ripe,0.0,0.5484283566474915,0.45157164335250854 +orange/test/unripe/62.jpg,unripe,ripe,0.0,0.5819182395935059,0.41808173060417175 +orange/test/unripe/63.jpg,unripe,unripe,0.5228508114814758,0.47714918851852417,0.0892021581530571 +orange/test/unripe/64.jpg,unripe,unripe,0.9301291108131409,0.06987091898918152,0.2278006374835968 +orange/test/unripe/65.jpg,unripe,ripe,0.2927613854408264,0.7072386145591736,0.26002809405326843 +orange/test/unripe/66.jpg,unripe,ripe,0.0,0.999952495098114,4.7483379603363574e-05 +orange/test/unripe/67.jpg,unripe,overripe,0.6010938882827759,0.37611982226371765,0.6238802075386047 +orange/test/unripe/68.jpg,unripe,ripe,0.4817601442337036,0.5182398557662964,0.04918743297457695 +orange/test/unripe/69.jpg,unripe,unripe,0.9051660895347595,0.09483391791582108,0.0 +orange/test/unripe/7.jpg,unripe,ripe,0.2882933020591736,0.7117066979408264,0.0 +orange/test/unripe/70.jpg,unripe,ripe,0.0,0.6914083361625671,0.30859166383743286 +orange/test/unripe/71.jpg,unripe,unripe,0.794587254524231,0.20541273057460785,0.0 +orange/test/unripe/72.jpg,unripe,ripe,0.25448688864707947,0.7455131411552429,0.0 +orange/test/unripe/73.jpg,unripe,ripe,0.29441356658935547,0.7055864334106445,0.0 +orange/test/unripe/74.jpg,unripe,unripe,0.6935322284698486,0.30646777153015137,0.004195523448288441 +orange/test/unripe/75.jpg,unripe,unripe,0.9291256666183472,0.07087436318397522,0.13709338009357452 +orange/test/unripe/76.jpg,unripe,ripe,0.12005575001239777,0.8719067573547363,0.12809322774410248 +orange/test/unripe/77.jpg,unripe,unripe,0.8013418316841125,0.19865818321704865,0.0338703915476799 +orange/test/unripe/78.jpg,unripe,ripe,0.2754170298576355,0.7245829701423645,0.17812739312648773 +orange/test/unripe/79.jpg,unripe,ripe,0.3417400121688843,0.6582599878311157,0.18355166912078857 +orange/test/unripe/8.jpg,unripe,unripe,0.6812118887901306,0.318788081407547,0.0 +orange/test/unripe/80.jpg,unripe,unripe,0.7141439318656921,0.28585606813430786,0.0 +orange/test/unripe/81.jpg,unripe,ripe,0.11520441621541977,0.884795606136322,0.0 +orange/test/unripe/82.jpg,unripe,ripe,0.0,0.6512240767478943,0.3487758934497833 +orange/test/unripe/83.jpg,unripe,ripe,0.04044454172253609,0.8152344822883606,0.1847655028104782 +orange/test/unripe/84.jpg,unripe,unripe,0.6763761043548584,0.3236238658428192,0.14248612523078918 +orange/test/unripe/85.jpg,unripe,unripe,0.7141439318656921,0.28585606813430786,0.0 +orange/test/unripe/86.jpg,unripe,ripe,0.24814251065254211,0.7518574595451355,0.0 +orange/test/unripe/87.jpg,unripe,ripe,0.2754170298576355,0.7245829701423645,0.17812739312648773 +orange/test/unripe/88.jpg,unripe,ripe,0.40528085827827454,0.5947191715240479,0.0 +orange/test/unripe/89.jpg,unripe,unripe,0.668935239315033,0.33106479048728943,0.0 +orange/test/unripe/9.jpg,unripe,unripe,0.823921799659729,0.1760781854391098,0.053343769162893295 +orange/test/unripe/90.jpg,unripe,unripe,0.8379561901092529,0.16204379498958588,0.11473999917507172 +orange/test/unripe/91.jpg,unripe,ripe,0.4777298867702484,0.5222700834274292,0.22315733134746552 +orange/test/unripe/92.jpg,unripe,ripe,0.1551656872034073,0.7265756726264954,0.27342429757118225 +orange/test/unripe/93.jpg,unripe,ripe,0.39363163709640503,0.606368362903595,0.036073844879865646 +orange/test/unripe/94.jpg,unripe,unripe,0.5861092209815979,0.4138908088207245,0.03911300003528595 +orange/test/unripe/95.jpg,unripe,ripe,0.0,0.6282822489738464,0.37171775102615356 +orange/test/unripe/96.jpg,unripe,overripe,0.0,0.42181646823883057,0.5781835317611694 +orange/test/unripe/97.jpg,unripe,ripe,0.16879698634147644,0.8312029838562012,0.0 +orange/test/unripe/98.jpg,unripe,unripe,0.6198109984397888,0.3801890015602112,0.024090038612484932 +orange/test/unripe/99.jpg,unripe,ripe,0.17494706809520721,0.8250529170036316,0.0 diff --git a/AgCloud/services/ripeness-baseline/eval/orange_argmax/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/orange_argmax/roc_curves.png new file mode 100644 index 000000000..4840e45f2 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/orange_argmax/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/orange_test/metrics.json b/AgCloud/services/ripeness-baseline/eval/orange_test/metrics.json new file mode 100644 index 000000000..8d5793e6e --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/orange_test/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.4467483506126296, + "report": { + "unripe": { + "precision": 0.9479166666666666, + "recall": 0.337037037037037, + "f1-score": 0.4972677595628415, + "support": 270.0 + }, + "ripe": { + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 388.0 + }, + "overripe": { + "precision": 0.42745535714285715, + "recall": 0.9503722084367245, + "f1-score": 0.5896843725943033, + "support": 403.0 + }, + "accuracy": 0.4467483506126296, + "macro avg": { + "precision": 0.4584573412698412, + "recall": 0.4291364151579205, + "f1-score": 0.36231737738571496, + "support": 1061.0 + }, + "weighted avg": { + "precision": 0.4035834202908308, + "recall": 0.4467483506126296, + "f1-score": 0.3505231830701898, + "support": 1061.0 + } + }, + "confusion_matrix": [ + [ + 91, + 54, + 125 + ], + [ + 0, + 0, + 388 + ], + [ + 5, + 15, + 383 + ] + ], + "samples": 1061, + "prefix": "orange/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/orange_test/per_image.csv b/AgCloud/services/ripeness-baseline/eval/orange_test/per_image.csv new file mode 100644 index 000000000..229aabdcc --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/orange_test/per_image.csv @@ -0,0 +1,1062 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +orange/test/overripe/Screen Shot 2018-06-12 at 11.18.34 PM.png,overripe,overripe,0.0,0.623788058757782,0.3762119710445404 +orange/test/overripe/Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,overripe,0.3501598834991455,0.6498401165008545,0.3403708338737488 +orange/test/overripe/Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.0,0.5016512870788574,0.49834874272346497 +orange/test/overripe/Screen Shot 2018-06-12 at 11.19.56 PM.png,overripe,overripe,0.0,0.40437766909599304,0.5956223607063293 +orange/test/overripe/Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,overripe,0.0,0.6313452124595642,0.3686548173427582 +orange/test/overripe/Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,overripe,0.3696143925189972,0.6303856372833252,0.2990463376045227 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.5048556923866272,0.4951443076133728 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.5000985264778137,0.49990150332450867 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,overripe,0.0,0.8499212265014648,0.15007875859737396 +orange/test/overripe/Screen Shot 2018-06-12 at 11.22.32 PM.png,overripe,ripe,0.0,0.9660597443580627,0.03394027799367905 +orange/test/overripe/Screen Shot 2018-06-12 at 11.23.03 PM.png,overripe,overripe,0.0,0.5115698575973511,0.48843011260032654 +orange/test/overripe/Screen Shot 2018-06-12 at 11.23.33 PM.png,overripe,overripe,0.0,0.4327687621116638,0.5672312378883362 +orange/test/overripe/Screen Shot 2018-06-12 at 11.24.08 PM.png,overripe,overripe,0.0,0.8915511965751648,0.10844879597425461 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.20 PM.png,overripe,overripe,0.7089061141014099,0.2910938858985901,0.14738552272319794 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.4726424813270569,0.5273575186729431 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.25856176018714905,0.6724131107330322,0.3275868892669678 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.07 PM.png,overripe,overripe,0.0,0.9368947148323059,0.0631052777171135 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.7266096472740173,0.27339038252830505 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.18 PM.png,overripe,unripe,0.12930162250995636,0.8706983923912048,0.03621421381831169 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.24 PM.png,overripe,overripe,0.0,0.5911696553230286,0.40883034467697144 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4000990390777588,0.5999009609222412 +orange/test/overripe/Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,overripe,0.0,0.6165541410446167,0.3834458291530609 +orange/test/overripe/Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.4794853627681732,0.5205146074295044 +orange/test/overripe/Screen Shot 2018-06-12 at 11.28.21 PM.png,overripe,overripe,0.0,0.4407489597797394,0.559251070022583 +orange/test/overripe/Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,overripe,0.0,0.4937618374824524,0.5062381625175476 +orange/test/overripe/Screen Shot 2018-06-12 at 11.29.44 PM.png,overripe,overripe,0.0,0.7482232451438904,0.2517767548561096 +orange/test/overripe/Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.4438692033290863,0.5561308264732361 +orange/test/overripe/Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,overripe,0.0,0.45875078439712524,0.5412492156028748 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.09 PM.png,overripe,overripe,0.0,0.4133763909339905,0.5866236090660095 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,overripe,0.0,0.653941810131073,0.346058189868927 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.21 PM.png,overripe,overripe,0.0,0.541118323802948,0.458881676197052 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,overripe,0.0,0.5203619599342346,0.4796380400657654 +orange/test/overripe/Screen Shot 2018-06-12 at 11.33.12 PM.png,overripe,overripe,0.0,0.40366679430007935,0.5963332056999207 +orange/test/overripe/Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/Screen Shot 2018-06-12 at 11.37.00 PM.png,overripe,overripe,0.0,0.5020381212234497,0.4979618489742279 +orange/test/overripe/Screen Shot 2018-06-12 at 11.37.52 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,overripe,0.0,0.5158267021179199,0.48417332768440247 +orange/test/overripe/Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.5009730458259583,0.49902695417404175 +orange/test/overripe/Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,overripe,0.0,0.5310564041137695,0.46894362568855286 +orange/test/overripe/Screen Shot 2018-06-12 at 11.41.35 PM.png,overripe,overripe,0.894435465335846,0.10556453466415405,0.25715985894203186 +orange/test/overripe/Screen Shot 2018-06-12 at 11.42.38 PM.png,overripe,overripe,0.0,0.6194368600845337,0.3805631697177887 +orange/test/overripe/Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.5121461153030396,0.48785388469696045 +orange/test/overripe/Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,overripe,0.0,0.7165592908859253,0.2834407389163971 +orange/test/overripe/Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,overripe,0.0,0.5097648501396179,0.4902351200580597 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.6114276051521301,0.3885723948478699,0.3658675253391266 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.37983939051628113,0.5926026701927185,0.4073973596096039 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.20.52 PM.png,overripe,overripe,0.0,0.7711544632911682,0.2288455069065094 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.5037845969200134,0.4962153732776642 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.5122167468070984,0.4877832531929016 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7703214287757874,0.22967858612537384 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.29 PM.png,overripe,ripe,0.0,0.959540843963623,0.04045917093753815 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.6909403204917908,0.30905967950820923 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.16 PM.png,overripe,overripe,0.15813466906547546,0.6077517867088318,0.3922482132911682 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.47755709290504456,0.5224428772926331 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.55 PM.png,overripe,overripe,0.5648265480995178,0.4351734519004822,0.2954075038433075 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.07 PM.png,overripe,overripe,0.0,0.9366294741630554,0.06337053328752518 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.6909451484680176,0.3090548813343048 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4013965427875519,0.5986034274101257 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.44 PM.png,overripe,overripe,0.0,0.7833675146102905,0.21663248538970947 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,overripe,0.0,0.5554704666137695,0.44452953338623047 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.29.58 PM.png,overripe,overripe,0.0,0.8968564867973328,0.10314353555440903 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,overripe,0.0,0.8009247183799744,0.19907528162002563 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.30.41 PM.png,overripe,overripe,0.0,0.5642847418785095,0.4357152581214905 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,overripe,0.0,0.507782518863678,0.49221745133399963 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.32.04 PM.png,overripe,overripe,0.0,0.4087161421775818,0.5912838578224182 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,overripe,0.0,0.42591044306755066,0.5740895867347717 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.34.13 PM.png,overripe,overripe,0.0,0.5047563314437866,0.4952436685562134 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.36.19 PM.png,overripe,overripe,0.0,0.6572629809379578,0.34273701906204224 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.6206209659576416,0.3793790340423584 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,overripe,0.0,0.5390933156013489,0.4609066843986511 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.40.51 PM.png,overripe,overripe,0.7618880271911621,0.2381119728088379,0.12496545165777206 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.4009784460067749,0.5990215539932251 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.43.49 PM.png,overripe,overripe,0.0,0.732208251953125,0.267791748046875 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.4600749909877777,0.5399250388145447 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.45.17 PM.png,overripe,overripe,0.0,0.4257363975048065,0.5742635726928711 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.45.42 PM.png,overripe,overripe,0.36438849568367004,0.6356115341186523,0.3555915057659149 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.6149780750274658,0.38502195477485657 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.43977633118629456,0.5602236986160278,0.3985944092273712 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,overripe,0.21678771078586578,0.5426236987113953,0.4573763310909271 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.19.37 PM.png,overripe,overripe,0.08700381219387054,0.7019689083099365,0.2980310916900635 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,unripe,0.6400546431541443,0.3599453270435333,0.034445252269506454 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.20.18 PM.png,overripe,overripe,0.0,0.47350701689720154,0.5264929533004761 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.17 PM.png,overripe,overripe,0.0,0.8378758430480957,0.1621241569519043 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.506382167339325,0.49361786246299744 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.43956848978996277,0.5604315400123596 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.22.21 PM.png,overripe,overripe,0.0,0.44053953886032104,0.559460461139679 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.22.47 PM.png,overripe,overripe,0.0,0.6014223098754883,0.3985776901245117 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.23.40 PM.png,overripe,overripe,0.0,0.4584539532661438,0.5415460467338562 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7735876441001892,0.2264123409986496 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.25.16 PM.png,overripe,overripe,0.1840340942144394,0.6012928485870361,0.39870715141296387 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.3395216464996338,0.6604783535003662,0.3182581663131714 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.6937323808670044,0.306267648935318 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.4731809198856354,0.526819109916687 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.28.21 PM.png,overripe,overripe,0.0,0.45340782403945923,0.5465921759605408 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.14 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.21 PM.png,overripe,overripe,0.0,0.4328031539916992,0.5671968460083008 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.36 PM.png,overripe,overripe,0.0,0.5025245547294617,0.49747544527053833 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.30.11 PM.png,overripe,overripe,0.0,0.7304263114929199,0.2695736587047577 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,overripe,0.0,0.7591381669044495,0.24086186289787292 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.31.17 PM.png,overripe,overripe,0.0,0.7760196924209595,0.22398027777671814 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.31.24 PM.png,overripe,overripe,0.0,0.40430107712745667,0.5956989526748657 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,overripe,0.0,0.6650177836418152,0.3349821865558624 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.33.00 PM.png,overripe,overripe,0.0,0.40202265977859497,0.597977340221405 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.33.23 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.06 PM.png,overripe,overripe,0.0,0.4195205867290497,0.5804794430732727 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,overripe,0.0,0.8403439521789551,0.15965603291988373 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.53 PM.png,overripe,overripe,0.0,0.41859230399131775,0.5814077258110046 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,overripe,0.0,0.5484172701835632,0.45158272981643677 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.5753323435783386,0.42466768622398376 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,overripe,0.0,0.5309470295906067,0.4690529406070709 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.4909035861492157,0.5090964436531067 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,overripe,0.0,0.7296587824821472,0.2703412175178528 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,overripe,0.0,0.44281142950057983,0.5571885704994202 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,overripe,0.0,0.5189117789268494,0.48108819127082825 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.40.42 PM.png,overripe,overripe,0.0,0.415747731924057,0.5842522382736206 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.1838436871767044,0.5487738847732544,0.4512260854244232 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.41.44 PM.png,overripe,overripe,0.9846440553665161,0.01535592507570982,0.118119016289711 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.42.05 PM.png,overripe,overripe,0.0,0.6507793068885803,0.3492206931114197 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.43.36 PM.png,overripe,overripe,0.0,0.4998275339603424,0.50017249584198 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.43.54 PM.png,overripe,overripe,0.6450724601745605,0.35492753982543945,0.30816781520843506 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.4601900279521942,0.5398100018501282 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.45737868547439575,0.5426213145256042 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.29 PM.png,overripe,overripe,0.2842518091201782,0.6355035901069641,0.3644964396953583 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,overripe,0.0,0.7033414840698242,0.2966585159301758 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,overripe,0.0,0.5869011282920837,0.41309884190559387 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,overripe,0.0,0.5090197920799255,0.49098020792007446 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.47 PM.png,overripe,overripe,0.0,0.4892771542072296,0.510722815990448 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.46.26 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.46.56 PM.png,overripe,overripe,0.057992368936538696,0.5731996297836304,0.42680034041404724 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,overripe,0.45756158232688904,0.5424384474754333,0.3218168318271637 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.2776239216327667,0.5742684006690979,0.4257315695285797 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.19.37 PM.png,overripe,overripe,0.0,0.6843256950378418,0.3156743347644806 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,overripe,0.0,0.6363784074783325,0.3636215925216675 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.40 PM.png,overripe,overripe,0.0,0.4534972608089447,0.5465027689933777 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,overripe,0.0,0.6583755016326904,0.34162452816963196 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.5228829383850098,0.47711706161499023 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.35 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.43687495589256287,0.5631250739097595 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.19 PM.png,overripe,overripe,0.0,0.4515390694141388,0.5484609007835388 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.507573664188385,0.4924263060092926 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.54 PM.png,overripe,ripe,0.0,0.9852086305618286,0.01479139644652605 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7754477858543396,0.2245522439479828 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.70649254322052,0.29350745677948 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.26.18 PM.png,overripe,unripe,0.10025376826524734,0.8997462391853333,0.04844320937991142 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,ripe,0.0,0.9693499803543091,0.03065001219511032 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,overripe,0.0,0.6256927847862244,0.37430721521377563 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.27.38 PM.png,overripe,overripe,0.0,0.4435088038444519,0.5564911961555481 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,overripe,0.0,0.508361279964447,0.491638720035553 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.29.10 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.29.26 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.46612921357154846,0.5338708162307739 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.30.48 PM.png,overripe,overripe,0.0,0.7488016486167908,0.25119835138320923 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.17 PM.png,overripe,overripe,0.0,0.7724435329437256,0.2275564819574356 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,overripe,0.0,0.48221418261528015,0.5177858471870422 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.44 PM.png,overripe,overripe,0.0,0.4290110468864441,0.5709889531135559 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,overripe,0.0,0.4970885217189789,0.5029115080833435 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.33.12 PM.png,overripe,overripe,0.0,0.40287768840789795,0.597122311592102 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.34.07 PM.png,overripe,overripe,0.0,0.4152103066444397,0.5847896933555603 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.36.19 PM.png,overripe,overripe,0.0,0.6539583802223206,0.34604164958000183 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.07 PM.png,overripe,overripe,0.0,0.4005439877510071,0.5994560122489929 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,overripe,0.01221830677241087,0.7534950971603394,0.24650491774082184 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,overripe,0.3774707317352295,0.6225292682647705,0.33660680055618286 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.38.19 PM.png,overripe,overripe,0.0,0.5350127220153809,0.46498727798461914 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,overripe,0.3321101665496826,0.5892989635467529,0.41070106625556946 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.04 PM.png,overripe,overripe,0.10955678671598434,0.5336468815803528,0.4663531184196472 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.13769900798797607,0.5389468669891357,0.46105316281318665 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.44 PM.png,overripe,overripe,0.9872350096702576,0.012764994986355305,0.11668470501899719 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,overripe,0.0,0.5248718857765198,0.47512808442115784 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.00 PM.png,overripe,overripe,0.0,0.5651063323020935,0.4348936676979065 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,overripe,0.0,0.8672279715538025,0.13277199864387512 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.9204577803611755,0.07954219728708267 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.43.03 PM.png,overripe,overripe,0.0,0.42659515142440796,0.573404848575592 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.45294851064682007,0.5470514893531799 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.44.24 PM.png,overripe,overripe,0.16578008234500885,0.5942981839179993,0.40570181608200073 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.45.21 PM.png,overripe,overripe,0.0038478707429021597,0.5378525853157043,0.46214738488197327 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.10 PM.png,overripe,overripe,0.0,0.4074366092681885,0.5925633907318115 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.6139321327209473,0.38606786727905273 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.56 PM.png,overripe,overripe,0.06614556163549423,0.5746222138404846,0.42537781596183777 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.4277583062648773,0.5722416639328003,0.4031793177127838 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,overripe,0.4575747847557068,0.5424252152442932,0.3223016858100891 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,overripe,0.19196674227714539,0.5364736914634705,0.46352627873420715 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.5214844942092896,0.47851553559303284 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.21.48 PM.png,overripe,overripe,0.0,0.5004066228866577,0.4995933771133423 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.22.47 PM.png,overripe,overripe,0.0,0.6036778688430786,0.396322101354599 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.03 PM.png,overripe,overripe,0.0,0.5271703600883484,0.4728296399116516 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.09 PM.png,overripe,overripe,0.9961324334144592,0.0038675738032907248,0.14988066256046295 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,overripe,0.0,0.5224456787109375,0.4775543510913849 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.5191765427589417,0.48082345724105835 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.17 PM.png,overripe,overripe,0.7950624823570251,0.20493750274181366,0.16959410905838013 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.24 PM.png,overripe,overripe,0.0,0.8523232936859131,0.14767669141292572 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.37 PM.png,overripe,overripe,0.8260910511016846,0.17390893399715424,0.15993301570415497 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,overripe,0.38779208064079285,0.6122079491615295,0.31151479482650757 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.25.20 PM.png,overripe,overripe,0.805362343788147,0.19463767111301422,0.15591177344322205 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.7128280997276306,0.287171870470047 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.26.49 PM.png,overripe,overripe,0.9758011698722839,0.024198854342103004,0.16463004052639008 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.27.26 PM.png,overripe,overripe,0.0,0.5729679465293884,0.4270320236682892 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,overripe,0.0,0.5049483180046082,0.49505165219306946 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.28.50 PM.png,overripe,overripe,0.0,0.5012218356132507,0.4987781345844269 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.29.03 PM.png,overripe,overripe,0.22515785694122314,0.7748421430587769,0.21526974439620972 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,overripe,0.0,0.5627428889274597,0.43725714087486267 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.30.35 PM.png,overripe,overripe,0.8418244123458862,0.15817560255527496,0.2313050627708435 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.30.41 PM.png,overripe,overripe,0.0,0.5721327662467957,0.42786723375320435 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,overripe,0.0,0.5145303606987,0.48546963930130005 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,overripe,0.0,0.4175206124782562,0.5824793577194214 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.16 PM.png,overripe,overripe,0.0,0.49606287479400635,0.5039371252059937 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.43 PM.png,overripe,overripe,0.0,0.41978269815444946,0.5802173018455505 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.55 PM.png,overripe,overripe,0.0,0.5086063146591187,0.49139368534088135 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.6185849905014038,0.3814150094985962 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,overripe,0.0,0.8385762572288513,0.1614237278699875 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.6421923637390137,0.35780763626098633 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.37.41 PM.png,overripe,overripe,0.0,0.5457102060317993,0.4542897939682007 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,overripe,0.0,0.7289868593215942,0.2710131108760834 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,overripe,0.0,0.44330546259880066,0.556694507598877 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.41.17 PM.png,overripe,overripe,0.4662848114967346,0.5337151885032654,0.3643788993358612 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.41.48 PM.png,overripe,overripe,0.9787027835845947,0.021297212690114975,0.10992763191461563 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.05 PM.png,overripe,overripe,0.0,0.6516975164413452,0.3483024835586548 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,overripe,0.0,0.8632894158363342,0.13671058416366577 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.40183401107788086,0.5981659889221191 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.43.26 PM.png,overripe,overripe,0.0,0.4274141192436218,0.5725858807563782 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.46033087372779846,0.5396690964698792 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.45.17 PM.png,overripe,overripe,0.0,0.42325952649116516,0.5767405033111572 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.45.21 PM.png,overripe,overripe,0.009877200238406658,0.5373923778533936,0.46260765194892883 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.18.28 PM.png,overripe,overripe,0.0,0.40382838249206543,0.5961716175079346 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,overripe,0.0,0.7758708596229553,0.22412914037704468 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,unripe,0.5916556119918823,0.40834441781044006,0.04228641092777252 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.22.36 PM.png,overripe,overripe,0.20391115546226501,0.7658722996711731,0.2341277152299881 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7737472653388977,0.2262527495622635 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.24 PM.png,overripe,overripe,0.14123888313770294,0.8587611317634583,0.1162375882267952 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.51 PM.png,overripe,overripe,0.0,0.6818659901618958,0.31813400983810425 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.3423839509487152,0.6576160192489624,0.33223068714141846 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.26.02 PM.png,overripe,overripe,0.09829766303300858,0.7891965508460999,0.21080343425273895 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.43968167901039124,0.5603182911872864 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.12 PM.png,overripe,overripe,0.0,0.5525981187820435,0.44740188121795654 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.30 PM.png,overripe,overripe,0.1459028124809265,0.5976096987724304,0.4023903012275696 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.28.00 PM.png,overripe,overripe,0.0,0.47240495681762695,0.527595043182373 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.29.58 PM.png,overripe,overripe,0.0,0.9150562882423401,0.08494371920824051 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.30.11 PM.png,overripe,overripe,0.0,0.7359859347343445,0.2640140652656555 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.31.01 PM.png,overripe,overripe,0.0,0.45474860072135925,0.5452513694763184 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,overripe,0.0,0.508637011051178,0.491362988948822 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.04 PM.png,overripe,overripe,0.0,0.40637320280075073,0.5936267971992493 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,overripe,0.0,0.5096819996833801,0.4903179705142975 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.50 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.34.07 PM.png,overripe,overripe,0.0,0.415662556886673,0.5843374133110046 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.35.51 PM.png,overripe,overripe,0.0,0.4067526161670685,0.5932473540306091 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.14 PM.png,overripe,overripe,0.0,0.4046246111392975,0.5953754186630249 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.35 PM.png,overripe,overripe,0.0,0.4219765067100525,0.5780234932899475 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,overripe,0.40836280584335327,0.5916371941566467,0.33681368827819824 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,overripe,0.0,0.5289739370346069,0.47102609276771545 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,overripe,0.0,0.7274875640869141,0.27251240611076355 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,overripe,0.5599839091300964,0.44001609086990356,0.37607333064079285 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.40.42 PM.png,overripe,overripe,0.0,0.4147505760192871,0.5852494239807129 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.3403100073337555,0.5842704772949219,0.4157295227050781 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,overripe,0.0,0.8659238815307617,0.13407614827156067 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.9226526618003845,0.07734733074903488 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.4711504578590393,0.5288495421409607 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.12 PM.png,overripe,overripe,0.0,0.6048455238342285,0.3951544761657715 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,overripe,0.0,0.5621389150619507,0.4378611147403717 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.47 PM.png,overripe,overripe,0.0,0.49597617983818054,0.5040238499641418 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.57 PM.png,overripe,overripe,0.0,0.42907530069351196,0.570924699306488 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.6154072880744934,0.3845927119255066 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.18.41 PM.png,overripe,overripe,0.0,0.48841753602027893,0.5115824341773987 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.08 PM.png,overripe,overripe,0.6189284920692444,0.3810714781284332,0.3493443429470062 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,overripe,0.0,0.777158796787262,0.22284120321273804 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.56 PM.png,overripe,overripe,0.0,0.40542423725128174,0.5945757627487183 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,unripe,0.564370334148407,0.43562963604927063,0.05491400882601738 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.21.17 PM.png,overripe,overripe,0.0,0.8455981016159058,0.15440188348293304 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.21.48 PM.png,overripe,overripe,0.0,0.4990530014038086,0.5009469985961914 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.22.41 PM.png,overripe,ripe,0.0,0.952315628528595,0.04768439009785652 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.5340536832809448,0.4659463167190552 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,overripe,0.0,0.44010916352272034,0.5598908066749573 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.17 PM.png,overripe,overripe,0.47830402851104736,0.5216959714889526,0.25719982385635376 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.29 PM.png,overripe,ripe,0.0,0.9652621150016785,0.03473788499832153 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.689185380935669,0.31081464886665344 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.25.38 PM.png,overripe,overripe,0.0,0.4019434154033661,0.5980565547943115 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.25.55 PM.png,overripe,overripe,0.4985194802284241,0.5014805197715759,0.31457504630088806 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.02 PM.png,overripe,overripe,0.22915400564670563,0.7708460092544556,0.19242386519908905 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.24 PM.png,overripe,overripe,0.0,0.5569786429405212,0.44302138686180115 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.40189918875694275,0.5981007814407349 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,overripe,0.0,0.6165269613265991,0.3834730386734009 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.27.12 PM.png,overripe,overripe,0.0,0.5361518859863281,0.46384814381599426 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.28.17 PM.png,overripe,overripe,0.0,0.4666409194469452,0.5333591103553772 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,overripe,0.0,0.5314688682556152,0.46853113174438477 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.29.44 PM.png,overripe,overripe,0.0,0.749756395816803,0.250243604183197 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.29.51 PM.png,overripe,overripe,0.0,0.590313732624054,0.40968626737594604 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,overripe,0.0,0.750929594039917,0.24907037615776062 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,overripe,0.0,0.4570779502391815,0.5429220795631409 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,overripe,0.0,0.6516443490982056,0.34835562109947205 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.41 PM.png,overripe,overripe,0.0,0.4039718210697174,0.596028208732605 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.55 PM.png,overripe,overripe,0.0,0.40073099732398987,0.5992690324783325 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.33.43 PM.png,overripe,overripe,0.0,0.4130188822746277,0.5869811177253723 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.33.50 PM.png,overripe,overripe,0.0,0.4299713671207428,0.5700286626815796 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.6289923191070557,0.37100768089294434 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,overripe,0.0,0.8449695706367493,0.15503044426441193 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.19 PM.png,overripe,overripe,0.0,0.4030059278011322,0.5969940423965454 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.5521321892738342,0.44786781072616577 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.52 PM.png,overripe,overripe,0.0,0.4016576111316681,0.5983423590660095 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.58 PM.png,overripe,overripe,0.0,0.4029410481452942,0.5970589518547058 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.38.19 PM.png,overripe,overripe,0.0,0.497751921415329,0.5022480487823486 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.5001961588859558,0.4998038709163666 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.39.19 PM.png,overripe,overripe,0.0,0.5679206848144531,0.4320793151855469 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,overripe,0.0,0.5258495807647705,0.4741504490375519 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,overripe,0.0,0.4964396059513092,0.5035604238510132 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.9205870628356934,0.07941290736198425 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.43.03 PM.png,overripe,overripe,0.0,0.4248034954071045,0.5751965045928955 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.44.24 PM.png,overripe,overripe,0.11434954404830933,0.569053590297699,0.430946409702301 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.46697908639907837,0.5330209136009216 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,overripe,0.0,0.5753021836280823,0.4246978461742401 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,overripe,0.0,0.5073549747467041,0.4926450550556183 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.18.28 PM.png,overripe,overripe,0.0,0.40681952238082886,0.5931804776191711 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,overripe,0.0,0.7738345861434937,0.22616539895534515 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.20.18 PM.png,overripe,overripe,0.0,0.47190481424331665,0.5280951857566833 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.20.30 PM.png,overripe,overripe,0.0,0.42522886395454407,0.5747711658477783 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.4342026710510254,0.5657973289489746 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,overripe,0.0,0.8354483246803284,0.16455166041851044 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.22.05 PM.png,overripe,overripe,0.0,0.4671156704425812,0.5328842997550964 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.22.32 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,overripe,0.0,0.6462924480438232,0.35370755195617676 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,overripe,0.0,0.41694292426109314,0.5830571055412292 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.7030848860740662,0.29691511392593384 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,overripe,0.3457377851009369,0.6542622447013855,0.3241702914237976 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.4767036437988281,0.5232963562011719 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.3765644431114197,0.6234355568885803,0.330080509185791 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.7180497646331787,0.2819502353668213 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4015514850616455,0.5984485149383545 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.44 PM.png,overripe,overripe,0.0,0.7882997989654541,0.2117002010345459 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.27.46 PM.png,overripe,overripe,0.508586585521698,0.491413414478302,0.44954097270965576 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,overripe,0.0,0.7306436896324158,0.26935628056526184 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,overripe,0.0,0.7510313391685486,0.24896864593029022 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.40566375851631165,0.594336211681366 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.09 PM.png,overripe,overripe,0.0,0.4154384434223175,0.5845615267753601 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.21 PM.png,overripe,overripe,0.0,0.5269967913627625,0.47300317883491516 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.41 PM.png,overripe,overripe,0.0,0.40256932377815247,0.5974306464195251 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.33.00 PM.png,overripe,overripe,0.0,0.40011391043663025,0.5998860597610474 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.33.16 PM.png,overripe,overripe,0.0,0.48638850450515747,0.5136114954948425 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.34.00 PM.png,overripe,overripe,0.0,0.5728752017021179,0.4271247982978821 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.4793972373008728,0.5206027626991272 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.35 PM.png,overripe,overripe,0.0,0.4315950572490692,0.5684049725532532 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.53 PM.png,overripe,overripe,0.0,0.4203891456127167,0.5796108245849609 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,overripe,0.3128305673599243,0.6385558843612671,0.3614441156387329 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.37.36 PM.png,overripe,overripe,0.0,0.5379618406295776,0.46203815937042236 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,overripe,0.0,0.4231966435909271,0.5768033266067505 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,overripe,0.4960969388484955,0.5039030909538269,0.38387203216552734 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.30 PM.png,overripe,overripe,0.0,0.6251282095909119,0.37487179040908813 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.36 PM.png,overripe,overripe,0.0,0.7762097716331482,0.22379019856452942 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.28316208720207214,0.5664686560630798,0.43353137373924255 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.41.17 PM.png,overripe,overripe,0.43731170892715454,0.5626882910728455,0.3459906578063965 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.42.38 PM.png,overripe,overripe,0.0,0.6115444898605347,0.3884555399417877 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.9197412133216858,0.08025878667831421 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.45991918444633484,0.5400807857513428 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.4657182991504669,0.5342816710472107 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.39 PM.png,overripe,overripe,0.7039154171943665,0.29608461260795593,0.2877446711063385 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.4719424545764923,0.5280575752258301 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.12 PM.png,overripe,overripe,0.0,0.5901257991790771,0.40987420082092285 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.42 PM.png,overripe,overripe,0.5773043632507324,0.42269566655158997,0.30410388112068176 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.57 PM.png,overripe,overripe,0.0,0.45854678750038147,0.5414532423019409 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.6036888957023621,0.3963111340999603 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.18.41 PM.png,overripe,overripe,0.0,0.4905241131782532,0.5094758868217468 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.8454653024673462,0.154534712433815,0.3194046914577484 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.0,0.4993921220302582,0.5006078481674194 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,overripe,0.5377217531204224,0.46227821707725525,0.38856714963912964 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.47 PM.png,overripe,overripe,0.0,0.41591089963912964,0.5840891003608704 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,overripe,0.0,0.6309824585914612,0.3690175414085388 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,overripe,0.3674832582473755,0.6325167417526245,0.3007284998893738 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.504555881023407,0.4954441487789154 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.44172921776771545,0.5582707524299622 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,overripe,0.0,0.8498753309249878,0.1501246988773346 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.05 PM.png,overripe,overripe,0.0,0.46495360136032104,0.535046398639679 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.12 PM.png,overripe,overripe,0.0,0.5190206170082092,0.48097938299179077 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.41 PM.png,overripe,ripe,0.0,0.957605242729187,0.0423947349190712 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.24 PM.png,overripe,overripe,0.0,0.40541356801986694,0.5945864319801331 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,overripe,0.0,0.5199727416038513,0.4800272583961487 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.40492820739746094,0.5950717926025391 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.54 PM.png,overripe,ripe,0.0,0.9980013966560364,0.0019986084662377834 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7728085517883301,0.22719143331050873 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,overripe,0.0,0.43759095668792725,0.5624090433120728 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,overripe,0.32799628376960754,0.6682789921760559,0.3317210078239441 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.47261467576026917,0.5273853540420532 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,ripe,0.0,1.0,0.0 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,overripe,0.0,0.7232409119606018,0.2767590880393982 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.4438798725605011,0.5561200976371765 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.31.44 PM.png,overripe,overripe,0.0,0.42491522431373596,0.5750848054885864 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,overripe,0.0,0.42199257016181946,0.5780074000358582 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.32.33 PM.png,overripe,overripe,0.6079726219177246,0.3920273780822754,0.28958195447921753 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.33.23 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.35.51 PM.png,overripe,overripe,0.0,0.40669912099838257,0.5933008790016174 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.36.06 PM.png,overripe,overripe,0.0,0.4208707809448242,0.5791292190551758 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.36.24 PM.png,overripe,overripe,0.0,0.8501434326171875,0.1498565673828125 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.5479135513305664,0.452086478471756 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.5007953643798828,0.4992046058177948 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.38.34 PM.png,overripe,overripe,0.0,0.4379611611366272,0.5620388388633728 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.11 PM.png,overripe,overripe,0.0,0.5797811150550842,0.42021888494491577 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.30 PM.png,overripe,overripe,0.0,0.6295262575149536,0.3704737722873688 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.51 PM.png,overripe,overripe,0.9778188467025757,0.022181162610650063,0.09581393003463745 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.41.22 PM.png,overripe,overripe,0.2679521143436432,0.6604816317558289,0.33951836824417114 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,overripe,0.0,0.49702200293540955,0.5029780268669128 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.40191441774368286,0.5980855822563171 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.9214776754379272,0.07852230221033096 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.44.34 PM.png,overripe,overripe,0.0,0.40435734391212463,0.595642626285553 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,overripe,0.0,0.7169800996780396,0.28301990032196045 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.46.10 PM.png,overripe,overripe,0.0,0.4065714180469513,0.5934286117553711 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.4557393491268158,0.5442606210708618 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.45574820041656494,0.5442517995834351 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.54 PM.png,ripe,overripe,0.0,0.4079465866088867,0.5920534133911133 +orange/test/ripe/Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.412589430809021,0.587410569190979 +orange/test/ripe/Screen Shot 2018-06-12 at 11.52.51 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/Screen Shot 2018-06-12 at 11.53.17 PM.png,ripe,overripe,0.0,0.4947759211063385,0.5052240490913391 +orange/test/ripe/Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4905087947845459,0.5094912052154541 +orange/test/ripe/Screen Shot 2018-06-12 at 11.54.03 PM.png,ripe,overripe,0.0,0.4664813280105591,0.5335186719894409 +orange/test/ripe/Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.42524346709251404,0.5747565627098083 +orange/test/ripe/Screen Shot 2018-06-12 at 11.55.23 PM.png,ripe,overripe,0.0,0.5802729725837708,0.41972702741622925 +orange/test/ripe/Screen Shot 2018-06-12 at 11.55.28 PM.png,ripe,overripe,0.0,0.45575594902038574,0.5442440509796143 +orange/test/ripe/Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.45116421580314636,0.548835813999176 +orange/test/ripe/Screen Shot 2018-06-12 at 11.57.52 PM.png,ripe,overripe,0.0,0.4231194853782654,0.5768805146217346 +orange/test/ripe/Screen Shot 2018-06-12 at 11.59.28 PM.png,ripe,overripe,0.0,0.40593594312667847,0.5940640568733215 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.7171819806098938,0.2828180491924286 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.43 AM.png,ripe,overripe,0.0,0.43744635581970215,0.5625536441802979 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.40273162722587585,0.5972684025764465 +orange/test/ripe/Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5638936161994934,0.4361063539981842 +orange/test/ripe/Screen Shot 2018-06-13 at 12.01.58 AM.png,ripe,overripe,0.0,0.4776741564273834,0.522325873374939 +orange/test/ripe/Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.7871890664100647,0.2128109335899353 +orange/test/ripe/Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.5383082628250122,0.4616917371749878 +orange/test/ripe/Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.4006193280220032,0.5993806719779968 +orange/test/ripe/Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.5244048833847046,0.4755951464176178 +orange/test/ripe/Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.0,0.7667946219444275,0.23320534825325012 +orange/test/ripe/Screen Shot 2018-06-13 at 12.06.43 AM.png,ripe,overripe,0.0,0.5815885663032532,0.4184114336967468 +orange/test/ripe/Screen Shot 2018-06-13 at 12.07.05 AM.png,ripe,overripe,0.0,0.5797308683395386,0.42026910185813904 +orange/test/ripe/Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.4273364841938019,0.5726635456085205 +orange/test/ripe/Screen Shot 2018-06-13 at 12.08.17 AM.png,ripe,overripe,0.0,0.4915642738342285,0.5084357261657715 +orange/test/ripe/Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,overripe,0.0,0.7032686471939087,0.2967313528060913 +orange/test/ripe/Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,overripe,0.0,0.4406518340110779,0.5593481659889221 +orange/test/ripe/Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,overripe,0.0,0.6460865139961243,0.35391348600387573 +orange/test/ripe/Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.665582537651062,0.3344174325466156 +orange/test/ripe/Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4089820683002472,0.5910179018974304 +orange/test/ripe/Screen Shot 2018-06-13 at 12.11.57 AM.png,ripe,overripe,0.0,0.5341579914093018,0.46584200859069824 +orange/test/ripe/Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.4339238405227661,0.5660761594772339 +orange/test/ripe/Screen Shot 2018-06-13 at 12.14.03 AM.png,ripe,overripe,0.0,0.4600003659725189,0.5399996042251587 +orange/test/ripe/Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.4599679410457611,0.5400320291519165,0.3497920334339142 +orange/test/ripe/Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,overripe,0.9500219821929932,0.04997802898287773,0.32323169708251953 +orange/test/ripe/Screen Shot 2018-06-13 at 12.17.37 AM.png,ripe,overripe,0.0,0.4422309696674347,0.5577690005302429 +orange/test/ripe/Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.43828704953193665,0.561712920665741 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.541085958480835,0.45891401171684265 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.34 AM.png,ripe,overripe,0.11087208986282349,0.5401439070701599,0.4598560929298401 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.40 AM.png,ripe,overripe,0.0,0.4489995539188385,0.5510004758834839 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4557024836540222,0.5442975163459778 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.50.47 PM.png,ripe,overripe,0.0,0.4264242947101593,0.5735756754875183 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.4708361327648163,0.5291638374328613 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.08 PM.png,ripe,overripe,0.0,0.44186216592788696,0.558137834072113 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.13 PM.png,ripe,overripe,0.0,0.526716411113739,0.473283588886261 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.4113832712173462,0.5886167287826538 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.5015276074409485,0.4984723925590515 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.496539443731308,0.5034605264663696 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.43229323625564575,0.5677067637443542 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.55.42 PM.png,ripe,overripe,0.0,0.40535229444503784,0.5946477055549622 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.11 PM.png,ripe,overripe,0.0,0.6064953804016113,0.3935045897960663 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,overripe,0.0,0.44502368569374084,0.5549762845039368 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.28 PM.png,ripe,overripe,0.0,0.4117506444454193,0.5882493853569031 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.42026910185813904,0.5797308683395386 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.00.35 AM.png,ripe,overripe,0.0,0.48851051926612854,0.5114894509315491 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.00.43 AM.png,ripe,overripe,0.0,0.46282413601875305,0.5371758341789246 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5690146088600159,0.43098536133766174 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.4873409867286682,0.5126590132713318 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.7976263165473938,0.2023736834526062 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.4288625121116638,0.5711374878883362 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.5431249737739563,0.4568750262260437 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.16426332294940948,0.8091299533843994,0.1908700168132782 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.4497423470020294,0.5502576231956482 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.42432430386543274,0.5756756663322449 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,overripe,0.0,0.5422582626342773,0.45774170756340027 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,overripe,0.0,0.6460406184196472,0.3539593815803528 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.09.43 AM.png,ripe,overripe,0.0,0.4497101902961731,0.5502898097038269 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.6722382307052612,0.32776179909706116 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.13.44 AM.png,ripe,overripe,0.0,0.41189321875572205,0.5881068110466003 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.14.43 AM.png,ripe,overripe,0.22368358075618744,0.6577401161193848,0.3422599136829376 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.15.39 AM.png,ripe,overripe,0.3936902582645416,0.6063097715377808,0.33028724789619446 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.16.16 AM.png,ripe,overripe,0.23587265610694885,0.568587064743042,0.431412935256958 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.44827184081077576,0.5517281889915466 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.19.08 AM.png,ripe,overripe,0.0,0.4661952555179596,0.5338047742843628 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.06 AM.png,ripe,overripe,0.0,0.6573817133903503,0.34261828660964966 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,overripe,0.0,0.7374320030212402,0.2625679671764374 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,overripe,0.0,0.5121640563011169,0.48783594369888306 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.45567452907562256,0.5443254709243774 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.19 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.4000646770000458,0.5999353528022766 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.54 PM.png,ripe,overripe,0.0,0.41486871242523193,0.5851312875747681 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.51.08 PM.png,ripe,overripe,0.0,0.43145468831062317,0.5685452818870544 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.51.47 PM.png,ripe,overripe,0.0,0.45684924721717834,0.543150782585144 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.41249606013298035,0.587503969669342 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,overripe,0.0,0.5095596313476562,0.49044039845466614 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.55 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.5008984208106995,0.49910157918930054 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.54.20 PM.png,ripe,overripe,0.0,0.6842697858810425,0.3157302141189575 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.41052165627479553,0.5894783139228821 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,overripe,0.0,0.5652648210525513,0.43473514914512634 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.24 PM.png,ripe,overripe,0.0,0.41628870368003845,0.5837112665176392 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.28 PM.png,ripe,overripe,0.0,0.5865821242332458,0.41341790556907654 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.5274049043655396,0.47259506583213806 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.40303900837898254,0.5969610214233398 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.48985421657562256,0.5101457834243774 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.02.45 AM.png,ripe,overripe,0.0,0.5122349262237549,0.4877650737762451 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.03.44 AM.png,ripe,overripe,0.0,0.47355738282203674,0.5264426469802856 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.5455148220062256,0.4544851779937744 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.26 AM.png,ripe,overripe,0.0,0.45948007702827454,0.5405198931694031 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.39 AM.png,ripe,overripe,0.0,0.4450407922267914,0.554959237575531 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.4006054103374481,0.5993946194648743 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.5144550204277039,0.48554500937461853 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.06.15 AM.png,ripe,overripe,0.0,0.5783284902572632,0.4216715097427368 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.07.32 AM.png,ripe,overripe,0.0,0.5567987561225891,0.4432012438774109 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,overripe,0.0,0.6126819252967834,0.38731807470321655 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4514753222465515,0.5485246777534485 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.6738008856773376,0.32619911432266235 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.27 AM.png,ripe,overripe,0.0,0.5994924902915955,0.40050753951072693 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9316888451576233,0.06831113994121552 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.41388002038002014,0.5861200094223022 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.12.04 AM.png,ripe,overripe,0.0,0.7560688853263855,0.2439311146736145 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.43389418721199036,0.5661057829856873 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.13.51 AM.png,ripe,overripe,0.2467268705368042,0.7532731294631958,0.15707524120807648 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.5302495360374451,0.46975043416023254 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,overripe,0.5565570592880249,0.4434429407119751,0.3979744613170624 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.16.45 AM.png,ripe,overripe,0.0,0.546725332736969,0.4532746374607086 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.4024691879749298,0.5975307822227478 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.19 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4517614543437958,0.5482385158538818 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,overripe,0.0,0.5162622928619385,0.4837377071380615 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,overripe,0.031069789081811905,0.6454484462738037,0.3545515537261963 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.4614216089248657,0.5385783910751343 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.41227391362190247,0.5877261161804199 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.52.40 PM.png,ripe,overripe,0.0,0.4572351574897766,0.5427648425102234 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,overripe,0.0,0.4542939364910126,0.5457060933113098 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,overripe,0.0,0.7984726428985596,0.20152738690376282 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,overripe,0.0,0.754692792892456,0.24530720710754395 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.57.37 PM.png,ripe,overripe,0.0,0.46691131591796875,0.5330886840820312 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.58.02 PM.png,ripe,overripe,0.0,0.5462356805801392,0.45376431941986084 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.4370858669281006,0.5629141330718994 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.4471164047718048,0.5528836250305176 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,overripe,0.0,0.4872148036956787,0.5127851963043213 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.4116150736808777,0.5883849263191223 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.530501663684845,0.46949830651283264 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.00.02 AM.png,ripe,overripe,0.0,0.7581989765167236,0.24180102348327637 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.7193014621734619,0.2806985080242157 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.01.49 AM.png,ripe,overripe,0.0,0.5699337124824524,0.4300662577152252 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.02.41 AM.png,ripe,overripe,0.0,0.480196088552475,0.5198039412498474 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.40820276737213135,0.5917972326278687 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.400665283203125,0.599334716796875 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.44575873017311096,0.5542412996292114 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.07.05 AM.png,ripe,overripe,0.0,0.5801787972450256,0.41982120275497437 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,overripe,0.0,0.7544752955436707,0.24552470445632935 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.08.58 AM.png,ripe,overripe,0.0,0.6801321506500244,0.319867879152298 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.09.32 AM.png,ripe,overripe,0.0,0.5281444191932678,0.47185561060905457 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.04 AM.png,ripe,overripe,0.0,0.4115666151046753,0.5884333848953247 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.4460248649120331,0.5539751052856445 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.45 AM.png,ripe,overripe,0.0,0.6924577951431274,0.30754220485687256 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9302554130554199,0.06974458694458008 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.11.28 AM.png,ripe,overripe,0.0,0.5075412392616272,0.4924587607383728 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.13.24 AM.png,ripe,overripe,0.0,0.4406881034374237,0.5593119263648987 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.5208391547203064,0.4791608452796936 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.15.01 AM.png,ripe,overripe,0.8613576889038086,0.1386423259973526,0.2705496847629547 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,overripe,0.09591621160507202,0.5398358106613159,0.4601641893386841 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.16.54 AM.png,ripe,overripe,0.0,0.4475320875644684,0.552467942237854 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.43289870023727417,0.5671012997627258 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.45524466037750244,0.5447553396224976 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.19 PM.png,ripe,overripe,0.0,0.4000701904296875,0.5999298095703125 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.33 PM.png,ripe,overripe,0.0,0.4003554582595825,0.5996445417404175 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.47 PM.png,ripe,overripe,0.0,0.42961710691452026,0.5703828930854797 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,overripe,0.0,0.5752292275428772,0.4247707724571228 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4978882372379303,0.5021117329597473 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,overripe,0.0,0.4585546553134918,0.5414453148841858 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.03 PM.png,ripe,overripe,0.0,0.4630794823169708,0.5369205474853516 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.10 PM.png,ripe,overripe,0.0,0.5627044439315796,0.4372955560684204 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.4207276999950409,0.5792723298072815 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.35 PM.png,ripe,overripe,0.0,0.577459454536438,0.4225405156612396 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,overripe,0.0,0.779942512512207,0.22005750238895416 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.48085758090019226,0.5191423892974854 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.02 PM.png,ripe,overripe,0.0,0.5023960471153259,0.4976039230823517 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,overripe,0.0,0.7631090879440308,0.23689094185829163 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.43 PM.png,ripe,overripe,0.0,0.4083002507686615,0.5916997194290161 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.58.43 PM.png,ripe,overripe,0.0,0.4411579370498657,0.5588420629501343 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.4349086284637451,0.5650913715362549 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.11 PM.png,ripe,overripe,0.0,0.6008834838867188,0.39911654591560364 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.44716471433639526,0.5528352856636047 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.33 PM.png,ripe,overripe,0.0,0.4166282117366791,0.5833718180656433 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.409866601228714,0.5901334285736084 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.5294159054756165,0.47058409452438354 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.4029925763607025,0.5970073938369751 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5697511434555054,0.43024885654449463 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.8295172452926636,0.17048272490501404 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.02.41 AM.png,ripe,overripe,0.0,0.4459933042526245,0.5540066957473755 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.03.44 AM.png,ripe,overripe,0.0,0.46036186814308167,0.5396381616592407 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.5890277028083801,0.4109722971916199 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.2125931829214096,0.7874068021774292,0.17534835636615753 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.52 AM.png,ripe,overripe,0.0,0.5472398996353149,0.45276010036468506 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.4464714825153351,0.5535285472869873 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.4311142861843109,0.5688856840133667 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.07.39 AM.png,ripe,overripe,0.0,0.6335048675537109,0.36649513244628906 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.45373275876045227,0.5462672114372253 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,overripe,0.0,0.6586714386940002,0.34132856130599976 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4573507010936737,0.5426492691040039 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.4470402002334595,0.5529597997665405 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.10.45 AM.png,ripe,overripe,0.0,0.6918224096298218,0.3081775903701782 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.11.02 AM.png,ripe,overripe,0.0,0.8075932264328003,0.1924067735671997 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4253341257572174,0.5746658444404602 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.43372902274131775,0.5662710070610046 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.5266954898834229,0.47330453991889954 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.3732598125934601,0.6267402172088623,0.3703801929950714 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.15.39 AM.png,ripe,overripe,0.19499030709266663,0.6532294750213623,0.3467704951763153 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.4324852526187897,0.5675147771835327 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.40356531739234924,0.5964346528053284 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.37 AM.png,ripe,overripe,0.0,0.4338398575782776,0.5661601424217224 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4520769715309143,0.5479230284690857 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.18.02 AM.png,ripe,overripe,0.0,0.4270336627960205,0.5729663372039795 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.45599159598350525,0.5440083742141724 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.471162885427475,0.5288371443748474 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.53.12 PM.png,ripe,overripe,0.0,0.46249300241470337,0.5375069975852966 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.4954667091369629,0.5045332908630371 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.4112648367881775,0.5887351632118225 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,overripe,0.0,0.7700141072273254,0.22998590767383575 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4787924587726593,0.5212075114250183 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.4562114477157593,0.5437885522842407 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,overripe,0.013567044399678707,0.5872291326522827,0.4127708673477173 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.24 PM.png,ripe,overripe,0.0,0.4151943624019623,0.5848056077957153 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.50 PM.png,ripe,overripe,0.0,0.4018402099609375,0.5981597900390625 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.42798855900764465,0.572011411190033 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.7147914171218872,0.2852085530757904 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,overripe,0.060069456696510315,0.592243492603302,0.407756507396698 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.487078458070755,0.5129215717315674 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.02.53 AM.png,ripe,overripe,0.0,0.5050879120826721,0.4949120879173279 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.42692723870277405,0.5730727314949036 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.03.55 AM.png,ripe,overripe,0.0,0.6008173823356628,0.39918258786201477 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.04.34 AM.png,ripe,overripe,0.0,0.4613032042980194,0.538696825504303 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.4006212651729584,0.599378764629364 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.05.13 AM.png,ripe,overripe,0.0,0.45476749539375305,0.5452325344085693 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.05.27 AM.png,ripe,overripe,0.0,0.43647336959838867,0.5635266304016113 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.06.01 AM.png,ripe,overripe,0.0,0.4830113649368286,0.5169886350631714 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.06.28 AM.png,ripe,overripe,0.0,0.5086261630058289,0.49137380719184875 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.07.32 AM.png,ripe,overripe,0.0,0.5827715396881104,0.41722843050956726 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.07.46 AM.png,ripe,overripe,0.0,0.538978099822998,0.46102187037467957 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.17 AM.png,ripe,overripe,0.0,0.5008777379989624,0.4991222321987152 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,overripe,0.0,0.5040247440338135,0.4959752857685089 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,overripe,0.0,0.7941361665725708,0.2058638334274292 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.10.27 AM.png,ripe,overripe,0.0,0.5900411009788513,0.4099588692188263 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.11.02 AM.png,ripe,overripe,0.0,0.8097063899040222,0.19029361009597778 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.11.57 AM.png,ripe,overripe,0.0,0.5409076809883118,0.45909231901168823 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.4331774115562439,0.5668225884437561 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.4140315353870392,0.5859684348106384,0.3640041649341583 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,overripe,0.0715128481388092,0.5370423793792725,0.46295762062072754 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.16.08 AM.png,ripe,overripe,0.0,0.49462997913360596,0.505370020866394 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,overripe,0.703008770942688,0.2969912588596344,0.37376609444618225 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.40982359647750854,0.5901764035224915 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.17.51 AM.png,ripe,overripe,0.0,0.40025991201400757,0.5997400879859924 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.5376062393188477,0.46239379048347473 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.23 AM.png,ripe,overripe,0.0,0.6030821800231934,0.39691781997680664 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.40 AM.png,ripe,overripe,0.0,0.5112688541412354,0.48873114585876465 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.52 AM.png,ripe,overripe,0.0,0.42194658517837524,0.5780534148216248 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.19.17 AM.png,ripe,overripe,0.0,0.4326508641242981,0.5673491358757019 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,overripe,0.13607339560985565,0.6550060510635376,0.34499391913414 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,overripe,0.0,0.7374415397644043,0.2625584602355957 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.25 AM.png,ripe,overripe,0.0,0.5094209909439087,0.4905790388584137 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,overripe,0.0,0.5201478600502014,0.47985216975212097 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.4573619067668915,0.5426380634307861 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,overripe,0.0,0.44032204151153564,0.5596779584884644 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.49204590916633606,0.5079540610313416 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4903869032859802,0.5096130967140198 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.42403891682624817,0.5759610533714294 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.422174334526062,0.577825665473938 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.4131825268268585,0.5868174433708191 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4115574061870575,0.5884425640106201 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.35 PM.png,ripe,overripe,0.0,0.4107111692428589,0.5892888307571411 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.43 PM.png,ripe,overripe,0.0,0.4098338782787323,0.5901661515235901 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.4495764374732971,0.5504235625267029 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.57.37 PM.png,ripe,overripe,0.0,0.47949445247650146,0.5205055475234985 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.58.18 PM.png,ripe,overripe,0.0,0.6556336283683777,0.3443663716316223 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.4302656948566437,0.5697342753410339 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.42114579677581787,0.5788542032241821 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.5026125907897949,0.49738743901252747 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.54 PM.png,ripe,overripe,0.0,0.4597080647945404,0.5402919054031372 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.7085134983062744,0.291486531496048 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,overripe,0.0,0.4296765923500061,0.5703234076499939 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.48454245924949646,0.5154575705528259 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.7859084606170654,0.21409152448177338 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.4095437824726105,0.5904561877250671 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.4320729672908783,0.5679270029067993 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.05.13 AM.png,ripe,overripe,0.0,0.45639315247535706,0.5436068177223206 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.06.28 AM.png,ripe,overripe,0.0,0.4965890645980835,0.5034109354019165 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.07.39 AM.png,ripe,overripe,0.0,0.671675443649292,0.3283245861530304 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,overripe,0.0,0.5484782457351685,0.45152175426483154 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,overripe,0.0,0.5045056343078613,0.49549436569213867 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,overripe,0.0,0.7045464515686035,0.29545357823371887 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,overripe,0.0,0.8121833801269531,0.18781660497188568 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.44793081283569336,0.5520691871643066 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,overripe,0.0,0.4402911365032196,0.559708833694458 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.32 AM.png,ripe,overripe,0.0,0.5140662789344788,0.48593375086784363 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4387023448944092,0.5612976551055908 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.44015029072761536,0.559849739074707 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.53 AM.png,ripe,overripe,0.0,0.8269152045249939,0.1730847954750061 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9307007193565369,0.06929926574230194 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.5968904495239258,0.40310952067375183 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.4648022949695587,0.5351977348327637,0.3470019996166229 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.43570801615715027,0.5642919540405273 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.17.55 AM.png,ripe,overripe,0.0,0.5885410904884338,0.41145893931388855 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,overripe,0.3155214190483093,0.6844785809516907,0.3025915026664734 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.19.43 AM.png,ripe,overripe,0.0,0.41428422927856445,0.5857157707214355 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,overripe,0.0,0.7469512224197388,0.25304877758026123 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,overripe,0.0,0.5016109943389893,0.49838900566101074 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.4001868963241577,0.5998131036758423 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.43251699209213257,0.5674830079078674 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.03 PM.png,ripe,overripe,0.0,0.4108960032463074,0.5891039967536926 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.21 PM.png,ripe,overripe,0.0,0.47727805376052856,0.5227219462394714 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.4824266731739044,0.5175732970237732 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.53.22 PM.png,ripe,overripe,0.0,0.4541965425014496,0.545803427696228 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.4764725863933563,0.5235273838043213 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.54.10 PM.png,ripe,overripe,0.0,0.5456830263137817,0.4543169438838959 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.4089697003364563,0.5910302996635437 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.23 PM.png,ripe,overripe,0.0,0.5771615505218506,0.4228384494781494 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4853454530239105,0.5146545171737671 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.58 PM.png,ripe,overripe,0.0,0.4197675585746765,0.5802324414253235 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,overripe,0.0,0.7604691386222839,0.23953087627887726 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.4248789846897125,0.5751210451126099 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.48224738240242004,0.5177526473999023 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.7130367755889893,0.28696322441101074 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,overripe,0.0,0.4310588240623474,0.5689411759376526 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.01.36 AM.png,ripe,overripe,0.0,0.4036077558994293,0.5963922739028931 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.02.05 AM.png,ripe,overripe,0.0,0.592316210269928,0.407683789730072 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.02.20 AM.png,ripe,overripe,0.0,0.49051162600517273,0.5094884037971497 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.03.17 AM.png,ripe,overripe,0.0,0.4000425636768341,0.5999574065208435 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.5214874148368835,0.47851261496543884 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.2035687416791916,0.7964312434196472,0.18428166210651398 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.06.43 AM.png,ripe,overripe,0.0,0.5843825340270996,0.4156174659729004 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.07.46 AM.png,ripe,overripe,0.0,0.5501986742019653,0.44980132579803467 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,overripe,0.0,0.8006922006607056,0.19930781424045563 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,overripe,0.0,0.5601208806037903,0.4398791491985321 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.09.37 AM.png,ripe,overripe,0.0,0.4397844672203064,0.5602155327796936 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.10 AM.png,ripe,overripe,0.0,0.7416325211524963,0.25836747884750366 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.6727451682090759,0.3272548317909241 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.38 AM.png,ripe,overripe,0.0,0.4009605050086975,0.5990394949913025 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.11.28 AM.png,ripe,overripe,0.0,0.4524249732494354,0.5475749969482422 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.14.03 AM.png,ripe,overripe,0.0,0.45858684182167053,0.5414131879806519 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,overripe,0.004433304537087679,0.5275381803512573,0.4724618196487427 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.4326431155204773,0.5673568844795227 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.4079085886478424,0.59209144115448 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.02 AM.png,ripe,overripe,0.0,0.4001968801021576,0.5998031497001648 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.4550987184047699,0.5449013113975525 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,overripe,0.0,0.4843297302722931,0.5156702399253845 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.40014657378196716,0.5998533964157104 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4557422697544098,0.5442577004432678 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.21 PM.png,ripe,overripe,0.0,0.5299801230430603,0.4700198769569397 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.40 PM.png,ripe,overripe,0.0,0.4486771821975708,0.5513228178024292 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.5100763440132141,0.4899236559867859 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.12 PM.png,ripe,overripe,0.0,0.4587361812591553,0.5412638187408447 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4905300438404083,0.5094699859619141 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,overripe,0.0,0.4673316180706024,0.5326683521270752 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.54.35 PM.png,ripe,overripe,0.0,0.5382410287857056,0.46175894141197205 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.419393926858902,0.5806060433387756 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.41253769397735596,0.587462306022644 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.56.16 PM.png,ripe,overripe,0.0,0.8175086975097656,0.18249133229255676 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,overripe,0.0,0.5574029684066772,0.44259703159332275 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,overripe,0.0,0.43120014667510986,0.5687998533248901 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.59.54 PM.png,ripe,overripe,0.0,0.4692533612251282,0.5307466387748718 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.7155708074569702,0.2844291627407074 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.00.35 AM.png,ripe,overripe,0.0,0.5112210512161255,0.4887789785861969 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5636321306228638,0.43636786937713623 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.01.58 AM.png,ripe,overripe,0.0,0.4774914085865021,0.5225085616111755 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.17 AM.png,ripe,overripe,0.0,0.4049835205078125,0.5950164794921875 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.40798673033714294,0.5920132994651794 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.55 AM.png,ripe,overripe,0.0,0.5798934102058411,0.42010658979415894 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.04.12 AM.png,ripe,overripe,0.0,0.47745880484580994,0.5225412249565125 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.04.26 AM.png,ripe,overripe,0.0,0.4720630347728729,0.5279369950294495 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.5245122909545898,0.47548773884773254 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.05.46 AM.png,ripe,overripe,0.0,0.5366672277450562,0.46333274245262146 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.06.01 AM.png,ripe,overripe,0.0,0.46734756231307983,0.5326524376869202 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.4522991180419922,0.5477008819580078 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,overripe,0.0,0.5028998851776123,0.4971001148223877 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.45126476883888245,0.5487352609634399 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.58 AM.png,ripe,overripe,0.0,0.6511533856391907,0.3488466143608093 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.43 AM.png,ripe,overripe,0.0,0.4426508843898773,0.5573491454124451 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4387322962284088,0.5612676739692688 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.54 AM.png,ripe,overripe,0.0,0.4874744117259979,0.5125256180763245 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.6654360890388489,0.3345639109611511 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.53 AM.png,ripe,overripe,0.0,0.823058545589447,0.17694146931171417 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9296550750732422,0.07034493237733841 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.13.35 AM.png,ripe,overripe,0.0,0.6315388679504395,0.36846116185188293 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.5126621723175049,0.48733779788017273,0.33755922317504883 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.16.22 AM.png,ripe,overripe,0.0,0.4719749689102173,0.5280250310897827 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.4028201401233673,0.5971798300743103 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.40978702902793884,0.5902129411697388 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.55 AM.png,ripe,overripe,0.0,0.5873329639434814,0.41266706585884094 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.18.23 AM.png,ripe,overripe,0.0,0.6343932151794434,0.36560675501823425 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,overripe,0.0,0.4528296887874603,0.5471702814102173 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.19.08 AM.png,ripe,overripe,0.0,0.4690687358379364,0.530931293964386 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.20.06 AM.png,ripe,overripe,0.0,0.6696648001670837,0.33033519983291626 +orange/test/unripe/1.jpg,unripe,unripe,0.07500000298023224,0.925000011920929,0.01406249962747097 +orange/test/unripe/10.jpg,unripe,unripe,0.4690404236316681,0.5309595465660095,0.0 +orange/test/unripe/100.jpg,unripe,ripe,0.0,0.9893579483032227,0.010642040520906448 +orange/test/unripe/101.jpg,unripe,overripe,0.0,0.9343104362487793,0.06568954139947891 +orange/test/unripe/102.jpg,unripe,overripe,0.0,0.5471991896629333,0.45280081033706665 +orange/test/unripe/103.jpg,unripe,overripe,0.0,0.45408689975738525,0.5459131002426147 +orange/test/unripe/104.jpg,unripe,overripe,0.0,0.438276082277298,0.5617239475250244 +orange/test/unripe/105.jpg,unripe,unripe,0.2600336968898773,0.7399663329124451,0.0 +orange/test/unripe/106.jpg,unripe,overripe,0.0,0.50368332862854,0.49631667137145996 +orange/test/unripe/107.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/108.jpg,unripe,overripe,0.0,0.43272605538368225,0.5672739744186401 +orange/test/unripe/109.jpg,unripe,overripe,0.0,0.430898517370224,0.5691014528274536 +orange/test/unripe/11.jpg,unripe,overripe,0.08848023414611816,0.8687827587127686,0.13121722638607025 +orange/test/unripe/110.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/111.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/112.jpg,unripe,overripe,0.0,0.5514237880706787,0.4485762119293213 +orange/test/unripe/113.jpg,unripe,unripe,0.21296261250972748,0.7870373725891113,0.0 +orange/test/unripe/114.jpg,unripe,unripe,0.2457638680934906,0.754236102104187,0.0 +orange/test/unripe/115.jpg,unripe,overripe,0.0,0.40284252166748047,0.5971574783325195 +orange/test/unripe/116.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/117.jpg,unripe,unripe,0.2855992913246155,0.7144007086753845,0.0 +orange/test/unripe/118.jpg,unripe,unripe,0.313338041305542,0.686661958694458,0.0 +orange/test/unripe/119.jpg,unripe,overripe,0.6619738936424255,0.33802613615989685,0.326415091753006 +orange/test/unripe/12.jpg,unripe,overripe,0.0,0.4505479037761688,0.5494521260261536 +orange/test/unripe/120.jpg,unripe,unripe,0.3502828776836395,0.6497171521186829,0.0 +orange/test/unripe/121.jpg,unripe,unripe,0.2219768762588501,0.7780231237411499,0.025388600304722786 +orange/test/unripe/122.jpg,unripe,overripe,0.0,0.5605598092079163,0.43944019079208374 +orange/test/unripe/123.jpg,unripe,unripe,0.1694968044757843,0.8305031657218933,0.005797101650387049 +orange/test/unripe/124.jpg,unripe,overripe,0.6916502118110657,0.30834975838661194,0.16315290331840515 +orange/test/unripe/125.jpg,unripe,ripe,0.0,0.9974333047866821,0.002566694747656584 +orange/test/unripe/127.jpg,unripe,overripe,0.0,0.4927663207054138,0.5072336792945862 +orange/test/unripe/128.jpg,unripe,overripe,0.06246773526072502,0.7780760526657104,0.22192393243312836 +orange/test/unripe/13.jpg,unripe,overripe,0.039017509669065475,0.6269612908363342,0.37303870916366577 +orange/test/unripe/131.jpg,unripe,unripe,0.11194927990436554,0.8880507349967957,0.0 +orange/test/unripe/132.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/133.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/134.jpg,unripe,overripe,0.0,0.708888590335846,0.29111140966415405 +orange/test/unripe/135.jpg,unripe,overripe,0.0,0.6525755524635315,0.3474244773387909 +orange/test/unripe/137.jpg,unripe,ripe,0.11934731900691986,0.880652666091919,0.0 +orange/test/unripe/139.jpg,unripe,ripe,0.0,0.9715075492858887,0.02849242463707924 +orange/test/unripe/14.jpg,unripe,overripe,0.2822713851928711,0.7177286148071289,0.2381121963262558 +orange/test/unripe/140.jpg,unripe,unripe,0.1303236186504364,0.869676411151886,0.0 +orange/test/unripe/141.jpg,unripe,unripe,0.11194927990436554,0.8880507349967957,0.0 +orange/test/unripe/142.jpg,unripe,ripe,0.06304222345352173,0.9369577765464783,0.0 +orange/test/unripe/143.jpg,unripe,unripe,0.3017352223396301,0.6982647776603699,0.0 +orange/test/unripe/144.jpg,unripe,overripe,0.0,0.47875282168388367,0.5212472081184387 +orange/test/unripe/145.jpg,unripe,overripe,0.0,0.47074365615844727,0.5292563438415527 +orange/test/unripe/146.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/147.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/148.jpg,unripe,overripe,0.03914075717329979,0.913998544216156,0.0860014334321022 +orange/test/unripe/149.jpg,unripe,overripe,0.23698846995830536,0.5716171860694885,0.42838284373283386 +orange/test/unripe/15.jpg,unripe,unripe,0.10209077596664429,0.8979092240333557,0.0 +orange/test/unripe/151.jpg,unripe,overripe,0.0,0.5714285969734192,0.4285714328289032 +orange/test/unripe/152.jpg,unripe,overripe,0.4470628798007965,0.5529371500015259,0.09388242661952972 +orange/test/unripe/153.jpg,unripe,overripe,0.0,0.43565478920936584,0.5643452405929565 +orange/test/unripe/154.jpg,unripe,unripe,0.18233326077461243,0.8176667094230652,0.0 +orange/test/unripe/155.jpg,unripe,overripe,0.0,0.5866213440895081,0.41337865591049194 +orange/test/unripe/156.jpg,unripe,unripe,0.14875739812850952,0.8512426018714905,0.0 +orange/test/unripe/157.jpg,unripe,unripe,0.7589911818504333,0.24100880324840546,0.0 +orange/test/unripe/159.jpg,unripe,ripe,0.0,0.9851729869842529,0.014827017672359943 +orange/test/unripe/16.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/160.jpg,unripe,unripe,0.3441169559955597,0.6558830738067627,0.0 +orange/test/unripe/161.jpg,unripe,unripe,0.9625440239906311,0.037455953657627106,0.0 +orange/test/unripe/163.jpg,unripe,overripe,0.0,0.7509677410125732,0.24903225898742676 +orange/test/unripe/164.jpg,unripe,unripe,0.7589911818504333,0.24100880324840546,0.0 +orange/test/unripe/165.jpg,unripe,overripe,0.0,0.520218551158905,0.4797814190387726 +orange/test/unripe/166.jpg,unripe,ripe,0.0,0.9834768772125244,0.016523126512765884 +orange/test/unripe/167.jpg,unripe,unripe,0.10140255838632584,0.8985974192619324,0.0020385051611810923 +orange/test/unripe/168.jpg,unripe,unripe,0.12506203353405,0.8749379515647888,0.0 +orange/test/unripe/169.jpg,unripe,overripe,0.28417518734931946,0.7158247828483582,0.05873465538024902 +orange/test/unripe/17.jpg,unripe,overripe,0.5536420345306396,0.44635799527168274,0.3073350489139557 +orange/test/unripe/170.jpg,unripe,overripe,0.0,0.5837455987930298,0.4162544310092926 +orange/test/unripe/171.jpg,unripe,overripe,0.8713027834892273,0.1286972314119339,0.37890011072158813 +orange/test/unripe/172.jpg,unripe,overripe,0.23698846995830536,0.5716171860694885,0.42838284373283386 +orange/test/unripe/174.jpg,unripe,overripe,0.0,0.5261722207069397,0.4738277792930603 +orange/test/unripe/175.jpg,unripe,overripe,0.0738433301448822,0.8401591777801514,0.15984083712100983 +orange/test/unripe/176.jpg,unripe,overripe,0.0,0.5471991896629333,0.45280081033706665 +orange/test/unripe/177.jpg,unripe,overripe,0.23593810200691223,0.7065741419792175,0.2934258282184601 +orange/test/unripe/178.jpg,unripe,unripe,0.11788380146026611,0.8821161985397339,0.0 +orange/test/unripe/179.jpg,unripe,overripe,0.33756035566329956,0.6624396443367004,0.09737152606248856 +orange/test/unripe/18.jpg,unripe,unripe,0.3200799226760864,0.6799200773239136,0.0 +orange/test/unripe/180.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/181.jpg,unripe,unripe,0.23970894515514374,0.7602910399436951,0.0 +orange/test/unripe/183.jpg,unripe,unripe,0.1334923505783081,0.8665076494216919,0.0 +orange/test/unripe/184.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/185.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/186.jpg,unripe,unripe,0.2964051067829132,0.7035949230194092,0.0 +orange/test/unripe/187.jpg,unripe,unripe,0.10249682515859604,0.8975031971931458,0.0 +orange/test/unripe/188.jpg,unripe,overripe,0.0,0.4820105731487274,0.5179893970489502 +orange/test/unripe/189.jpg,unripe,overripe,0.9353082776069641,0.06469172984361649,0.1821592152118683 +orange/test/unripe/19.jpg,unripe,unripe,0.10839351266622543,0.8916065096855164,0.0 +orange/test/unripe/190.jpg,unripe,ripe,0.8187967538833618,0.181203231215477,0.0 +orange/test/unripe/191.jpg,unripe,overripe,0.2905983030796051,0.7094017267227173,0.17475993931293488 +orange/test/unripe/193.jpg,unripe,overripe,0.14205867052078247,0.6128035187721252,0.38719648122787476 +orange/test/unripe/194.jpg,unripe,overripe,0.0,0.8517329692840576,0.14826704561710358 +orange/test/unripe/195.jpg,unripe,unripe,0.21883390843868256,0.7811660766601562,0.0 +orange/test/unripe/196.jpg,unripe,overripe,0.11559895426034927,0.633488118648529,0.36651188135147095 +orange/test/unripe/197.jpg,unripe,overripe,0.0,0.44760438799858093,0.5523955821990967 +orange/test/unripe/198.jpg,unripe,overripe,0.0,0.4238095283508301,0.5761904716491699 +orange/test/unripe/199.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/2.jpg,unripe,overripe,0.0,0.6191695928573608,0.38083040714263916 +orange/test/unripe/20.jpg,unripe,unripe,0.986274778842926,0.013725209049880505,0.0 +orange/test/unripe/202.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/205.jpg,unripe,overripe,0.17745564877986908,0.6822431087493896,0.31775692105293274 +orange/test/unripe/207.jpg,unripe,overripe,0.0,0.4073340594768524,0.5926659107208252 +orange/test/unripe/209.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/21.jpg,unripe,overripe,0.5536420345306396,0.44635799527168274,0.3073350489139557 +orange/test/unripe/211.jpg,unripe,overripe,0.0,0.5416155457496643,0.4583844542503357 +orange/test/unripe/212.jpg,unripe,overripe,0.0,0.5764108300209045,0.42358916997909546 +orange/test/unripe/213.jpg,unripe,ripe,0.0,0.9714075922966003,0.02859243005514145 +orange/test/unripe/214.jpg,unripe,overripe,0.0,0.6506483554840088,0.3493516743183136 +orange/test/unripe/215.jpg,unripe,overripe,0.0,0.7108098864555359,0.2891900837421417 +orange/test/unripe/216.jpg,unripe,unripe,0.6368480324745178,0.3631519675254822,0.0 +orange/test/unripe/217.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/218.jpg,unripe,overripe,0.0,0.7834115624427795,0.21658842265605927 +orange/test/unripe/22.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/220.jpg,unripe,unripe,0.408615380525589,0.5913845896720886,0.0 +orange/test/unripe/221.jpg,unripe,overripe,0.0,0.4572073221206665,0.5427926778793335 +orange/test/unripe/222.jpg,unripe,unripe,0.4189017713069916,0.5810982584953308,0.0 +orange/test/unripe/223.jpg,unripe,unripe,0.9049546718597412,0.09504534304141998,0.0 +orange/test/unripe/225.jpg,unripe,unripe,0.1438189297914505,0.8561810851097107,0.0 +orange/test/unripe/226.jpg,unripe,unripe,0.44104549288749695,0.5589544773101807,0.0 +orange/test/unripe/23.jpg,unripe,unripe,0.3200799226760864,0.6799200773239136,0.0 +orange/test/unripe/231.jpg,unripe,overripe,0.0,0.4295160174369812,0.5704839825630188 +orange/test/unripe/233.jpg,unripe,overripe,0.6903032660484314,0.3096967041492462,0.23489034175872803 +orange/test/unripe/234.jpg,unripe,overripe,0.04521816596388817,0.7727066874504089,0.22729328274726868 +orange/test/unripe/235.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/236.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/24.jpg,unripe,unripe,0.986274778842926,0.013725209049880505,0.0 +orange/test/unripe/240.jpg,unripe,overripe,0.0,0.6953641176223755,0.3046359121799469 +orange/test/unripe/241.jpg,unripe,unripe,0.8994259238243103,0.1005740538239479,0.0 +orange/test/unripe/242.jpg,unripe,overripe,0.0,0.42650356888771057,0.5734964609146118 +orange/test/unripe/244.jpg,unripe,overripe,0.0,0.4295160174369812,0.5704839825630188 +orange/test/unripe/245.jpg,unripe,overripe,0.0,0.40728598833084106,0.5927140116691589 +orange/test/unripe/247.jpg,unripe,overripe,0.030507037416100502,0.709440290927887,0.2905597388744354 +orange/test/unripe/249.jpg,unripe,unripe,0.11359437555074692,0.8864056468009949,0.0 +orange/test/unripe/25.jpg,unripe,unripe,0.10839351266622543,0.8916065096855164,0.0 +orange/test/unripe/253.jpg,unripe,overripe,0.0,0.6240000128746033,0.37599998712539673 +orange/test/unripe/256.jpg,unripe,overripe,0.0,0.6331106424331665,0.3668893873691559 +orange/test/unripe/257.jpg,unripe,ripe,0.0,0.9831078052520752,0.016892189159989357 +orange/test/unripe/258.jpg,unripe,unripe,0.9378126859664917,0.06218733638525009,0.0 +orange/test/unripe/26.jpg,unripe,unripe,0.17465445399284363,0.825345516204834,0.0 +orange/test/unripe/261.jpg,unripe,overripe,0.08605769276618958,0.8166666626930237,0.18333333730697632 +orange/test/unripe/263.jpg,unripe,unripe,0.6115971207618713,0.38840287923812866,0.0 +orange/test/unripe/265.jpg,unripe,overripe,0.0,0.6879349946975708,0.3120650053024292 +orange/test/unripe/266.jpg,unripe,overripe,0.0,0.44771820306777954,0.5522817969322205 +orange/test/unripe/269.jpg,unripe,ripe,0.09204928576946259,0.9079506993293762,0.0 +orange/test/unripe/27.jpg,unripe,unripe,0.9760245680809021,0.02397545427083969,0.0 +orange/test/unripe/271.jpg,unripe,unripe,0.2224275767803192,0.7775724530220032,0.0 +orange/test/unripe/272.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/273.jpg,unripe,unripe,0.4310528039932251,0.5689471960067749,0.0 +orange/test/unripe/277.jpg,unripe,overripe,0.0,0.46009284257888794,0.5399071574211121 +orange/test/unripe/279.jpg,unripe,overripe,0.0,0.6552380919456482,0.3447619080543518 +orange/test/unripe/28.jpg,unripe,unripe,0.07428350299596786,0.9257165193557739,0.02791478857398033 +orange/test/unripe/280.jpg,unripe,overripe,0.9485398530960083,0.05146012455224991,0.19856438040733337 +orange/test/unripe/282.jpg,unripe,unripe,0.9378126859664917,0.06218733638525009,0.0 +orange/test/unripe/283.jpg,unripe,unripe,0.11359437555074692,0.8864056468009949,0.0 +orange/test/unripe/286.jpg,unripe,overripe,0.9485398530960083,0.05146012455224991,0.19856438040733337 +orange/test/unripe/287.jpg,unripe,unripe,0.3404466509819031,0.6595533490180969,0.0 +orange/test/unripe/29.jpg,unripe,unripe,0.13346795737743378,0.866532027721405,0.0 +orange/test/unripe/290.jpg,unripe,unripe,0.6449810266494751,0.3550189733505249,0.0 +orange/test/unripe/291.jpg,unripe,overripe,0.0805075541138649,0.8073229193687439,0.19267705082893372 +orange/test/unripe/294.jpg,unripe,unripe,0.18967671692371368,0.8103232979774475,0.0 +orange/test/unripe/295.jpg,unripe,unripe,0.16483569145202637,0.8351643085479736,0.0 +orange/test/unripe/297.jpg,unripe,overripe,0.0,0.9601159691810608,0.0398840568959713 +orange/test/unripe/298.jpg,unripe,unripe,0.6115971207618713,0.38840287923812866,0.0 +orange/test/unripe/3.jpg,unripe,overripe,0.04789882153272629,0.7291709780693054,0.2708289921283722 +orange/test/unripe/30.jpg,unripe,unripe,0.9655597805976868,0.03444019332528114,0.0013093745801597834 +orange/test/unripe/300.jpg,unripe,unripe,0.29422345757484436,0.705776572227478,0.0 +orange/test/unripe/303.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/305.jpg,unripe,overripe,0.0,0.4039373993873596,0.5960626006126404 +orange/test/unripe/308.jpg,unripe,unripe,0.29422345757484436,0.705776572227478,0.0 +orange/test/unripe/309.jpg,unripe,overripe,0.0,0.40725085139274597,0.5927491188049316 +orange/test/unripe/31.jpg,unripe,ripe,0.0,0.9781500101089478,0.021850015968084335 +orange/test/unripe/311.jpg,unripe,overripe,0.5038461685180664,0.4961538314819336,0.2944444417953491 +orange/test/unripe/312.jpg,unripe,overripe,0.0,0.5317957401275635,0.4682042896747589 +orange/test/unripe/317.jpg,unripe,overripe,0.0,0.4673400819301605,0.5326599478721619 +orange/test/unripe/32.jpg,unripe,overripe,0.5305757522583008,0.4694242477416992,0.28610265254974365 +orange/test/unripe/325.jpg,unripe,overripe,0.0,0.4673400819301605,0.5326599478721619 +orange/test/unripe/327.jpg,unripe,unripe,0.8335406184196472,0.16645938158035278,0.0 +orange/test/unripe/33.jpg,unripe,unripe,0.18848298490047455,0.8115170001983643,0.0 +orange/test/unripe/336.jpg,unripe,overripe,0.0,0.513908863067627,0.48609113693237305 +orange/test/unripe/339.jpg,unripe,overripe,0.0,0.5021897554397583,0.4978102147579193 +orange/test/unripe/34.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/343.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/35.jpg,unripe,overripe,0.0,0.5637292861938477,0.43627071380615234 +orange/test/unripe/353.jpg,unripe,overripe,0.15912668406963348,0.8408733010292053,0.07708103954792023 +orange/test/unripe/358.jpg,unripe,unripe,0.7839726209640503,0.2160273790359497,0.0 +orange/test/unripe/359.jpg,unripe,overripe,0.0,0.5021897554397583,0.4978102147579193 +orange/test/unripe/36.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/366.jpg,unripe,overripe,0.0,0.799406111240387,0.20059391856193542 +orange/test/unripe/368.jpg,unripe,overripe,0.0,0.5021897554397583,0.4978102147579193 +orange/test/unripe/37.jpg,unripe,ripe,0.0,0.989006519317627,0.010993452742695808 +orange/test/unripe/372.jpg,unripe,unripe,0.1314949244260788,0.86850506067276,0.0 +orange/test/unripe/373.jpg,unripe,ripe,0.03831188380718231,0.9616881012916565,0.0 +orange/test/unripe/377.jpg,unripe,overripe,0.0,0.467244029045105,0.532755970954895 +orange/test/unripe/379.jpg,unripe,overripe,0.0,0.5219967365264893,0.47800326347351074 +orange/test/unripe/38.jpg,unripe,overripe,0.0,0.4515710473060608,0.5484289526939392 +orange/test/unripe/383.jpg,unripe,overripe,0.0,0.467244029045105,0.532755970954895 +orange/test/unripe/384.jpg,unripe,ripe,0.03831188380718231,0.9616881012916565,0.0 +orange/test/unripe/385.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/387.jpg,unripe,overripe,0.0,0.5219967365264893,0.47800326347351074 +orange/test/unripe/39.jpg,unripe,overripe,0.0,0.9420607089996338,0.057939302176237106 +orange/test/unripe/398.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/4.jpg,unripe,unripe,0.11310269683599472,0.8868973255157471,0.0 +orange/test/unripe/40.jpg,unripe,unripe,0.24262166023254395,0.757378339767456,0.0 +orange/test/unripe/41.jpg,unripe,overripe,0.519149899482727,0.48085010051727295,0.2569632828235626 +orange/test/unripe/42.jpg,unripe,unripe,0.30330365896224976,0.6966963410377502,0.0 +orange/test/unripe/43.jpg,unripe,ripe,0.17685523629188538,0.8231447339057922,0.0 +orange/test/unripe/44.jpg,unripe,ripe,0.0,0.989006519317627,0.010993452742695808 +orange/test/unripe/45.jpg,unripe,overripe,0.0,0.4642895758152008,0.5357104539871216 +orange/test/unripe/46.jpg,unripe,overripe,0.0,0.4556247293949127,0.5443752408027649 +orange/test/unripe/47.jpg,unripe,overripe,0.38308659195899963,0.616913378238678,0.06750524044036865 +orange/test/unripe/48.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/49.jpg,unripe,overripe,0.22521016001701355,0.7747898101806641,0.16708986461162567 +orange/test/unripe/5.jpg,unripe,unripe,0.669175922870636,0.3308241069316864,0.05411317199468613 +orange/test/unripe/50.jpg,unripe,unripe,0.17716187238693237,0.8228381276130676,0.0 +orange/test/unripe/51.jpg,unripe,overripe,0.0,0.411825567483902,0.5881744027137756 +orange/test/unripe/52.jpg,unripe,ripe,0.0,0.9711261987686157,0.02887377142906189 +orange/test/unripe/53.jpg,unripe,overripe,0.6277053356170654,0.37229466438293457,0.3460317552089691 +orange/test/unripe/54.jpg,unripe,unripe,0.44991013407707214,0.5500898957252502,0.0 +orange/test/unripe/55.jpg,unripe,overripe,0.0,0.46332454681396484,0.5366754531860352 +orange/test/unripe/56.jpg,unripe,unripe,0.24262166023254395,0.757378339767456,0.0 +orange/test/unripe/57.jpg,unripe,overripe,0.0,0.5224569439888,0.47754302620887756 +orange/test/unripe/58.jpg,unripe,overripe,0.0,0.6760849356651306,0.3239150643348694 +orange/test/unripe/59.jpg,unripe,ripe,0.0,0.977790355682373,0.022209661081433296 +orange/test/unripe/6.jpg,unripe,overripe,0.0,0.4607934057712555,0.5392066240310669 +orange/test/unripe/60.jpg,unripe,overripe,0.24565455317497253,0.7543454766273499,0.06991210579872131 +orange/test/unripe/61.jpg,unripe,overripe,0.0,0.5224569439888,0.47754302620887756 +orange/test/unripe/62.jpg,unripe,overripe,0.0,0.509990394115448,0.490009605884552 +orange/test/unripe/63.jpg,unripe,unripe,0.17391519248485565,0.8260847926139832,0.0 +orange/test/unripe/64.jpg,unripe,overripe,0.9301291108131409,0.06987091898918152,0.2278006374835968 +orange/test/unripe/65.jpg,unripe,unripe,0.21078871190547943,0.7892112731933594,0.0 +orange/test/unripe/66.jpg,unripe,ripe,0.0,0.970407247543335,0.0295927245169878 +orange/test/unripe/67.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/68.jpg,unripe,overripe,0.4546456038951874,0.5453543663024902,0.06938379257917404 +orange/test/unripe/69.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/7.jpg,unripe,ripe,0.5509248971939087,0.4490751028060913,0.0 +orange/test/unripe/70.jpg,unripe,overripe,0.0,0.6755596399307251,0.3244403302669525 +orange/test/unripe/71.jpg,unripe,unripe,0.6403846144676208,0.35961538553237915,0.0 +orange/test/unripe/72.jpg,unripe,unripe,0.24262166023254395,0.757378339767456,0.0 +orange/test/unripe/73.jpg,unripe,unripe,0.12069649249315262,0.879303514957428,0.0 +orange/test/unripe/74.jpg,unripe,ripe,0.0,0.9723129272460938,0.0276870746165514 +orange/test/unripe/75.jpg,unripe,overripe,0.9715617895126343,0.028438229113817215,0.21952861547470093 +orange/test/unripe/76.jpg,unripe,overripe,0.0,0.6713675260543823,0.3286324739456177 +orange/test/unripe/77.jpg,unripe,unripe,0.3454255759716034,0.6545743942260742,0.028994083404541016 +orange/test/unripe/78.jpg,unripe,overripe,0.0,0.5773533582687378,0.4226466715335846 +orange/test/unripe/79.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/8.jpg,unripe,unripe,0.6600732803344727,0.33992674946784973,0.0 +orange/test/unripe/80.jpg,unripe,ripe,0.0,0.9862918257713318,0.013708189129829407 +orange/test/unripe/81.jpg,unripe,unripe,0.1138998344540596,0.886100172996521,0.0 +orange/test/unripe/82.jpg,unripe,unripe,0.18304026126861572,0.8169597387313843,0.0 +orange/test/unripe/83.jpg,unripe,overripe,0.029022224247455597,0.8100845217704773,0.1899154931306839 +orange/test/unripe/84.jpg,unripe,ripe,0.06280991435050964,0.937190055847168,0.0 +orange/test/unripe/85.jpg,unripe,ripe,0.0,0.9862918257713318,0.013708189129829407 +orange/test/unripe/86.jpg,unripe,unripe,0.20414866507053375,0.7958513498306274,0.0 +orange/test/unripe/87.jpg,unripe,overripe,0.0,0.5773533582687378,0.4226466715335846 +orange/test/unripe/88.jpg,unripe,unripe,0.2993007004261017,0.7006993293762207,0.0 +orange/test/unripe/89.jpg,unripe,unripe,0.37604817748069763,0.6239518523216248,0.0 +orange/test/unripe/9.jpg,unripe,unripe,0.31934860348701477,0.6806514263153076,0.0 +orange/test/unripe/90.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/91.jpg,unripe,overripe,0.9652421474456787,0.0347578339278698,0.13574504852294922 +orange/test/unripe/92.jpg,unripe,unripe,0.06299968808889389,0.9370003342628479,0.0 +orange/test/unripe/93.jpg,unripe,unripe,0.21209673583507538,0.7879032492637634,0.0 +orange/test/unripe/94.jpg,unripe,unripe,0.21296261250972748,0.7870373725891113,0.0 +orange/test/unripe/95.jpg,unripe,overripe,0.0,0.768831193447113,0.23116883635520935 +orange/test/unripe/96.jpg,unripe,overripe,0.0,0.42283105850219727,0.5771689414978027 +orange/test/unripe/97.jpg,unripe,unripe,0.15679462254047394,0.8432053923606873,0.0 +orange/test/unripe/98.jpg,unripe,unripe,0.537257194519043,0.46274280548095703,0.0 +orange/test/unripe/99.jpg,unripe,unripe,0.1431451290845871,0.8568548560142517,0.0 diff --git a/AgCloud/services/ripeness-baseline/eval/orange_test/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/orange_test/roc_curves.png new file mode 100644 index 000000000..4b4b17050 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/orange_test/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/eval/orange_tuned/metrics.json b/AgCloud/services/ripeness-baseline/eval/orange_tuned/metrics.json new file mode 100644 index 000000000..e1c698a49 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/orange_tuned/metrics.json @@ -0,0 +1,56 @@ +{ + "accuracy": 0.38454288407163056, + "report": { + "unripe": { + "precision": 1.0, + "recall": 0.018518518518518517, + "f1-score": 0.03636363636363636, + "support": 270.0 + }, + "ripe": { + "precision": 0.0, + "recall": 0.0, + "f1-score": 0.0, + "support": 388.0 + }, + "overripe": { + "precision": 0.4278131634819533, + "recall": 1.0, + "f1-score": 0.5992565055762081, + "support": 403.0 + }, + "accuracy": 0.38454288407163056, + "macro avg": { + "precision": 0.4759377211606511, + "recall": 0.3395061728395062, + "f1-score": 0.21187338064661485, + "support": 1061.0 + }, + "weighted avg": { + "precision": 0.416973331652429, + "recall": 0.38454288407163056, + "f1-score": 0.236869513256733, + "support": 1061.0 + } + }, + "confusion_matrix": [ + [ + 5, + 114, + 151 + ], + [ + 0, + 0, + 388 + ], + [ + 0, + 0, + 403 + ] + ], + "samples": 1061, + "prefix": "orange/test", + "bucket": "imagery" +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/eval/orange_tuned/per_image.csv b/AgCloud/services/ripeness-baseline/eval/orange_tuned/per_image.csv new file mode 100644 index 000000000..bce96a877 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/eval/orange_tuned/per_image.csv @@ -0,0 +1,1062 @@ +object_key,truth,pred,score_unripe,score_ripe,score_overripe +orange/test/overripe/Screen Shot 2018-06-12 at 11.18.34 PM.png,overripe,overripe,0.0,0.5465501546859741,0.4534498453140259 +orange/test/overripe/Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,overripe,0.0,0.4113272726535797,0.5886726975440979 +orange/test/overripe/Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.0,0.42804014682769775,0.5719598531723022 +orange/test/overripe/Screen Shot 2018-06-12 at 11.19.56 PM.png,overripe,overripe,0.0,0.40284356474876404,0.5971564054489136 +orange/test/overripe/Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,overripe,0.0,0.6155761480331421,0.3844238817691803 +orange/test/overripe/Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,overripe,0.0,0.5387961268424988,0.4612038731575012 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.45342984795570374,0.5465701222419739 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.4012085199356079,0.5987914800643921 +orange/test/overripe/Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,overripe,0.0,0.8375071883201599,0.16249282658100128 +orange/test/overripe/Screen Shot 2018-06-12 at 11.22.32 PM.png,overripe,overripe,0.0,0.8050832748413086,0.19491669535636902 +orange/test/overripe/Screen Shot 2018-06-12 at 11.23.03 PM.png,overripe,overripe,0.0,0.42092999815940857,0.579069972038269 +orange/test/overripe/Screen Shot 2018-06-12 at 11.23.33 PM.png,overripe,overripe,0.0,0.40036237239837646,0.5996376276016235 +orange/test/overripe/Screen Shot 2018-06-12 at 11.24.08 PM.png,overripe,overripe,0.0,0.83368319272995,0.16631677746772766 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.20 PM.png,overripe,overripe,0.0,0.5782192349433899,0.4217807352542877 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.4014260470867157,0.5985739231109619 +orange/test/overripe/Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.0,0.41610682010650635,0.5838931798934937 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.07 PM.png,overripe,overripe,0.0,0.829289436340332,0.17071057856082916 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.5542857050895691,0.4457142949104309 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.18 PM.png,overripe,overripe,0.0,0.925989031791687,0.07401097565889359 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.24 PM.png,overripe,overripe,0.0,0.5363100171089172,0.4636899530887604 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,overripe,0.0,0.8348244428634644,0.16517557203769684 +orange/test/overripe/Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,overripe,0.0,0.5337799787521362,0.4662199914455414 +orange/test/overripe/Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.44370049238204956,0.5562995076179504 +orange/test/overripe/Screen Shot 2018-06-12 at 11.28.21 PM.png,overripe,overripe,0.0,0.4043291509151459,0.5956708788871765 +orange/test/overripe/Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,overripe,0.0,0.43243104219436646,0.5675689578056335 +orange/test/overripe/Screen Shot 2018-06-12 at 11.29.44 PM.png,overripe,overripe,0.0,0.7418874502182007,0.2581125497817993 +orange/test/overripe/Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.42841944098472595,0.5715805292129517 +orange/test/overripe/Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,overripe,0.0,0.4145498275756836,0.5854501724243164 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.09 PM.png,overripe,overripe,0.0,0.4049011766910553,0.5950988531112671 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,overripe,0.0,0.4159739911556244,0.5840259790420532 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.21 PM.png,overripe,overripe,0.0,0.4000642001628876,0.59993577003479 +orange/test/overripe/Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,overripe,0.0,0.49079635739326477,0.5092036724090576 +orange/test/overripe/Screen Shot 2018-06-12 at 11.33.12 PM.png,overripe,overripe,0.0,0.40468987822532654,0.5953100919723511 +orange/test/overripe/Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,overripe,0.0,0.9967008829116821,0.0032991296611726284 +orange/test/overripe/Screen Shot 2018-06-12 at 11.37.00 PM.png,overripe,overripe,0.0,0.40640872716903687,0.5935912728309631 +orange/test/overripe/Screen Shot 2018-06-12 at 11.37.52 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,overripe,0.0,0.40124180912971497,0.5987582206726074 +orange/test/overripe/Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.401203453540802,0.598796546459198 +orange/test/overripe/Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,overripe,0.0,0.4538966715335846,0.546103298664093 +orange/test/overripe/Screen Shot 2018-06-12 at 11.41.35 PM.png,overripe,overripe,0.0,0.4201434254646301,0.5798565745353699 +orange/test/overripe/Screen Shot 2018-06-12 at 11.42.38 PM.png,overripe,overripe,0.0,0.5650395750999451,0.43496042490005493 +orange/test/overripe/Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.43400800228118896,0.565991997718811 +orange/test/overripe/Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,overripe,0.0,0.5483419299125671,0.45165807008743286 +orange/test/overripe/Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,overripe,0.0,0.49139201641082764,0.5086079835891724 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.0,0.4059287905693054,0.5940712094306946 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.0,0.42362791299819946,0.5763720870018005 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.20.52 PM.png,overripe,overripe,0.0,0.75217205286026,0.24782794713974 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.44947952032089233,0.5505204796791077 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.40184396505355835,0.5981560349464417 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7803724408149719,0.21962757408618927 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.29 PM.png,overripe,overripe,0.0,0.9115854501724243,0.08841457217931747 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.6616992950439453,0.3383007049560547 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.16 PM.png,overripe,overripe,0.0,0.46126237511634827,0.5387375950813293 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.40177181363105774,0.5982282161712646 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.25.55 PM.png,overripe,overripe,0.0,0.4031059145927429,0.5968940854072571 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.07 PM.png,overripe,overripe,0.0,0.824708878993988,0.17529110610485077 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.5319263339042664,0.46807366609573364 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.26.44 PM.png,overripe,overripe,0.0,0.7360752820968628,0.2639247179031372 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,overripe,0.0,0.42977648973464966,0.5702235102653503 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.29.58 PM.png,overripe,overripe,0.0,0.8767346143722534,0.12326537072658539 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,overripe,0.0,0.550600528717041,0.449399471282959 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.30.41 PM.png,overripe,overripe,0.0,0.5742276906967163,0.4257723391056061 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,overripe,0.0,0.5360850095748901,0.46391499042510986 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.32.04 PM.png,overripe,overripe,0.0,0.40404513478279114,0.5959548950195312 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,overripe,0.0,0.4223516881465912,0.5776482820510864 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.34.13 PM.png,overripe,overripe,0.0,0.40002161264419556,0.5999783873558044 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.36.19 PM.png,overripe,overripe,0.0,0.607452929019928,0.392547070980072 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.4119863510131836,0.5880136489868164 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,overripe,0.0,0.4561128318309784,0.543887197971344 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.40.51 PM.png,overripe,overripe,0.0,0.661964476108551,0.33803555369377136 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.43.49 PM.png,overripe,overripe,0.0,0.721710741519928,0.27828922867774963 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.46296730637550354,0.5370326638221741 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.45.17 PM.png,overripe,overripe,0.0,0.42259663343429565,0.5774033665657043 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.45.42 PM.png,overripe,overripe,0.0,0.4630676209926605,0.5369323492050171 +orange/test/overripe/rotated_by_15_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.6018050312995911,0.39819496870040894 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.0,0.4057433009147644,0.5942566990852356 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,overripe,0.0,0.40194159746170044,0.5980584025382996 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.19.37 PM.png,overripe,overripe,0.0,0.5510573387145996,0.448942631483078 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,overripe,0.0,0.8912700414657593,0.10872996598482132 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.20.18 PM.png,overripe,overripe,0.0,0.42658695578575134,0.573413074016571 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.17 PM.png,overripe,overripe,0.0,0.6019973754882812,0.39800262451171875 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.45771142840385437,0.5422885417938232 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.434280663728714,0.5657193064689636 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.22.21 PM.png,overripe,overripe,0.0,0.4363807141780853,0.5636193156242371 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.22.47 PM.png,overripe,overripe,0.0,0.58526611328125,0.41473388671875 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.23.40 PM.png,overripe,overripe,0.0,0.4269882142543793,0.5730117559432983 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7810708284378052,0.21892917156219482 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.25.16 PM.png,overripe,overripe,0.0,0.4624260663986206,0.5375739336013794 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.0,0.414968341588974,0.5850316882133484 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.5319681167602539,0.4680318534374237 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.4437108039855957,0.5562891960144043 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.28.21 PM.png,overripe,overripe,0.0,0.4044130742549896,0.5955869555473328 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.14 PM.png,overripe,overripe,0.0,0.9441819787025452,0.055818021297454834 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.21 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.29.36 PM.png,overripe,overripe,0.0,0.4119778275489807,0.5880221724510193 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.30.11 PM.png,overripe,overripe,0.0,0.6813796162605286,0.31862038373947144 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,overripe,0.0,0.7221758365631104,0.27782419323921204 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.31.17 PM.png,overripe,overripe,0.0,0.7051301598548889,0.2948698401451111 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.31.24 PM.png,overripe,overripe,0.0,0.40099799633026123,0.5990020036697388 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,overripe,0.0,0.4112389385700226,0.588761031627655 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.33.00 PM.png,overripe,overripe,0.0,0.4174706041812897,0.5825294256210327 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.33.23 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.06 PM.png,overripe,overripe,0.0,0.401370108127594,0.598629891872406 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,overripe,0.0,0.7864734530448914,0.21352657675743103 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.36.53 PM.png,overripe,overripe,0.0,0.40234309434890747,0.5976569056510925 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,overripe,0.0,0.4558376967906952,0.5441623330116272 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.45473355054855347,0.5452664494514465 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,overripe,0.0,0.40061452984809875,0.5993854403495789 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.4008482098579407,0.5991517901420593 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,overripe,0.0,0.7146769762039185,0.28532302379608154 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,overripe,0.0,0.4045131504535675,0.5954868793487549 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,overripe,0.0,0.4546692371368408,0.5453307628631592 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.40.42 PM.png,overripe,overripe,0.0,0.40791910886764526,0.5920808911323547 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.0,0.4000151753425598,0.5999848246574402 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.41.44 PM.png,overripe,overripe,0.0,0.5278763175010681,0.4721237123012543 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.42.05 PM.png,overripe,overripe,0.0,0.4358725845813751,0.5641273856163025 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.43.36 PM.png,overripe,overripe,0.0,0.48686110973358154,0.5131388902664185 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.43.54 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.4614916741847992,0.5385083556175232 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.4351569712162018,0.5648429989814758 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.29 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,overripe,0.0,0.5533947944641113,0.44660520553588867 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,overripe,0.0,0.5828563570976257,0.41714364290237427 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,overripe,0.0,0.4835543930530548,0.5164455771446228 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.45.47 PM.png,overripe,overripe,0.0,0.48328471183776855,0.5167152881622314 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.46.26 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_30_Screen Shot 2018-06-12 at 11.46.56 PM.png,overripe,overripe,0.0,0.4586730897426605,0.5413269400596619 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,overripe,0.0,0.4105454385280609,0.5894545912742615 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.0,0.4229714274406433,0.5770285725593567 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.19.37 PM.png,overripe,overripe,0.0,0.548916757106781,0.4510832726955414 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,overripe,0.0,0.6104890704154968,0.3895108997821808 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.40 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,overripe,0.0,0.5189635157585144,0.481036514043808 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.4028681516647339,0.5971318483352661 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.35 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.4324828088283539,0.5675172209739685 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.19 PM.png,overripe,overripe,0.0,0.4001689851284027,0.5998310446739197 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.4071677029132843,0.5928323268890381 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.23.54 PM.png,overripe,overripe,0.0,0.9866513609886169,0.013348651118576527 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7830771207809448,0.21692287921905518 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.6614071130752563,0.33859288692474365 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.26.18 PM.png,overripe,overripe,0.0,0.9430534243583679,0.05694659426808357 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,overripe,0.0,0.8769425749778748,0.12305745482444763 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,overripe,0.0,0.5302291512489319,0.4697708487510681 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.27.38 PM.png,overripe,overripe,0.0,0.4625113010406494,0.5374886989593506 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,overripe,0.0,0.4798281788825989,0.5201718211174011 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.29.10 PM.png,overripe,overripe,0.0,0.4013165831565857,0.5986834168434143 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.29.26 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.42668014764785767,0.5733198523521423 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.30.48 PM.png,overripe,overripe,0.0,0.6648649573326111,0.3351350426673889 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.17 PM.png,overripe,overripe,0.0,0.7000311017036438,0.2999689280986786 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,overripe,0.0,0.41099968552589417,0.5890003442764282 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.31.44 PM.png,overripe,overripe,0.0,0.40710514783859253,0.5928948521614075 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,overripe,0.0,0.4796046316623688,0.5203953981399536 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.33.12 PM.png,overripe,overripe,0.0,0.4035676121711731,0.5964323878288269 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.34.07 PM.png,overripe,overripe,0.0,0.4221082329750061,0.5778917670249939 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.36.19 PM.png,overripe,overripe,0.0,0.5995864868164062,0.40041351318359375 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,overripe,0.0,0.998141348361969,0.001858631381765008 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.07 PM.png,overripe,overripe,0.0,0.4005332589149475,0.5994667410850525 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.13 PM.png,overripe,overripe,0.0,0.45538508892059326,0.5446149110794067 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,overripe,0.0,0.4043894410133362,0.5956105589866638 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.38.19 PM.png,overripe,overripe,0.0,0.4003666341304779,0.5996333956718445 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,overripe,0.0,0.4052952826023102,0.5947046875953674 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.04 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.0,0.4001069962978363,0.5998929738998413 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.44 PM.png,overripe,overripe,0.0,0.5286813974380493,0.4713185727596283 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,overripe,0.0,0.4956986904144287,0.5043013095855713 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.00 PM.png,overripe,overripe,0.0,0.5455523729324341,0.45444759726524353 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,overripe,0.0,0.8442989587783813,0.15570101141929626 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.8986786007881165,0.10132139176130295 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.43.03 PM.png,overripe,overripe,0.0,0.4116533100605011,0.5883466601371765 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.43485620617866516,0.5651437640190125 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.44.24 PM.png,overripe,overripe,0.0,0.41531702876091003,0.5846830010414124 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.45.21 PM.png,overripe,overripe,0.0,0.4209743142127991,0.5790256857872009 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.10 PM.png,overripe,overripe,0.0,0.4061603248119354,0.593839704990387 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.5998648405075073,0.40013518929481506 +orange/test/overripe/rotated_by_45_Screen Shot 2018-06-12 at 11.46.56 PM.png,overripe,overripe,0.0,0.46016615629196167,0.5398338437080383 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.0,0.4057123363018036,0.5942876935005188 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.18.53 PM.png,overripe,overripe,0.0,0.410433828830719,0.589566171169281 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,overripe,0.0,0.4020634591579437,0.5979365706443787 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.21.29 PM.png,overripe,overripe,0.0,0.40315118432044983,0.5968487858772278 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.21.48 PM.png,overripe,overripe,0.0,0.503688395023346,0.49631160497665405 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.22.47 PM.png,overripe,overripe,0.0,0.584974467754364,0.415025532245636 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.03 PM.png,overripe,overripe,0.0,0.43109288811683655,0.5689070820808411 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.09 PM.png,overripe,overripe,0.0,0.4288609027862549,0.5711390972137451 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,overripe,0.0,0.52248215675354,0.47751787304878235 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.40681150555610657,0.5931885242462158 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.17 PM.png,overripe,overripe,0.0,0.4479112923145294,0.552088737487793 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.24 PM.png,overripe,overripe,0.0,0.7375028729438782,0.2624971270561218 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.24.37 PM.png,overripe,overripe,0.0,0.4130899906158447,0.5869100093841553 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,overripe,0.0,0.40156981348991394,0.5984301567077637 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.25.20 PM.png,overripe,overripe,0.0,0.5480342507362366,0.4519657492637634 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.5459277629852295,0.4540722370147705 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.26.49 PM.png,overripe,overripe,0.0,0.4218737483024597,0.5781262516975403 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.27.26 PM.png,overripe,overripe,0.0,0.49674877524375916,0.5032511949539185 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,overripe,0.0,0.4788305163383484,0.5211694836616516 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.28.50 PM.png,overripe,overripe,0.0,0.4443144202232361,0.5556855797767639 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.29.03 PM.png,overripe,overripe,0.0,0.7150823473930359,0.2849176824092865 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.29.31 PM.png,overripe,overripe,0.0,0.4281792640686035,0.5718207359313965 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.30.35 PM.png,overripe,overripe,0.0,0.4093981981277466,0.5906018018722534 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.30.41 PM.png,overripe,overripe,0.0,0.5775925517082214,0.4224074184894562 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,overripe,0.0,0.5378645062446594,0.4621354639530182 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,overripe,0.0,0.4249333441257477,0.5750666260719299 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.16 PM.png,overripe,overripe,0.0,0.4000570476055145,0.5999429821968079 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.43 PM.png,overripe,overripe,0.0,0.4045105576515198,0.5954894423484802 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.33.55 PM.png,overripe,overripe,0.0,0.537666916847229,0.462333083152771 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.41005566716194153,0.5899443626403809 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,overripe,0.0,0.7857075333595276,0.2142924964427948 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.4551923871040344,0.5448076128959656 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.37.41 PM.png,overripe,overripe,0.0,0.48806384205818176,0.5119361877441406 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,overripe,0.0,0.7160021066665649,0.28399792313575745 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,overripe,0.0,0.40447714924812317,0.5955228209495544 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.41.17 PM.png,overripe,overripe,0.0,0.4012751579284668,0.5987248420715332 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.41.48 PM.png,overripe,overripe,0.0,0.5577865839004517,0.44221341609954834 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.05 PM.png,overripe,overripe,0.0,0.43602612614631653,0.5639738440513611 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,overripe,0.0,0.844566285610199,0.15543368458747864 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.4013741910457611,0.5986257791519165 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.43.26 PM.png,overripe,overripe,0.0,0.4013577699661255,0.5986422300338745 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.4616963267326355,0.5383036732673645 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.45.17 PM.png,overripe,overripe,0.0,0.4199761748313904,0.5800238251686096 +orange/test/overripe/rotated_by_60_Screen Shot 2018-06-12 at 11.45.21 PM.png,overripe,overripe,0.0,0.42098990082740784,0.5790101289749146 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.18.28 PM.png,overripe,overripe,0.0,0.4016802906990051,0.5983197093009949 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,overripe,0.0,0.7758351564407349,0.22416482865810394 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,overripe,0.0,0.8810184001922607,0.11898158490657806 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.22.36 PM.png,overripe,overripe,0.0,0.5306815505027771,0.4693184494972229 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7819700241088867,0.2180299460887909 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.24 PM.png,overripe,overripe,0.0,0.730472207069397,0.269527792930603 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.24.51 PM.png,overripe,overripe,0.0,0.6627470254898071,0.33725297451019287 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.0,0.4061896502971649,0.5938103199005127 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.26.02 PM.png,overripe,overripe,0.0,0.6745072603225708,0.3254927694797516 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.07 PM.png,overripe,overripe,0.0,0.43001997470855713,0.5699800252914429 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.12 PM.png,overripe,overripe,0.0,0.49467140436172485,0.5053285956382751 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.27.30 PM.png,overripe,overripe,0.0,0.4711993336677551,0.5288006663322449 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.28.00 PM.png,overripe,overripe,0.0,0.4104587733745575,0.5895412564277649 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.29.58 PM.png,overripe,overripe,0.0,0.8974378705024719,0.10256210714578629 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.30.11 PM.png,overripe,overripe,0.0,0.6812619566917419,0.31873801350593567 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.31.01 PM.png,overripe,overripe,0.0,0.41842424869537354,0.5815757513046265 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.31.48 PM.png,overripe,overripe,0.0,0.5361812710762024,0.4638187289237976 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.04 PM.png,overripe,overripe,0.0,0.4042244553565979,0.5957755446434021 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.46 PM.png,overripe,overripe,0.0,0.4801589548587799,0.5198410749435425 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.32.50 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.34.07 PM.png,overripe,overripe,0.0,0.42181023955345154,0.5781897306442261 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.35.51 PM.png,overripe,overripe,0.0,0.4042609632015228,0.5957390069961548 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.14 PM.png,overripe,overripe,0.0,0.4047667980194092,0.5952332019805908 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.35 PM.png,overripe,overripe,0.0,0.4022544026374817,0.5977455973625183 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.36.42 PM.png,overripe,overripe,0.0,1.0,0.0 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,overripe,0.0,0.4046003818511963,0.5953996181488037 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.38.13 PM.png,overripe,overripe,0.0,0.4007953405380249,0.5992046594619751 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.38.46 PM.png,overripe,overripe,0.0,0.7166879177093506,0.283312052488327 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,overripe,0.0,0.40604153275489807,0.5939584970474243 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.40.42 PM.png,overripe,overripe,0.0,0.4063659906387329,0.5936340093612671 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.0,0.40001532435417175,0.5999847054481506 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.42.10 PM.png,overripe,overripe,0.0,0.8445281982421875,0.1554717719554901 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.8986151814460754,0.10138481110334396 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.45217597484588623,0.5478240251541138 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.12 PM.png,overripe,overripe,0.0,0.5220043659210205,0.4779956340789795 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,overripe,0.0,0.5853123068809509,0.4146876931190491 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.47 PM.png,overripe,overripe,0.0,0.4726443886756897,0.5273556113243103 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.45.57 PM.png,overripe,overripe,0.0,0.40361088514328003,0.59638911485672 +orange/test/overripe/rotated_by_75_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.602722704410553,0.39727726578712463 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.18.41 PM.png,overripe,overripe,0.0,0.4299727976322174,0.570027232170105 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.08 PM.png,overripe,overripe,0.0,0.40206584334373474,0.5979341268539429 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,overripe,0.0,0.7774520516395569,0.2225479632616043 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.19.56 PM.png,overripe,overripe,0.0,0.4049410820007324,0.5950589179992676 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.20.13 PM.png,overripe,overripe,0.0,0.8702501058578491,0.12974990904331207 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.21.17 PM.png,overripe,overripe,0.0,0.6021936535835266,0.397806316614151 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.21.48 PM.png,overripe,overripe,0.0,0.5004580616950989,0.4995419383049011 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.22.41 PM.png,overripe,overripe,0.0,0.7945145964622498,0.20548543334007263 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.40891456604003906,0.5910854339599609 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,overripe,0.0,0.44598719477653503,0.5540128350257874 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.17 PM.png,overripe,overripe,0.0,0.49101904034614563,0.5089809894561768 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.29 PM.png,overripe,overripe,0.0,0.9106550812721252,0.08934489637613297 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.6572193503379822,0.34278061985969543 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.25.38 PM.png,overripe,overripe,0.0,0.40203043818473816,0.5979695916175842 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.25.55 PM.png,overripe,overripe,0.0,0.40531525015830994,0.5946847796440125 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.02 PM.png,overripe,overripe,0.0,0.6607599258422852,0.33924010396003723 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.24 PM.png,overripe,overripe,0.0,0.5379749536514282,0.4620250165462494 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.40208062529563904,0.5979194045066833 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.27.01 PM.png,overripe,overripe,0.0,0.5345678925514221,0.46543213725090027 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.27.12 PM.png,overripe,overripe,0.0,0.4953173100948334,0.504682719707489 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.28.17 PM.png,overripe,overripe,0.0,0.41148093342781067,0.5885190367698669 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.28.33 PM.png,overripe,overripe,0.0,0.48115086555480957,0.5188491344451904 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.29.44 PM.png,overripe,overripe,0.0,0.7412126660346985,0.2587873339653015 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.29.51 PM.png,overripe,overripe,0.0,0.5534701347351074,0.4465298652648926 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,overripe,0.0,0.7280462384223938,0.2719537913799286 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.31.39 PM.png,overripe,overripe,0.0,0.4167838990688324,0.58321613073349 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.13 PM.png,overripe,overripe,0.0,0.41778847575187683,0.5822115540504456 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.41 PM.png,overripe,overripe,0.0,0.4040473401546478,0.5959526300430298 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.32.55 PM.png,overripe,overripe,0.0,0.40804705023765564,0.5919529795646667 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.33.43 PM.png,overripe,overripe,0.0,0.4068211615085602,0.5931788086891174 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.33.50 PM.png,overripe,overripe,0.0,0.41078025102615356,0.5892197489738464 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.4147254526615143,0.5852745771408081 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.36.48 PM.png,overripe,overripe,0.0,0.7877112627029419,0.2122887521982193 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.19 PM.png,overripe,overripe,0.0,0.40282413363456726,0.5971758961677551 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.45408111810684204,0.545918881893158 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.52 PM.png,overripe,overripe,0.0,0.4018377661705017,0.5981622338294983 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.37.58 PM.png,overripe,overripe,0.0,0.40305304527282715,0.5969469547271729 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.38.19 PM.png,overripe,overripe,0.0,0.40332648158073425,0.5966734886169434 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.4028938412666321,0.5971061587333679 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.39.19 PM.png,overripe,overripe,0.0,0.571527898311615,0.428472101688385 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.40.23 PM.png,overripe,overripe,0.0,0.45584890246391296,0.5441510677337646 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,overripe,0.0,0.5041157007217407,0.49588432908058167 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.8931359648704529,0.10686401277780533 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.43.03 PM.png,overripe,overripe,0.0,0.4168471693992615,0.5831528306007385 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.44.24 PM.png,overripe,overripe,0.0,0.42125174403190613,0.5787482857704163 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.4585931599140167,0.5414068102836609 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.45.28 PM.png,overripe,overripe,0.0,0.5878448486328125,0.4121551513671875 +orange/test/overripe/saltandpepper_Screen Shot 2018-06-12 at 11.45.33 PM.png,overripe,overripe,0.0,0.4926905632019043,0.5073094367980957 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.18.28 PM.png,overripe,overripe,0.0,0.40345239639282227,0.5965476036071777 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.19.16 PM.png,overripe,overripe,0.0,0.7740722894668579,0.2259277105331421 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.20.18 PM.png,overripe,overripe,0.0,0.4264484643936157,0.5735515356063843 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.20.30 PM.png,overripe,overripe,0.0,0.40176668763160706,0.5982333421707153 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.43110036849975586,0.5688996315002441 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,overripe,0.0,0.8228006362915039,0.1771993786096573 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.22.05 PM.png,overripe,overripe,0.0,0.4476332366466522,0.5523667335510254 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.22.32 PM.png,overripe,overripe,0.0,0.982677161693573,0.017322834581136703 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,overripe,0.0,0.5378592014312744,0.4621407985687256 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,overripe,0.0,0.4173382818698883,0.5826617479324341 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.24.43 PM.png,overripe,overripe,0.0,0.6628471612930298,0.3371528387069702 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,overripe,0.0,0.40195173025131226,0.5980482697486877 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.40037691593170166,0.5996230840682983 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.25.33 PM.png,overripe,overripe,0.0,0.40175455808639526,0.5982454419136047 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.12 PM.png,overripe,overripe,0.0,0.5661727786064148,0.4338272511959076 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.36 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.26.44 PM.png,overripe,overripe,0.0,0.7404582500457764,0.25954174995422363 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.27.46 PM.png,overripe,overripe,0.0,0.4011189341545105,0.5988810658454895 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,overripe,0.0,0.5518874526023865,0.4481125473976135 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.16 PM.png,overripe,overripe,0.0,0.7205187082290649,0.27948129177093506 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.40577995777130127,0.5942200422286987 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.09 PM.png,overripe,overripe,0.0,0.4059009850025177,0.5940989851951599 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.21 PM.png,overripe,overripe,0.0,0.4000750482082367,0.5999249815940857 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.32.41 PM.png,overripe,overripe,0.0,0.40212583541870117,0.5978741645812988 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.33.00 PM.png,overripe,overripe,0.0,0.40045326948165894,0.5995467305183411 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.33.16 PM.png,overripe,overripe,0.0,0.4010617136955261,0.5989382863044739 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.34.00 PM.png,overripe,overripe,0.0,0.5004704594612122,0.49952954053878784 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.31 PM.png,overripe,overripe,0.0,0.41388139128685,0.5861186385154724 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.35 PM.png,overripe,overripe,0.0,0.40219202637672424,0.5978080034255981 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.36.53 PM.png,overripe,overripe,0.0,0.4016750454902649,0.5983249545097351 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.37.25 PM.png,overripe,overripe,0.0,0.4069770872592926,0.5930229425430298 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.37.36 PM.png,overripe,overripe,0.0,0.540550708770752,0.45944932103157043 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.39.02 PM.png,overripe,overripe,0.0,0.40434902906417847,0.5956509709358215 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.18 PM.png,overripe,overripe,0.0,0.40607765316963196,0.5939223766326904 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.30 PM.png,overripe,overripe,0.0,0.6029918789863586,0.39700815081596375 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.40.36 PM.png,overripe,overripe,0.0,0.7765624523162842,0.22343753278255463 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.41.08 PM.png,overripe,overripe,0.0,0.4001893103122711,0.5998106598854065 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.41.17 PM.png,overripe,overripe,0.0,0.40056294202804565,0.5994370579719543 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.42.38 PM.png,overripe,overripe,0.0,0.5544090270996094,0.4455909729003906 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.8975929021835327,0.10240712016820908 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.06 PM.png,overripe,overripe,0.0,0.4612405598163605,0.5387594103813171 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.16 PM.png,overripe,overripe,0.0,0.44057849049568176,0.5594215393066406 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.39 PM.png,overripe,overripe,0.0,0.42739978432655334,0.5726001858711243 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.44.54 PM.png,overripe,overripe,0.0,0.45296162366867065,0.5470383763313293 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.12 PM.png,overripe,overripe,0.0,0.5121460556983948,0.48785391449928284 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.42 PM.png,overripe,overripe,0.0,0.4629429280757904,0.5370570421218872 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.45.57 PM.png,overripe,overripe,0.0,0.4042777419090271,0.5957222580909729 +orange/test/overripe/translation_Screen Shot 2018-06-12 at 11.46.17 PM.png,overripe,overripe,0.0,0.5931536555290222,0.4068463146686554 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.18.41 PM.png,overripe,overripe,0.0,0.4288705587387085,0.5711294412612915 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.18.46 PM.png,overripe,overripe,0.0,0.4067687392234802,0.5932312607765198 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.01 PM.png,overripe,overripe,0.0,0.42824122309684753,0.5717588067054749 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.22 PM.png,overripe,overripe,0.0,0.4028748571872711,0.5971251726150513 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.19.47 PM.png,overripe,overripe,0.0,0.4027535915374756,0.5972464084625244 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.20.05 PM.png,overripe,overripe,0.0,0.6160404682159424,0.38395956158638 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.20.59 PM.png,overripe,overripe,0.0,0.5381160974502563,0.46188390254974365 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.22 PM.png,overripe,overripe,0.0,0.45671942830085754,0.5432806015014648 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.40 PM.png,overripe,overripe,0.0,0.43631306290626526,0.5636869072914124 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.21.54 PM.png,overripe,overripe,0.0,0.8362544178962708,0.16374559700489044 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.05 PM.png,overripe,overripe,0.0,0.44188639521598816,0.5581136345863342 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.12 PM.png,overripe,overripe,0.0,0.5008472800254822,0.49915269017219543 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.22.41 PM.png,overripe,overripe,0.0,0.7983575463294983,0.2016424536705017 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.24 PM.png,overripe,overripe,0.0,0.4064602851867676,0.5935397148132324 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.29 PM.png,overripe,overripe,0.0,0.5205323100090027,0.4794676899909973 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.46 PM.png,overripe,overripe,0.0,0.40726807713508606,0.5927319526672363 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.23.54 PM.png,overripe,overripe,0.0,0.9856794476509094,0.01432055700570345 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.24.04 PM.png,overripe,overripe,0.0,0.7768810391426086,0.22311897575855255 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.24.12 PM.png,overripe,overripe,0.0,0.4438348114490509,0.5561652183532715 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.25.07 PM.png,overripe,overripe,0.0,0.4017972946166992,0.5982027053833008 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.25.25 PM.png,overripe,overripe,0.0,0.40139511227607727,0.5986048579216003 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.26.28 PM.png,overripe,overripe,0.0,0.8346876502037048,0.16531236469745636 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.30.06 PM.png,overripe,overripe,0.0,0.5543012619018555,0.44569873809814453 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.30.28 PM.png,overripe,overripe,0.0,0.42851221561431885,0.5714877843856812 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.31.44 PM.png,overripe,overripe,0.0,0.40965163707733154,0.5903483629226685 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.32.17 PM.png,overripe,overripe,0.0,0.4234316647052765,0.5765683054924011 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.32.33 PM.png,overripe,overripe,0.0,0.4109918475151062,0.5890081524848938 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.33.23 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.35.51 PM.png,overripe,overripe,0.0,0.40451785922050476,0.5954821705818176 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.36.06 PM.png,overripe,overripe,0.0,0.40204960107803345,0.5979503989219666 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.36.24 PM.png,overripe,overripe,0.0,0.7884870767593384,0.21151290833950043 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.37.31 PM.png,overripe,overripe,0.0,0.45287907123565674,0.5471209287643433 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.38.26 PM.png,overripe,overripe,0.0,0.40119731426239014,0.5988026857376099 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.38.34 PM.png,overripe,overripe,0.0,0.4006815254688263,0.5993185043334961 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.11 PM.png,overripe,overripe,0.0,0.5019064545631409,0.49809351563453674 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.30 PM.png,overripe,overripe,0.0,0.6027091145515442,0.3972909152507782 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.40.51 PM.png,overripe,overripe,0.0,0.6628382205963135,0.3371617794036865 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.41.22 PM.png,overripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.41.52 PM.png,overripe,overripe,0.0,0.5027392506599426,0.4972607493400574 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.42.45 PM.png,overripe,overripe,0.0,0.40047144889831543,0.5995285511016846 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.42.56 PM.png,overripe,overripe,0.0,0.894010066986084,0.10598991066217422 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.44.34 PM.png,overripe,overripe,0.0,0.4028486907482147,0.5971513390541077 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.44.48 PM.png,overripe,overripe,0.0,0.5480459928512573,0.45195403695106506 +orange/test/overripe/vertical_flip_Screen Shot 2018-06-12 at 11.46.10 PM.png,overripe,overripe,0.0,0.4064030945301056,0.593596875667572 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.4335423707962036,0.5664576292037964 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4337223470211029,0.5662776827812195 +orange/test/ripe/Screen Shot 2018-06-12 at 11.50.54 PM.png,ripe,overripe,0.0,0.40066272020339966,0.5993372797966003 +orange/test/ripe/Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.40788915753364563,0.592110812664032 +orange/test/ripe/Screen Shot 2018-06-12 at 11.52.51 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/Screen Shot 2018-06-12 at 11.53.17 PM.png,ripe,overripe,0.0,0.4365522563457489,0.5634477734565735 +orange/test/ripe/Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4605514109134674,0.539448618888855 +orange/test/ripe/Screen Shot 2018-06-12 at 11.54.03 PM.png,ripe,overripe,0.0,0.4181668162345886,0.5818331837654114 +orange/test/ripe/Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.4210408627986908,0.5789591670036316 +orange/test/ripe/Screen Shot 2018-06-12 at 11.55.23 PM.png,ripe,overripe,0.0,0.5442709922790527,0.45572903752326965 +orange/test/ripe/Screen Shot 2018-06-12 at 11.55.28 PM.png,ripe,overripe,0.0,0.43626630306243896,0.563733696937561 +orange/test/ripe/Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.41494959592819214,0.5850504040718079 +orange/test/ripe/Screen Shot 2018-06-12 at 11.57.52 PM.png,ripe,overripe,0.0,0.41801950335502625,0.5819804668426514 +orange/test/ripe/Screen Shot 2018-06-12 at 11.59.28 PM.png,ripe,overripe,0.0,0.40419062972068787,0.5958093404769897 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.686504065990448,0.313495934009552 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.43 AM.png,ripe,overripe,0.0,0.40175503492355347,0.5982449650764465 +orange/test/ripe/Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.40343913435935974,0.5965608954429626 +orange/test/ripe/Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5569904446601868,0.4430095851421356 +orange/test/ripe/Screen Shot 2018-06-13 at 12.01.58 AM.png,ripe,overripe,0.0,0.40200841426849365,0.5979915857315063 +orange/test/ripe/Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.7442454695701599,0.2557545602321625 +orange/test/ripe/Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.47007250785827637,0.5299274921417236 +orange/test/ripe/Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.40062591433525085,0.5993740558624268 +orange/test/ripe/Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.4621105194091797,0.5378894805908203 +orange/test/ripe/Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.0,0.6788046956062317,0.3211952745914459 +orange/test/ripe/Screen Shot 2018-06-13 at 12.06.43 AM.png,ripe,overripe,0.0,0.4600461423397064,0.5399538278579712 +orange/test/ripe/Screen Shot 2018-06-13 at 12.07.05 AM.png,ripe,overripe,0.0,0.5527213215827942,0.4472786486148834 +orange/test/ripe/Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.4207989573478699,0.5792010426521301 +orange/test/ripe/Screen Shot 2018-06-13 at 12.08.17 AM.png,ripe,overripe,0.0,0.42308759689331055,0.5769124031066895 +orange/test/ripe/Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,overripe,0.0,0.679557204246521,0.3204427659511566 +orange/test/ripe/Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,overripe,0.0,0.43748557567596436,0.5625144243240356 +orange/test/ripe/Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,overripe,0.0,0.4710678160190582,0.5289322137832642 +orange/test/ripe/Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.5609626770019531,0.43903735280036926 +orange/test/ripe/Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.40007486939430237,0.5999251008033752 +orange/test/ripe/Screen Shot 2018-06-13 at 12.11.57 AM.png,ripe,overripe,0.0,0.45824065804481506,0.5417593717575073 +orange/test/ripe/Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.4319014847278595,0.5680984854698181 +orange/test/ripe/Screen Shot 2018-06-13 at 12.14.03 AM.png,ripe,overripe,0.0,0.45137450098991394,0.5486254692077637 +orange/test/ripe/Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.0,0.42196032404899597,0.5780397057533264 +orange/test/ripe/Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,overripe,0.0,0.40616747736930847,0.5938324928283691 +orange/test/ripe/Screen Shot 2018-06-13 at 12.17.37 AM.png,ripe,overripe,0.0,0.40769320726394653,0.5923067927360535 +orange/test/ripe/Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4105232357978821,0.5894767642021179 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.34 AM.png,ripe,overripe,0.0,0.40037626028060913,0.5996237397193909 +orange/test/ripe/Screen Shot 2018-06-13 at 12.18.40 AM.png,ripe,overripe,0.0,0.40294092893600464,0.5970590710639954 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.43433865904808044,0.5656613111495972 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.50.47 PM.png,ripe,overripe,0.0,0.40763887763023376,0.5923611521720886 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.41719385981559753,0.5828061699867249 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.08 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.51.13 PM.png,ripe,overripe,0.0,0.40165260434150696,0.5983474254608154 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.4050164818763733,0.5949835181236267 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.40137943625450134,0.5986205339431763 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.4540253281593323,0.5459746718406677 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.55.42 PM.png,ripe,overripe,0.0,0.40088576078414917,0.5991142392158508 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.11 PM.png,ripe,overripe,0.0,0.5438951849937439,0.4561047852039337 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,overripe,0.0,0.40275508165359497,0.597244918346405 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.28 PM.png,ripe,overripe,0.0,0.4040672481060028,0.5959327816963196 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.40407034754753113,0.5959296226501465 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.00.35 AM.png,ripe,overripe,0.0,0.4084365665912628,0.5915634632110596 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.00.43 AM.png,ripe,overripe,0.0,0.4015670418739319,0.5984329581260681 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5625366568565369,0.4374633729457855 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.4425560235977173,0.5574439764022827 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.756267249584198,0.2437327653169632 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.4240713119506836,0.5759286880493164 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.4689674973487854,0.5310325026512146 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.0,0.6783803105354309,0.3216196894645691 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.40324777364730835,0.5967522263526917 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.42107152938842773,0.5789284706115723 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,overripe,0.0,0.5006057024002075,0.49939432740211487 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,overripe,0.0,0.4938332438468933,0.5061667561531067 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.09.43 AM.png,ripe,overripe,0.0,0.4196399748325348,0.5803600549697876 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.5635204315185547,0.4364795684814453 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.13.44 AM.png,ripe,overripe,0.0,0.4095478951931,0.5904521346092224 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.14.43 AM.png,ripe,overripe,0.0,0.5434105396270752,0.4565894603729248 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.15.39 AM.png,ripe,overripe,0.0,0.40247079730033875,0.5975292325019836 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.16.16 AM.png,ripe,overripe,0.0,0.4047936499118805,0.5952063798904419 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4095912277698517,0.5904087424278259 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.19.08 AM.png,ripe,overripe,0.0,0.40084439516067505,0.599155604839325 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.06 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_15_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,overripe,0.0,0.40028536319732666,0.5997146368026733 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.4349445402622223,0.5650554299354553 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.19 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.50.54 PM.png,ripe,overripe,0.0,0.40055811405181885,0.5994418859481812 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.51.08 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.51.47 PM.png,ripe,overripe,0.0,0.43490883708000183,0.5650911331176758 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.40401768684387207,0.5959823131561279 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,overripe,0.0,0.4003765285015106,0.599623441696167 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.52.55 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.45980700850486755,0.5401929616928101 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.54.20 PM.png,ripe,overripe,0.0,0.562410831451416,0.437589168548584 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.40019491314888,0.5998050570487976 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,overripe,0.0,0.4189373254776001,0.5810626745223999 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.24 PM.png,ripe,overripe,0.0,0.40051910281181335,0.599480926990509 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.58.28 PM.png,ripe,overripe,0.0,0.5211280584335327,0.4788719713687897 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.46273374557495117,0.5372662544250488 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.40394678711891174,0.5960532426834106 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.44319039583206177,0.5568096041679382 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.02.45 AM.png,ripe,overripe,0.0,0.41207659244537354,0.5879234075546265 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.03.44 AM.png,ripe,overripe,0.0,0.4002414643764496,0.5997585654258728 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.46825453639030457,0.531745433807373 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.26 AM.png,ripe,overripe,0.0,0.4207531809806824,0.5792468190193176 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.39 AM.png,ripe,overripe,0.0,0.4184781312942505,0.5815218687057495 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.40066149830818176,0.5993385314941406 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.4608907699584961,0.5391092300415039 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.06.15 AM.png,ripe,overripe,0.0,0.4859659969806671,0.5140339732170105 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.07.32 AM.png,ripe,overripe,0.0,0.4655631184577942,0.5344368815422058 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,overripe,0.0,0.5022974610328674,0.49770256876945496 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.409732460975647,0.590267539024353 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.5745957493782043,0.42540428042411804 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.27 AM.png,ripe,overripe,0.0,0.4770071506500244,0.5229928493499756 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9265409708023071,0.07345905154943466 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4000314772129059,0.5999684929847717 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.12.04 AM.png,ripe,overripe,0.0,0.6497442722320557,0.35025572776794434 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.4287729263305664,0.5712270736694336 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.13.51 AM.png,ripe,overripe,0.0,0.7116619348526001,0.2883380651473999 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,overripe,0.0,0.40587472915649414,0.5941252708435059 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.16.45 AM.png,ripe,overripe,0.0,0.4404315948486328,0.5595684051513672 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.4026504158973694,0.5973495841026306 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.19 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.40915367007255554,0.5908463001251221 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,overripe,0.0,0.40443864464759827,0.5955613851547241 +orange/test/ripe/rotated_by_30_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,overripe,0.0,0.5491697192192078,0.45083025097846985 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.4179164171218872,0.5820835828781128 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.52.26 PM.png,ripe,overripe,0.0,0.4038989841938019,0.5961010456085205 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.52.40 PM.png,ripe,overripe,0.0,0.40101829171180725,0.5989817380905151 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,overripe,0.0,0.4174499809741974,0.5825499892234802 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,overripe,0.0,0.7180221080780029,0.28197792172431946 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,overripe,0.0,0.6949619054794312,0.30503806471824646 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.57.37 PM.png,ripe,overripe,0.0,0.40370967984199524,0.5962902903556824 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.58.02 PM.png,ripe,overripe,0.0,0.46294140815734863,0.5370585918426514 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.4003576338291168,0.5996423959732056 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.4054124653339386,0.594587504863739 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,overripe,0.0,0.4026213586330414,0.5973786115646362 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.40459927916526794,0.5954007506370544 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.46379077434539795,0.536209225654602 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.00.02 AM.png,ripe,overripe,0.0,0.6447979807853699,0.3552020490169525 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.6965498328208923,0.30345016717910767 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.01.49 AM.png,ripe,overripe,0.0,0.4059062898159027,0.5940936803817749 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.02.41 AM.png,ripe,overripe,0.0,0.42058342695236206,0.5794165730476379 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.40845194458961487,0.5915480852127075 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.40069887042045593,0.5993011593818665 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.40242472290992737,0.5975752472877502 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.07.05 AM.png,ripe,overripe,0.0,0.555185854434967,0.44481414556503296 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,overripe,0.0,0.7357015013694763,0.2642984688282013 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.08.58 AM.png,ripe,overripe,0.0,0.5800322890281677,0.4199676811695099 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.09.32 AM.png,ripe,overripe,0.0,0.4679105281829834,0.5320894718170166 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.04 AM.png,ripe,overripe,0.0,0.40001392364501953,0.5999860763549805 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.41473016142845154,0.5852698087692261 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.45 AM.png,ripe,overripe,0.0,0.6923728585243225,0.3076271116733551 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9264397621154785,0.0735602080821991 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.11.28 AM.png,ripe,overripe,0.0,0.4032646715641022,0.5967353582382202 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.13.24 AM.png,ripe,overripe,0.0,0.4000300467014313,0.5999699831008911 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.4004180431365967,0.5995819568634033 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.15.01 AM.png,ripe,overripe,0.0,0.4480901062488556,0.5519099235534668 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,overripe,0.0,0.41677984595298767,0.5832201838493347 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.16.54 AM.png,ripe,overripe,0.0,0.4023481011390686,0.5976518988609314 +orange/test/ripe/rotated_by_45_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.41639554500579834,0.5836044549942017 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.14 PM.png,ripe,overripe,0.0,0.4345209300518036,0.5654790997505188 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.19 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.33 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.50.47 PM.png,ripe,overripe,0.0,0.4079257845878601,0.5920742154121399 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,overripe,0.0,0.40037673711776733,0.5996232628822327 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.46769964694976807,0.5323003530502319 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,overripe,0.0,0.4173039197921753,0.5826960802078247 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.03 PM.png,ripe,overripe,0.0,0.41916343569755554,0.5808365941047668 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.10 PM.png,ripe,overripe,0.0,0.4380883574485779,0.5619116425514221 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.4169052541255951,0.5830947756767273 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.54.35 PM.png,ripe,overripe,0.0,0.4478834569454193,0.5521165728569031 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,overripe,0.0,0.7183789610862732,0.2816210091114044 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.40016257762908936,0.5998374223709106 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.02 PM.png,ripe,overripe,0.0,0.4087181091308594,0.5912818908691406 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,overripe,0.0,0.6946899890899658,0.3053100109100342 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.56.43 PM.png,ripe,overripe,0.0,0.407955527305603,0.592044472694397 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.58.43 PM.png,ripe,overripe,0.0,0.4009178578853607,0.5990821123123169 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.40031251311302185,0.5996874570846558 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.11 PM.png,ripe,overripe,0.0,0.5513634085655212,0.44863656163215637 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.4056667387485504,0.594333291053772 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.33 PM.png,ripe,overripe,0.0,0.40157514810562134,0.5984248518943787 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.38 PM.png,ripe,overripe,0.0,0.40345829725265503,0.596541702747345 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.46307066082954407,0.5369293093681335 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.00.54 AM.png,ripe,overripe,0.0,0.4039514362812042,0.5960485339164734 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5616693496704102,0.43833065032958984 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.7509338855743408,0.2490660846233368 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.02.41 AM.png,ripe,overripe,0.0,0.4201929271221161,0.5798071026802063 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.03.44 AM.png,ripe,overripe,0.0,0.4001978039741516,0.5998021960258484 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.460367351770401,0.5396326184272766 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.0,0.685490071773529,0.31450995802879333 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.52 AM.png,ripe,overripe,0.0,0.509645402431488,0.49035462737083435 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.4027911424636841,0.5972088575363159 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.07.17 AM.png,ripe,overripe,0.0,0.42294079065322876,0.5770592093467712 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.07.39 AM.png,ripe,overripe,0.0,0.5649420022964478,0.43505799770355225 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.41188278794288635,0.588117241859436 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.09.14 AM.png,ripe,overripe,0.0,0.5032802224159241,0.49671974778175354 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4091758728027344,0.5908241271972656 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.4147604703903198,0.5852395296096802 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.10.45 AM.png,ripe,overripe,0.0,0.6924223899841309,0.30757758021354675 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.11.02 AM.png,ripe,overripe,0.0,0.7995529770851135,0.20044705271720886 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.11.17 AM.png,ripe,overripe,0.0,0.4000157117843628,0.5999842882156372 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.42847201228141785,0.5715280175209045 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.40035340189933777,0.5996466279029846 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.0,0.4226474463939667,0.5773525238037109 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.15.39 AM.png,ripe,overripe,0.0,0.40248534083366394,0.5975146293640137 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.41621726751327515,0.5837827324867249 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.4028426706790924,0.59715735912323 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.37 AM.png,ripe,overripe,0.0,0.40267011523246765,0.5973299145698547 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.418155699968338,0.5818442702293396 +orange/test/ripe/rotated_by_60_Screen Shot 2018-06-13 at 12.18.02 AM.png,ripe,overripe,0.0,0.40100613236427307,0.5989938378334045 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.434240460395813,0.565759539604187 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.4019716680049896,0.5980283617973328 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.53.12 PM.png,ripe,overripe,0.0,0.4003807008266449,0.5996192693710327 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.4579617977142334,0.5420382022857666 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.4020552933216095,0.5979446768760681 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.37 PM.png,ripe,overripe,0.0,0.7146114706993103,0.2853884994983673 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.40020889043807983,0.5997911095619202 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.41546857357025146,0.5845314264297485 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,overripe,0.0,0.4174860119819641,0.5825139880180359 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.24 PM.png,ripe,overripe,0.0,0.40053796768188477,0.5994620323181152 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.58.50 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.4056634306907654,0.5943365693092346 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.6929829716682434,0.3070169985294342 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,overripe,0.0,0.42248156666755676,0.5775184631347656 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.4420408010482788,0.5579591989517212 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.02.53 AM.png,ripe,overripe,0.0,0.4559575915336609,0.5440424084663391 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.42404451966285706,0.5759554505348206 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.03.55 AM.png,ripe,overripe,0.0,0.5217795372009277,0.47822049260139465 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.04.34 AM.png,ripe,overripe,0.0,0.41988635063171387,0.5801136493682861 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.04.46 AM.png,ripe,overripe,0.0,0.4006688892841339,0.5993311405181885 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.05.13 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.05.27 AM.png,ripe,overripe,0.0,0.4062556326389313,0.5937443375587463 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.06.01 AM.png,ripe,overripe,0.0,0.41062650084495544,0.5893735289573669 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.06.28 AM.png,ripe,overripe,0.0,0.4893014132976532,0.5106985569000244 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.07.32 AM.png,ripe,overripe,0.0,0.48468488454818726,0.5153151154518127 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.07.46 AM.png,ripe,overripe,0.0,0.46652752161026,0.53347247838974 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.17 AM.png,ripe,overripe,0.0,0.42356693744659424,0.5764330625534058 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,overripe,0.0,0.44957053661346436,0.5504294633865356 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,overripe,0.0,0.6642388105392456,0.3357611894607544 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.10.27 AM.png,ripe,overripe,0.0,0.47363030910491943,0.5263696908950806 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.11.02 AM.png,ripe,overripe,0.0,0.7991301417350769,0.2008698582649231 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.11.57 AM.png,ripe,overripe,0.0,0.4506983458995819,0.5493016242980957 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.13.29 AM.png,ripe,overripe,0.0,0.429716557264328,0.5702834725379944 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.0,0.4222407639026642,0.5777592658996582 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,overripe,0.0,0.4180994927883148,0.5819005370140076 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.16.08 AM.png,ripe,overripe,0.0,0.4612061381340027,0.5387938618659973 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.16.33 AM.png,ripe,overripe,0.0,0.406547874212265,0.5934520959854126 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.4089736342430115,0.5910263657569885 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.17.51 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.23 AM.png,ripe,overripe,0.0,0.40670228004455566,0.5932977199554443 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.40 AM.png,ripe,overripe,0.0,0.40282395482063293,0.5971760153770447 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.18.52 AM.png,ripe,overripe,0.0,0.40514564514160156,0.5948543548583984 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.19.17 AM.png,ripe,overripe,0.0,0.41080930829048157,0.5891907215118408 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,overripe,0.0,0.5440984964370728,0.45590150356292725 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.25 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/rotated_by_75_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,overripe,0.0,0.4001976251602173,0.5998023748397827 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.51.02 PM.png,ripe,overripe,0.0,0.4186004102230072,0.5813995599746704 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.52.32 PM.png,ripe,overripe,0.0,0.40301820635795593,0.5969818234443665 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.4558236598968506,0.5441763401031494 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4620853066444397,0.5379146933555603 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.4226866662502289,0.5773133635520935 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.40217670798301697,0.5978232622146606 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.40433913469314575,0.5956608653068542 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4021660387516022,0.5978339910507202 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.35 PM.png,ripe,overripe,0.0,0.4023319184780121,0.5976680517196655 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.43 PM.png,ripe,overripe,0.0,0.4091532230377197,0.5908467769622803 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.4167226254940033,0.5832774043083191 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.57.37 PM.png,ripe,overripe,0.0,0.4063703119754791,0.5936296582221985 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.58.18 PM.png,ripe,overripe,0.0,0.6580840349197388,0.34191596508026123 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.58.56 PM.png,ripe,overripe,0.0,0.4030565023422241,0.5969434976577759 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.19 PM.png,ripe,overripe,0.0,0.4078630208969116,0.5921369791030884 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.46317386627197266,0.5368261337280273 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-12 at 11.59.54 PM.png,ripe,overripe,0.0,0.42652106285095215,0.5734789371490479 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.6868755221366882,0.31312447786331177 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,overripe,0.0,0.42521563172340393,0.5747843384742737 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.01.32 AM.png,ripe,overripe,0.0,0.44196343421936035,0.5580365657806396 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.02.11 AM.png,ripe,overripe,0.0,0.7447037100791931,0.2552962899208069 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.41036179661750793,0.5896381735801697 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.03.27 AM.png,ripe,overripe,0.0,0.42618677020072937,0.5738131999969482 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.05.13 AM.png,ripe,overripe,0.0,0.40193918347358704,0.5980607867240906 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.06.28 AM.png,ripe,overripe,0.0,0.4697388708591461,0.5302611589431763 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.07.39 AM.png,ripe,overripe,0.0,0.5865234136581421,0.4134765863418579 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.09 AM.png,ripe,overripe,0.0,0.5023175477981567,0.49768248200416565 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,overripe,0.0,0.45101386308670044,0.5489861369132996 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.41 AM.png,ripe,overripe,0.0,0.6796407103538513,0.32035931944847107 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,overripe,0.0,0.6281474232673645,0.3718525767326355 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.41494354605674744,0.5850564241409302 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,overripe,0.0,0.43903177976608276,0.5609682202339172 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.32 AM.png,ripe,overripe,0.0,0.46417930722236633,0.5358206629753113 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.4159221351146698,0.5840778946876526 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.21 AM.png,ripe,overripe,0.0,0.4151309132575989,0.5848690867424011 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.53 AM.png,ripe,overripe,0.0,0.8170055747032166,0.18299445509910583 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9227342009544373,0.07726580649614334 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.14.09 AM.png,ripe,overripe,0.0,0.4021969437599182,0.5978030562400818 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.0,0.42327407002449036,0.576725959777832 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.17.43 AM.png,ripe,overripe,0.0,0.4117398262023926,0.5882601737976074 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.17.55 AM.png,ripe,overripe,0.0,0.4678674638271332,0.5321325659751892 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.19.36 AM.png,ripe,overripe,0.0,0.5295490622520447,0.4704509377479553 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.19.43 AM.png,ripe,overripe,0.0,0.4020839035511017,0.5979160666465759 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.20.15 AM.png,ripe,overripe,0.0,0.40215420722961426,0.5978457927703857 +orange/test/ripe/saltandpepper_Screen Shot 2018-06-13 at 12.20.39 AM.png,ripe,overripe,0.0,0.401804119348526,0.5981958508491516 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.40011391043663025,0.5998860597610474 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.43066564202308655,0.5693343281745911 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.03 PM.png,ripe,overripe,0.0,0.4075765013694763,0.5924234986305237 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.21 PM.png,ripe,overripe,0.0,0.40188515186309814,0.5981148481369019 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.400274395942688,0.599725604057312 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.53.22 PM.png,ripe,overripe,0.0,0.4000319242477417,0.5999680757522583 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.53.33 PM.png,ripe,overripe,0.0,0.45900654792785645,0.5409934520721436 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.54.10 PM.png,ripe,overripe,0.0,0.4241611361503601,0.5758388638496399 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.54.27 PM.png,ripe,overripe,0.0,0.40746062994003296,0.592539370059967 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.23 PM.png,ripe,overripe,0.0,0.5546778440475464,0.445322185754776 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.48 PM.png,ripe,overripe,0.0,0.4002510607242584,0.599748969078064 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.55.58 PM.png,ripe,overripe,0.0,0.406985342502594,0.593014657497406 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.56.20 PM.png,ripe,overripe,0.0,0.6843841075897217,0.3156158924102783 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.56.55 PM.png,ripe,overripe,0.0,0.40777507424354553,0.5922248959541321 +orange/test/ripe/translation_Screen Shot 2018-06-12 at 11.59.48 PM.png,ripe,overripe,0.0,0.4646848440170288,0.5353151559829712 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.6956424713134766,0.30435752868652344 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.01.23 AM.png,ripe,overripe,0.0,0.42424890398979187,0.5757510662078857 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.01.36 AM.png,ripe,overripe,0.0,0.4009783864021301,0.5990216135978699 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.02.05 AM.png,ripe,overripe,0.0,0.5893847942352295,0.4106151759624481 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.02.20 AM.png,ripe,overripe,0.0,0.423935204744339,0.5760648250579834 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.03.17 AM.png,ripe,overripe,0.0,0.40004271268844604,0.599957287311554 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.04.01 AM.png,ripe,overripe,0.0,0.46439817547798157,0.535601794719696 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.06.36 AM.png,ripe,overripe,0.0,0.6786687970161438,0.3213312029838562 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.06.43 AM.png,ripe,overripe,0.0,0.4513436555862427,0.5486563444137573 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.07.46 AM.png,ripe,overripe,0.0,0.4627790153026581,0.5372209548950195 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.08.48 AM.png,ripe,overripe,0.0,0.6411512494087219,0.35884878039360046 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.09.05 AM.png,ripe,overripe,0.0,0.42887213826179504,0.5711278319358826 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.09.37 AM.png,ripe,overripe,0.0,0.42007073760032654,0.5799292325973511 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.10 AM.png,ripe,overripe,0.0,0.620449423789978,0.379550576210022 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.5700668692588806,0.4299331307411194 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.10.38 AM.png,ripe,overripe,0.0,0.4003574550151825,0.5996425747871399 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.11.28 AM.png,ripe,overripe,0.0,0.402208536863327,0.5977914333343506 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.14.03 AM.png,ripe,overripe,0.0,0.43593844771385193,0.5640615820884705 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.15.08 AM.png,ripe,overripe,0.0,0.41766539216041565,0.5823346376419067 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.17.01 AM.png,ripe,overripe,0.0,0.41579824686050415,0.5842017531394958 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.4063868820667267,0.5936130881309509 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.02 AM.png,ripe,overripe,0.0,0.40014898777008057,0.5998510122299194 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.07 AM.png,ripe,overripe,0.0,0.40026700496673584,0.5997329950332642 +orange/test/ripe/translation_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,overripe,0.0,0.4008373022079468,0.5991626977920532 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.50.28 PM.png,ripe,overripe,0.0,0.40006211400032043,0.5999378561973572 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.50.41 PM.png,ripe,overripe,0.0,0.4337199330329895,0.5662800669670105 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.21 PM.png,ripe,overripe,0.0,0.4023519456386566,0.5976480841636658 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.40 PM.png,ripe,overripe,0.0,0.4011448323726654,0.598855197429657 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.52.46 PM.png,ripe,overripe,0.0,0.4016675353050232,0.5983324646949768 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.12 PM.png,ripe,overripe,0.0,0.4004839062690735,0.5995160937309265 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.43 PM.png,ripe,overripe,0.0,0.4606296420097351,0.5393703579902649 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.53.53 PM.png,ripe,overripe,0.0,0.4164189100265503,0.5835810899734497 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.54.35 PM.png,ripe,overripe,0.0,0.45761433243751526,0.5423856377601624 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.54.55 PM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.55.05 PM.png,ripe,overripe,0.0,0.40233469009399414,0.5976653099060059 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.56.16 PM.png,ripe,overripe,0.0,0.7620337009429932,0.23796628415584564 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.58.11 PM.png,ripe,overripe,0.0,0.42025741934776306,0.5797425508499146 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.59.23 PM.png,ripe,overripe,0.0,0.4033469557762146,0.5966530442237854 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-12 at 11.59.54 PM.png,ripe,overripe,0.0,0.4245757758617401,0.5754241943359375 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.00.06 AM.png,ripe,overripe,0.0,0.6865912079811096,0.31340882182121277 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.00.35 AM.png,ripe,overripe,0.0,0.41023948788642883,0.5897604823112488 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.01.03 AM.png,ripe,overripe,0.0,0.5569937229156494,0.4430062770843506 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.01.58 AM.png,ripe,overripe,0.0,0.40199014544487,0.5980098247528076 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.17 AM.png,ripe,overripe,0.0,0.4008665084838867,0.5991334915161133 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.21 AM.png,ripe,overripe,0.0,0.40824246406555176,0.5917575359344482 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.03.55 AM.png,ripe,overripe,0.0,0.5284034609794617,0.47159650921821594 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.04.12 AM.png,ripe,overripe,0.0,0.4004710018634796,0.5995290279388428 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.04.26 AM.png,ripe,overripe,0.0,0.4270927309989929,0.5729072690010071 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.05.33 AM.png,ripe,overripe,0.0,0.4619922339916229,0.5380077362060547 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.05.46 AM.png,ripe,overripe,0.0,0.5205737352371216,0.47942623496055603 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.06.01 AM.png,ripe,overripe,0.0,0.4113631546497345,0.5886368751525879 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.06.59 AM.png,ripe,overripe,0.0,0.40408244729042053,0.5959175825119019 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.29 AM.png,ripe,overripe,0.0,0.4495634436607361,0.5504365563392639 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.54 AM.png,ripe,overripe,0.0,0.4128876328468323,0.5871123671531677 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.08.58 AM.png,ripe,overripe,0.0,0.577181875705719,0.4228181540966034 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.43 AM.png,ripe,overripe,0.0,0.4186861217021942,0.5813138484954834 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.49 AM.png,ripe,overripe,0.0,0.41255292296409607,0.5874471068382263 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.09.54 AM.png,ripe,overripe,0.0,0.4030071496963501,0.5969928503036499 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.16 AM.png,ripe,overripe,0.0,0.5609200596809387,0.4390799105167389 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.53 AM.png,ripe,overripe,0.0,0.8180553913116455,0.1819446086883545 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.10.57 AM.png,ripe,overripe,0.0,0.9244189262390137,0.07558108866214752 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.13.35 AM.png,ripe,overripe,0.0,0.41222766041755676,0.5877723693847656 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.14.18 AM.png,ripe,overripe,0.0,0.42196550965309143,0.578034520149231 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.16.22 AM.png,ripe,overripe,0.0,0.46914172172546387,0.5308582782745361 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.05 AM.png,ripe,overripe,0.0,0.4028591811656952,0.5971407890319824 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.31 AM.png,ripe,overripe,0.0,0.40922585129737854,0.5907741785049438 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.17.55 AM.png,ripe,overripe,0.0,0.46623584628105164,0.533764123916626 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.18.23 AM.png,ripe,overripe,0.0,0.40678954124450684,0.5932104587554932 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.18.27 AM.png,ripe,overripe,0.0,0.40476399660110474,0.5952360033988953 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.19.08 AM.png,ripe,overripe,0.0,0.40206485986709595,0.597935140132904 +orange/test/ripe/vertical_flip_Screen Shot 2018-06-13 at 12.20.06 AM.png,ripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/1.jpg,unripe,overripe,0.05882352963089943,0.9411764740943909,0.0117647061124444 +orange/test/unripe/10.jpg,unripe,overripe,0.05491723492741585,0.9450827836990356,0.017721518874168396 +orange/test/unripe/100.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/101.jpg,unripe,overripe,0.0,0.8472921848297119,0.1527077853679657 +orange/test/unripe/102.jpg,unripe,overripe,0.0,0.46488240361213684,0.5351176261901855 +orange/test/unripe/103.jpg,unripe,overripe,0.0,0.4205549955368042,0.5794450044631958 +orange/test/unripe/104.jpg,unripe,overripe,0.0,0.4010075628757477,0.5989924669265747 +orange/test/unripe/105.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/106.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/107.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/108.jpg,unripe,overripe,0.0,0.41361933946609497,0.586380660533905 +orange/test/unripe/109.jpg,unripe,overripe,0.0,0.42739036679267883,0.5726096034049988 +orange/test/unripe/11.jpg,unripe,overripe,0.015874162316322327,0.7585463523864746,0.24145366251468658 +orange/test/unripe/110.jpg,unripe,overripe,0.3393162488937378,0.6606837511062622,0.1111111119389534 +orange/test/unripe/111.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/112.jpg,unripe,overripe,0.0,0.4801968038082123,0.5198032259941101 +orange/test/unripe/113.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/114.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/115.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/116.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/117.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/118.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/119.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/12.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/120.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/121.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/122.jpg,unripe,overripe,0.0,0.4137609899044037,0.5862390398979187 +orange/test/unripe/123.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/124.jpg,unripe,overripe,0.0,0.5014492869377136,0.4985507130622864 +orange/test/unripe/125.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/127.jpg,unripe,overripe,0.0,0.4902777671813965,0.5097222328186035 +orange/test/unripe/128.jpg,unripe,overripe,0.0,0.43703705072402954,0.5629629492759705 +orange/test/unripe/13.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/131.jpg,unripe,overripe,0.015092765912413597,0.714363157749176,0.285636842250824 +orange/test/unripe/132.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/133.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/134.jpg,unripe,overripe,0.0,0.4011750817298889,0.5988249182701111 +orange/test/unripe/135.jpg,unripe,overripe,0.0,0.6456408500671387,0.35435912013053894 +orange/test/unripe/137.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/139.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/14.jpg,unripe,overripe,0.0,0.40197092294692993,0.5980290770530701 +orange/test/unripe/140.jpg,unripe,overripe,0.025451092049479485,0.9102880954742432,0.08971193432807922 +orange/test/unripe/141.jpg,unripe,overripe,0.015092765912413597,0.714363157749176,0.285636842250824 +orange/test/unripe/142.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/143.jpg,unripe,unripe,0.46180883049964905,0.5381911396980286,0.0 +orange/test/unripe/144.jpg,unripe,overripe,0.0,0.45541810989379883,0.5445818901062012 +orange/test/unripe/145.jpg,unripe,overripe,0.0,0.4025464653968811,0.5974535346031189 +orange/test/unripe/146.jpg,unripe,ripe,0.0,1.0,0.0 +orange/test/unripe/147.jpg,unripe,overripe,0.0,0.5141528248786926,0.48584720492362976 +orange/test/unripe/148.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/149.jpg,unripe,overripe,0.0,0.40326300263404846,0.5967369675636292 +orange/test/unripe/15.jpg,unripe,overripe,0.039540860801935196,0.9315515756607056,0.06844840198755264 +orange/test/unripe/151.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/152.jpg,unripe,overripe,0.0,0.4013802707195282,0.5986197590827942 +orange/test/unripe/153.jpg,unripe,overripe,0.0,0.4147774279117584,0.5852225422859192 +orange/test/unripe/154.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/155.jpg,unripe,overripe,0.0,0.4010586142539978,0.5989413857460022 +orange/test/unripe/156.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/157.jpg,unripe,overripe,0.09615384787321091,0.9038461446762085,0.01666666753590107 +orange/test/unripe/159.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/16.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/160.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/161.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/163.jpg,unripe,overripe,0.0,0.40506330132484436,0.594936728477478 +orange/test/unripe/164.jpg,unripe,overripe,0.09615384787321091,0.9038461446762085,0.01666666753590107 +orange/test/unripe/165.jpg,unripe,overripe,0.0,0.4410628080368042,0.5589371919631958 +orange/test/unripe/166.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/167.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/168.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/169.jpg,unripe,overripe,0.0,0.4168919026851654,0.583108127117157 +orange/test/unripe/17.jpg,unripe,overripe,0.0,0.4260782301425934,0.5739217400550842 +orange/test/unripe/170.jpg,unripe,overripe,0.0,0.5644562244415283,0.4355437755584717 +orange/test/unripe/171.jpg,unripe,overripe,0.0,0.41486987471580505,0.5851300954818726 +orange/test/unripe/172.jpg,unripe,overripe,0.0,0.40326300263404846,0.5967369675636292 +orange/test/unripe/174.jpg,unripe,overripe,0.0,0.42941176891326904,0.570588231086731 +orange/test/unripe/175.jpg,unripe,overripe,0.0,0.4058266580104828,0.5941733717918396 +orange/test/unripe/176.jpg,unripe,overripe,0.0,0.46488240361213684,0.5351176261901855 +orange/test/unripe/177.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/178.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/179.jpg,unripe,overripe,0.04403182864189148,0.9141066074371338,0.085893414914608 +orange/test/unripe/18.jpg,unripe,overripe,0.0,0.6370370388031006,0.3629629611968994 +orange/test/unripe/180.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/181.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/183.jpg,unripe,overripe,0.025400104001164436,0.8161073923110962,0.183892622590065 +orange/test/unripe/184.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/185.jpg,unripe,unripe,0.26459598541259766,0.7354040145874023,0.0 +orange/test/unripe/186.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/187.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/188.jpg,unripe,overripe,0.0,0.4003271162509918,0.5996728539466858 +orange/test/unripe/189.jpg,unripe,overripe,0.0,0.45394784212112427,0.5460521578788757 +orange/test/unripe/19.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/190.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/191.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/193.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/194.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/195.jpg,unripe,overripe,0.020299701020121574,0.7272727489471436,0.27272728085517883 +orange/test/unripe/196.jpg,unripe,overripe,0.0,0.4013020694255829,0.5986979007720947 +orange/test/unripe/197.jpg,unripe,overripe,0.0,0.45002156496047974,0.5499784350395203 +orange/test/unripe/198.jpg,unripe,overripe,0.0,0.40123364329338074,0.5987663269042969 +orange/test/unripe/199.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/2.jpg,unripe,overripe,0.0,0.40101009607315063,0.5989899039268494 +orange/test/unripe/20.jpg,unripe,unripe,0.26384931802749634,0.7361506819725037,0.0 +orange/test/unripe/202.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/205.jpg,unripe,overripe,0.0,0.43889203667640686,0.5611079335212708 +orange/test/unripe/207.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/209.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/21.jpg,unripe,overripe,0.0,0.4260782301425934,0.5739217400550842 +orange/test/unripe/211.jpg,unripe,overripe,0.0,0.5720361471176147,0.42796385288238525 +orange/test/unripe/212.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/213.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/214.jpg,unripe,overripe,0.0,0.4511463940143585,0.5488536357879639 +orange/test/unripe/215.jpg,unripe,overripe,0.0,0.8489428758621216,0.15105712413787842 +orange/test/unripe/216.jpg,unripe,unripe,0.3309091031551361,0.6690909266471863,0.0 +orange/test/unripe/217.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/218.jpg,unripe,overripe,0.0,0.4980325698852539,0.5019674301147461 +orange/test/unripe/22.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/220.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/221.jpg,unripe,overripe,0.0,0.44795942306518555,0.5520405769348145 +orange/test/unripe/222.jpg,unripe,ripe,0.12228287756443024,0.877717137336731,0.0 +orange/test/unripe/223.jpg,unripe,overripe,0.0,0.4168350100517273,0.5831649899482727 +orange/test/unripe/225.jpg,unripe,overripe,0.0,0.6331838369369507,0.36681613326072693 +orange/test/unripe/226.jpg,unripe,overripe,0.06953693181276321,0.9215588569641113,0.07844112813472748 +orange/test/unripe/23.jpg,unripe,overripe,0.0,0.6370370388031006,0.3629629611968994 +orange/test/unripe/231.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/233.jpg,unripe,overripe,0.0,0.407013863325119,0.5929861664772034 +orange/test/unripe/234.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/235.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/236.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/24.jpg,unripe,unripe,0.26384931802749634,0.7361506819725037,0.0 +orange/test/unripe/240.jpg,unripe,overripe,0.0,0.5650475025177002,0.4349524676799774 +orange/test/unripe/241.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/242.jpg,unripe,overripe,0.0,0.44458597898483276,0.5554140210151672 +orange/test/unripe/244.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/245.jpg,unripe,overripe,0.0,0.4226537346839905,0.5773462653160095 +orange/test/unripe/247.jpg,unripe,overripe,0.0,0.5676891803741455,0.4323108494281769 +orange/test/unripe/249.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/25.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/253.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/256.jpg,unripe,overripe,0.0,0.4740740656852722,0.5259259343147278 +orange/test/unripe/257.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/258.jpg,unripe,overripe,0.0,0.4346175193786621,0.5653824806213379 +orange/test/unripe/26.jpg,unripe,ripe,0.08910256624221802,0.910897433757782,0.0 +orange/test/unripe/261.jpg,unripe,overripe,0.0,0.4138224422931671,0.5861775875091553 +orange/test/unripe/263.jpg,unripe,ripe,0.18689458072185516,0.8131054043769836,0.0 +orange/test/unripe/265.jpg,unripe,overripe,0.0,0.819459080696106,0.18054093420505524 +orange/test/unripe/266.jpg,unripe,overripe,0.0,0.4124683439731598,0.5875316858291626 +orange/test/unripe/269.jpg,unripe,overripe,0.0,0.5079365372657776,0.4920634925365448 +orange/test/unripe/27.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/271.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/272.jpg,unripe,overripe,0.0,0.4134378433227539,0.5865621566772461 +orange/test/unripe/273.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/277.jpg,unripe,overripe,0.0,0.41663938760757446,0.5833606123924255 +orange/test/unripe/279.jpg,unripe,overripe,0.0,0.6559890508651733,0.34401094913482666 +orange/test/unripe/28.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/280.jpg,unripe,overripe,0.0,0.46016713976860046,0.5398328900337219 +orange/test/unripe/282.jpg,unripe,overripe,0.0,0.4346175193786621,0.5653824806213379 +orange/test/unripe/283.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/286.jpg,unripe,overripe,0.0,0.46016713976860046,0.5398328900337219 +orange/test/unripe/287.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/29.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/290.jpg,unripe,ripe,0.11799576878547668,0.8820042610168457,0.0 +orange/test/unripe/291.jpg,unripe,overripe,0.0,0.5535532832145691,0.4464466869831085 +orange/test/unripe/294.jpg,unripe,ripe,0.19120068848133087,0.8087993264198303,0.0 +orange/test/unripe/295.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/297.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/298.jpg,unripe,ripe,0.18689458072185516,0.8131054043769836,0.0 +orange/test/unripe/3.jpg,unripe,ripe,0.14017094671726227,0.8598290681838989,0.0 +orange/test/unripe/30.jpg,unripe,overripe,0.10675229132175446,0.8932477235794067,0.043032869696617126 +orange/test/unripe/300.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/303.jpg,unripe,overripe,0.0,0.4857763350009918,0.5142236948013306 +orange/test/unripe/305.jpg,unripe,overripe,0.0,0.4002361595630646,0.5997638702392578 +orange/test/unripe/308.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/309.jpg,unripe,overripe,0.0,0.403809517621994,0.5961904525756836 +orange/test/unripe/31.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/311.jpg,unripe,overripe,0.0,0.4217785894870758,0.5782214403152466 +orange/test/unripe/312.jpg,unripe,overripe,0.0,0.40294334292411804,0.5970566868782043 +orange/test/unripe/317.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/32.jpg,unripe,overripe,0.0,0.4404040277004242,0.5595959424972534 +orange/test/unripe/325.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/327.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/33.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/336.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/339.jpg,unripe,overripe,0.0,0.597300112247467,0.40269988775253296 +orange/test/unripe/34.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/343.jpg,unripe,overripe,0.0,0.42225131392478943,0.577748715877533 +orange/test/unripe/35.jpg,unripe,overripe,0.0,0.4684670567512512,0.5315329432487488 +orange/test/unripe/353.jpg,unripe,overripe,0.0,0.41333332657814026,0.5866666436195374 +orange/test/unripe/358.jpg,unripe,overripe,0.0,0.5116427540779114,0.4883572459220886 +orange/test/unripe/359.jpg,unripe,overripe,0.0,0.597300112247467,0.40269988775253296 +orange/test/unripe/36.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/366.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/368.jpg,unripe,overripe,0.0,0.597300112247467,0.40269988775253296 +orange/test/unripe/37.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/372.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/373.jpg,unripe,overripe,0.0,0.42761340737342834,0.5723865628242493 +orange/test/unripe/377.jpg,unripe,overripe,0.0,0.40843722224235535,0.5915627479553223 +orange/test/unripe/379.jpg,unripe,overripe,0.0,0.41218119859695435,0.5878188014030457 +orange/test/unripe/38.jpg,unripe,overripe,0.0,0.4016016125679016,0.5983983874320984 +orange/test/unripe/383.jpg,unripe,overripe,0.0,0.40843722224235535,0.5915627479553223 +orange/test/unripe/384.jpg,unripe,overripe,0.0,0.42761340737342834,0.5723865628242493 +orange/test/unripe/385.jpg,unripe,overripe,0.0,0.420968234539032,0.579031765460968 +orange/test/unripe/387.jpg,unripe,overripe,0.0,0.41218119859695435,0.5878188014030457 +orange/test/unripe/39.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/398.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/4.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/40.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/41.jpg,unripe,overripe,0.0,0.40147003531455994,0.5985299348831177 +orange/test/unripe/42.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/43.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/44.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/45.jpg,unripe,overripe,0.0,0.4228809177875519,0.5771191120147705 +orange/test/unripe/46.jpg,unripe,overripe,0.0,0.4004208743572235,0.5995790958404541 +orange/test/unripe/47.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/48.jpg,unripe,overripe,0.0,0.514387845993042,0.485612154006958 +orange/test/unripe/49.jpg,unripe,overripe,0.0,0.4721102714538574,0.5278897285461426 +orange/test/unripe/5.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/50.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/51.jpg,unripe,overripe,0.0,0.40701186656951904,0.592988133430481 +orange/test/unripe/52.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/53.jpg,unripe,overripe,0.0,0.4117354154586792,0.5882645845413208 +orange/test/unripe/54.jpg,unripe,overripe,0.011564274318516254,0.655033528804779,0.34496644139289856 +orange/test/unripe/55.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/56.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/57.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/58.jpg,unripe,overripe,0.0,0.4056786000728607,0.5943214297294617 +orange/test/unripe/59.jpg,unripe,overripe,0.0,0.4230942130088806,0.5769057869911194 +orange/test/unripe/6.jpg,unripe,overripe,0.0,0.4010598957538605,0.5989401340484619 +orange/test/unripe/60.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/61.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/62.jpg,unripe,overripe,0.0,0.4257400333881378,0.5742599964141846 +orange/test/unripe/63.jpg,unripe,overripe,0.0,0.4484848380088806,0.5515151619911194 +orange/test/unripe/64.jpg,unripe,overripe,0.0,0.44344305992126465,0.5565569400787354 +orange/test/unripe/65.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/66.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/67.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/68.jpg,unripe,overripe,0.0,0.5363993644714355,0.46360063552856445 +orange/test/unripe/69.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/7.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/70.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/71.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/72.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/73.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/74.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/75.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/76.jpg,unripe,overripe,0.0,0.446540892124176,0.553459107875824 +orange/test/unripe/77.jpg,unripe,overripe,0.015384615398943424,0.7401360273361206,0.259863942861557 +orange/test/unripe/78.jpg,unripe,overripe,0.0,0.40298953652381897,0.5970104336738586 +orange/test/unripe/79.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/8.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/80.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/81.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/82.jpg,unripe,overripe,0.0,0.4022020399570465,0.5977979898452759 +orange/test/unripe/83.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/84.jpg,unripe,overripe,0.0,0.42688173055648804,0.573118269443512 +orange/test/unripe/85.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/86.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/87.jpg,unripe,overripe,0.0,0.40298953652381897,0.5970104336738586 +orange/test/unripe/88.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/89.jpg,unripe,ripe,0.14635108411312103,0.8536489009857178,0.0 +orange/test/unripe/9.jpg,unripe,overripe,0.0514548234641552,0.9485451579093933,0.01398672815412283 +orange/test/unripe/90.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/91.jpg,unripe,overripe,0.0,0.4000000059604645,0.6000000238418579 +orange/test/unripe/92.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/93.jpg,unripe,ripe,0.0615384615957737,0.9384615421295166,0.0 +orange/test/unripe/94.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/95.jpg,unripe,overripe,0.0,0.6207931637763977,0.3792068660259247 +orange/test/unripe/96.jpg,unripe,overripe,0.0,0.42527076601982117,0.5747292637825012 +orange/test/unripe/97.jpg,unripe,overripe,0.0666508898139,0.9333491325378418,0.003076923079788685 +orange/test/unripe/98.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 +orange/test/unripe/99.jpg,unripe,ripe,0.0,0.6000000238418579,0.4000000059604645 diff --git a/AgCloud/services/ripeness-baseline/eval/orange_tuned/roc_curves.png b/AgCloud/services/ripeness-baseline/eval/orange_tuned/roc_curves.png new file mode 100644 index 000000000..3543d7174 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/eval/orange_tuned/roc_curves.png differ diff --git a/AgCloud/services/ripeness-baseline/requirements.txt b/AgCloud/services/ripeness-baseline/requirements.txt new file mode 100644 index 000000000..4786e192b --- /dev/null +++ b/AgCloud/services/ripeness-baseline/requirements.txt @@ -0,0 +1,8 @@ +opencv-python-headless==4.10.0.84 +numpy==2.1.1 +psycopg[binary]>=3.1 +python-dotenv==1.0.1 +minio>=7.2 +scikit-learn>=1.3 +matplotlib>=3.8 +requests==2.32.3 \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/samples/apple-colorful-vector-design_341269-1123.jpg b/AgCloud/services/ripeness-baseline/samples/apple-colorful-vector-design_341269-1123.jpg new file mode 100644 index 000000000..36e4a1b77 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/samples/apple-colorful-vector-design_341269-1123.jpg differ diff --git a/AgCloud/services/ripeness-baseline/samples/red-apple-isolated-clipping-path-19130134.webp b/AgCloud/services/ripeness-baseline/samples/red-apple-isolated-clipping-path-19130134.webp new file mode 100644 index 000000000..30ee03370 Binary files /dev/null and b/AgCloud/services/ripeness-baseline/samples/red-apple-isolated-clipping-path-19130134.webp differ diff --git a/AgCloud/services/ripeness-baseline/src/config.py b/AgCloud/services/ripeness-baseline/src/config.py new file mode 100644 index 000000000..fbc7daa24 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/config.py @@ -0,0 +1,29 @@ +import os +from dotenv import load_dotenv +load_dotenv() + +PG = { + "host": os.getenv("PGHOST", "localhost"), + "port": int(os.getenv("PGPORT", "5432")), + "db": os.getenv("PGDATABASE", "agcloud"), + "user": os.getenv("PGUSER", "postgres"), + "password": os.getenv("PGPASSWORD", "postgres"), +} + +SAMPLES_DIR = os.getenv("SAMPLES_DIR", "./samples") +FRUIT_TYPE = os.getenv("FRUIT_TYPE", "apple") + +THRESHOLDS = { + "overripe_brown_ratio": 0.10, + "overripe_min_v": 60, + "unripe_h_min": 30, + "unripe_h_max": 95, + "low_light_v": 60, + "blurry_lap_var": 80.0, + "small_mask_cov": 0.20, + "near_brown_delta": 0.03, + "green_leaf_ratio_thr": float(os.getenv("GREEN_LEAF_FLAG_THR", "0.10")) +} + +LOOKBACK_DAYS = int(os.getenv("LOOKBACK_DAYS", "7")) +READ_FROM_LOGS = os.getenv("READ_FROM_LOGS", "0") in ("1", "true", "True") diff --git a/AgCloud/services/ripeness-baseline/src/db.py b/AgCloud/services/ripeness-baseline/src/db.py new file mode 100644 index 000000000..2b1b9fa62 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/db.py @@ -0,0 +1,88 @@ +import psycopg +from pathlib import Path +from typing import List, Tuple, Optional + + +def dsn(pg): + return f"host={pg['host']} port={pg['port']} dbname={pg['db']} user={pg['user']} password={pg['password']}" + +def ensure_schema(con, schema_sql_path: Path): + sql = Path(schema_sql_path).read_text(encoding="utf-8") + with con.cursor() as cur: + cur.execute("SET lock_timeout = '2s'; SET statement_timeout = '8s';") + try: + cur.execute(sql) + except psycopg.errors.LockNotAvailable as e: + print(f"[warn] lock timeout while applying {schema_sql_path}; skipping", flush=True) + except Exception as e: + print(f"[warn] failed to apply {schema_sql_path}: {e}", flush=True) + +def insert_detection(cur, fruit_type, captured_at, source_path, feat, ripeness, flags): + cur.execute( + """ + INSERT INTO images (fruit_type, captured_at, source_path) + VALUES (%s,%s,%s) + ON CONFLICT (source_path) DO UPDATE + SET fruit_type = EXCLUDED.fruit_type, + captured_at = EXCLUDED.captured_at + RETURNING image_id; + """, + (fruit_type, captured_at, source_path) + ) + image_id = cur.fetchone()[0] + cur.execute( + """ + INSERT INTO detections + (image_id, mean_h, mean_s, mean_v, laplacian_var, brown_ratio, ripeness, quality_flags, created_at) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s) + """, + ( + image_id, + feat.mean_h, feat.mean_s, feat.mean_v, + feat.lap_var, feat.brown_ratio, + ripeness, flags,captured_at + ), + ) + +def run_weekly_upsert(con, upsert_sql_path: Path): + sql = Path(upsert_sql_path).read_text(encoding="utf-8") + with con.cursor() as cur: + cur.execute(sql) + +def apply_sql_autocommit(dsn: str, sql_path): + sql = Path(sql_path).read_text(encoding="utf-8") + with psycopg.connect(dsn, autocommit=True) as cn: + with cn.cursor() as cur: + cur.execute("SET lock_timeout='2s'; SET statement_timeout='8s';") + try: + cur.execute(sql) + except Exception as e: + print(f"[warn] failed to apply {sql_path}: {e}", flush=True) + +def fetch_inference_logs(pg: dict, lookback_days: int = 7, + fruit_filter: Optional[str] = None, + limit: Optional[int] = None) -> List[Tuple[str, str]]: + """ + Returns [(fruit_type, image_url), ...] from the last N days. + """ + sql_parts = [ + "SELECT fruit_type, image_url", + "FROM inference_logs", + "WHERE ts >= NOW() - make_interval(days => %s)" + ] + params = [lookback_days] + + if fruit_filter: + sql_parts.append("AND fruit_type = %s") + params.append(fruit_filter) + + sql_parts.append("ORDER BY ts DESC") + if limit: + sql_parts.append(f"LIMIT {int(limit)}") + + sql = " ".join(sql_parts) + + with psycopg.connect(dsn(pg)) as con: + with con.cursor() as cur: + cur.execute(sql, params) + return [(r[0], r[1]) for r in cur.fetchall()] diff --git a/AgCloud/services/ripeness-baseline/src/evaluate_minio.py b/AgCloud/services/ripeness-baseline/src/evaluate_minio.py new file mode 100644 index 000000000..4296dd806 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/evaluate_minio.py @@ -0,0 +1,208 @@ + +""" +Evaluate the baseline (segment + heuristics) on images stored in MinIO. + +Assumes MinIO layout like: + imagery/apple/test/{unripe,ripe,overripe}/*.jpg|png|bmp|webp + imagery/apple/train/{unripe,ripe,overripe}/*.jpg|png|bmp|webp + +Outputs: + - confusion_matrix.png + - roc_curves.png + - per_image.csv + - metrics.json +""" + +import os, io, csv, json, argparse, time +from pathlib import Path +from urllib.parse import urlparse + +import numpy as np +import cv2 as cv +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from sklearn.metrics import accuracy_score, classification_report, confusion_matrix, roc_curve, auc + +from minio import Minio +from minio.error import S3Error + +from config import THRESHOLDS +from segment import segment_fruit +from heuristics import compute_features, classify_ripeness + +LABELS = ["unripe", "ripe", "overripe"] +LABEL2IDX = {l:i for i,l in enumerate(LABELS)} + +def _minio_client(url, access_key, secret_key): + u = urlparse(url) + secure = (u.scheme == "https") + return Minio(u.netloc, access_key=access_key, secret_key=secret_key, secure=secure) + +def _truth_from_key(key, anchor="apple"): + parts = key.split("/") + try: + i = parts.index(anchor) + truth = parts[i+2].lower() + if truth == "rotten": + truth = "overripe" + return parts[i+1].lower(), truth # split, truth + except Exception: + for p in parts: + pp = p.lower() + if pp in LABELS: + return None, pp + return None, None + +def _scores_from_features(f): + or_brown = max(0.0, (f.brown_ratio - THRESHOLDS["overripe_brown_ratio"]) / max(1e-6, (1.0 - THRESHOLDS["overripe_brown_ratio"]))) + or_dark = max(0.0, (THRESHOLDS["overripe_min_v"] - f.mean_v) / max(1e-6, THRESHOLDS["overripe_min_v"])) + s_overripe = float(np.clip(0.6*or_brown + 0.4*or_dark, 0.0, 1.0)) + + hmin = THRESHOLDS["unripe_h_min"]; hmax = THRESHOLDS["unripe_h_max"] + hmid = 0.5*(hmin+hmax); halfw = max(1e-6, 0.5*(hmax-hmin)) + dist = abs(f.mean_h - hmid) / halfw + s_unripe = float(np.clip(1.0 - dist, 0.0, 1.0)) if (f.mean_h >= hmin-10 and f.mean_h <= hmax+10) else 0.0 + + s_ripe = float(np.clip(1.0 - max(s_overripe, s_unripe), 0.0, 1.0)) + return np.array([s_unripe, s_ripe, s_overripe], dtype=np.float32) + +def evaluate(minio_url, access_key, secret_key, bucket, prefix, outdir, fruit, limit=0, use_argmax=False): + outdir = Path(outdir); outdir.mkdir(parents=True, exist_ok=True) + client = _minio_client(minio_url, access_key, secret_key) + + if not client.bucket_exists(bucket): + raise SystemExit(f"Bucket '{bucket}' does not exist") + + exts = (".jpg",".jpeg",".png",".bmp",".webp") + objs = client.list_objects(bucket, prefix=prefix, recursive=True) + import json + thr = {} + if args.thresholds_json: + with open(args.thresholds_json, "r", encoding="utf-8") as f: + thr = json.load(f) + y_true, y_pred, y_scores, rows = [], [], [], [] + n = 0 + for o in objs: + key = o.object_name + if not key.lower().endswith(exts): + continue + split, truth = _truth_from_key(key, anchor=fruit) + if truth not in LABELS: + continue + + try: + resp = client.get_object(bucket, key) + data = resp.read() + resp.close(); resp.release_conn() + except S3Error as e: + rows.append([key, truth, "", "", "S3Error", str(e)]) + continue + + img_arr = np.frombuffer(data, dtype=np.uint8) + img = cv.imdecode(img_arr, cv.IMREAD_COLOR) + if img is None: + rows.append([key, truth, "", "", "DecodeError", "cv2.imdecode returned None"]) + continue + + mask, leaf_ratio = segment_fruit(img,fruit) + feat = compute_features(img, mask) + scores = _scores_from_features(feat) + + if use_argmax: + labels = ["unripe", "ripe", "overripe"] + pred_label = labels[int(np.argmax(scores))] + else: + pred_label = classify_ripeness(feat, thr if thr else THRESHOLDS).lower().replace("rotten","overripe") + + y_true.append(LABEL2IDX[truth]) + y_pred.append(LABEL2IDX.get(pred_label, -1)) + y_scores.append(scores) + rows.append([key, truth, pred_label, float(scores[0]), float(scores[1]), float(scores[2])]) + + n += 1 + if limit and n >= limit: break + + if not y_true: + raise SystemExit("No images evaluated; check your prefix and bucket") + + y_true = np.array(y_true, dtype=int) + y_pred = np.array(y_pred, dtype=int) + y_scores = np.stack(y_scores, axis=0) + + acc = accuracy_score(y_true, y_pred) + report = classification_report(y_true, y_pred, target_names=LABELS, output_dict=True, zero_division=0) + cm = confusion_matrix(y_true, y_pred, labels=[0,1,2]) + + # Confusion matrix plot + fig = plt.figure(figsize=(5,4)) + im = plt.imshow(cm, interpolation="nearest") + plt.title("Confusion Matrix (baseline)") + plt.colorbar(im) + ticks = np.arange(len(LABELS)) + plt.xticks(ticks, LABELS, rotation=45, ha="right") + plt.yticks(ticks, LABELS) + for i in range(cm.shape[0]): + for j in range(cm.shape[1]): + plt.text(j, i, str(cm[i, j]), ha="center", va="center") + plt.tight_layout() + plt.savefig(outdir/"confusion_matrix.png", dpi=160) + plt.close(fig) + + # ROC curves + fig = plt.figure(figsize=(5,4)) + for i, name in enumerate(LABELS): + y_true_bin = (y_true == i).astype(int) + fpr, tpr, _ = roc_curve(y_true_bin, y_scores[:, i]) + roc_auc = auc(fpr, tpr) + plt.plot(fpr, tpr, label=f"{name} (AUC={roc_auc:.3f})") + plt.plot([0,1], [0,1], "--") + plt.xlabel("False Positive Rate"); plt.ylabel("True Positive Rate") + plt.title("ROC (one-vs-rest)"); plt.legend(loc="lower right") + plt.tight_layout() + plt.savefig(outdir/"roc_curves.png", dpi=160); plt.close(fig) + + # per-image CSV + with open(outdir/"per_image.csv", "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(["object_key","truth","pred","score_unripe","score_ripe","score_overripe"]) + w.writerows(rows) + + # metrics JSON + metrics = { + "accuracy": acc, + "report": report, + "confusion_matrix": cm.tolist(), + "samples": int(len(y_true)), + "prefix": prefix, + "bucket": bucket, + } + with open(outdir/"metrics.json", "w", encoding="utf-8") as f: + json.dump(metrics, f, indent=2) + + print("\n=== Baseline Evaluation ===") + print(f"Evaluated samples: {len(y_true)} | Accuracy: {acc:.3%}") + for cls in LABELS: + pr = report[cls] + print(f" {cls:9s} P={pr['precision']:.3f} R={pr['recall']:.3f} F1={pr['f1-score']:.3f} (n={int(pr['support'])})") + print(f"\nSaved to: {outdir}") + +def parse_args(): + ap = argparse.ArgumentParser() + ap.add_argument("--minio-url", required=True) + ap.add_argument("--access-key", required=True) + ap.add_argument("--secret-key", required=True) + ap.add_argument("--bucket", default="imagery") + ap.add_argument("--prefix", default="apple/test") + ap.add_argument("--outdir", default="./eval/test") + ap.add_argument("--limit", type=int, default=0) + ap.add_argument("--fruit", default="apple") + ap.add_argument("--thresholds-json", default=None, + help="Path to tuned thresholds JSON (optional)") + ap.add_argument("--use-argmax", action="store_true", + help="Use argmax over soft scores for prediction (diagnostic).") + return ap.parse_args() + +if __name__ == "__main__": + args = parse_args() + evaluate(args.minio_url, args.access_key, args.secret_key, args.bucket, args.prefix, args.outdir, args.fruit, args.limit, args.use_argmax) diff --git a/AgCloud/services/ripeness-baseline/src/heuristics.py b/AgCloud/services/ripeness-baseline/src/heuristics.py new file mode 100644 index 000000000..f21671b55 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/heuristics.py @@ -0,0 +1,62 @@ +from dataclasses import dataclass +import numpy as np +import cv2 as cv + +@dataclass +class Features: + mean_h: float + mean_s: float + mean_v: float + lap_var: float + brown_ratio: float + mask_cov: float + +def _laplacian_variance(gray: np.ndarray, mask: np.ndarray) -> float: + lap = cv.Laplacian(gray, cv.CV_64F) + m = mask.astype(bool) + return float(np.var(lap[m])) if np.any(m) else 0.0 + +def _brown_ratio(h: np.ndarray, v: np.ndarray, mask: np.ndarray) -> float: + m = mask.astype(bool) + brown_hue = ((h >= 10) & (h <= 30)) & m + dark = (v < 50) & m + tot = np.count_nonzero(m) + if tot == 0: return 0.0 + return float(np.count_nonzero(brown_hue | dark) / tot) + +def compute_features(img_bgr: np.ndarray, mask: np.ndarray) -> Features: + hsv = cv.cvtColor(img_bgr, cv.COLOR_BGR2HSV) + h, s, v = cv.split(hsv) + m = mask.astype(bool) + + mean_h = float(np.mean(h[m])) if np.any(m) else 0.0 + mean_s = float(np.mean(s[m])) if np.any(m) else 0.0 + mean_v = float(np.mean(v[m])) if np.any(m) else 0.0 + + gray = cv.cvtColor(img_bgr, cv.COLOR_BGR2GRAY) + lap_var = _laplacian_variance(gray, mask) + brown_ratio = _brown_ratio(h, v, mask) + + mask_cov = float(np.count_nonzero(m) / (img_bgr.shape[0] * img_bgr.shape[1])) + return Features(mean_h, mean_s, mean_v, lap_var, brown_ratio, mask_cov) + +def classify_ripeness(feat, thr): + # --- thresholds + hmin = float(thr.get("unripe_h_min", 30)) + hmax = float(thr.get("unripe_h_max", 95)) + min_s = float(thr.get("unripe_min_s", 25)) + br_th = float(thr.get("overripe_brown_ratio", 0.12)) + v_dark = float(thr.get("overripe_min_v", 55)) + + mean_h = float(getattr(feat, "mean_h")) + mean_s = float(getattr(feat, "mean_s", 0.0)) + mean_v = float(getattr(feat, "mean_v")) + brown_ratio = float(getattr(feat, "brown_ratio")) + + if brown_ratio >= br_th or (brown_ratio >= br_th*0.7 and mean_v <= v_dark and not (hmin <= mean_h <= hmax)): + return "Overripe" + + if (hmin <= mean_h <= hmax) and (mean_s >= min_s): + return "Unripe" + + return "Ripe" diff --git a/AgCloud/services/ripeness-baseline/src/main.py b/AgCloud/services/ripeness-baseline/src/main.py new file mode 100644 index 000000000..574b42af8 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/main.py @@ -0,0 +1,112 @@ +import os, datetime, numpy as np +import cv2 as cv +import requests +from urllib.parse import quote +from urllib.parse import urlparse, unquote +from minio import Minio +from quality import quality_flags + +from segment import segment_fruit +from heuristics import compute_features, classify_ripeness +from config import PG, SAMPLES_DIR, FRUIT_TYPE, THRESHOLDS, LOOKBACK_DAYS, READ_FROM_LOGS +from db import dsn, apply_sql_autocommit, ensure_schema, insert_detection, run_weekly_upsert, fetch_inference_logs +from urllib.parse import urlparse, quote +import numpy as np, cv2 as cv +import psycopg +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA_SQL = ROOT/"deploy/sql/01_schema.sql" +VIEW_SQL = ROOT/"deploy/sql/02_rollup_view.sql" +ROLLUP_SQL = ROOT/"deploy/sql/03_weekly_upsert.sql" +MINIO_URL = os.getenv("MINIO_URL") +MINIO_AK = os.getenv("MINIO_ACCESS_KEY") +MINIO_SK = os.getenv("MINIO_SECRET_KEY") +MINIO_BUCKET= os.getenv("MINIO_BUCKET", "imagery") +MINIO_PREFIX= os.getenv("MINIO_PREFIX", f"{FRUIT_TYPE}/test") + +def list_minio_objects(): + u = urlparse(MINIO_URL); secure = (u.scheme == "https") + cli = Minio(u.netloc, access_key=MINIO_AK, secret_key=MINIO_SK, secure=secure) + for o in cli.list_objects(MINIO_BUCKET, prefix=MINIO_PREFIX, recursive=True): + key = o.object_name + if key.lower().endswith((".jpg",".jpeg",".png",".webp",".bmp")): + yield cli, key + +def imread_from_any(url: str): + u = urlparse(url) + minio_base = os.getenv("MINIO_URL", "http://minio-hot-1:9000") + mu = urlparse(minio_base) + + is_minio = (u.hostname == mu.hostname and (u.port or 80) == (mu.port or 80)) or \ + (u.hostname == "minio-hot-1" and (u.port or 80) == 9000) + + if is_minio: + cli = Minio( + f"{mu.hostname}:{mu.port or (443 if mu.scheme=='https' else 80)}", + access_key=os.getenv("MINIO_ACCESS_KEY"), + secret_key=os.getenv("MINIO_SECRET_KEY"), + secure=(mu.scheme == "https"), + ) + # path-style: // + parts = u.path.lstrip("/").split("/", 1) + if len(parts) != 2: + raise RuntimeError(f"Bad MinIO URL path: {u.path}") + bucket, key = parts[0], unquote(parts[1]) + + resp = cli.get_object(bucket, key) + data = resp.read() + resp.close(); resp.release_conn() + + arr = np.frombuffer(data, dtype=np.uint8) + return cv.imdecode(arr, cv.IMREAD_COLOR) + + safe = quote(url, safe="/:?=&%()[]") + r = requests.get(safe, timeout=30) + r.raise_for_status() + arr = np.frombuffer(r.content, dtype=np.uint8) + return cv.imdecode(arr, cv.IMREAD_COLOR) + + +def process_all(): + + DSN = dsn(PG) + + lookback_days = int(os.getenv("LOOKBACK_DAYS", "7")) + rows = fetch_inference_logs(PG, lookback_days=lookback_days, + fruit_filter=None, limit=None) + inserted = 0 + with psycopg.connect(DSN) as con: + with con.cursor() as cur: + for fruit_type_raw, image_url in rows: + try: + + + fruit = fruit_type_raw + img = imread_from_any(image_url) + if img is None: + print(f"[skip] cannot read image: {image_url}") + continue + + mask, leaf_ratio = segment_fruit(img, fruit) + feat = compute_features(img, mask) + ripeness = classify_ripeness(feat,THRESHOLDS) + flags = quality_flags(feat, THRESHOLDS, leaf_ratio, mark_outlier=False) + + + insert_detection(cur, fruit, datetime.datetime.now(), image_url, + feat, ripeness, flags) + inserted += 1 + + except Exception as e: + print(f"[warn] {image_url} | {e}", flush=True) + con.rollback() + con.commit() + run_weekly_upsert(con, ROLLUP_SQL) + con.commit() + return inserted + + +if __name__ == "__main__": + inserted = process_all() + print(f"Done. Inserted detections {inserted} and updated weekly_rollups.") diff --git a/AgCloud/services/ripeness-baseline/src/quality.py b/AgCloud/services/ripeness-baseline/src/quality.py new file mode 100644 index 000000000..df94b1d2e --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/quality.py @@ -0,0 +1,35 @@ +from heuristics import Features + +# bitmask: +LOW_LIGHT = 1 +BLURRY = 2 +SMALL_MASK = 4 +NEAR_THRESHOLD = 8 +OUTLIER = 16 +GREEN_LEAF_BIT = 32 + +def near_threshold(f: Features, thr: dict) -> bool: + close_brown = abs(f.brown_ratio - thr["overripe_brown_ratio"]) < thr["near_brown_delta"] + close_hue = (thr["unripe_h_min"] <= f.mean_h <= thr["unripe_h_min"]+5) or \ + (thr["unripe_h_max"]-5 <= f.mean_h <= thr["unripe_h_max"]) + return close_brown or close_hue + +def quality_flags(f: Features, thr: dict, leaf_ratio: float, mark_outlier: bool=False) -> int: + + flags = 0 + if f.mean_v < thr["low_light_v"]: + flags |= LOW_LIGHT + if f.lap_var < thr["blurry_lap_var"]: + flags |= BLURRY + if f.mask_cov < thr["small_mask_cov"]: + flags |= SMALL_MASK + if near_threshold(f, thr): + flags |= NEAR_THRESHOLD + + gl_thr = thr.get("green_leaf_ratio_thr", 0.10) + if leaf_ratio > gl_thr: + flags |= GREEN_LEAF_BIT + + if mark_outlier: + flags |= OUTLIER + return flags diff --git a/AgCloud/services/ripeness-baseline/src/segment.py b/AgCloud/services/ripeness-baseline/src/segment.py new file mode 100644 index 000000000..61b97c2ad --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/segment.py @@ -0,0 +1,78 @@ +import cv2 as cv +import numpy as np + +def _mask_apple(img): + hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) + lower = np.array([0, 30, 30], np.uint8) + upper = np.array([179, 255, 255], np.uint8) + mask = cv.inRange(hsv, lower, upper) + mask = cv.medianBlur(mask, 5) + mask = cv.morphologyEx(mask, cv.MORPH_CLOSE, cv.getStructuringElement(cv.MORPH_ELLIPSE,(9,9))) + return mask + +def _mask_banana(img): + hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) + lower = np.array([18, 60, 40], np.uint8) + upper = np.array([45, 255, 255], np.uint8) + mask_yellow = cv.inRange(hsv, lower, upper) + lower_brown = np.array([5, 50, 20], np.uint8) + upper_brown = np.array([25, 255, 200], np.uint8) + mask_brown = cv.inRange(hsv, lower_brown, upper_brown) + mask = cv.bitwise_or(mask_yellow, mask_brown) + mask = cv.medianBlur(mask, 5) + mask = cv.morphologyEx(mask, cv.MORPH_CLOSE, cv.getStructuringElement(cv.MORPH_ELLIPSE,(9,9))) + return mask + +def _mask_orange(img): + hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) + lower = np.array([8, 60, 40], np.uint8) + upper = np.array([28, 255, 255], np.uint8) + mask = cv.inRange(hsv, lower, upper) + mask = cv.medianBlur(mask, 5) + mask = cv.morphologyEx(mask, cv.MORPH_CLOSE, cv.getStructuringElement(cv.MORPH_ELLIPSE,(9,9))) + return mask + +def segment_fruit(img, fruit): + """ + Segments fruit in HSV, removes low-sat/dark background, + removes green leaf pixels, cleans noise, and keeps largest blob. + Returns: (fruit_mask_uint8, leaf_ratio_float) + """ + img = img.astype(np.float32) + for c in range(3): + img[..., c] *= (img.mean() / (img[..., c].mean() + 1e-6)) + img = np.clip(img, 0, 255).astype(np.uint8) + + lab = cv.cvtColor(img, cv.COLOR_BGR2LAB) + l, a, b = cv.split(lab) + clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + l = clahe.apply(l) + img = cv.cvtColor(cv.merge([l, a, b]), cv.COLOR_LAB2BGR) + + + hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) + h, s, v = cv.split(hsv) + + if fruit == "banana": + mask = _mask_banana(img) + elif fruit == "orange": + mask = _mask_orange(img) + else: + mask = _mask_apple(img) + green_mask = ((h >= 40) & (h <= 85) & (s >= 100)).astype(np.uint8) * 255 + + denom = max(1, int((mask > 0).sum())) + leaf_ratio = float((green_mask > 0).sum()) / float(denom) + + fruit_mask = cv.bitwise_and(mask, cv.bitwise_not(green_mask)) + + kernel = np.ones((5, 5), np.uint8) + fruit_mask = cv.morphologyEx(fruit_mask, cv.MORPH_OPEN, kernel, iterations=2) + fruit_mask = cv.morphologyEx(fruit_mask, cv.MORPH_CLOSE, kernel, iterations=2) + + num_labels, labels, stats, _ = cv.connectedComponentsWithStats(fruit_mask, 8, cv.CV_32S) + if num_labels <= 1: + return np.zeros_like(fruit_mask), 0.0 + largest = 1 + np.argmax(stats[1:, cv.CC_STAT_AREA]) + out = (labels == largest).astype(np.uint8) * 255 + return out, leaf_ratio diff --git a/AgCloud/services/ripeness-baseline/src/tune_thresholds_minio.py b/AgCloud/services/ripeness-baseline/src/tune_thresholds_minio.py new file mode 100644 index 000000000..4088330a3 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/src/tune_thresholds_minio.py @@ -0,0 +1,146 @@ +import io, json, math, random, argparse +from dataclasses import dataclass +from typing import Dict, List, Tuple +import numpy as np +import cv2 as cv +from minio import Minio +from sklearn.metrics import f1_score +import argparse +from segment import segment_fruit +from heuristics import compute_features + +LABELS = ["unripe", "ripe", "overripe"] +L2I = {l:i for i,l in enumerate(LABELS)} + +@dataclass +class Feat: + mean_h: float + mean_s: float + mean_v: float + brown_ratio: float + +def _to_bgr(img_bytes: bytes) -> np.ndarray: + arr = np.frombuffer(img_bytes, np.uint8) + img = cv.imdecode(arr, cv.IMREAD_COLOR) + if img is None: + raise ValueError("failed to decode image") + return img + +def _white_balance_and_clahe(img: np.ndarray) -> np.ndarray: + imgf = img.astype(np.float32) + m = imgf.mean() + for c in range(3): + imgf[..., c] *= m / (imgf[..., c].mean() + 1e-6) + imgf = np.clip(imgf, 0, 255).astype(np.uint8) + lab = cv.cvtColor(imgf, cv.COLOR_BGR2LAB) + l, a, b = cv.split(lab) + clahe = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) + l = clahe.apply(l) + return cv.cvtColor(cv.merge([l, a, b]), cv.COLOR_LAB2BGR) + + +def extract_features(img: np.ndarray) -> Feat: + img = _white_balance_and_clahe(img) + mask,_ = segment_fruit(img, fruit=args.fruit) + hsv = cv.cvtColor(img, cv.COLOR_BGR2HSV) + H,S,V = hsv[...,0][mask], hsv[...,1][mask], hsv[...,2][mask] + if H.size == 0: + return Feat(0,0,0,0) + brown_h = ((H>=5)&(H<=25)) + brown_vs = ((V<=90)&(S>=60)) + brown = (brown_h | brown_vs) + return Feat(float(H.mean()), float(S.mean()), float(V.mean()), + float(brown.mean())) + +def classify(feat: Feat, thr: Dict[str,float]) -> str: + hmin = float(thr.get("unripe_h_min", 30)) + hmax = float(thr.get("unripe_h_max", 95)) + min_s = float(thr.get("unripe_min_s", 25)) + br_th = float(thr.get("overripe_brown_ratio", 0.12)) + v_dark = float(thr.get("overripe_min_v", 55)) + + if feat.brown_ratio >= br_th or (feat.brown_ratio >= br_th*0.7 and feat.mean_v <= v_dark and not (hmin <= feat.mean_h <= hmax)): + return "overripe" + if (hmin <= feat.mean_h <= hmax) and (feat.mean_s >= min_s): + return "unripe" + return "ripe" + +def load_minio(minio_url, ak, sk, bucket, prefix,fruit, limit=None) -> List[Tuple[Feat,int]]: + mc = Minio(minio_url.replace("http://","").replace("https://",""), + access_key=ak, secret_key=sk, + secure=minio_url.startswith("https")) + objects = mc.list_objects(bucket, prefix=prefix, recursive=True) + out = [] + for i,obj in enumerate(objects): + if limit and i>=limit: break + name = obj.object_name.lower() + if not (name.endswith(".jpg") or name.endswith(".jpeg") or name.endswith(".png")): + continue + data = mc.get_object(bucket, obj.object_name).read() + img = _to_bgr(data) + mask, _ = segment_fruit(img, fruit=fruit) + feat = compute_features(img, mask) + if "/unripe" in name: y=L2I["unripe"] + elif "/ripe" in name: y=L2I["ripe"] + elif "/overripe" in name: y=L2I["overripe"] + else: continue + out.append((feat,y)) + return out + +def score(features: List[Feat], y: np.ndarray, thr: Dict[str,float]) -> float: + preds = np.array([L2I[classify(f, thr)] for f,_ in zip(features,y)]) + f1 = f1_score(y, preds, average="macro") + + penalty = np.mean((y==L2I["unripe"]) & (preds==L2I["overripe"])) + return float(f1 - 0.5*penalty) + +SEARCH_SPACE = { + "unripe_h_min": (25, 40), + "unripe_h_max": (85, 100), + "unripe_min_s": (15, 35), + "overripe_brown_ratio": (0.08, 0.2), + "overripe_min_v": (45, 70), +} + +def sample(thr0: Dict[str,float]) -> Dict[str,float]: + t = dict(thr0) + for k,(lo,hi) in SEARCH_SPACE.items(): + t[k] = random.uniform(lo, hi) + return t + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--minio-url", required=True) + ap.add_argument("--access-key", required=True) + ap.add_argument("--secret-key", required=True) + ap.add_argument("--bucket", required=True) + ap.add_argument("--prefix", required=True) + ap.add_argument("--fruit", required=True, choices=["apple","banana","orange"]) + ap.add_argument("--iters", type=int, default=300) + ap.add_argument("--limit", type=int, default=None) + ap.add_argument("--out", default="./thresholds.json") + args = ap.parse_args() + + print("[tune] loading data from MinIO…") + data = load_minio(args.minio_url, args.access_key, args.secret_key, + args.bucket, args.prefix,args.fruit, limit=args.limit) + feats, y = zip(*data) + y = np.array(y) + + random.seed(42) + best_thr = {k:np.mean(v) if isinstance(v, tuple) else v for k,v in SEARCH_SPACE.items()} + best = -1.0 + for i in range(1, args.iters+1): + thr = sample(best_thr) + s = score(feats, y, thr) + if s > best: + best, best_thr = s, thr + if i % 25 == 0: + print(f"[tune] {i:4d}/{args.iters} best_score={best:.4f} best={best_thr}") + + with open(args.out, "w", encoding="utf-8") as f: + json.dump(best_thr, f, indent=2) + print(f"[tune] DONE. wrote: {args.out}") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/ripeness-baseline/thresholds.apple.json b/AgCloud/services/ripeness-baseline/thresholds.apple.json new file mode 100644 index 000000000..78af32dec --- /dev/null +++ b/AgCloud/services/ripeness-baseline/thresholds.apple.json @@ -0,0 +1,7 @@ +{ + "unripe_h_min": 34.591401976868255, + "unripe_h_max": 85.37516132834, + "unripe_min_s": 20.500586367382386, + "overripe_brown_ratio": 0.10678528857785874, + "overripe_min_v": 63.41178035410031 +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/thresholds.banana.json b/AgCloud/services/ripeness-baseline/thresholds.banana.json new file mode 100644 index 000000000..026865080 --- /dev/null +++ b/AgCloud/services/ripeness-baseline/thresholds.banana.json @@ -0,0 +1,7 @@ +{ + "unripe_h_min": 33.64882065244899, + "unripe_h_max": 88.82083759207873, + "unripe_min_s": 29.175705676683414, + "overripe_brown_ratio": 0.08020295338623554, + "overripe_min_v": 68.13937913747706 +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-baseline/thresholds.orange.json b/AgCloud/services/ripeness-baseline/thresholds.orange.json new file mode 100644 index 000000000..17ba13a6a --- /dev/null +++ b/AgCloud/services/ripeness-baseline/thresholds.orange.json @@ -0,0 +1,7 @@ +{ + "unripe_h_min": 37.08728877749212, + "unripe_h_max": 95.4720909248234, + "unripe_min_s": 21.80501033035984, + "overripe_brown_ratio": 0.09865753997741379, + "overripe_min_v": 68.93032680516953 +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-ml/.gitignore b/AgCloud/services/ripeness-ml/.gitignore new file mode 100644 index 000000000..17751b717 --- /dev/null +++ b/AgCloud/services/ripeness-ml/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.onnx +datasets/ +*.png +.venv/ +venv/ +ENV/ \ No newline at end of file diff --git a/AgCloud/services/ripeness-ml/README.md b/AgCloud/services/ripeness-ml/README.md new file mode 100644 index 000000000..19de71518 --- /dev/null +++ b/AgCloud/services/ripeness-ml/README.md @@ -0,0 +1,226 @@ +# Ripeness ML – API & Weekly Job + +A small **FastAPI** service that: +- Predicts fruit ripeness (**ripe / unripe / overripe**) for new images from **MinIO** based on the trained conditional model, and writes results to **Postgres**. +- Creates a **weekly rollup snapshot** (with TS window) per fruit. + +--- + +## 🧩 Repo layout (service) + +``` +services/ripeness-ml/ +├─ api/ +│ └─ ripeness_api.py # FastAPI endpoints (predict + rollup) +├─ jobs/ +│ └─ weekly_ripeness_job.py # model/minio/db helpers reused by the API +├─ model/ +│ ├─ architecture/ +│ │ └─ mobilenet_v3_large_head.py # Model architecture definition +│ └─ data/ +│ └─ data_multitask.py # Data loading and preprocessing +├─ checkpoints/ +│ └─ mobilenet_v3_large/ +│ └─ best_conditional.pt # trained model weights +├─ deploy/ +│ ├─ Dockerfile +│ └─ docker-compose.ripeness.yml +├─ configs/ +│ └─ config.yaml # Model and training configuration +├─ requirements.txt +└─ .env (optional) +``` + +--- + +## ⚙️ Requirements + +- Docker Desktop +- External Docker network: **agcloud_ag_cloud** (same as your existing stack) + +**Running services on that network:** +- Postgres (`postgres:5432`, DB: `missions_db`, user: `missions_user`) +- MinIO (`minio-hot:9000`) + +--- + +## 🌍 Environment variables + +Set via `docker-compose.ripeness.yml` or `.env`: + +| Name | Default | Notes | +|------|----------|-------| +| `PGHOST` | postgres | DB host (inside Docker network) | +| `PGPORT` | 5432 | | +| `PGDATABASE` | missions_db | | +| `PGUSER` | missions_user | | +| `PGPASSWORD` | pg123 | | +| `MINIO_ENDPOINT` | minio-hot:9000 | S3 API port is 9000 inside Docker | +| `MINIO_SECURE` | false | set true if TLS to MinIO | +| `MINIO_ACCESS_KEY` | minioadmin | | +| `MINIO_SECRET_KEY` | minioadmin | | +| `MODEL_PATH` | /models/best_conditional.pt | mounted from host | +| `MODEL_NAME` | best_conditional | stored in DB | +| `BATCH_LIMIT` | 500 | safety cap per run | +| `FRUITS` (optional) | Apple,Orange,Grape,Strawberry | if enabled in code | + +If you’re behind **NetFree/proxy**, copy your CA file to `deploy/certs/` and use the Dockerfile section that installs CA + `update-ca-certificates`. + +--- + +## 🐳 Build & Run (Docker) + +From `services/ripeness-ml/`: + +```bash +docker compose -f docker-compose.ripeness.yml build ripeness-api +docker compose -f docker-compose.ripeness.yml up -d ripeness-api +``` + +**Health check:** + +```bash +curl http://localhost:8088/healthz +``` + +# **logs** +```bash +docker logs -n 200 ripeness-api +``` + +--- + +## 🔌 API + +**Base URL:** `http://localhost:8088` + +### POST `/predict-last-week` +Runs prediction for images from the last 7 days that don’t have a record yet in `ripeness_predictions`. + +```bash +curl -X POST http://localhost:8088/predict-last-week +# -> {"processed": 17} +``` + +### POST `/predict-batch` +Run for a custom time window and limit. + +**Request body (JSON):** +```json +{ + "since_ts": "2025-10-01T00:00:00", + "limit": 1000 +} +``` + +**Example:** +```bash +curl -X POST http://localhost:8088/predict-batch -H "Content-Type: application/json" -d '{"since_ts":"2025-10-01T00:00:00","limit":1000}' +``` + +### POST `/rollup/weekly` +Creates a weekly snapshot into `ripeness_weekly_rollups_ts` for the last 7 days (creates the table if missing). + +```bash +curl -X POST http://localhost:8088/rollup/weekly +# -> {"ok": true} +``` + +--- + +## 🧮 Database schema + +### Predictions table +```sql +CREATE TABLE IF NOT EXISTS ripeness_predictions ( + id BIGSERIAL PRIMARY KEY, + inference_log_id BIGINT NOT NULL REFERENCES inference_logs(id) ON DELETE CASCADE, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), + ripeness_label TEXT NOT NULL CHECK (ripeness_label IN ('ripe','unripe','overripe')), + ripeness_score DOUBLE PRECISION NOT NULL, + model_name TEXT NOT NULL, + UNIQUE (inference_log_id) +); +``` + +### Weekly rollups +```sql +CREATE TABLE IF NOT EXISTS ripeness_weekly_rollups_ts ( + id BIGSERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), -- snapshot time + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + fruit_type TEXT NOT NULL, + cnt_total INTEGER NOT NULL, + cnt_ripe INTEGER NOT NULL, + cnt_unripe INTEGER NOT NULL, + cnt_overripe INTEGER NOT NULL, + pct_ripe DOUBLE PRECISION NOT NULL +); +``` + +--- + +## 🔍 Useful queries + +**Show latest predictions joined with inference logs:** +```sql +SELECT il.id, il.fruit_type, il.image_url, rp.ripeness_label, rp.ripeness_score, rp.model_name, rp.ts +FROM inference_logs il +JOIN ripeness_predictions rp ON rp.inference_log_id = il.id +ORDER BY rp.ts DESC +LIMIT 20; +``` + +**Show rollup snapshots:** +```sql +SELECT ts::date AS snapshot_day, fruit_type, cnt_total, +cnt_ripe, cnt_unripe, cnt_overripe, +ROUND(pct_ripe*100,2) AS pct_ripe_pct +FROM ripeness_weekly_rollups_ts +ORDER BY ts DESC, fruit_type; +``` + +**From Docker (network agcloud_ag_cloud):** +```bash +docker run --rm --network agcloud_ag_cloud -e PGPASSWORD=pg123 postgres:16-alpine psql -h postgres -U missions_user -d missions_db -c "SELECT ts::date AS snapshot_day, fruit_type, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, ROUND(pct_ripe*100,2) AS pct_ripe_pct + FROM ripeness_weekly_rollups_ts + ORDER BY ts DESC, fruit_type;" +``` + +--- + +## 🕒 Scheduling (Windows Task Scheduler) + +Create a weekly job that first predicts, then rolls up. + +**run_weekly.ps1:** +```powershell +Invoke-RestMethod -Method Post -Uri "http://localhost:8088/predict-last-week" +# note: /predict-last-week now triggers the weekly rollup automatically, +# so a single call is sufficient (no duplicate predictions are inserted). +``` + +**Register task:** +```bash +schtasks /Create /TN "RipenessWeekly" /TR "powershell.exe -ExecutionPolicy Bypass -File C:\path\run_weekly.ps1" /SC WEEKLY /D MON /ST 03:00 +``` + +--- + +## 🧰 Troubleshooting + +- **MinIO errors / 9000 vs 9001:** inside Docker network always use `minio-hot:9000` (S3 API). + Ports 9001/9002 are host-exposed console/proxy. +- **SignatureDoesNotMatch:** wrong `MINIO_ACCESS_KEY`/`SECRET_KEY` or endpoint (should be the S3 API). +- **Model FRUITS mismatch:** ensure the FRUITS list in code matches the model checkpoint (e.g. include Grape if trained). +- **SSL to PyPI (NetFree/proxy):** add your CA to the image and run `update-ca-certificates`. +- **No rows processed:** endpoint processes only inference logs without an existing prediction; expand window with `/predict-batch`. + +--- + +## 👩‍💻 Maintainer + +**Name:** Ayala +**Service name:** ripeness-api +**Ports:** 8088/tcp diff --git a/AgCloud/services/ripeness-ml/api/ripeness_api.py b/AgCloud/services/ripeness-ml/api/ripeness_api.py new file mode 100644 index 000000000..0ea1f6991 --- /dev/null +++ b/AgCloud/services/ripeness-ml/api/ripeness_api.py @@ -0,0 +1,175 @@ +# scripts/ripeness_api.py +from fastapi import FastAPI +from pydantic import BaseModel +from datetime import datetime, timedelta +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), "")) + +from jobs.weekly_ripeness_job import ( + get_conn, + fetch_from_minio, + load_image_for_model, + predict_ripeness, +) + +app = FastAPI(title="Ripeness Service") + + +class BatchRequest(BaseModel): + since_ts: datetime | None = None + limit: int = 500 + + +def run_batch(since_ts: datetime | None, limit: int) -> int: + if since_ts is None: + since_ts = datetime.utcnow() - timedelta(days=7) + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT il.id, il.ts, il.fruit_type, il.image_url + FROM inference_logs il + LEFT JOIN ripeness_predictions rp ON rp.inference_log_id = il.id + WHERE il.ts >= %s + AND rp.id IS NULL + ORDER BY il.id ASC + LIMIT %s; + """, (since_ts, limit)) + rows = cur.fetchall() + + processed = 0 + # Generate a new run_id for this batch (once per batch) + with get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT gen_random_uuid()") + run_id = cur.fetchone()[0] + + for inflog_id, ts, fruit_type, image_url in rows: + try: + img_bytes = fetch_from_minio(image_url) + tensor = load_image_for_model(img_bytes) + label, score = predict_ripeness(tensor, fruit_type) + + # Parse bucket and object_key from image_url (expects format minio://bucket/object_key) + device_id = None + if image_url.startswith("minio://"): + path = image_url[len("minio://"):] + if "/" in path: + bucket, object_key = path.split("/", 1) + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT device_id FROM files + WHERE bucket = %s AND object_key = %s + """, (bucket, object_key)) + res = cur.fetchone() + device_id = res[0] if res else None + + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + INSERT INTO ripeness_predictions + (inference_log_id, ts, ripeness_label, ripeness_score, model_name, run_id, device_id) + VALUES (%s, now(), %s, %s, %s, %s, %s) + ON CONFLICT (inference_log_id) DO NOTHING; + """, (inflog_id, label, score, os.getenv("MODEL_NAME", "best_conditional"), run_id, device_id)) + processed += 1 + except Exception as e: + print(f"[ERR] inflog_id={inflog_id} :: {e}") + return processed + + +@app.get("/healthz") +def healthz(): + return {"ok": True} + + +@app.post("/predict-batch") +def predict_batch(req: BatchRequest): + n = run_batch(req.since_ts, req.limit) + return {"processed": n} + + +@app.post("/predict-last-week") +def predict_last_week(): + n = run_batch(None, int(os.getenv("BATCH_LIMIT", "500"))) + # After predicting new images, immediately create the weekly rollup + # This keeps the workflow to a single endpoint call (no duplicates because + # predictions use ON CONFLICT DO NOTHING) + try: + insert_weekly_rollup() + return {"processed": n, "rollup": True} + except Exception as e: + # Log the error but still return the number of processed items + print(f"[ERR] rollup: {e}") + return {"processed": n, "rollup": False, "error": str(e)} + + +def insert_weekly_rollup(): + ddl = """ + CREATE TABLE IF NOT EXISTS ripeness_weekly_rollups_ts ( + id BIGSERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), + window_start TIMESTAMPTZ NOT NULL, + window_end TIMESTAMPTZ NOT NULL, + fruit_type TEXT NOT NULL, + device_id TEXT, + run_id UUID, + cnt_total INTEGER NOT NULL, + cnt_ripe INTEGER NOT NULL, + cnt_unripe INTEGER NOT NULL, + cnt_overripe INTEGER NOT NULL, + pct_ripe DOUBLE PRECISION NOT NULL + ); + CREATE INDEX IF NOT EXISTS ix_rwrt_ts ON ripeness_weekly_rollups_ts(ts); + CREATE INDEX IF NOT EXISTS ix_rwrt_fruit_ts ON ripeness_weekly_rollups_ts(fruit_type, ts); + CREATE INDEX IF NOT EXISTS ix_rwrt_device ON ripeness_weekly_rollups_ts(device_id); + CREATE INDEX IF NOT EXISTS ix_rwrt_run ON ripeness_weekly_rollups_ts(run_id); + """ + + # optional filter by fruits from environment (comma-separated) + fruits_env = os.getenv("FRUITS") + fruits = None + fruit_where = "" + if fruits_env: + fruits = [f.strip() for f in fruits_env.split(",") if f.strip()] + # use = ANY(%s) with a TEXT[] parameter + fruit_where = "WHERE il.fruit_type = ANY(%s)" + + sql = """ + WITH w AS ( + SELECT now() - interval '7 days' AS ws, now() AS we + ), + agg AS ( + SELECT + il.fruit_type, + rp.device_id, + rp.run_id, + COUNT(*) AS cnt_total, + SUM(CASE WHEN rp.ripeness_label='ripe' THEN 1 ELSE 0 END) AS cnt_ripe, + SUM(CASE WHEN rp.ripeness_label='unripe' THEN 1 ELSE 0 END) AS cnt_unripe, + SUM(CASE WHEN rp.ripeness_label='overripe' THEN 1 ELSE 0 END) AS cnt_overripe + FROM ripeness_predictions rp + JOIN inference_logs il ON il.id = rp.inference_log_id + JOIN w ON rp.ts >= w.ws AND rp.ts < w.we + """ + ("\n " + fruit_where if fruit_where else "") + """ + GROUP BY il.fruit_type, rp.device_id, rp.run_id + ) + INSERT INTO ripeness_weekly_rollups_ts + (ts, window_start, window_end, fruit_type, device_id, run_id, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, pct_ripe) + SELECT + now(), (SELECT ws FROM w), (SELECT we FROM w), + fruit_type, device_id, run_id, cnt_total, cnt_ripe, cnt_unripe, cnt_overripe, + CASE WHEN cnt_total>0 THEN cnt_ripe::double precision/cnt_total ELSE 0 END + FROM agg; + """ + + with get_conn() as conn, conn.cursor() as cur: + cur.execute(ddl) + if fruits: + # psycopg2 adapts Python list to SQL array + cur.execute(sql, (fruits,)) + else: + cur.execute(sql) + return True + + +@app.post("/rollup/weekly") +def rollup_weekly(): + insert_weekly_rollup() + return {"ok": True} diff --git a/AgCloud/services/ripeness-ml/checkpoints/eval/classification_report.txt b/AgCloud/services/ripeness-ml/checkpoints/eval/classification_report.txt new file mode 100644 index 000000000..f2e2f5a54 --- /dev/null +++ b/AgCloud/services/ripeness-ml/checkpoints/eval/classification_report.txt @@ -0,0 +1,13 @@ + precision recall f1-score support + + unripe 1.0000 0.9981 0.9990 1041 + ripe 0.9983 1.0000 0.9991 1164 + overripe 1.0000 1.0000 1.0000 1534 + + accuracy 0.9995 3739 + macro avg 0.9994 0.9994 0.9994 3739 +weighted avg 0.9995 0.9995 0.9995 3739 + + +Accuracy: 0.9995 +Macro-F1: 0.9994 diff --git a/AgCloud/services/ripeness-ml/checkpoints/eval/metrics.json b/AgCloud/services/ripeness-ml/checkpoints/eval/metrics.json new file mode 100644 index 000000000..00bdd6890 --- /dev/null +++ b/AgCloud/services/ripeness-ml/checkpoints/eval/metrics.json @@ -0,0 +1,9 @@ +{ + "accuracy": 0.9994650976196844, + "macro_f1": 0.9993933641465831, + "per_class_f1": { + "unripe": 0.9990384615384615, + "ripe": 0.9991416309012876, + "overripe": 1.0 + } +} \ No newline at end of file diff --git a/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional.pt b/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional.pt new file mode 100644 index 000000000..0637d2fd0 Binary files /dev/null and b/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional.pt differ diff --git a/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt b/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt new file mode 100644 index 000000000..29b9cfb1c Binary files /dev/null and b/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt differ diff --git a/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt b/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt new file mode 100644 index 000000000..c9ef651e1 Binary files /dev/null and b/AgCloud/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt differ diff --git a/AgCloud/services/ripeness-ml/configs/config.yaml b/AgCloud/services/ripeness-ml/configs/config.yaml new file mode 100644 index 000000000..bfd6c7863 --- /dev/null +++ b/AgCloud/services/ripeness-ml/configs/config.yaml @@ -0,0 +1,25 @@ +seed: 42 +classes: ["unripe", "ripe", "overripe"] +img_size: 224 +batch_size: 32 +num_workers: 0 +epochs_frozen: 5 +epochs_unfrozen: 10 +lr: 0.0003 +weight_decay: 0.0001 +label_smoothing: 0.05 +use_class_weights: true +train_dir: "data/train" +val_dir: "data/val" +test_dir: "data/test" +checkpoint_dir: "checkpoints/mobilenet_v3_large" +best_metric: "f1_macro" + +fruits: ["apple","banana","orange"] +ripeness: ["unripe","ripe","overripe"] + +csv: + train: "data_mt_train/train.csv" + val: "data_mt_train/val.csv" + test: "data_mt_test/test.csv" + diff --git a/AgCloud/services/ripeness-ml/deploy/Dockerfile b/AgCloud/services/ripeness-ml/deploy/Dockerfile new file mode 100644 index 000000000..a232874d7 --- /dev/null +++ b/AgCloud/services/ripeness-ml/deploy/Dockerfile @@ -0,0 +1,59 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates openssl libpq-dev build-essential gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY deploy/certs/ /usr/local/share/ca-certificates/ +RUN set -eux; \ + for f in /usr/local/share/ca-certificates/*.cer; do \ + [ -f "$f" ] && openssl x509 -inform der -in "$f" -out "${f%.cer}.crt" && rm -f "$f" || true; \ + done; \ + update-ca-certificates + +RUN printf "[global]\n\ +cert = /etc/ssl/certs/ca-certificates.crt\n\ +index-url = https://pypi.org/simple\n\ +trusted-host =\n\ + pypi.org\n\ + files.pythonhosted.org\n\ + download.pytorch.org\n" > /etc/pip.conf + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +COPY requirements.txt /app/ +RUN pip install --no-cache-dir --timeout 120 --index-url https://download.pytorch.org/whl/cpu \ + --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org \ + torch==2.3.1 torchvision==0.18.1 \ + && pip install --no-cache-dir -r /app/requirements.txt \ + && pip install --no-cache-dir fastapi "uvicorn[standard]" +COPY api/ /app/api/ +COPY model/ /app/model +COPY jobs/ /app/jobs/ +COPY configs/ /app/configs/ +# Create models directory and copy model file +RUN mkdir -p /app/models +COPY checkpoints/mobilenet_v3_large/best_conditional.pt /app/models/best_conditional.pt + +# Create __init__.py files for Python modules +RUN touch /app/model/__init__.py \ + && touch /app/model/architecture/__init__.py \ + && touch /app/model/data/__init__.py \ + && touch /app/jobs/__init__.py \ + && touch /app/api/__init__.py + +ENV PYTHONPATH=/app + +EXPOSE 8088 +ENV MODEL_PATH=/app/models/best_conditional.pt \ + MODEL_NAME=best_conditional \ + BATCH_LIMIT=500 + +CMD ["uvicorn", "api.ripeness_api:app", "--host", "0.0.0.0", "--port", "8088", "--reload"] diff --git a/AgCloud/services/ripeness-ml/deploy/docker-compose.ripeness.yml b/AgCloud/services/ripeness-ml/deploy/docker-compose.ripeness.yml new file mode 100644 index 000000000..0ac0a30f2 --- /dev/null +++ b/AgCloud/services/ripeness-ml/deploy/docker-compose.ripeness.yml @@ -0,0 +1,36 @@ +services: + ripeness-api: + image: ripeness-api:latest + build: + context: .. + dockerfile: deploy/Dockerfile + container_name: ripeness-api + environment: + PGHOST: postgres + PGPORT: "5432" + PGDATABASE: missions_db + PGUSER: missions_user + PGPASSWORD: pg123 + + MINIO_ENDPOINT: minio-hot:9000 + MINIO_SECURE: "false" + MINIO_ACCESS_KEY: minioadmin + MINIO_SECRET_KEY: minioadmin123 + + MODEL_NAME: best_conditional + BATCH_LIMIT: "500" + FRUITS: "Apple,Banana,Orange" + volumes: + - ../checkpoints:/app/checkpoints + - ../configs:/app/configs + - ../model:/app/model + networks: + - agcloud_net + ports: + - "8091:8088" + restart: unless-stopped + +networks: + agcloud_net: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/services/ripeness-ml/jobs/weekly_ripeness_job.py b/AgCloud/services/ripeness-ml/jobs/weekly_ripeness_job.py new file mode 100644 index 000000000..387b7c034 --- /dev/null +++ b/AgCloud/services/ripeness-ml/jobs/weekly_ripeness_job.py @@ -0,0 +1,167 @@ +# file: services/weekly_ripeness_job.py +import io +import time +import torch +import psycopg2 +import datetime as dt +from urllib.parse import urlparse +from minio import Minio +from PIL import Image +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) # so "models" is importable +from model.architecture.mobilenet_v3_large_head import build_conditional +from tqdm.auto import tqdm + + +from pathlib import Path +try: + from dotenv import load_dotenv + env_path = Path(__file__).resolve().parents[1] / ".env" + if env_path.exists(): + load_dotenv(env_path.as_posix()) +except Exception: + pass + +# ---- ENV ---- +PGHOST = os.getenv("PGHOST", "db") +PGPORT = int(os.getenv("PGPORT", "5432")) +PGDATABASE = os.getenv("PGDATABASE", "missions_db") +PGUSER = os.getenv("PGUSER", "missions_user") +PGPASSWORD = os.getenv("PGPASSWORD", "pg123") + +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "127.0.0.1:9000") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").lower() == "true" +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin") + +MODEL_PATH = os.getenv("MODEL_PATH", "/models/best_conditional.pt") +MODEL_NAME = os.getenv("MODEL_NAME", "best_conditional") +BATCH_LIMIT = int(os.getenv("BATCH_LIMIT", "200")) + +# ----- labels & fruits mapping ----- +LABELS = ["unripe", "ripe", "overripe"] +FRUITS = ["Apple", "Banana", "Orange", "."] +FRUIT2IDX = {name.lower(): i for i, name in enumerate(FRUITS)} + +# ----- build model & load weights ----- +device = "cuda" if torch.cuda.is_available() else "cpu" +num_ripeness = len(LABELS) +num_fruits = len(FRUITS) + +model = build_conditional(num_ripeness=num_ripeness, num_fruits=num_fruits, embed_dim=16).to(device) + +ckpt = torch.load(MODEL_PATH, map_location=device) +state = ckpt["state_dict"] if (isinstance(ckpt, dict) and "state_dict" in ckpt) else ckpt + +assert state["fruit_embed.weight"].shape[0] == num_fruits, \ + f"Checkpoint expects {state['fruit_embed.weight'].shape[0]} fruits, but FRUITS has {num_fruits}" + +model.load_state_dict(state, strict=True) +model.eval() + +def load_image_for_model(img_bytes): + im = Image.open(io.BytesIO(img_bytes)).convert("RGB") + from torchvision import transforms + preprocess = transforms.Compose([ + transforms.Resize((224, 224)), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]) + ]) + return preprocess(im).unsqueeze(0).to(device) + +@torch.no_grad() +def predict_ripeness(img_tensor, fruit_type: str): + idx = FRUIT2IDX.get(fruit_type.lower()) + if idx is None: + raise KeyError(f"skip: fruit '{fruit_type}' not in trained set {FRUITS}") + fruit_idx_tensor = torch.tensor([idx], dtype=torch.long, device=device) + logits = model(img_tensor, fruit_idx_tensor) + probs = torch.softmax(logits, dim=1).squeeze(0).cpu().numpy() + j = int(probs.argmax()) + return LABELS[j], float(probs[j]) + +# ---- MINIO ---- +minio_client = Minio(MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, secure=MINIO_SECURE) + +def fetch_from_minio(image_url: str) -> bytes: + p = urlparse(image_url) + path = p.path.lstrip("/") + bucket, *rest = path.split("/", 1) + if not rest: + raise ValueError(f"Invalid URL path for MinIO: {image_url}") + obj = rest[0] + resp = minio_client.get_object(bucket, obj) + data = resp.read() + resp.close() + resp.release_conn() + return data + +# ---- DB ---- +def get_conn(): + return psycopg2.connect( + host=PGHOST, port=PGPORT, dbname=PGDATABASE, user=PGUSER, password=PGPASSWORD + ) + +def main(): + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT il.id, il.ts, il.fruit_type, il.image_url + FROM inference_logs il + LEFT JOIN ripeness_predictions rp ON rp.inference_log_id = il.id + WHERE il.ts >= now() - interval '7 days' + AND rp.id IS NULL + ORDER BY il.id ASC + LIMIT %s; + """, (BATCH_LIMIT,)) + rows = cur.fetchall() + + processed = 0 + + # generate a single run_id for this batch + with get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT gen_random_uuid()") + run_id = cur.fetchone()[0] + + for inflog_id, ts, fruit_type, image_url in tqdm(rows, desc="Predicting ripeness"): + try: + if processed % 20 == 0: + print(f"...processed {processed} so far") + img_bytes = fetch_from_minio(image_url) + tensor = load_image_for_model(img_bytes) + try: + label, score = predict_ripeness(tensor, fruit_type) + except KeyError as skip: + print(f"[SKIP] inflog_id={inflog_id} :: {skip}") + continue + + # derive bucket/object_key and lookup device_id + device_id = None + try: + p = urlparse(image_url) + path = p.path.lstrip('/') + if '/' in path: + bucket, object_key = path.split('/', 1) + with get_conn() as conn, conn.cursor() as cur: + cur.execute("SELECT device_id FROM files WHERE bucket = %s AND object_key = %s", (bucket, object_key)) + res = cur.fetchone() + device_id = res[0] if res else None + except Exception: + # keep device_id as None if parsing/lookup fails + device_id = None + + with get_conn() as conn, conn.cursor() as cur: + cur.execute(""" + INSERT INTO ripeness_predictions + (inference_log_id, ts, ripeness_label, ripeness_score, model_name, run_id, device_id) + VALUES (%s, now(), %s, %s, %s, %s, %s) + ON CONFLICT (inference_log_id) DO NOTHING; + """, (inflog_id, label, score, MODEL_NAME, run_id, device_id)) + processed += 1 + print(f"[OK] inflog_id={inflog_id} -> {label} ({score:.4f})") + except Exception as e: + print(f"[ERR] inflog_id={inflog_id} url={image_url} :: {e}") + + print(f"Done. processed={processed}") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/ripeness-ml/model/architecture/mobilenet_v3_large_head.py b/AgCloud/services/ripeness-ml/model/architecture/mobilenet_v3_large_head.py new file mode 100644 index 000000000..3457d6953 --- /dev/null +++ b/AgCloud/services/ripeness-ml/model/architecture/mobilenet_v3_large_head.py @@ -0,0 +1,34 @@ +import torch.nn as nn +import torch +from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights + +class RipenessModelConditional(nn.Module): + """ + Image -> MobileNetV3 backbone + Fruit type (idx) -> Embedding + Concatenate -> Linear -> ripeness logits + """ + def __init__(self, num_ripeness: int, num_fruits: int, embed_dim: int = 16): + super().__init__() + weights = MobileNet_V3_Large_Weights.IMAGENET1K_V2 + self.backbone = mobilenet_v3_large(weights=weights) + in_feats = self.backbone.classifier[-1].in_features + self.backbone.classifier[-1] = nn.Identity() + self.fruit_embed = nn.Embedding(num_fruits, embed_dim) + self.head = nn.Linear(in_feats + embed_dim, num_ripeness) + + def forward(self, x, fruit_idx): + feats = self.backbone(x) # [B, in_feats] + fvec = self.fruit_embed(fruit_idx) # [B, embed_dim] + out = torch.cat([feats, fvec], dim=1) # [B, in_feats+embed_dim] + return self.head(out) # [B, num_ripeness] + +def build_conditional(num_ripeness: int, num_fruits: int, embed_dim: int = 16) -> nn.Module: + return RipenessModelConditional(num_ripeness, num_fruits, embed_dim) + +def build_model(num_classes: int) -> nn.Module: + weights = MobileNet_V3_Large_Weights.IMAGENET1K_V2 + model = mobilenet_v3_large(weights=weights) + in_feats = model.classifier[-1].in_features + model.classifier[-1] = nn.Linear(in_feats, num_classes) + return model diff --git a/AgCloud/services/ripeness-ml/model/data/data_multitask.py b/AgCloud/services/ripeness-ml/model/data/data_multitask.py new file mode 100644 index 000000000..9b01959a3 --- /dev/null +++ b/AgCloud/services/ripeness-ml/model/data/data_multitask.py @@ -0,0 +1,48 @@ +from torch.utils.data import Dataset, DataLoader +from PIL import Image +from torchvision import transforms +import pandas as pd + +IMAGENET_MEAN=(0.485,0.456,0.406); IMAGENET_STD=(0.229,0.224,0.225) + +def build_transforms(img_size=224): + from torchvision import transforms as T + t_train = T.Compose([ + T.RandomResizedCrop(img_size, scale=(0.7,1.0)), + T.RandomHorizontalFlip(), + T.ColorJitter(0.2,0.2,0.2,0.05), + T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + t_val = T.Compose([ + T.Resize(int(img_size*1.15)), T.CenterCrop(img_size), + T.ToTensor(), T.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + return t_train, t_val + +class CSVConditional(Dataset): + def __init__(self, csv_path, fruit_to_idx, ripeness_to_idx, transform=None): + self.df = pd.read_csv(csv_path) + self.fruit_to_idx = fruit_to_idx + self.ripeness_to_idx = ripeness_to_idx + self.transform = transform + + def __len__(self): return len(self.df) + + def __getitem__(self, i): + row = self.df.iloc[i] + img = Image.open(row["path"]).convert("RGB") + if self.transform: img = self.transform(img) + fruit_idx = self.fruit_to_idx[row["fruit"]] + ripeness_idx = self.ripeness_to_idx[row["ripeness"]] + return img, fruit_idx, ripeness_idx + +def make_loaders(csv_train, csv_val, img_size, batch_size, num_workers, fruits, ripeness): + t_train, t_val = build_transforms(img_size) + f2i = {f:i for i,f in enumerate(fruits)} + r2i = {r:i for i,r in enumerate(ripeness)} + dtr = CSVConditional(csv_train, f2i, r2i, t_train) + dva = CSVConditional(csv_val, f2i, r2i, t_val) + from torch.utils.data import DataLoader + ltr = DataLoader(dtr, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True) + lva = DataLoader(dva, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True) + return ltr, lva, f2i, r2i diff --git a/AgCloud/services/ripeness-ml/model/data/transforms.py b/AgCloud/services/ripeness-ml/model/data/transforms.py new file mode 100644 index 000000000..a8f2c4b5b --- /dev/null +++ b/AgCloud/services/ripeness-ml/model/data/transforms.py @@ -0,0 +1,18 @@ +from torchvision import transforms +IMAGENET_MEAN=(0.485,0.456,0.406); IMAGENET_STD=(0.229,0.224,0.225) + +def build_transforms(img_size=224): + t_train = transforms.Compose([ + transforms.RandomResizedCrop(img_size, scale=(0.7,1.0)), + transforms.RandomHorizontalFlip(), + transforms.ColorJitter(0.2,0.2,0.2,0.05), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + t_val = transforms.Compose([ + transforms.Resize(int(img_size*1.15)), + transforms.CenterCrop(img_size), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + return t_train, t_val diff --git a/AgCloud/services/ripeness-ml/model/training/evaluate_conditional.py b/AgCloud/services/ripeness-ml/model/training/evaluate_conditional.py new file mode 100644 index 000000000..e10977e01 --- /dev/null +++ b/AgCloud/services/ripeness-ml/model/training/evaluate_conditional.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +# Evaluate the conditional ripeness model on test/val CSVs. +# Outputs: +# - metrics.json (accuracy, macro_f1, per-class F1) +# - classification_report.txt +# - confusion_matrix.png + +import os, sys, json, yaml +from pathlib import Path +import numpy as np +import torch +from sklearn.metrics import accuracy_score, f1_score, classification_report, confusion_matrix +import matplotlib.pyplot as plt + +# --- make 'models' & 'training' importable when running as a script --- +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + +from model.architecture.mobilenet_v3_large_head import build_conditional +from data.data_multitask import CSVConditional, build_transforms + +IMAGENET_MEAN=(0.485,0.456,0.406) +IMAGENET_STD=(0.229,0.224,0.225) + +def softmax(x): + x = x - x.max(axis=1, keepdims=True) + e = np.exp(x) + return e / e.sum(axis=1, keepdims=True) + +def load_cfg(): + return yaml.safe_load(open(os.path.join(PROJECT_ROOT, "configs/config.yaml"), "r", encoding="utf-8")) + +def make_loader(csv_path, fruits, ripeness, img_size=224, batch_size=64, num_workers=0): + _, t_val = build_transforms(img_size) + f2i = {f:i for i,f in enumerate(fruits)} + r2i = {r:i for i,r in enumerate(ripeness)} + ds = CSVConditional(csv_path, f2i, r2i, transform=t_val) + from torch.utils.data import DataLoader + return DataLoader(ds, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True) + +def plot_confusion_matrix(cm, classes, out_png): + fig = plt.figure(figsize=(5.5, 4.5)) + ax = fig.add_subplot(111) + im = ax.imshow(cm, interpolation='nearest') + ax.set_title('Confusion Matrix') + fig.colorbar(im) + tick_marks = np.arange(len(classes)) + ax.set_xticks(tick_marks); ax.set_xticklabels(classes, rotation=45, ha="right") + ax.set_yticks(tick_marks); ax.set_yticklabels(classes) + ax.set_ylabel('True'); ax.set_xlabel('Predicted') + # write counts + thresh = cm.max() / 2.0 if cm.size else 0.5 + for i in range(cm.shape[0]): + for j in range(cm.shape[1]): + ax.text(j, i, format(cm[i, j], 'd'), + ha="center", va="center", + color="white" if cm[i, j] > thresh else "black") + fig.tight_layout() + fig.savefig(out_png, dpi=160) + plt.close(fig) + +if __name__ == "__main__": + cfg = load_cfg() + device = "cuda" if torch.cuda.is_available() else "cpu" + + fruits = cfg["fruits"] + ripeness = cfg["ripeness"] + + # choose CSV: prefer test.csv; if missing/empty -> use val.csv + csv_test = Path(cfg["csv"].get("test", "data_mt/test.csv")) + csv_val = Path(cfg["csv"].get("val", "data_mt/val.csv")) + csv_path = csv_test if csv_test.exists() and csv_test.stat().st_size > 50 else csv_val + if not csv_path.exists(): + raise SystemExit(f"CSV not found: {csv_path}. Run ingest to create it.") + + # dataloader + loader = make_loader( + str(csv_path), fruits, ripeness, + img_size=cfg.get("img_size", 224), + batch_size=cfg.get("batch_size", 32), + num_workers=cfg.get("num_workers", 0) + ) + + # model + ckpt_dir = cfg["checkpoint_dir"] + ckpt = os.path.join(ckpt_dir, "best_conditional.pt") + if not os.path.exists(ckpt): + raise SystemExit(f"Checkpoint not found: {ckpt}") + + model = build_conditional(num_ripeness=len(ripeness), num_fruits=len(fruits)) + model.load_state_dict(torch.load(ckpt, map_location="cpu")) + model.eval().to(device) + + # predict + y_true, y_pred = [], [] + probs_all = [] + with torch.no_grad(): + for x, fidx, ridx in loader: + x = x.to(device) + fidx = torch.as_tensor(fidx, device=device) + logits = model(x, fidx).cpu().numpy() + prob = softmax(logits) + preds = prob.argmax(1) + y_pred.extend(preds.tolist()) + y_true.extend(ridx.numpy().tolist()) + probs_all.append(prob) + + y_true = np.array(y_true) + y_pred = np.array(y_pred) + probs = np.concatenate(probs_all, axis=0) if probs_all else np.empty((0,len(ripeness))) + + # metrics + acc = float(accuracy_score(y_true, y_pred)) + macro_f1 = float(f1_score(y_true, y_pred, average="macro")) + per_class_f1 = f1_score(y_true, y_pred, average=None) + per_class = {ripeness[i]: float(per_class_f1[i]) for i in range(len(ripeness))} + report = classification_report(y_true, y_pred, target_names=ripeness, digits=4) + cm = confusion_matrix(y_true, y_pred) + + # outputs + out_dir = os.path.join(PROJECT_ROOT, "checkpoints", "eval") + os.makedirs(out_dir, exist_ok=True) + # confusion matrix PNG + cm_png = os.path.join(out_dir, "confusion_matrix.png") + plot_confusion_matrix(cm, ripeness, cm_png) + # classification report + with open(os.path.join(out_dir, "classification_report.txt"), "w", encoding="utf-8") as f: + f.write(report + "\n") + f.write(f"\nAccuracy: {acc:.4f}\nMacro-F1: {macro_f1:.4f}\n") + # json metrics + with open(os.path.join(out_dir, "metrics.json"), "w", encoding="utf-8") as f: + json.dump({"accuracy": acc, "macro_f1": macro_f1, "per_class_f1": per_class}, f, indent=2) + + print(f"Evaluated on: {csv_path}") + print(f"Accuracy: {acc:.4f} | Macro-F1: {macro_f1:.4f}") + print("Per-class F1:", per_class) + print(f"Saved: {cm_png} and classification_report.txt, metrics.json") diff --git a/AgCloud/services/ripeness-ml/model/training/train_conditional.py b/AgCloud/services/ripeness-ml/model/training/train_conditional.py new file mode 100644 index 000000000..c2bf56357 --- /dev/null +++ b/AgCloud/services/ripeness-ml/model/training/train_conditional.py @@ -0,0 +1,114 @@ +import os, yaml, torch +from torch import nn +from sklearn.metrics import accuracy_score, f1_score + +from model.architecture.mobilenet_v3_large_head import build_conditional +from data.data_multitask import make_loaders + + +def evaluate(model, loader, device): + model.eval() + y_true, y_pred = [], [] + with torch.no_grad(): + for x, fidx, ridx in loader: + x = x.to(device) + fidx = torch.as_tensor(fidx, device=device) + logits = model(x, fidx) + y_pred.extend(logits.argmax(1).cpu().numpy()) + y_true.extend(ridx.numpy()) + acc = accuracy_score(y_true, y_pred) + f1 = f1_score(y_true, y_pred, average="macro") + return acc, f1 + + +def train_phase(model, ltr, lva, device, epochs, lr, wd, ckpt_dir, tag, ce, patience=2): + from torch.optim import AdamW + from torch.optim.lr_scheduler import CosineAnnealingLR + + os.makedirs(ckpt_dir, exist_ok=True) + opt = AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=lr, weight_decay=wd) + sch = CosineAnnealingLR(opt, T_max=epochs) + + best_f1 = -1.0 + best_state = None + no_improve = 0 + + try: + for ep in range(1, epochs + 1): + model.train() + for x, fidx, ridx in ltr: + x = x.to(device) + fidx = torch.as_tensor(fidx, device=device) + ridx = torch.as_tensor(ridx, device=device) + + logits = model(x, fidx) + loss = ce(logits, ridx) + + opt.zero_grad() + loss.backward() + opt.step() + + acc, f1 = evaluate(model, lva, device) + sch.step() + print(f"[{tag} Epoch {ep}] val_acc={acc:.3f} val_f1={f1:.3f}") + + if f1 > best_f1 + 1e-4: + best_f1 = f1 + best_state = {k: v.cpu() for k, v in model.state_dict().items()} + torch.save(best_state, os.path.join(ckpt_dir, f"best_conditional_{tag}.pt")) + no_improve = 0 + else: + no_improve += 1 + if no_improve >= patience: + print(f"Early stopping ({tag}) — no improvement for {patience} epochs") + break + + except KeyboardInterrupt: + print("KeyboardInterrupt — saving best checkpoint so far...") + + finally: + if best_state is not None: + model.load_state_dict(best_state) + return model, best_f1 + + +if __name__ == "__main__": + cfg = yaml.safe_load(open("configs/config.yaml", "r", encoding="utf-8")) + device = "cuda" if torch.cuda.is_available() else "cpu" + + train_csv = cfg["csv"]["train"] + val_csv = cfg["csv"]["val"] + fruits = cfg["fruits"] + ripeness = cfg["ripeness"] + + ltr, lva, f2i, r2i = make_loaders( + train_csv, val_csv, + cfg["img_size"], cfg["batch_size"], cfg["num_workers"], + fruits, ripeness + ) + + model = build_conditional(num_ripeness=len(ripeness), num_fruits=len(fruits)).to(device) + ce = nn.CrossEntropyLoss() + + for p in model.backbone.features.parameters(): + p.requires_grad = False + + model, _ = train_phase( + model, ltr, lva, device, + cfg["epochs_frozen"], cfg["lr"], cfg["weight_decay"], + cfg["checkpoint_dir"], tag="frozen", ce=ce, patience=2 + + ) + + for p in model.parameters(): + p.requires_grad = True + + model, best_f1 = train_phase( + model, ltr, lva, device, + cfg["epochs_unfrozen"], cfg["lr"]/3, cfg["weight_decay"], + cfg["checkpoint_dir"], tag="unfrozen", ce=ce, patience=2 + ) + + os.makedirs(cfg["checkpoint_dir"], exist_ok=True) + torch.save(model.state_dict(), os.path.join(cfg["checkpoint_dir"], "best_conditional.pt")) + print("Saved:", os.path.join(cfg["checkpoint_dir"], "best_conditional.pt"), "| best F1:", best_f1) diff --git a/AgCloud/services/ripeness-ml/model/training/utils.py b/AgCloud/services/ripeness-ml/model/training/utils.py new file mode 100644 index 000000000..23e47edc7 --- /dev/null +++ b/AgCloud/services/ripeness-ml/model/training/utils.py @@ -0,0 +1,14 @@ +# import torch, random, numpy as np +# from collections import Counter + +# def set_seed(s=42): +# random.seed(s); np.random.seed(s); torch.manual_seed(s); torch.cuda.manual_seed_all(s) + +# def load_class_weights(trainloader, use=True): +# if not use: return None +# counts = Counter() +# for _,y in trainloader: +# for i in y.numpy(): counts[int(i)]+=1 +# total = sum(counts.values()) +# weights = [total/counts[i] for i in range(len(counts))] +# return torch.tensor(weights, dtype=torch.float32) diff --git a/AgCloud/services/ripeness-ml/requirements.txt b/AgCloud/services/ripeness-ml/requirements.txt new file mode 100644 index 000000000..0aa2be591 --- /dev/null +++ b/AgCloud/services/ripeness-ml/requirements.txt @@ -0,0 +1,16 @@ +torch==2.3.1 +torchvision==0.18.1 +# timm==1.0.9 +# scikit-learn==1.5.1 +matplotlib==3.9.0 +pillow==10.4.0 +pyyaml==6.0.2 +tqdm==4.66.4 +pandas==2.2.2 +onnx==1.16.0 +onnxruntime==1.18.1 +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +minio==7.2.10 +python-dotenv==1.0.1 +psycopg2-binary diff --git a/AgCloud/services/ripeness-ml/tools/data_prep/ingest_kaggle_multitask.py b/AgCloud/services/ripeness-ml/tools/data_prep/ingest_kaggle_multitask.py new file mode 100644 index 000000000..46daeed76 --- /dev/null +++ b/AgCloud/services/ripeness-ml/tools/data_prep/ingest_kaggle_multitask.py @@ -0,0 +1,79 @@ + +import argparse, csv, random +from pathlib import Path + +IMG_EXT = {".jpg",".jpeg",".png",".bmp",".tif",".tiff",".webp"} + +RIPENESS_MAP = { + "unripe": "unripe", + "fresh": "ripe", + "ripe": "ripe", + "rotten": "overripe", +} + +FRUIT_KEYS = ["apple", "banana", "orange", "pineapple"] + +def detect_from_path(p: Path): + names = [pp.name.lower().replace(" ", "").replace("_","") for pp in [p] + list(p.parents)] + fruit = None + ripeness = None + + for n in names: + for fk in FRUIT_KEYS: + if fk in n: + fruit = fk + break + for key, mapped in RIPENESS_MAP.items(): + if key in n: + ripeness = mapped + break + if fruit and ripeness: + return fruit, ripeness + return fruit, ripeness + +def gather(root: Path): + rows = [] # (path, fruit, ripeness) + for fp in root.rglob("*"): + if fp.is_file() and fp.suffix.lower() in IMG_EXT: + fruit, ripeness = detect_from_path(fp) + if fruit and ripeness: + rows.append((fp.resolve().as_posix(), fruit, ripeness)) + return rows + +def write_csv(path: Path, rows): + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w", newline="", encoding="utf-8") as f: + w = csv.writer(f) + w.writerow(["path","fruit","ripeness"]) + w.writerows(rows) + +if __name__ == "__main__": + ap = argparse.ArgumentParser(description="Create CSVs (train/val/test) with path,fruit,ripeness from Kaggle folders") + ap.add_argument("--src", required=True, help="path to .../dataset (the folder that contains train/ and test/)") + ap.add_argument("--outdir", default="data_mt", help="output folder for CSVs") + ap.add_argument("--split", default="0.8,0.2,0.0", help="train,val,test ratios") + ap.add_argument("--seed", type=int, default=42) + args = ap.parse_args() + + root = Path(args.src).resolve() + all_rows = gather(root) + if not all_rows: + raise SystemExit(f"No images found under: {root}. Check --src path.") + + random.seed(args.seed) + random.shuffle(all_rows) + + tr, va, te = [float(x) for x in args.split.split(",")] + assert abs(tr+va+te - 1.0) < 1e-6, "--split must sum to 1.0" + n = len(all_rows); ntr = int(tr*n); nv = int(va*n) + rows_tr = all_rows[:ntr]; rows_va = all_rows[ntr:ntr+nv]; rows_te = all_rows[ntr+nv:] + + out = Path(args.outdir) + write_csv(out/"train.csv", rows_tr) + write_csv(out/"val.csv", rows_va) + write_csv(out/"test.csv", rows_te) + + print(f"Saved CSVs in {out.resolve()}") + print(f" train.csv: {len(rows_tr)}") + print(f" val.csv: {len(rows_va)}") + print(f" test.csv: {len(rows_te)}") diff --git a/AgCloud/services/ripeness-ml/tools/data_prep/prepare_from_minio.py b/AgCloud/services/ripeness-ml/tools/data_prep/prepare_from_minio.py new file mode 100644 index 000000000..0afb84c54 --- /dev/null +++ b/AgCloud/services/ripeness-ml/tools/data_prep/prepare_from_minio.py @@ -0,0 +1,161 @@ +# AGCLOUD/services/ripeness-ml/scripts/prepare_from_minio.py +import os, io, csv, argparse, sys, re, datetime as dt +from pathlib import Path +from typing import Dict, List, Tuple, Optional +from minio import Minio +from minio.error import S3Error +from tqdm import tqdm +import random + +def parse_args(): + p = argparse.ArgumentParser(description="Sync labeled images from MinIO into local data/train|val|test/") + p.add_argument("--minio-url", required=True, help="e.g. http://127.0.0.1:9000") + p.add_argument("--access-key", required=False, default=os.getenv("MINIO_ACCESS_KEY","minioadmin")) + p.add_argument("--secret-key", required=False, default=os.getenv("MINIO_SECRET_KEY","minioadmin")) + p.add_argument("--secure", action="store_true", help="use HTTPS") + p.add_argument("--bucket", required=True, help="e.g. classification") + p.add_argument("--prefix", required=True, help="e.g. samples/2025/ or samples/") + p.add_argument("--outdir", default="data", help="local output root") + p.add_argument("--split", default="0.7,0.15,0.15", help="train,val,test ratios") + p.add_argument("--labels-csv", help="path to labels.csv (local file) OR object path in bucket (starts without leading /)") + p.add_argument("--infer-label-from-folder", action="store_true", help="take class name from folder under prefix") + p.add_argument("--from-date", help="YYYY-MM-DD (inclusive)") + p.add_argument("--to-date", help="YYYY-MM-DD (inclusive)") + p.add_argument("--last-days", type=int, help="Use only last N days under prefix (overrides from/to)") + p.add_argument("--dry-run", action="store_true") + return p.parse_args() + +def list_objects(client: Minio, bucket: str, prefix: str): + return client.list_objects(bucket, prefix=prefix, recursive=True) + +DATE_RE = re.compile(r"/(\d{4})/(\d{2})/(\d{2})(?:/|$)") + +def object_date(obj_name: str) -> Optional[dt.date]: + m = DATE_RE.search("/"+obj_name.strip("/")) + if not m: return None + y, mth, d = map(int, m.groups()) + return dt.date(y, mth, d) + +def load_labels_from_csv_local(csv_path: str) -> Dict[str, str]: + mapping = {} + with open(csv_path, "r", newline="", encoding="utf-8") as f: + r = csv.DictReader(f) + for row in r: + mapping[row["object"].strip()] = row["label"].strip() + return mapping + +def load_labels_from_csv_minio(client: Minio, bucket: str, obj_path: str) -> Dict[str, str]: + resp = client.get_object(bucket, obj_path) + data = resp.read().decode("utf-8") + mapping = {} + for row in csv.DictReader(io.StringIO(data)): + mapping[row["object"].strip()] = row["label"].strip() + return mapping + +def ensure_dirs(root: Path, classes: List[str]): + for split in ["train","val","test"]: + for c in classes: + (root/split/c).mkdir(parents=True, exist_ok=True) + +def main(): + args = parse_args() + tr, va, te = [float(x) for x in args.split.split(",")] + assert abs(tr+va+te - 1.0) < 1e-6, "--split must sum to 1.0" + + secure = args.secure or args.minio_url.startswith("https://") + endpoint = args.minio_url.replace("http://","").replace("https://","") + client = Minio(endpoint, access_key=args.access_key, secret_key=args.secret_key, secure=secure) + + # python arg names can't contain hyphen; fallback + access = getattr(args, "access_key", getattr(args, "access-key", None)) + secret = getattr(args, "secret_key", getattr(args, "secret-key", None)) + client = Minio(endpoint, access_key=access, secret_key=secret, secure=secure) + + # gather all candidate objects under prefix + objs = list(list_objects(client, args.bucket, args.prefix)) + if len(objs)==0: + print("No objects under prefix:", args.prefix); sys.exit(1) + + # filter by date + if args.last_days: + cutoff = dt.date.today() - dt.timedelta(days=args.last_days) + objs = [o for o in objs if (object_date(o.object_name) or dt.date.min) >= cutoff] + else: + dfrom = dt.date.fromisoformat(args.from_date) if args.from_date else None + dto = dt.date.fromisoformat(args.to_date) if args.to_date else None + if dfrom or dto: + def inrange(o): + od = object_date(o.object_name) + if not od: return False + if dfrom and od < dfrom: return False + if dto and od > dto: return False + return True + objs = [o for o in objs if inrange(o)] + + # Build label mapping + label_map: Dict[str,str] = {} + classes: set = set() + + if args.labels_csv: + if os.path.exists(args.labels_csv): + label_map = load_labels_from_csv_local(args.labels_csv) + else: + label_map = load_labels_from_csv_minio(client, args.bucket, args.labels_csv) + classes = set(label_map.values()) + candidates = [(o.object_name, label_map.get(o.object_name)) for o in objs if o.object_name in label_map] + elif args.infer_label_from_folder: + # Expect ...//... somewhere AFTER prefix + pref = args.prefix.strip("/") + candidates = [] + for o in objs: + rel = o.object_name[len(pref):].strip("/") + parts = rel.split("/") + if len(parts)>=2: + cls = parts[0] + candidates.append((o.object_name, cls)) + classes.add(cls) + if not classes: + print("Could not infer classes from folders; provide --labels-csv", file=sys.stderr) + sys.exit(2) + else: + print("Provide either --labels-csv or --infer-label-from-folder", file=sys.stderr) + sys.exit(2) + + classes = sorted(list(classes)) + print("Classes:", classes, "| samples:", len(candidates)) + root = Path(args.outdir) + ensure_dirs(root, classes) + + # stratified split by class + by_cls: Dict[str, List[str]] = {c: [] for c in classes} + for obj, lab in candidates: + if lab in by_cls: + by_cls[lab].append(obj) + for c in classes: random.shuffle(by_cls[c]) + + plan: List[Tuple[str, str]] = [] # (object_name, target_path) + for c in classes: + items = by_cls[c] + n = len(items); ntr = int(tr*n); nv = int(va*n) + tr_items = items[:ntr]; va_items = items[ntr:ntr+nv]; te_items = items[ntr+nv:] + for src in tr_items: + plan.append((src, str(root/ "train"/c/ Path(src).name))) + for src in va_items: + plan.append((src, str(root/ "val"/c/ Path(src).name))) + for src in te_items: + plan.append((src, str(root/ "test"/c/ Path(src).name))) + + if args.dry_run: + print(f"DRY-RUN: would download {len(plan)} files.") + return + + # download + for src, dst in tqdm(plan, desc="Downloading"): + dpath = Path(dst) + if dpath.exists(): continue + dpath.parent.mkdir(parents=True, exist_ok=True) + client.fget_object(args.bucket, src, dst) + print("Done. Data prepared under:", root.resolve()) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/ripeness-ml/tools/export/export_onnx_conditional.py b/AgCloud/services/ripeness-ml/tools/export/export_onnx_conditional.py new file mode 100644 index 000000000..1431ea0e6 --- /dev/null +++ b/AgCloud/services/ripeness-ml/tools/export/export_onnx_conditional.py @@ -0,0 +1,25 @@ +import torch, yaml, os +from model.architecture.mobilenet_v3_large_head import build_conditional + +if __name__ == "__main__": + cfg = yaml.safe_load(open("configs/config.yaml")) + fruits = cfg["fruits"] + ripeness = cfg["ripeness"] + + model = build_conditional(num_ripeness=len(ripeness), num_fruits=len(fruits)) + ckpt_path = os.path.join(cfg["checkpoint_dir"], "best_conditional.pt") + model.load_state_dict(torch.load(ckpt_path, map_location="cpu")) + model.eval() + + dummy_x = torch.randn(1, 3, cfg["img_size"], cfg["img_size"]) + dummy_f = torch.zeros(1, dtype=torch.long) # example fruit index + torch.onnx.export( + model, (dummy_x, dummy_f), + "ripeness_conditional.onnx", + input_names=["image", "fruit_idx"], + output_names=["ripeness_logits"], + dynamic_axes={"image": {0: "batch"}, "ripeness_logits": {0: "batch"}}, + opset_version=13 + ) + + print("✅ Exported: ripeness_conditional.onnx") diff --git a/AgCloud/services/ripeness-ml/tools/inference/infer_minio_batch.py b/AgCloud/services/ripeness-ml/tools/inference/infer_minio_batch.py new file mode 100644 index 000000000..0206a3791 --- /dev/null +++ b/AgCloud/services/ripeness-ml/tools/inference/infer_minio_batch.py @@ -0,0 +1,193 @@ +# AGCLOUD/services/ripeness-ml/scripts/infer_minio_batch.py +import argparse, os, sys, csv, json +from io import BytesIO +from pathlib import Path + +import numpy as np +from PIL import Image +from minio import Minio +from tqdm import tqdm +import onnxruntime as ort +from torchvision import transforms + +# ---- Configurable defaults ---- +IMAGENET_MEAN = (0.485, 0.456, 0.406) +IMAGENET_STD = (0.229, 0.224, 0.225) +IMG_TFM = transforms.Compose([ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), +]) + +DEFAULT_FRUITS = ["apple", "banana", "orange", "pineapple"] # order matters! +RIPENESS = ["unripe", "ripe", "overripe"] + +IMG_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp") + + +def parse_args(): + p = argparse.ArgumentParser( + description="Batch inference from MinIO prefix with conditional ONNX model (image + fruit_idx)." + ) + p.add_argument("--minio-url", required=True, help="http://127.0.0.1:9001") + p.add_argument("--access-key", default=os.getenv("MINIO_ACCESS_KEY", "minioadmin")) + p.add_argument("--secret-key", default=os.getenv("MINIO_SECRET_KEY", "minioadmin")) + p.add_argument("--secure", action="store_true", help="Use HTTPS") + + p.add_argument("--bucket", required=True, help="MinIO bucket name") + p.add_argument("--prefix", help="Prefix to scan, e.g. samples/2025/10/15 (ignored if --pairs-csv is used)") + + p.add_argument("--onnx", default="ripeness_conditional.onnx", help="Path to conditional ONNX model") + p.add_argument("--providers", nargs="*", default=None, help="ONNX Runtime providers list (default: CPU)") + + # Fruit specification + p.add_argument("--fruit", help="Fruit for ALL objects (apple|banana|orange|pineapple)") + p.add_argument("--pairs-csv", help="CSV file with columns: object,fruit (mapping per object)") + + # Fruits list order (so fruit_idx matches training) + p.add_argument("--fruits", default=None, + help='Fruits list in order, e.g. \'["apple","banana","orange","pineapple"]\' or "apple,banana,orange,pineapple"') + + # Output + p.add_argument("--out-csv", help="Optional: write results to CSV (object,fruit,label,prob_unripe,prob_ripe,prob_overripe)") + p.add_argument("--quiet", action="store_true", help="Do not print JSON lines to stdout") + + args = p.parse_args() + + if not args.pairs_csv and not (args.prefix and args.fruit): + p.error("Provide either --pairs-csv OR both --prefix and --fruit.") + + return args + + +def parse_fruits_list(fruits_arg): + if not fruits_arg: + return DEFAULT_FRUITS + s = fruits_arg.strip() + if s.startswith("["): + # JSON-ish + try: + import json as _json + lst = _json.loads(s) + return [x.strip().lower() for x in lst] + except Exception: + pass + # comma separated + return [x.strip().lower() for x in s.split(",") if x.strip()] + + +def softmax(x): + x = x - x.max(axis=1, keepdims=True) + e = np.exp(x) + return e / e.sum(axis=1, keepdims=True) + + +def is_image(name: str) -> bool: + return name.lower().endswith(IMG_EXTS) + + +def load_pairs_csv(path: str): + mapping = {} + with open(path, "r", newline="", encoding="utf-8") as f: + r = csv.DictReader(f) + if "object" not in r.fieldnames or "fruit" not in r.fieldnames: + raise SystemExit("pairs CSV must have columns: object,fruit") + for row in r: + obj = row["object"].strip() + fruit = row["fruit"].strip().lower() + mapping[obj] = fruit + return mapping + + +def open_minio(args): + secure = args.secure or args.minio_url.startswith("https://") + endpoint = args.minio_url.replace("http://", "").replace("https://", "") + return Minio(endpoint, access_key=args.access_key, secret_key=args.secret_key, secure=secure) + + +def main(): + args = parse_args() + fruits = parse_fruits_list(args.fruits) + + # Validate fruit names + fruit_set = set(fruits) + + # Prepare ONNX Runtime session + providers = args.providers or ["CPUExecutionProvider"] + sess = ort.InferenceSession(args.onnx, providers=providers) + + client = open_minio(args) + + # Prepare iterator over (object_name, fruit) + if args.pairs_csv: + mapping = load_pairs_csv(args.pairs_csv) + # Only iterate the keys present in the CSV (no MinIO list needed) + iterator = [(obj, mapping[obj]) for obj in mapping] + else: + fixed_fruit = args.fruit.lower() + if fixed_fruit not in fruit_set: + raise SystemExit(f"--fruit must be one of {fruits}; got {fixed_fruit}") + iterator = [] + for obj in client.list_objects(args.bucket, prefix=args.prefix, recursive=True): + if is_image(obj.object_name): + iterator.append((obj.object_name, fixed_fruit)) + + # Output CSV writer (optional) + csv_writer = None + if args.out_csv: + Path(args.out_csv).parent.mkdir(parents=True, exist_ok=True) + fcsv = open(args.out_csv, "w", newline="", encoding="utf-8") + csv_writer = csv.writer(fcsv) + csv_writer.writerow(["object", "fruit", "label", "prob_unripe", "prob_ripe", "prob_overripe"]) + + # Run predictions + for obj_name, fruit in tqdm(iterator, desc="Predicting"): + if fruit not in fruit_set: + # Unknown fruit -> skip + if not args.quiet: + print(json.dumps({"object": obj_name, "error": f"unknown fruit '{fruit}' (allowed {fruits})"}, ensure_ascii=False)) + continue + + # Fetch image bytes + if args.pairs_csv: + # object names in CSV must be full paths in bucket + resp = client.get_object(args.bucket, obj_name) + else: + resp = client.get_object(args.bucket, obj_name) + + try: + img = Image.open(BytesIO(resp.read())).convert("RGB") + finally: + resp.close(); resp.release_conn() + + x = IMG_TFM(img).unsqueeze(0).numpy() + fidx = np.array([fruits.index(fruit)], dtype=np.int64) + + logits = sess.run(["ripeness_logits"], {"images": x, "fruit_idx": fidx})[0] + prob = softmax(logits)[0] + idx = int(prob.argmax()) + label = RIPENESS[idx] + + record = { + "object": obj_name, + "fruit": fruit, + "label": label, + "probs": {RIPENESS[i]: float(prob[i]) for i in range(len(RIPENESS))} + } + + if not args.quiet: + print(json.dumps(record, ensure_ascii=False)) + + if csv_writer: + csv_writer.writerow([ + obj_name, fruit, label, + f"{prob[0]:.6f}", f"{prob[1]:.6f}", f"{prob[2]:.6f}" + ]) + + if csv_writer: + fcsv.close() + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/security/.dockerignore b/AgCloud/services/security/.dockerignore new file mode 100644 index 000000000..139597f9c --- /dev/null +++ b/AgCloud/services/security/.dockerignore @@ -0,0 +1,2 @@ + + diff --git a/AgCloud/services/security/.gitignore b/AgCloud/services/security/.gitignore new file mode 100644 index 000000000..092b0f3d7 --- /dev/null +++ b/AgCloud/services/security/.gitignore @@ -0,0 +1,60 @@ +# ---- Python cache / build artefacts ---- +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +build/ +dist/ +*.egg-info/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ +.conda/ +*.conda +*.ipynb_checkpoints + +# PyInstaller / py2exe artefacts +*.manifest +*.spec + +# Unit test / coverage reports +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ +.tox/ +.nox/ +.mypy_cache/ +.dmypy.json +.pyre/ +.pytype/ + +# Logs +*.log + +# IDE / editor settings +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db +desktop.ini + +# Jupyter notebooks (if you create some later) +.ipynb_checkpoints/ + +# Temporary files +*.tmp +*.bak +*.orig + + + diff --git a/AgCloud/services/security/README.md b/AgCloud/services/security/README.md new file mode 100644 index 000000000..e178059e1 --- /dev/null +++ b/AgCloud/services/security/README.md @@ -0,0 +1,58 @@ +# AgGuard Model Weights + +This directory contains the model weight files required for the AgGuard Security service. + +The weights **are not stored in Git** because they are large. +You must download them manually from Google Drive and place them directly in this folder. + +--- + +## 📥 1. Download Model Weights + +Download the following files from our shared Google Drive folder: + +🔗 **Google Drive (AgGuard Models)** +https://drive.google.com/drive/u/0/folders/1Qu7F4eG2XcINoUWFZt-1qpBQmRiPECPG + +Files you should download: + +``` +mask_yolov8.onnx +yolov8n-cls.pt +``` + +--- + +## 📁 2. Copy Files Into This Folder + +After downloading, copy BOTH files into: + +``` +services/security/weights/ +``` + +The final structure must look exactly like this: + +``` +services/security/weights/ +│ +├── mask_yolov8.onnx +└── yolov8n-cls.pt +``` + +No subfolders. +No renaming. + +--- + +## ⚠️ Important + +- Do **not** commit these files to Git. +- Do **not** rename the files. +- Anytime a new or updated weight is added to Google Drive, download it again and replace it here. + +--- + +## ❓ Need help? + +Ask Yehudit or the AgGuard developers if you are unsure about any of the required weight files. diff --git a/AgCloud/services/security/agguard/__init__.py b/AgCloud/services/security/agguard/__init__.py new file mode 100644 index 000000000..883256898 --- /dev/null +++ b/AgCloud/services/security/agguard/__init__.py @@ -0,0 +1,9 @@ +# __all__ = [ +# "logging_utils", +# "types", +# "roi", +# "motion", +# "detector", +# "tracker", +# "dispatch", +# ] diff --git a/AgCloud/services/security/agguard/adapters/__init__.py b/AgCloud/services/security/agguard/adapters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/adapters/s3_client.py b/AgCloud/services/security/agguard/adapters/s3_client.py new file mode 100644 index 000000000..905bca200 --- /dev/null +++ b/AgCloud/services/security/agguard/adapters/s3_client.py @@ -0,0 +1,107 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Optional +import cv2, numpy as np +import botocore +import boto3 +from botocore.config import Config as BotoConfig + +@dataclass(frozen=True) +class S3Config: + # For AWS S3 you usually set only region_name; credentials come from env/role. + region_name: str = "us-east-1" + # Optional explicit creds (otherwise rely on IAM role, env vars, or shared config) + aws_access_key_id: Optional[str] = None + aws_secret_access_key: Optional[str] = None + aws_session_token: Optional[str] = None + + # Optional: only if you still want to talk to a non-AWS S3-compatible endpoint (e.g., MinIO) + endpoint_url: Optional[str] = None + # Optional knobs + connect_timeout: float = 3.0 + read_timeout: float = 10.0 + max_attempts: int = 3 # boto retries on transient errors + + +class S3Client: + def __init__(self, cfg: S3Config): + self.cfg = cfg + boto_cfg = BotoConfig( + region_name=cfg.region_name, + s3={"addressing_style": "path"}, # AWS default + retries={"max_attempts": cfg.max_attempts, "mode": "standard"}, + connect_timeout=cfg.connect_timeout, + read_timeout=cfg.read_timeout, + # s3={"addressing_style": "path"}, + ) + session = boto3.Session( + aws_access_key_id=cfg.aws_access_key_id, + aws_secret_access_key=cfg.aws_secret_access_key, + aws_session_token=cfg.aws_session_token, + region_name=cfg.region_name, + ) + self.s3 = session.client("s3", endpoint_url=cfg.endpoint_url, config=boto_cfg) + + def fetch_image_bgr(self, bucket: str, object_key: str) -> np.ndarray: + """GET s3://bucket/object_key and decode as BGR numpy image.""" + try: + resp = self.s3.get_object(Bucket=bucket, Key=object_key) + data: bytes = resp["Body"].read() + except self.s3.exceptions.NoSuchKey: + raise FileNotFoundError(f"s3://{bucket}/{object_key} not found (NoSuchKey)") + except self.s3.exceptions.NoSuchBucket: + raise FileNotFoundError(f"s3://{bucket} does not exist (NoSuchBucket)") + except botocore.exceptions.EndpointConnectionError as e: + raise RuntimeError(f"S3 endpoint unreachable: {e}") + except botocore.exceptions.ClientError as e: + # surface 403/404/etc with context + code = e.response.get("Error", {}).get("Code") + msg = e.response.get("Error", {}).get("Message") + raise RuntimeError(f"S3 get_object failed ({code}): {msg}") from e + + arr = np.frombuffer(data, dtype=np.uint8) + img = cv2.imdecode(arr, cv2.IMREAD_COLOR) + if img is None: + raise ValueError(f"Failed to decode image bytes from s3://{bucket}/{object_key}") + return img + # add inside class S3Client: + + def put_file(self, bucket: str, key: str, local_path: str, content_type: Optional[str] = None) -> None: + extra = {"ContentType": content_type} if content_type else {} + self.s3.upload_file(local_path, bucket, key, ExtraArgs=extra or None) + + def delete_object(self, bucket: str, key: str) -> None: + self.s3.delete_object(Bucket=bucket, Key=key) + + def delete_prefix(self, bucket: str, prefix: str) -> None: + # batch delete up to 1000 keys per call + token = None + while True: + resp = self.s3.list_objects_v2(Bucket=bucket, Prefix=prefix, ContinuationToken=token) \ + if token else self.s3.list_objects_v2(Bucket=bucket, Prefix=prefix) + contents = resp.get("Contents", []) + if contents: + objects = [{"Key": obj["Key"]} for obj in contents] + self.s3.delete_objects(Bucket=bucket, Delete={"Objects": objects}) + if resp.get("IsTruncated"): + token = resp.get("NextContinuationToken") + else: + break + + + def get_object_stream(self, bucket: str, key: str, range_header: Optional[str] = None): + """Return boto3 get_object response (Body is a stream).""" + params = {"Bucket": bucket, "Key": key} + print(params) + if range_header: + params["Range"] = range_header + return self.s3.get_object(**params) + + # in agguard/adapters/s3_client.py + def put_bytes(self, bucket: str, key: str, data: bytes, content_type: str = "application/octet-stream"): + import io + bio = io.BytesIO(data) + self.client.upload_fileobj(bio, bucket, key, ExtraArgs={"ContentType": content_type}) + + + diff --git a/AgCloud/services/security/agguard/app/Dockerfile b/AgCloud/services/security/agguard/app/Dockerfile new file mode 100644 index 000000000..c7a526983 --- /dev/null +++ b/AgCloud/services/security/agguard/app/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Install basic dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 libgl1 ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy app +WORKDIR /app +COPY agguard/app/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY agguard ./agguard +COPY configs ./configs + +EXPOSE 8080 +HEALTHCHECK CMD curl -f http://localhost:8080/ || exit 1 + +CMD ["uvicorn", "agguard.app.media_proxy:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/AgCloud/services/security/agguard/app/__init__.py b/AgCloud/services/security/agguard/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/app/media_proxy.py b/AgCloud/services/security/agguard/app/media_proxy.py new file mode 100644 index 000000000..6035697bd --- /dev/null +++ b/AgCloud/services/security/agguard/app/media_proxy.py @@ -0,0 +1,166 @@ +# agguard/app/media_proxy.py +from __future__ import annotations +import os, re, mimetypes +from typing import Optional +from fastapi import FastAPI, Header, HTTPException, Request +from fastapi.responses import Response, StreamingResponse +import yaml + +from agguard.adapters.s3_client import S3Client, S3Config + +app = FastAPI(title="AgGuard Media Proxy", version="1.0") + +# ---------- load config ---------- +CFG_PATH = os.getenv("AGGUARD_CFG", "/app/configs/default.yaml") +with open(CFG_PATH, "r", encoding="utf-8") as f: + _cfg = yaml.safe_load(f) or {} + +_s3cfg = _cfg.get("s3", {}) or {} +_vcfg = _cfg.get("video", {}) or {} +BUCKET = _vcfg.get("bucket") +PREFIX = (_vcfg.get("prefix") or "security/incidents").strip("/") + +if not BUCKET: + raise RuntimeError("video.bucket is not configured in your YAML (configs/default.yaml)") + +s3 = S3Client(S3Config( + region_name=_s3cfg.get("region_name", "us-east-1"), + aws_access_key_id=_s3cfg.get("aws_access_key_id"), + aws_secret_access_key=_s3cfg.get("aws_secret_access_key"), + aws_session_token=_s3cfg.get("aws_session_token"), + endpoint_url=_s3cfg.get("endpoint_url"), + connect_timeout=float(_s3cfg.get("connect_timeout", 3.0)), + read_timeout=float(_s3cfg.get("read_timeout", 10.0)), + max_attempts=int(_s3cfg.get("max_attempts", 3)), +)) + +MEDIA_AUTH_TOKEN = os.getenv("MEDIA_AUTH_TOKEN") or _cfg.get("media_auth_token") # optional + +_M3U8_CT = "application/vnd.apple.mpegurl" +_TS_CT = "video/MP2T" +_MP4_CT = "video/mp4" + +SAFE_NAME = re.compile(r"^[A-Za-z0-9_.-]+$") # prevent path traversal + +def _require_auth(req: Request) -> None: + """Simple Bearer check. If MEDIA_AUTH_TOKEN unset, auth is disabled (dev).""" + print("in") + if not MEDIA_AUTH_TOKEN: + return + auth = req.headers.get("Authorization", "") + if auth.lower().startswith("bearer "): + token = auth[7:].strip() + else: + token = None + if token != MEDIA_AUTH_TOKEN: + print("unauthorazied") + raise HTTPException(status_code=401, detail="Unauthorized") + +def _ct_for_name(name: str) -> str: + if name.endswith(".m3u8"): return _M3U8_CT + if name.endswith(".ts"): return _TS_CT + if name.endswith(".mp4"): return _MP4_CT + if name.endswith(".m4s"): return _MP4_CT + return mimetypes.guess_type(name)[0] or "application/octet-stream" + +def _object_key_for_hls(camera: str, incident: str, name: str) -> str: + # e.g., security/incidents///segment_00001.ts + return f"{PREFIX}/{camera}/{incident}/{name}" + + + + +import time +from botocore.exceptions import ClientError + +def _exists(bucket: str, key: str) -> bool: + try: + s3.head_object(bucket, key) # implement head_object in your S3Client + return True + except Exception: + return False + +def _wait_for_key(bucket: str, key: str, timeout=3.0, interval=0.2) -> bool: + t0 = time.time() + while time.time() - t0 < timeout: + if _exists(bucket, key): + return True + time.sleep(interval) + return False + +@app.get("/hls/{camera}/{incident}/index.m3u8") +def get_playlist(camera: str, incident: str, request: Request): + _require_auth(request) + names = ["index.m3u8", "master.m3u8", "playlist.m3u8"] + for name in names: + key = _object_key_for_hls(camera, incident, name) + print("key: ",key) + if True:#_wait_for_key(BUCKET, key, timeout=8.0, interval=0.2): + # print(key) + obj = s3.get_object_stream(BUCKET, key) + body = obj["Body"].read() + return Response( + content=body, + media_type=_M3U8_CT, + headers={ + "Cache-Control": "no-store, must-revalidate", + "Pragma": "no-cache", + }, + ) + raise HTTPException(status_code=404, detail="playlist not found (not ready)") + +# ---------- SEGMENTS (and CMAF init.mp4) ---------- +@app.get("/hls/{camera}/{incident}/{name}") +def get_segment(camera: str, incident: str, name: str, request: Request, range: Optional[str] = Header(default=None)): + _require_auth(request) + if "/" in name or ".." in name or not SAFE_NAME.match(name): + raise HTTPException(status_code=400, detail="bad segment name") + key = _object_key_for_hls(camera, incident, name) + try: + obj = s3.get_object_stream(BUCKET, key, range_header=range) + except Exception: + raise HTTPException(status_code=404, detail="segment not found") + + headers = {"Accept-Ranges": "bytes", "Content-Type": _ct_for_name(name)} + status = 200 + cr = obj.get("ContentRange") or obj.get("Content-Range") + if cr: + headers["Content-Range"] = cr + status = 206 + return StreamingResponse(obj["Body"].iter_chunks(), headers=headers, status_code=status, media_type=headers["Content-Type"]) + +# ---------- FINAL MP4 (VOD) ---------- +@app.get("/vod/{camera}/{incident}/final.mp4") +def get_final_mp4(camera: str, incident: str, request: Request, range: Optional[str] = Header(default=None)): + print("requesting vod") + _require_auth(request) + key = _object_key_for_hls(camera, incident, "final.mp4") + try: + obj = s3.get_object_stream(BUCKET, key, range_header=range) + except Exception: + raise HTTPException(status_code=404, detail="vod not found") + + headers = {"Accept-Ranges": "bytes", "Content-Type": _MP4_CT} + status = 200 + cr = obj.get("ContentRange") or obj.get("Content-Range") + if cr: + headers["Content-Range"] = cr + status = 206 + return StreamingResponse(obj["Body"].iter_chunks(), headers=headers, status_code=status, media_type=_MP4_CT) + +@app.get("/img/{camera}/{incident}/{filename}") +def get_image(camera: str, incident: str, filename: str, request: Request): + _require_auth(request) + if "/" in filename or ".." in filename or not SAFE_NAME.match(filename): + raise HTTPException(status_code=400, detail="bad filename") + + key = f"{PREFIX}/{camera}/{incident}/{filename}" + try: + obj = s3.get_object_stream(BUCKET, key) + except Exception: + raise HTTPException(status_code=404, detail="image not found") + + mime = _ct_for_name(filename) + headers = {"Cache-Control": "no-store, must-revalidate"} + return StreamingResponse(obj["Body"].iter_chunks(), headers=headers, media_type=mime) + diff --git a/AgCloud/services/security/agguard/app/requirements.txt b/AgCloud/services/security/agguard/app/requirements.txt new file mode 100644 index 000000000..df7b90ec6 --- /dev/null +++ b/AgCloud/services/security/agguard/app/requirements.txt @@ -0,0 +1,17 @@ +fastapi==0.115.2 +uvicorn[standard]==0.32.0 + +# AWS SDK for S3 access +boto3==1.35.29 +botocore==1.35.29 + +# Streaming + YAML parsing +PyYAML==6.0.2 + +# Optional: for your adapters +requests>=2.32.3 + +# If S3Client uses aiohttp for async streams +aiohttp>=3.10.5 + +opencv-python-headless<5 diff --git a/AgCloud/services/security/agguard/core/__init__.py b/AgCloud/services/security/agguard/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/core/events/__init__.py b/AgCloud/services/security/agguard/core/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/core/events/aggregator.py b/AgCloud/services/security/agguard/core/events/aggregator.py new file mode 100644 index 000000000..7f4e17486 --- /dev/null +++ b/AgCloud/services/security/agguard/core/events/aggregator.py @@ -0,0 +1,515 @@ +# agguard/events/aggregator.py +from __future__ import annotations +from typing import Dict, List, Tuple, Optional, Any +from dataclasses import dataclass, field +import uuid, datetime as _dt +import cv2, numpy as np,time + +from .models import Rule, Incident, Box +from agguard.media.hls_recorder import HlsRecorder, HlsConfig +from agguard.media.mp4_recorder import Mp4Recorder +import logging +log = logging.getLogger(__name__) + +@dataclass +class IncidentEvent: + opened_incident_id: str | None = None + updated_incident_id: str | None = None + closed_incident_id: str | None = None + opened_data: Optional[Any] = None + closed_data: Optional[Any] = None + +@dataclass +class _EventState: + consec: int = 0 + cooldown_left: int = 0 + open_incident: Optional[Incident] = None + last_seen_frame: int = -1 + last_seen_ts: float = 0.0 + + # detections for the *current* frame (for record_frame): list of dicts + # {"x1": int, "y1": int, "x2": int, "y2": int, "conf": float|None} + detections: List[Dict[str, Any]] = field(default_factory=list) + + # For severity = mean tracks per frame during the incident + total_tracks: int = 0 + total_frames: int = 0 + subject: Optional[str] = None + + +def _iou(a: Box, b: Box) -> float: + x1, y1, x2, y2 = a + X1, Y1, X2, Y2 = b + ix1, iy1 = max(x1, X1), max(y1, Y1) + ix2, iy2 = min(x2, X2), min(y2, Y2) + inter = max(0, ix2 - ix1) * max(0, iy2 - iy1) + union = (x2 - x1) * (y2 - y1) + (X2 - X1) * (Y2 - Y1) - inter + return inter / max(union, 1e-6) + + +def _clamp_box(box: Box, w: int, h: int) -> Box: + x1, y1, x2, y2 = box + x1 = max(0, min(w - 1, int(x1))) + y1 = max(0, min(h - 1, int(y1))) + x2 = max(0, min(w - 1, int(x2))) + y2 = max(0, min(h - 1, int(y2))) + if x2 < x1: + x1, x2 = x2, x1 + if y2 < y1: + y1, y2 = y2, y1 + return (x1, y1, x2, y2) + + +class IncidentAggregator: + """ + Aggregates evidence PER (camera_id, rule.name). + Computes severity as mean tracks per frame. + Persists ALL detections (bbox+conf) per frame in a single DB row. + """ + + def __init__( + self, + rules: List[Rule], + camera_id: Optional[str] = None, + roi_pixels: Optional[List[Tuple[int, int]]] = None, + assoc_iou: float = 0.3, + sample_every: int = 1, + s3=None, video_bucket=None, video_prefix="security/incidents", + fps=12, hls_segment_time=3.0, hls_list_size=20, hls_use_cmaf=False, + draw_thickness=2, media_base: Optional[str] = None, media_token: Optional[str] = None): + + self.rules = rules + self.camera_id = camera_id + self.roi_pixels = roi_pixels + self.assoc_iou = float(assoc_iou) + self.sample_every = int(sample_every) + # key: (camera_id, rule.name) -> _EventState + self._states: Dict[Tuple[str, str], _EventState] = {} + self.s3 = s3 + self.video_bucket = video_bucket + self.video_prefix = video_prefix.strip("/") + self.fps = int(max(1, fps)) + self._hls_cfg = HlsConfig( + fps=self.fps, segment_time=hls_segment_time, list_size=hls_list_size, + use_cmaf=hls_use_cmaf, preset="veryfast", crf=23, gop_segments=2, upload_interval_sec=0.25 + ) + self.draw_thickness = int(max(1, draw_thickness)) + + + self.media_base = (media_base or "").rstrip("/") + self.media_token = media_token or "" + + # helper for drawing (add this method inside the class) + def _render_frame_with_boxes(self, frame_bgr, dets): + out = frame_bgr.copy() + t = self.draw_thickness + for d in dets or []: + x1,y1,x2,y2 = int(d["x1"]),int(d["y1"]),int(d["x2"]),int(d["y2"]) + cv2.rectangle(out, (x1,y1), (x2,y2), (0,255,0), t) + tid = d.get("track_id") + if tid is not None: + cv2.putText(out, str(tid), (x1, max(0, y1-5)), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1, cv2.LINE_AA) + return out + +# helper to compute s3 prefix + def _hls_prefix(self, inc): + cam = self.camera_id or "unknown" + return f"{self.video_prefix}/{cam}/{inc.incident_id}" + + # ------------- convenience setters ------------- + + def set_camera(self, camera_id: Optional[str]) -> None: + self.camera_id = camera_id + + + + def set_roi_pixels(self, roi_pixels: Optional[List[Tuple[int, int]]]) -> None: + self.roi_pixels = roi_pixels + + # ------------- internals ------------- + + def _key(self, rule: Rule) -> Tuple[str, str]: + cam = self.camera_id or "unknown" + return (cam, rule.name) + + def _state(self, rule: Rule) -> _EventState: + k = self._key(rule) + if k not in self._states: + self._states[k] = _EventState() + return self._states[k] + + @staticmethod + def _class_match(t_cls: Any, rule: Rule) -> bool: + """ + True if track class matches rule by name or id (if provided). + If rule doesn't restrict class, accept all. + """ + # return True + t_name = str(t_cls).lower() + by_name = (rule.target_cls and t_name == str(rule.target_cls).lower()) + by_id = (rule.target_cls_id is not None and str(t_cls) == str(rule.target_cls_id)) + return bool(by_name or by_id) if (rule.target_cls or rule.target_cls_id is not None) else True + + def _match_classes(self, rule: Rule, preds: List) -> bool: + """ + Return True if any prediction class name matches one of rule.match_classes. + Includes detailed debug logging for diagnosis. + """ + if not preds: + log.info("[_match_classes] ⚠️ No predictions passed for rule '%s'", rule.name) + return False + + classes = [c.lower().strip() for c in (rule.match_classes or [])] + if not classes: + log.info("[_match_classes] 🟢 Rule '%s' has empty match_classes → treating as always True", rule.name) + return True + + log.info("[_match_classes] 🔍 Evaluating rule='%s' | match_classes=%s | min_conf=%.2f", + rule.name, classes, float(rule.min_conf or 0.0)) + + matched = False + for idx, p in enumerate(preds): + try: + # Handle dict-style predictions + if isinstance(p, dict): + cls_name = str(p.get("label") or p.get("cls") or p.get("class_name") or "").strip().lower() + conf = float(p.get("confidence", p.get("conf", 0.0))) + # Handle object-style (protobuf / custom) + elif hasattr(p, "label") or hasattr(p, "cls") or hasattr(p, "class_name"): + cls_name = str(getattr(p, "label", getattr(p, "cls", getattr(p, "class_name", "")))).strip().lower() + conf = float(getattr(p, "confidence", getattr(p, "conf", 0.0))) + else: + log.warning("[_match_classes] 🚫 Unsupported prediction type %s → skipping", type(p)) + continue + + log.info("[_match_classes] • Pred #%d → label='%s' conf=%.3f", idx, cls_name, conf) + + # Check confidence + if conf < float(rule.min_conf or 0.0): + log.info("[_match_classes] ↳ below min_conf=%.2f → SKIP", float(rule.min_conf or 0.0)) + continue + + # Check class match + for c in classes: + if c == cls_name: + log.info("[_match_classes] ✅ EXACT MATCH '%s' for rule '%s'", cls_name, rule.name) + matched = True + break + elif c in cls_name or cls_name in c: + log.info("[_match_classes] ⚡ PARTIAL MATCH '%s' ~ '%s' for rule '%s'", cls_name, c, rule.name) + matched = True + break + + except Exception as e: + log.exception("[_match_classes] ❌ Error parsing prediction #%d: %s", idx, e) + + if not matched: + log.info("[_match_classes] ❌ No matches found for rule '%s'. Predictions checked: %d", + rule.name, len(preds)) + + return matched + + + + + + def _open_incident(self, st: _EventState, rule: Rule, ts_sec: float, frame_idx: int, frame_bgr) -> dict: + log.info("[_open_incident] Opening new incident for rule '%s' at frame %d ts=%.3f", rule.name, frame_idx, ts_sec) + inc = Incident( + incident_id=str(uuid.uuid4()), + kind=rule.name, + camera_id=self.camera_id, + started_ts=ts_sec, + frame_start=frame_idx, + roi=self.roi_pixels, + severity=getattr(rule, "severity", 0) + ) + st.open_incident = inc + st.cooldown_left = int(rule.cooldown) + st.total_tracks = 0 + st.total_frames = 0 + + # Start HLS recorder immediately (so index.m3u8 appears fast) + if self.s3 and self.video_bucket: + # --- Live HLS recorder --- + st._hls = HlsRecorder( + s3=self.s3, bucket=self.video_bucket, + prefix=self._hls_prefix(inc), cfg=self._hls_cfg, + ) + H, W = frame_bgr.shape[:2] + st._hls.start((H, W)) + st._hls.write_bgr(self._render_frame_with_boxes(frame_bgr, st.detections)) + + # --- MP4 recorder (for final video only) --- + st._mp4 = Mp4Recorder( + s3=self.s3, bucket=self.video_bucket, + prefix=self._hls_prefix(inc), cfg=self._hls_cfg, + ) + st._mp4.start((H, W)) + st._mp4.write_bgr(self._render_frame_with_boxes(frame_bgr, st.detections)) + + + + + # Only notify external world once the playlist definitely exists + if self.media_base and hasattr(st, "_hls") and st._hls: + log.info("[_open_incident] Waiting for playlist readiness...") + st._hls.wait_ready(timeout=6.0) # usually quick (first segment_time) + camera = inc.camera_id + incident_id = inc.incident_id + hls_url = f"{camera}/{incident_id}/index.m3u8" + vod_url = f"{camera}/{incident_id}/final.mp4" + anomaly = inc.kind or "unknown" + sev = "info" + log.info("[_open_incident] Sending alert to Alertmanager for incident_id=%s hls_url=%s", incident_id, hls_url) + + opened_data = { + "incident_id": inc.incident_id, + "camera_id": inc.camera_id, + "kind": rule.name, + "ts_iso": _dt.datetime.utcfromtimestamp(ts_sec).isoformat() + "Z", + "frame_start": frame_idx, + "roi": self.roi_pixels, + "severity": getattr(rule, "severity", 0), + "hls": hls_url, + "subject": getattr(st, "subject", None), # ✅ Add this line + } + return opened_data + + def _close_incident(self, key: Tuple[str, str], st: _EventState, ts_sec: float, frame_idx: int)->dict: + inc = st.open_incident + if not inc: + log.info("[_close_incident] No open incident to close for key=%s", key) + return + + log.info("[_close_incident] Closing incident %s (rule=%s)", inc.incident_id, key[1]) + + inc.ended_ts = ts_sec + inc.frame_end = frame_idx + inc.duration_sec = max(0.0, inc.ended_ts - inc.started_ts) + + # severity = mean tracks per frame during the incident + severity = round(float(st.total_tracks) / max(st.total_frames, 1)) + log.info("[_close_incident] Computed severity=%.3f (tracks=%d frames=%d)", severity, st.total_tracks, st.total_frames) + + + if self.s3 and self.video_bucket and hasattr(st, "_hls") and st._hls: + try: + log.info("[_close_incident] Finalizing HLS to MP4 for incident_id=%s", inc.incident_id) + # mp4_key = st._mp4.finalize() if hasattr(st, "_mp4") and st._mp4 else None + if st._hls: + try: + # 1️⃣ Stop ffmpeg FIRST — always + st._hls.stop() + log.info("[DEBUG] ffmpeg alive? %s", st._hls._proc and st._hls._proc.poll() is None) + + time.sleep(0.2) + + # 2️⃣ Finalize MP4 — now safe + mp4_key = st._mp4.finalize() if st._mp4 else None + + # 3️⃣ Delete HLS fragments — ffmpeg is now dead + # 3️⃣ DELETE REMOTE HLS FRAGMENTS FROM S3 + try: + st._hls.delete_remote_hls() + except Exception as e: + log.exception("[_close_incident] remote delete error: %s", e) + + # 4️⃣ delete LOCAL files (optional for cleanup) + st._hls.delete_hls_files_only() + + + except Exception as e: + log.exception("[_close_incident] Cleanup error: %s", e) + log.info("[_close_incident] finalize_to_mp4() returned mp4_key=%s", mp4_key) + except Exception as e: + log.exception("[_close_incident] Error finalizing MP4: %s", e) + poster_file_id = None + else: + log.info("[_close_incident] Skipping MP4 finalization — missing s3/video_bucket or no _hls") + + + self._states[key] = _EventState() + + closed_data = { + "incident_id": inc.incident_id, + "kind": inc.kind, + "ended_at_iso": _dt.datetime.utcfromtimestamp(ts_sec).isoformat() + "Z", + "ts_iso": _dt.datetime.utcfromtimestamp(inc.started_ts).isoformat() + "Z", + "duration_sec": inc.duration_sec, + "frame_end": frame_idx, + "severity":inc.severity+severity, + "vod":f"{inc.camera_id}/{inc.incident_id}/final.mp4", + "subject": getattr(st, "subject", None) + + } + return closed_data + + + # ------------- public API ------------- + + def update(self, frame_idx: int, ts_sec: float, frame_bgr, tracks: List, outputs: Dict[str, List]) -> IncidentEvent: + """ + Evaluate evidence per (camera_id, rule). Maintain incident state. + Also captures ALL detections (bbox + conf) for record_frame(). + Returns IncidentEvent to signal opens/updates/closes to the caller. + """ + log.info("[update] frame_idx=%d ts=%.3f num_tracks=%d num_outputs=%d", + frame_idx, ts_sec, len(tracks), len(outputs or {})) + + H, W = frame_bgr.shape[:2] + by_cls = outputs or {} + evt = IncidentEvent() + + for rule in self.rules: + # Only skip 'intruding animal' if climbing_fence truly matched + if rule.name == "intruding animal": + cf_preds = by_cls.get("climbing_fence", []) + climbing_rule = next((r for r in self.rules if r.name == "climbing_fence"), None) + + if climbing_rule and self._match_classes(climbing_rule, cf_preds): + log.info("[update] Valid climbing_fence detected → suppressing intruding animal.") + continue + + log.info("[update] Evaluating rule '%s' (target_cls=%s cooldown=%s)", + rule.name, getattr(rule, 'target_cls', None), getattr(rule, 'cooldown', None)) + + candidate_tracks = [t for t in tracks if self._class_match(t.cls, rule)] + preds = by_cls.get(rule.name, []) or by_cls.get(rule.target_cls, []) + # preds = by_cls.get(rule.target_cls, []) if rule.target_cls else [] + + + st = self._state(rule) + + # 🧠 Prefer subject propagated from "intruding animal" (via outputs["_subject"]) + if "_subject" in outputs and outputs["_subject"]: + st.subject = outputs["_subject"][0] # e.g. "bear" + conf_val = None # no confidence value for propagated subject + log.info( + "[update] 🐾 Propagated subject from intruding animal: %s", + st.subject, + ) + + # 🐾 Otherwise, derive subject from current rule's predictions + elif preds: + best_pred = max( + preds, + key=lambda p: getattr( + p, "confidence", + p.get("confidence", 0.0) if isinstance(p, dict) else 0.0 + ), + ) + + if isinstance(best_pred, dict): + st.subject = best_pred.get("label") + conf_val = best_pred.get("confidence", 0.0) + else: + st.subject = getattr(best_pred, "label", None) + conf_val = getattr(best_pred, "confidence", 0.0) + + log.info( + "[update] 🐾 Subject detected for rule '%s': %s (conf=%.2f)", + rule.name, st.subject, conf_val + ) + else: + st.subject = None + conf_val = 0.0 + + log.info("[update] Found %d candidate tracks and %d predictions for rule '%s'", + len(candidate_tracks), len(preds), rule.name) + + evidence = False + frame_detections: List[Dict[str, Any]] = [] + + for t in candidate_tracks: + bx = _clamp_box(tuple(map(int, t.bbox)), W, H) + if self._match_classes(rule, preds): + evidence = True + log.info("[update] Evidence matched for rule '%s' on track_id=%s bbox=%s", + rule.name, getattr(t, 'track_id', None), bx) + + x1, y1, x2, y2 = bx + try: + conf_val = float(t.conf) + except Exception: + conf_val = None + frame_detections.append({ + "track_id": int(t.track_id) if getattr(t, "track_id", None) is not None else None, + "x1": x1, "y1": y1, "x2": x2, "y2": y2, + "conf": conf_val, + }) + + st = self._state(rule) + key = self._key(rule) + log.info("[update] Current state for key=%s consec=%d cooldown_left=%d open_incident=%s", + key, st.consec, st.cooldown_left, getattr(st.open_incident, 'incident_id', None)) + + # If recording, write current frame (with boxes) continuously + if st.open_incident is not None: + rendered = self._render_frame_with_boxes(frame_bgr, st.detections) + if hasattr(st, "_hls") and st._hls: + st._hls.write_bgr(rendered) # continuous live feed + if hasattr(st, "_mp4") and st._mp4: + st._mp4.write_bgr(rendered) # only called when new frames arrive + + st.total_tracks += len(frame_detections) + st.total_frames += 1 + log.info("[update] Recorded frame for active incident=%s total_tracks=%d total_frames=%d", + st.open_incident.incident_id, st.total_tracks, st.total_frames) + + st.last_seen_frame = frame_idx + st.last_seen_ts = ts_sec + st.detections = frame_detections + + prev_consec = st.consec + st.consec = st.consec + 1 if evidence else 0 + log.info("[update] Consecutive evidence count changed from %d -> %d (rule='%s')", + prev_consec, st.consec, rule.name) + + if st.open_incident is None and st.consec >= int(rule.min_consec or 1): + log.info("[update] Triggering _open_incident for rule '%s'", rule.name) + opened_data = self._open_incident(st, rule, ts_sec, frame_idx, frame_bgr) + evt.opened_incident_id = st.open_incident.incident_id + evt.opened_data = opened_data + log.info("[update] Opened new incident_id=%s for rule='%s'", + evt.opened_incident_id, rule.name) + + if st.open_incident is not None: + # sample a representative bbox to append to incident trail + if self.sample_every > 0 and (frame_idx % self.sample_every == 0) and st.detections: + bx = max( + ((d["x2"] - d["x1"]) * (d["y2"] - d["y1"]), d) for d in st.detections + )[1] + st.open_incident.boxes.append((bx["x1"], bx["y1"], bx["x2"], bx["y2"])) + st.open_incident.confs.append(1.0) + log.info("[update] Appended sample bbox=%s to incident trail for %s", bx, st.open_incident.incident_id) + + # cooldown logic + prev_cooldown = st.cooldown_left + st.cooldown_left = int(rule.cooldown) if evidence else (st.cooldown_left - 1) + log.info("[update] Cooldown changed %d -> %d (evidence=%s)", + prev_cooldown, st.cooldown_left, evidence) + + if not evidence and st.cooldown_left <= 0: + closed_id = st.open_incident.incident_id + log.info("[update] Closing incident %s (cooldown expired)", closed_id) + closed_data = self._close_incident(key, st, ts_sec, frame_idx) + evt.closed_incident_id = closed_id + evt.closed_data = closed_data + + else: + evt.updated_incident_id = st.open_incident.incident_id + log.info("[update] Updating active incident %s (evidence=%s cooldown=%d)", + st.open_incident.incident_id, evidence, st.cooldown_left) + + log.info("[update] Returning IncidentEvent opened=%s updated=%s closed=%s", + evt.opened_incident_id, evt.updated_incident_id, evt.closed_incident_id) + return evt + + + def flush(self, ts_sec: float, frame_idx: int): + """Close any open incidents across all cameras/rules.""" + for key, st in list(self._states.items()): + if st.open_incident is not None: + self._close_incident(key, st, ts_sec, frame_idx) \ No newline at end of file diff --git a/AgCloud/services/security/agguard/core/events/models.py b/AgCloud/services/security/agguard/core/events/models.py new file mode 100644 index 000000000..307719827 --- /dev/null +++ b/AgCloud/services/security/agguard/core/events/models.py @@ -0,0 +1,43 @@ +from dataclasses import dataclass, field, asdict +from typing import List, Tuple, Dict, Optional, Union +from datetime import datetime + +Box = Tuple[int,int,int,int] + +@dataclass +class Rule: + name: str + target_cls: str # e.g. "person" + target_cls_id: Optional[Union[str,int]] = None # e.g. 0, "0" + match_classes: List[str] = field(default_factory=list) # ← new field + severity: int = 3 + min_conf: float = 0.6 + min_consec: int = 5 + cooldown: int = 12 + + +@dataclass +class Incident: + incident_id: str + kind: str + camera_id: Optional[str] = None + started_ts: float = 0.0 + ended_ts: float = 0.0 + duration_sec: float = 0.0 + frame_start: int = 0 + frame_end: int = 0 + severity: int = 1 + track_id: Optional[int] = None + roi: Optional[List[Tuple[int,int]]] = None + boxes: List[Box] = field(default_factory=list) + confs: List[float] = field(default_factory=list) + classes: List[str] = field(default_factory=list) + snapshot_path: Optional[str] = None + artifacts: Dict[str,str] = field(default_factory=dict) + meta: Dict[str, str|int|float] = field(default_factory=dict) + + def to_dict(self) -> Dict: + d = asdict(self) + d["started_iso"] = datetime.utcfromtimestamp(self.started_ts).isoformat()+"Z" + d["ended_iso"] = datetime.utcfromtimestamp(self.ended_ts).isoformat()+"Z" + return d diff --git a/AgCloud/services/security/agguard/core/motion.py b/AgCloud/services/security/agguard/core/motion.py new file mode 100644 index 000000000..41ebccb8d --- /dev/null +++ b/AgCloud/services/security/agguard/core/motion.py @@ -0,0 +1,198 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import List, Tuple, Optional +import numpy as np +import cv2 +import logging +from .roi import Roi + +log = logging.getLogger(__name__) + +@dataclass +class ChangeReading: + score: float + area_px: int + total_px: int + fgmask: np.ndarray + bboxes: List[Tuple[int, int, int, int]] + +def _iou(a, b): + ax1, ay1, ax2, ay2 = a; bx1, by1, bx2, by2 = b + ix1, iy1 = max(ax1, bx1), max(ay1, by1); ix2, iy2 = min(ax2, bx2), min(ay2, by2) + iw, ih = max(0, ix2 - ix1 + 1), max(0, iy2 - iy1 + 1) + inter = iw * ih + if inter <= 0: + return 0.0 + aA = (ax2 - ax1 + 1) * (ay2 - ay1 + 1); bA = (bx2 - bx1 + 1) * (by2 - by1 + 1) + return inter / float(aA + bA - inter) + +def _merge_overlapping(boxes, iou_thresh=0.35, max_iters=5): + boxes = boxes[:] + for _ in range(max_iters): + merged, used = [], [False] * len(boxes) + for i in range(len(boxes)): + if used[i]: + continue + cur, changed = boxes[i], True + while changed: + changed = False + for j in range(i + 1, len(boxes)): + if used[j]: + continue + if _iou(cur, boxes[j]) >= iou_thresh: + x1 = min(cur[0], boxes[j][0]); y1 = min(cur[1], boxes[j][1]) + x2 = max(cur[2], boxes[j][2]); y2 = max(cur[3], boxes[j][3]) + cur = (x1, y1, x2, y2) + used[j] = True + changed = True + used[i] = True + merged.append(cur) + if len(merged) == len(boxes): + return merged + boxes = merged + return boxes + +class MotionGate: + """ + Same API/output intent as your original: MOG2 on BGR + ROI + morphology + CC + merge, + but runs on a downscaled frame for speed. Kernels and min-area are scaled so the + *effective* behavior matches full-res. + """ + + def __init__( + self, + roi: Roi, + history: int = 300, + var_threshold: float = 16.0, + shadow: bool = True, # keep your default + min_blob_area: int = 80, + morph_open: int = 3, + smooth_alpha: float = 0.2, + dilate_px: int = 2, + morph_close: int = 5, + merge_iou: float = 0.35, + target_max_dim: int = 480, # downscale max side; set to 0/None to disable + ): + self.roi = roi + self.min_blob_area = int(min_blob_area) + self.morph_open = int(morph_open) + self.smooth_alpha = float(smooth_alpha) + self.dilate_px = int(dilate_px) + self.morph_close = int(morph_close) + self.merge_iou = float(merge_iou) + self.target_max_dim = int(target_max_dim) if target_max_dim else 0 + + self._ema: Optional[float] = None + self._roi_mask = roi.mask() # full-res mask + self._roi_mask_small: Optional[np.ndarray] = None + self._bg_size: Optional[Tuple[int, int]] = None + + self._bg_shadow = bool(shadow) + self._history = int(history) + self._var_threshold = float(var_threshold) + self.bg = None # created lazily when small size is known + + def _ensure_background(self, sh: int, sw: int): + if self._bg_size != (sh, sw): + self.bg = cv2.createBackgroundSubtractorMOG2( + history=self._history, + varThreshold=self._var_threshold, + detectShadows=self._bg_shadow, + ) + self._bg_size = (sh, sw) + + def _ensure_small_roi_mask(self, h: int, w: int, sh: int, sw: int): + if (w, h) != self.roi.size: + self.roi.size = (w, h) + self._roi_mask = self.roi.mask() + self._roi_mask_small = None + if self._roi_mask_small is None or self._roi_mask_small.shape != (sh, sw): + self._roi_mask_small = cv2.resize( + self._roi_mask, (sw, sh), interpolation=cv2.INTER_NEAREST + ) + + def update(self, frame_bgr: np.ndarray) -> ChangeReading: + h, w = frame_bgr.shape[:2] + + # scale factor (downscale for speed; 1.0 means no downscale) + if self.target_max_dim and max(h, w) > self.target_max_dim: + scale = max(h, w) / float(self.target_max_dim) + sw, sh = int(round(w / scale)), int(round(h / scale)) + else: + scale = 1.0 + sw, sh = w, h + + # prepare small BGR (keep BGR like your original) + small = frame_bgr if scale == 1.0 else cv2.resize(frame_bgr, (sw, sh), interpolation=cv2.INTER_AREA) + + self._ensure_background(sh, sw) + self._ensure_small_roi_mask(h, w, sh, sw) + + # ---- MOG2 on BGR (like original) ---- + fg = self.bg.apply(small) # if detectShadows=True, returns 0/127/255 + if self._bg_shadow: + # keep your original binarying step + _, fg = cv2.threshold(fg, 200, 255, cv2.THRESH_BINARY) + + # ROI on small + fg = cv2.bitwise_and(fg, self._roi_mask_small) + + # ---- scale morphology kernels to preserve behavior ---- + def k_odd(x: int) -> int: + return x if x % 2 == 1 else max(1, x - 1) + + # kernels shrink by ~1/scale on the small image + dilate_small = max(0, int(round(self.dilate_px / scale))) + close_small = max(0, int(round(self.morph_close / scale))) + open_small = max(0, int(round(self.morph_open / scale))) + + if dilate_small > 0: + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k_odd(2*dilate_small + 1), k_odd(2*dilate_small + 1))) + fg = cv2.dilate(fg, k, iterations=1) + if close_small > 0: + k = cv2.getStructuringElement(cv2.MORPH_RECT, (k_odd(close_small), k_odd(close_small))) + fg = cv2.morphologyEx(fg, cv2.MORPH_CLOSE, k, iterations=1) + if open_small > 0: + k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k_odd(open_small), k_odd(open_small))) + fg = cv2.morphologyEx(fg, cv2.MORPH_OPEN, k, iterations=1) + + # CC on small; area threshold scaled by 1/scale^2 + min_area_small = max(1, int(round(self.min_blob_area / (scale * scale)))) + num, labels, stats, _ = cv2.connectedComponentsWithStats(fg, connectivity=8) + + boxes_small: List[Tuple[int, int, int, int]] = [] + clean_small = np.zeros_like(fg) + for i in range(1, num): + x, y, w0, h0, area = stats[i] + if area < min_area_small: + continue + x1, y1, x2, y2 = x, y, x + w0 - 1, y + h0 - 1 + boxes_small.append((x1, y1, x2, y2)) + clean_small[y : y + h0, x : x + w0] = 255 + + if len(boxes_small) > 1 and self.merge_iou > 0: + boxes_small = _merge_overlapping(boxes_small, iou_thresh=self.merge_iou) + + # map back to full-res + if scale != 1.0: + bboxes = [ + (int(round(x1 * scale)), int(round(y1 * scale)), + int(round(x2 * scale)), int(round(y2 * scale))) + for (x1, y1, x2, y2) in boxes_small + ] + clean = cv2.resize(clean_small, (w, h), interpolation=cv2.INTER_NEAREST) + else: + bboxes = boxes_small + clean = clean_small + + moving = int((clean == 255).sum()) + total = int((self._roi_mask == 255).sum()) + raw = float(moving) / float(max(total, 1)) + if self.smooth_alpha > 0: + self._ema = raw if self._ema is None else (1 - self.smooth_alpha) * self._ema + self.smooth_alpha * raw + score = float(self._ema) + else: + score = raw + + log.debug("MotionGate: score=%.3f, blobs=%d, area=%d/%d", score, len(bboxes), moving, total) + return ChangeReading(score=score, area_px=moving, total_px=total, fgmask=clean, bboxes=bboxes) diff --git a/AgCloud/services/security/agguard/core/roi.py b/AgCloud/services/security/agguard/core/roi.py new file mode 100644 index 000000000..0b7f3a07d --- /dev/null +++ b/AgCloud/services/security/agguard/core/roi.py @@ -0,0 +1,49 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Sequence, Tuple +import numpy as np +import cv2 + +Point = Tuple[float, float] + +def _ensure_np(arr: Sequence[Point]) -> np.ndarray: + a = np.asarray(arr, dtype=np.float32) + if a.ndim != 2 or a.shape[1] != 2: + raise ValueError("Expected Nx2 points") + return a + +@dataclass +class Roi: + """Polygonal ROI stored in normalized coords [0,1] and sized in pixels.""" + poly_norm: np.ndarray + size: Tuple[int, int] # (w, h) + + @staticmethod + def from_normalized(poly_norm: Sequence[Point], frame_size: Tuple[int, int]) -> "Roi": + pn = _ensure_np(poly_norm) + if (pn < 0).any() or (pn > 1).any(): + raise ValueError("ROI must be normalized to [0,1]") + return Roi(pn, frame_size) + + @staticmethod + def from_pixels(poly_px: Sequence[Point], frame_size: Tuple[int, int]) -> "Roi": + w, h = frame_size + pp = _ensure_np(poly_px) + pn = np.stack([pp[:, 0] / w, pp[:, 1] / h], axis=1) + return Roi(pn, frame_size) + + @property + def poly_px(self) -> np.ndarray: + w, h = self.size + pn = self.poly_norm + pp = np.stack([pn[:, 0] * w, pn[:, 1] * h], axis=1).astype(np.float32) + return pp + + def as_cv2(self) -> np.ndarray: + return self.poly_px.reshape((-1, 1, 2)).astype(np.int32) + + def mask(self) -> np.ndarray: + w, h = self.size + m = np.zeros((h, w), dtype=np.uint8) + cv2.fillPoly(m, [self.as_cv2()], 255) + return m diff --git a/AgCloud/services/security/agguard/core/tracker.py b/AgCloud/services/security/agguard/core/tracker.py new file mode 100644 index 000000000..df92e44cc --- /dev/null +++ b/AgCloud/services/security/agguard/core/tracker.py @@ -0,0 +1,56 @@ +from .types import Track + +import numpy as np +from dataclasses import dataclass +from boxmot import ByteTrack + +@dataclass +class Track: + track_id: int + cls: str + conf: float + bbox: tuple # (x1, y1, x2, y2) + + +class BoxMOTWrapper: + def __init__(self, method="bytetrack", class_map=None, **kwargs): + """ + class_map: dict[str,int] — e.g. {"animal":1, "person":2, "vehicle":3} + """ + self.trk = ByteTrack(**kwargs) + self.class_map = class_map or {"animal": 1, "person": 2, "vehicle": 3} + self.inv_class_map = {v: k for k, v in self.class_map.items()} + + def update(self, dets, frame): + if not dets: + self.trk.update(np.empty((0,6), dtype=float), frame) + return [] + + # normalize detections (supports both Detection objects and tuples) + norm_dets = [] + for d in dets: + if isinstance(d, tuple) and len(d) == 3: + cls, conf, bbox = d + else: + cls, conf, bbox = getattr(d, "cls", None), getattr(d, "conf", None), getattr(d, "bbox", None) + if bbox is None: + continue + norm_dets.append((cls, conf, bbox)) + + if not norm_dets: + return [] + + boxes = np.array([b for _, _, b in norm_dets], dtype=float) + confs = np.array([[float(c or 0.0)] for _, c, _ in norm_dets]) + clss = np.array([[self.class_map.get(c, 0)] for c, _, _ in norm_dets], dtype=float) + + detections = np.concatenate([boxes, confs, clss], axis=1) + tracks = self.trk.update(detections, frame) + + results = [] + for t in tracks: + x1, y1, x2, y2, tid, conf, cls_id = map(float, t[:7]) + cls_name = self.inv_class_map.get(int(cls_id), str(int(cls_id))) + results.append(Track(track_id=int(tid), cls=cls_name, conf=conf, bbox=(x1, y1, x2, y2))) + + return results diff --git a/AgCloud/services/security/agguard/core/types.py b/AgCloud/services/security/agguard/core/types.py new file mode 100644 index 000000000..cee091213 --- /dev/null +++ b/AgCloud/services/security/agguard/core/types.py @@ -0,0 +1,20 @@ +from __future__ import annotations +from dataclasses import dataclass +from typing import Tuple + +BBox = Tuple[int, int, int, int] # x1, y1, x2, y2 + +@dataclass +class Detection: + cls: str + conf: float + bbox: BBox + +@dataclass +class Track: + track_id: int + cls: str # change to str to match aggregator expectations + conf: float + bbox: tuple # (x1, y1, x2, y2) + hits: int = 0 + miss: int = 0 diff --git a/AgCloud/services/security/agguard/logging_utils.py b/AgCloud/services/security/agguard/logging_utils.py new file mode 100644 index 000000000..b1154e2c8 --- /dev/null +++ b/AgCloud/services/security/agguard/logging_utils.py @@ -0,0 +1,12 @@ +import logging +from logging.handlers import RotatingFileHandler +from typing import Optional + +def setup_logging(level: str = "INFO", log_file: Optional[str] = None) -> None: + fmt = "%(asctime)s | %(levelname)s | %(name)s | %(message)s" + logging.basicConfig(level=getattr(logging, level.upper(), logging.INFO), format=fmt) + if log_file: + handler = RotatingFileHandler(log_file, maxBytes=5_000_000, backupCount=3) + handler.setLevel(getattr(logging, level.upper(), logging.INFO)) + handler.setFormatter(logging.Formatter(fmt)) + logging.getLogger().addHandler(handler) diff --git a/AgCloud/services/security/agguard/media/hls_recorder.py b/AgCloud/services/security/agguard/media/hls_recorder.py new file mode 100644 index 000000000..487e1042f --- /dev/null +++ b/AgCloud/services/security/agguard/media/hls_recorder.py @@ -0,0 +1,660 @@ +# agguard/media/hls_recorder.py +from __future__ import annotations + +import os +import time +import shutil +import tempfile +import subprocess +import pathlib +import threading +import re +from dataclasses import dataclass, field +from typing import Optional, Tuple +from urllib.parse import urlparse + +import numpy as np # for blank/held frames + +_CT = { + ".m3u8": "application/vnd.apple.mpegurl", + ".ts": "video/MP2T", + ".m4s": "video/mp4", + ".mp4": "video/mp4", +} + +@dataclass +class HlsConfig: + # Fixed cadence; 4–6 fps works well for security UIs + fps: int = 5 + # 1.0s segments: good LIVE smoothness + 1s DVR seeks + segment_time: float = 1.0 + # Event playlist grows; proxy trims for LIVE + list_size: int = 240 + # Stick to TS for VLC; switch True for CMAF if wanted + use_cmaf: bool = False + # x264 knobs + preset: str = "veryfast" + crf: int = 23 + # one GOP per segment => clean random access + gop_segments: int = 1 + # S3 mirror cadence + upload_interval_sec: float = 0.02 + # Optional downscale (keeps aspect) + target_width: Optional[int] = None + # Some players behave better with silent audio + add_silent_audio: bool = True + + +@dataclass +class HlsRecorder: + s3: any + bucket: str + prefix: str + cfg: HlsConfig = field(default_factory=HlsConfig) + + _tmpdir: Optional[str] = None + _proc: Optional[subprocess.Popen] = None + + # Uploader state + _sync_thread: Optional[threading.Thread] = None + _stop_evt: threading.Event = field(default_factory=threading.Event) + _uploaded_keys: set = field(default_factory=set) + _ready_evt: threading.Event = field(default_factory=threading.Event) + + # Pacing/feeder state + _feeder_thread: Optional[threading.Thread] = None + _feeder_stop: threading.Event = field(default_factory=threading.Event) + _last_frame: Optional[np.ndarray] = None + _last_frame_lock: threading.Lock = field(default_factory=threading.Lock) + _frame_shape: Optional[Tuple[int, int]] = None + _first_frame_evt: threading.Event = field(default_factory=threading.Event) + + # ──────────────────────────────────────────────────────────────────────── + # Public API + # ──────────────────────────────────────────────────────────────────────── + def start(self, frame_size: Tuple[int, int]) -> None: + """ + Start ffmpeg HLS writer + background uploader + paced feeder. + This variant waits for the FIRST call to write_bgr() before feeding, + so we don't encode initial black frames. + """ + H, W = frame_size + self._frame_shape = (H, W) + self._tmpdir = tempfile.mkdtemp(prefix="hls_") + out = pathlib.Path(self._tmpdir) + + seg_ext = ".m4s" if self.cfg.use_cmaf else ".ts" + seg_pattern = str(out / f"segment_%05d{seg_ext}") + m3u8_path = str(out / "index.m3u8") + + fps = max(1, int(self.cfg.fps)) + seg_time = float(self.cfg.segment_time) + g_exact = max(1, int(round(fps * seg_time * max(1, self.cfg.gop_segments)))) + + vf_parts = [] + if self.cfg.target_width and W > self.cfg.target_width: + vf_parts.append(f"scale={self.cfg.target_width}:-2") + vf_parts.append("format=yuv420p") + vf = ",".join(vf_parts) + + cmd = [ + "ffmpeg", + "-loglevel", "warning", + # raw BGR frames on stdin; feeder will pace these + "-f", "rawvideo", + "-pix_fmt", "bgr24", + "-s:v", f"{W}x{H}", + "-r", str(fps), + "-i", "pipe:0", + ] + + # Optional silent audio (improves compatibility) + if self.cfg.add_silent_audio: + cmd += ["-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=48000"] + map_args = ["-map", "0:v:0", "-map", "1:a:0"] + else: + map_args = ["-map", "0:v:0"] + + cmd += [ + *map_args, + "-vf", vf, + "-c:v", "libx264", + "-preset", self.cfg.preset, + "-crf", str(self.cfg.crf), + "-profile:v", "main", + "-level:v", "4.1", + "-tune", "zerolatency", + "-g", str(g_exact), + "-keyint_min", str(g_exact), + "-sc_threshold", "0", + "-force_key_frames", f"expr:gte(t,n_forced*{seg_time})", + ] + + if not self.cfg.use_cmaf: + # Make TS segments self-contained + cmd += ["-mpegts_flags", "resend_headers+initial_discontinuity"] + + if self.cfg.add_silent_audio: + cmd += ["-c:a", "aac", "-ar", "48000", "-b:a", "128k"] + + # HLS muxing — EVENT playlist (grows); we’ll freeze it at finalize + hls_flags = "append_list+program_date_time+independent_segments+temp_file+split_by_time" + cmd += [ + "-f", "hls", + "-hls_time", str(seg_time), + "-hls_list_size", str(self.cfg.list_size), + "-hls_flags", hls_flags, + "-hls_segment_filename", seg_pattern, + "-hls_playlist_type", "event", + "-mpegts_flags", "resend_headers+initial_discontinuity", + "-movflags", "faststart", + m3u8_path, + ] + + if self.cfg.use_cmaf: + cmd += [ + "-hls_segment_type", "fmp4", + "-hls_fmp4_init_filename", "init.mp4", + "-hls_part_size", "0.2", + ] + + # Launch ffmpeg + self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, cwd=self._tmpdir) + + # We have not received any real frame yet + self._first_frame_evt.clear() + + # Start uploader (mirrors local files to S3 fast) + self._stop_evt.clear() + self._sync_thread = threading.Thread(target=self._sync_loop, daemon=True) + self._sync_thread.start() + + # Start paced feeder + self._feeder_stop.clear() + self._feeder_thread = threading.Thread(target=self._feeder_loop, daemon=True) + self._feeder_thread.start() + + def write_bgr(self, frame_bgr) -> None: + """Update the latest processed frame; feeder thread handles pacing.""" + if frame_bgr is None: + return + with self._last_frame_lock: + self._last_frame = np.ascontiguousarray(frame_bgr) + # If the feeder was waiting for the first frame, release it immediately + if not self._first_frame_evt.is_set(): + self._first_frame_evt.set() + + + + # ──────────────────────────────────────────────────────────────────────── + # Internals + # ──────────────────────────────────────────────────────────────────────── + def _feeder_loop(self): + """Write frames into ffmpeg stdin at a fixed cadence; hold last frame when idle.""" + fps = max(1, int(self.cfg.fps)) + period = 1.0 / float(fps) + + # Wait for the first real frame to avoid initial black. + # Since your aggregator calls write_bgr() immediately after start(), + # this should trigger right away. + self._first_frame_evt.wait(timeout=3.0) + + # If absolutely nothing arrived, we can still fall back to blank. + blank = None + if self._frame_shape: + H, W = self._frame_shape + blank = np.zeros((H, W, 3), dtype=np.uint8) + + while not self._feeder_stop.is_set(): + t0 = time.perf_counter() + with self._last_frame_lock: + frame = self._last_frame + if frame is None: + frame = blank + if frame is not None and self._proc and self._proc.poll() is None: + try: + self._proc.stdin.write(frame.tobytes()) + except BrokenPipeError: + self._feeder_stop.set() + break + except Exception: + pass + dt = time.perf_counter() - t0 + time.sleep(max(0.0, period - dt)) + + + + + def _parse_tail_refs(self, m3u8_path: pathlib.Path) -> list[str]: + """ + Return the list of segment filenames (URIs) in order, last item is the freshest. + Works for both TS and CMAF (fMP4) playlists produced by ffmpeg. + """ + try: + txt = m3u8_path.read_text(encoding="utf-8", errors="ignore") + except Exception: + return [] + uris = [] + for line in txt.splitlines(): + s = line.strip() + if not s or s.startswith("#"): + continue + # keep raw line (local filename) + uris.append(os.path.basename(s)) + return uris + def _make_publishable_index(self, m3u8_path: pathlib.Path, exist_names: set[str]) -> Optional[pathlib.Path]: + """ + Create a temp 'index.publish.m3u8' that is identical to the local m3u8 + but with any trailing (#EXTINF, URI) pairs removed if their URI file + does not exist in 'exist_names'. Keeps headers & MAP intact. + Returns path to the temp publishable file, or None if nothing to publish yet. + """ + try: + lines = m3u8_path.read_text(encoding="utf-8", errors="ignore").splitlines() + except Exception: + return None + + headers: list[str] = [] + body_pairs: list[tuple[str, str]] = [] # (extinf, uri) + pending_extinf: Optional[str] = None + map_line: Optional[str] = None + + for raw in lines: + s = raw.strip() + if not s: + continue + if s.startswith("#"): + if s.startswith("#EXTINF:"): + pending_extinf = s + elif s.startswith("#EXT-X-MAP:"): + map_line = s + elif s.startswith("#EXT-X-ENDLIST"): + # drop ENDLIST for live/event publishing + continue + else: + headers.append(s) + else: + # media URI + uri = os.path.basename(s) + if pending_extinf is None: + # defensive: if EXTINF missing, synthesize a 1s tag + pending_extinf = "#EXTINF:1.000," + body_pairs.append((pending_extinf, uri)) + pending_extinf = None + + # Trim from the tail until last URI exists + while body_pairs and body_pairs[-1][1] not in exist_names: + body_pairs.pop() + + if not body_pairs: + return None + + out: list[str] = ["#EXTM3U"] + # normalize a few important headers; keep original others + have_version = any(h.startswith("#EXT-X-VERSION:") for h in headers) + have_indep = any(h.startswith("#EXT-X-INDEPENDENT-SEGMENTS") for h in headers) + have_target = any(h.startswith("#EXT-X-TARGETDURATION:") for h in headers) + + if not have_version: + out.append("#EXT-X-VERSION:6") + for h in headers: + out.append(h) + if not have_indep: + out.append("#EXT-X-INDEPENDENT-SEGMENTS") + if not have_target: + out.append("#EXT-X-TARGETDURATION:1") + + if map_line: + out.append(map_line) + + for extinf, uri in body_pairs: + out.append(extinf) + out.append(uri) + + tmp = m3u8_path.parent / "index.publish.m3u8" + tmp.write_text("\n".join(out) + "\n", encoding="utf-8") + return tmp + + def _sync_loop(self) -> None: + """Continuously mirror new/changed local HLS files to S3 in a safe order. + + Order: + 1) init.mp4 (if CMAF) + 2) segments (.ts or .m4s) + 3) audio files (rare) + 4) index.m3u8 (LAST, and only after newest referenced segment is uploaded) + """ + if not self._tmpdir: + return + root = pathlib.Path(self._tmpdir) + seen: dict[str, tuple[int, int]] = {} + have_index = False + have_init = not self.cfg.use_cmaf + + interval = float(max(0.005, self.cfg.upload_interval_sec)) + while not self._stop_evt.is_set(): + try: + # Snapshot current files + files = [p for p in root.iterdir() if p.is_file()] + # Partition by type + init_files = [p for p in files if p.name == "init.mp4"] + seg_files_ts = [p for p in files if p.suffix.lower() == ".ts"] + seg_files_m4s = [p for p in files if p.suffix.lower() == ".m4s"] + audio_files = [p for p in files if p.suffix.lower() in (".aac", ".mp3", ".wav")] + m3u8_files = [p for p in files if p.name == "index.m3u8"] + + # 1) init.mp4 first (CMAF) + for p in init_files: + stat = p.stat() + sig = (stat.st_size, getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1e9))) + key = f"{self.prefix}/{p.name}" + if seen.get(key) != sig: + self.s3.put_file(self.bucket, key, str(p), _CT.get(".mp4", "video/mp4")) + seen[key] = sig + self._uploaded_keys.add(key) + have_init = True + + # 2) segments, sorted by name (sequence) + segs = sorted(seg_files_ts + seg_files_m4s, key=lambda x: x.name) + for p in segs: + stat = p.stat() + sig = (stat.st_size, getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1e9))) + key = f"{self.prefix}/{p.name}" + if seen.get(key) == sig: + continue + # Because ffmpeg uses -hls_flags temp_file, a segment appears atomically + # when fully written; we can upload immediately. + self.s3.put_file(self.bucket, key, str(p), _CT.get(p.suffix.lower(), "application/octet-stream")) + seen[key] = sig + self._uploaded_keys.add(key) + + # 3) any audio + for p in audio_files: + stat = p.stat() + sig = (stat.st_size, getattr(stat, "st_mtime_ns", int(stat.st_mtime * 1e9))) + key = f"{self.prefix}/{p.name}" + if seen.get(key) != sig: + self.s3.put_file(self.bucket, key, str(p), _CT.get(p.suffix.lower(), "application/octet-stream")) + seen[key] = sig + self._uploaded_keys.add(key) + + # 4) finally index.m3u8 — publish a trimmed version so it never points ahead + for p in m3u8_files: + stat = p.stat() + if stat.st_size <= 0: + continue + + # Build set of locally existing segment names (what we can publish) + existing = {q.name for q in segs} # segs you already built in step 2 + if self.cfg.use_cmaf: + existing |= {q.name for q in init_files} # init.mp4 is allowed in MAP + + publishable = self._make_publishable_index(p, existing) + if not publishable: + # Nothing safe to publish yet; try next loop + continue + + pub_stat = publishable.stat() + sig = (pub_stat.st_size, getattr(pub_stat, "st_mtime_ns", int(pub_stat.st_mtime * 1e9))) + key = f"{self.prefix}/index.m3u8" + if seen.get(key) != sig: + self.s3.put_file(self.bucket, key, str(publishable), _CT[".m3u8"]) + seen[key] = sig + self._uploaded_keys.add(key) + have_index = True + + + if have_index and have_init and not self._ready_evt.is_set(): + self._ready_evt.set() + + except Exception: + # best-effort sync + pass + + time.sleep(interval) + + + def wait_ready(self, timeout: float = 6.0) -> bool: + """Block until the playlist (+ init for CMAF) has been uploaded once (or timeout).""" + return self._ready_evt.wait(timeout) + + def _cleanup_local(self) -> None: + try: + if self._tmpdir and os.path.isdir(self._tmpdir): + shutil.rmtree(self._tmpdir, ignore_errors=True) + finally: + self._tmpdir = None + + + + + # ──────────────────────────────────────────────────────────────────────── + # Playlist freezing / reconstruction for finalize + # ──────────────────────────────────────────────────────────────────────── + def _build_temp_playlist( + self, + workdir: pathlib.Path, + segs_ts: list[pathlib.Path], + segs_m4s: list[pathlib.Path], + have_cmaf: bool, + ) -> Optional[pathlib.Path]: + """Create a local m3u8 referencing all segments we have.""" + if have_cmaf: + if not segs_m4s: + return None + td = 1 + p = workdir / "reconstructed.m3u8" + with p.open("w", encoding="utf-8") as f: + f.write("#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-PLAYLIST-TYPE:EVENT\n") + f.write("#EXT-X-INDEPENDENT-SEGMENTS\n") + f.write(f"#EXT-X-TARGETDURATION:{td}\n") + if (workdir / "init.mp4").exists(): + f.write('#EXT-X-MAP:URI="init.mp4"\n') + for s in segs_m4s: + f.write("#EXTINF:1.000,\n") + f.write(f"{s.name}\n") + return p + else: + if not segs_ts: + return None + td = 1 + p = workdir / "reconstructed.m3u8" + with p.open("w", encoding="utf-8") as f: + f.write("#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-PLAYLIST-TYPE:EVENT\n") + f.write("#EXT-X-INDEPENDENT-SEGMENTS\n") + f.write(f"#EXT-X-TARGETDURATION:{td}\n") + for s in segs_ts: + f.write("#EXTINF:1.000,\n") + f.write(f"{s.name}\n") + return p + + def stop(self): + """Stop all threads and kill ffmpeg reliably.""" + try: + # Stop feeder + self._feeder_stop.set() + if self._feeder_thread and self._feeder_thread.is_alive(): + self._feeder_thread.join(timeout=0.5) + + # Stop uploader + self._stop_evt.set() + if self._sync_thread and self._sync_thread.is_alive(): + self._sync_thread.join(timeout=0.5) + + # Close ffmpeg stdin + if self._proc and self._proc.stdin: + try: + self._proc.stdin.close() + except: + pass + + # HARD kill ffmpeg no matter what + if self._proc: + try: + self._proc.terminate() + self._proc.wait(timeout=0.5) + except Exception: + pass + finally: + try: + self._proc.kill() + self._proc.wait(timeout=0.5) + except: + pass + + finally: + # Always mark tmpdir as stopped + self._stopped_tmpdir = self._tmpdir + self._tmpdir = None + self._proc = None + + + + + + + def _freeze_local_playlist(self, m3u8_path: pathlib.Path, workdir: pathlib.Path) -> pathlib.Path: + """ + Produce a 'finalize.m3u8' that: + - keeps essential headers (normalized), + - rewrites URIs to local filenames, + - appends #EXT-X-ENDLIST, + - avoids HTTP or proxy (/seg?u=) paths so ffmpeg won't poll. + """ + def _maybe_localize_uri(uri: str) -> str: + m = re.search(r'[?&]u=([^&]+)', uri) + inner = m.group(1) if m else uri + u = urlparse(inner) + candidate = os.path.basename(u.path if u.scheme in ("http", "https") else inner) + return candidate # ffmpeg will read from cwd + + lines = m3u8_path.read_text(encoding="utf-8", errors="ignore").splitlines() + + version = 6 + target = 1 + have_independent = False + + body: list[str] = [] + for line in lines: + s = line.strip() + if not s: + continue + if s.startswith("#"): + if s.startswith("#EXT-X-VERSION:"): + version = 6 # normalize + elif s.startswith("#EXT-X-TARGETDURATION:"): + try: + target = max(target, int(float(s.split(":", 1)[1]))) + except Exception: + pass + elif s.startswith("#EXT-X-INDEPENDENT-SEGMENTS"): + have_independent = True + elif s.startswith("#EXT-X-MAP:"): + m = re.search(r'URI="([^"]+)"', s) + if m: + body.append(f'#EXT-X-MAP:URI="{_maybe_localize_uri(m.group(1))}"') + elif s.startswith("#EXTINF:"): + body.append(s) + # ignore PROGRAM-DATE-TIME, PLAYLIST-TYPE, MEDIA-SEQUENCE, etc. + continue + # media URI + body.append(_maybe_localize_uri(s)) + + frozen = ["#EXTM3U", f"#EXT-X-VERSION:{version}"] + if not have_independent: + frozen.append("#EXT-X-INDEPENDENT-SEGMENTS") + frozen.append(f"#EXT-X-TARGETDURATION:{max(1, int(target))}") + frozen.extend(body) + frozen.append("#EXT-X-ENDLIST") + + out_path = workdir / "finalize.m3u8" + out_path.write_text("\n".join(frozen) + "\n", encoding="utf-8") + return out_path + + def delete_hls_files_only(self) -> None: + """ + Delete all HLS-related files (.ts, .m3u8, .m4s, .aac, .wav, .mp3, .tmp) + from the directory where HLS was recorded. + """ + + # Use the directory saved in stop() + base = getattr(self, "_stopped_tmpdir", None) or self._tmpdir + if not base or not os.path.isdir(base): + print("[HLS DEBUG] No directory to clean:", base) + return + + base_dir = pathlib.Path(base) + + exts_to_delete = { + ".ts", ".m3u8", ".m4s", + ".aac", ".wav", ".mp3", + ".tmp" + } + + print("\n[HLS DEBUG] BEFORE DELETE:") + for p in base_dir.iterdir(): + print(" -", p.name) + + for p in base_dir.iterdir(): + if not p.is_file(): + continue + + suffix = p.suffix.lower() + if suffix in exts_to_delete or p.name.endswith(".tmp") or p.name == "init.mp4": + try: + p.unlink() + except Exception as e: + print(f"[HlsRecorder] ⚠️ Failed to delete {p}: {e}") + + print("\n[HLS DEBUG] AFTER DELETE:") + for p in base_dir.iterdir(): + print(" -", p.name) + + print(f"[HlsRecorder] ✅ Deleted HLS files in {base}") + + def delete_remote_hls(self): + """ + Delete ALL uploaded HLS fragments from S3/MinIO except final.mp4. + Uses S3Client.delete_prefix for efficiency. + """ + prefix = self.prefix.rstrip("/") + "/" + + print("[HLS] Deleting remote HLS under prefix:", prefix) + + # 1️⃣ List objects under this prefix + try: + resp = self.s3.s3.list_objects_v2( + Bucket=self.bucket, + Prefix=prefix + ) + except Exception as e: + print("[HLS] ❌ Failed to list remote objects:", e) + return + + objects = resp.get("Contents", []) + if not objects: + print("[HLS] (no remote objects found)") + return + + # 2️⃣ Build deletion list EXCEPT final.mp4 + to_delete = [] + for obj in objects: + key = obj["Key"] + name = key.split("/")[-1] + if name != "final.mp4": + to_delete.append({"Key": key}) + + if not to_delete: + print("[HLS] No HLS fragments to delete (only final.mp4 exists).") + return + + # 3️⃣ Batch delete + try: + self.s3.s3.delete_objects( + Bucket=self.bucket, + Delete={"Objects": to_delete} + ) + print(f"[HLS] ✅ Deleted {len(to_delete)} remote HLS objects.") + except Exception as e: + print("[HLS] ❌ Failed remote batch delete:", e) + diff --git a/AgCloud/services/security/agguard/media/mp4_recorder.py b/AgCloud/services/security/agguard/media/mp4_recorder.py new file mode 100644 index 000000000..2d9f26712 --- /dev/null +++ b/AgCloud/services/security/agguard/media/mp4_recorder.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import os +import time +import shutil +import tempfile +import subprocess +import pathlib +import threading +import re +from dataclasses import dataclass, field +from typing import Optional, Tuple +from urllib.parse import urlparse +from agguard.media.hls_recorder import HlsConfig + +import numpy as np # for blank/held frames +@dataclass +class Mp4Recorder: + """Simpler recorder used to build final MP4 only from new frames.""" + s3: any + bucket: str + prefix: str + cfg: HlsConfig = field(default_factory=HlsConfig) + _tmpdir: Optional[str] = None + _proc: Optional[subprocess.Popen] = None + + def start(self, frame_size: Tuple[int, int]) -> None: + H, W = frame_size + self._tmpdir = tempfile.mkdtemp(prefix="mp4_") + out_path = pathlib.Path(self._tmpdir) / "frames_pipe.mp4" + + cmd = [ + "ffmpeg", + "-y", + "-loglevel", "warning", + "-f", "rawvideo", + "-pix_fmt", "bgr24", + "-s:v", f"{W}x{H}", + "-r", str(self.cfg.fps), + "-i", "pipe:0", + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "23", + "-movflags", "+faststart", + str(out_path), + ] + + self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, cwd=self._tmpdir) + + def write_bgr(self, frame_bgr) -> None: + if self._proc and self._proc.poll() is None and frame_bgr is not None: + try: + self._proc.stdin.write(frame_bgr.tobytes()) + except BrokenPipeError: + pass + + def finalize(self) -> str: + if self._proc: + try: + if self._proc.stdin: + self._proc.stdin.close() + self._proc.wait(timeout=10) + except Exception: + pass + tmp = pathlib.Path(self._tmpdir or ".") + out_mp4 = tmp / "final.mp4" + + # Rename if needed (ffmpeg already wrote to frames_pipe.mp4) + pipe_out = tmp / "frames_pipe.mp4" + if pipe_out.exists(): + pipe_out.rename(out_mp4) + + mp4_key = f"{self.prefix}/final.mp4" + try: + self.s3.put_file(self.bucket, mp4_key, str(out_mp4), content_type="video/mp4") + except Exception: + pass + + shutil.rmtree(self._tmpdir, ignore_errors=True) + return mp4_key diff --git a/AgCloud/services/security/agguard/metrics/__init__.py b/AgCloud/services/security/agguard/metrics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/metrics/monitoring.py b/AgCloud/services/security/agguard/metrics/monitoring.py new file mode 100644 index 000000000..829ed9fed --- /dev/null +++ b/AgCloud/services/security/agguard/metrics/monitoring.py @@ -0,0 +1,142 @@ + +from prometheus_client import Counter, Histogram, Gauge, start_http_server +import threading, os, time, psutil + +# Optional torch + NVML for GPU monitoring +try: + import torch +except ImportError: + torch = None +try: + from pynvml import ( + nvmlInit, nvmlDeviceGetHandleByIndex, + nvmlDeviceGetUtilizationRates, nvmlDeviceGetMemoryInfo, + nvmlDeviceGetCount + ) + nvmlInit() +except Exception: + nvmlDeviceGetCount = None + + +# ───────────────────────────────────────────── +# Shared Prometheus metrics +# ───────────────────────────────────────────── +INFER_REQUESTS = Counter( + "inference_requests_total", + "Total inference requests", + ["service"] +) + +INFER_ERRORS = Counter( + "inference_errors_total", + "Total inference errors", + ["service"] +) + +INFER_LATENCY = Histogram( + "inference_latency_seconds", + "Inference latency in seconds", + ["service"] +) + +MODEL_LOAD_SEC = Gauge( + "model_load_seconds", + "Model load time in seconds", + ["service"] +) + +# ───────────────────────────────────────────── +# System / GPU metrics +# ───────────────────────────────────────────── +CPU_USAGE_PERCENT = Gauge( + "system_cpu_usage_percent", + "System CPU utilization percentage", +) + +PROC_CPU_PERCENT = Gauge( + "process_cpu_usage_percent", + "This process's CPU utilization percentage", +) + +PROC_MEM_MB = Gauge( + "process_memory_megabytes", + "This process's resident memory (MB)", +) + +GPU_USAGE_PERCENT = Gauge( + "gpu_utilization_percent", + "GPU utilization percentage per device", + ["gpu_id"] +) + +GPU_MEM_USED_MB = Gauge( + "gpu_memory_used_megabytes", + "GPU memory used (MB per device)", + ["gpu_id"] +) + +GPU_MEM_TOTAL_MB = Gauge( + "gpu_memory_total_megabytes", + "GPU total memory (MB per device)", + ["gpu_id"] +) + + +# ───────────────────────────────────────────── +# Background collector for system metrics +# ───────────────────────────────────────────── +def _collect_system_metrics(interval: int = 5): + """Continuously update CPU, memory, GPU metrics.""" + process = psutil.Process(os.getpid()) + while True: + try: + # System + process CPU/mem + CPU_USAGE_PERCENT.set(psutil.cpu_percent(interval=None)) + PROC_CPU_PERCENT.set(process.cpu_percent(interval=None)) + PROC_MEM_MB.set(process.memory_info().rss / (1024 * 1024)) + + # GPU metrics + if torch and torch.cuda.is_available(): + num_gpus = torch.cuda.device_count() + for i in range(num_gpus): + used = torch.cuda.memory_allocated(i) / (1024 * 1024) + total = torch.cuda.get_device_properties(i).total_memory / (1024 * 1024) + GPU_MEM_USED_MB.labels(gpu_id=str(i)).set(used) + GPU_MEM_TOTAL_MB.labels(gpu_id=str(i)).set(total) + GPU_USAGE_PERCENT.labels(gpu_id=str(i)).set((used / total) * 100) + elif nvmlDeviceGetCount: + for i in range(nvmlDeviceGetCount()): + handle = nvmlDeviceGetHandleByIndex(i) + util = nvmlDeviceGetUtilizationRates(handle) + mem = nvmlDeviceGetMemoryInfo(handle) + GPU_USAGE_PERCENT.labels(gpu_id=str(i)).set(util.gpu) + GPU_MEM_USED_MB.labels(gpu_id=str(i)).set(mem.used / (1024 * 1024)) + GPU_MEM_TOTAL_MB.labels(gpu_id=str(i)).set(mem.total / (1024 * 1024)) + else: + # No GPU — clear gauges + GPU_USAGE_PERCENT.clear() + GPU_MEM_USED_MB.clear() + GPU_MEM_TOTAL_MB.clear() + + except Exception as e: + print(f"[Metrics] ⚠️ System metrics update failed: {e}") + + time.sleep(interval) + + +# ───────────────────────────────────────────── +# Helper to start background /metrics server +# ───────────────────────────────────────────── +def start_metrics_server(): + """Start Prometheus metrics endpoint and background collector.""" + port = int(os.getenv("METRICS_PORT", "8000")) + threading.Thread(target=start_http_server, args=(port,), daemon=True).start() + threading.Thread(target=_collect_system_metrics, daemon=True).start() + print(f"[Metrics] 📊 Prometheus metrics exposed on :{port}/metrics") + + + + + + + diff --git a/AgCloud/services/security/agguard/pipeline/Dockerfile b/AgCloud/services/security/agguard/pipeline/Dockerfile new file mode 100644 index 000000000..c3543193f --- /dev/null +++ b/AgCloud/services/security/agguard/pipeline/Dockerfile @@ -0,0 +1,127 @@ + +# syntax=docker/dockerfile:1.6 + +############################################################ +# Stage 1 — Build Python virtual environment inside Flink base +############################################################ +FROM flink:1.19.3-scala_2.12-java11 AS builder + +USER root + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + OPENCV_OPENCL_RUNTIME=disabled + +# ─── System dependencies (for OpenCV, Torch, gRPC build) ─── +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip build-essential \ + libglib2.0-0 libgl1 libstdc++6 libgomp1 ffmpeg \ + ca-certificates curl wget && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /opt + +# ─── Create virtual environment ─── +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" + +# ─── Install Python dependencies with caching ─── +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip && \ + pip install \ + "numpy<2.0" apache-flink==1.19.0 \ + boto3 pyyaml opencv-python-headless \ + grpcio grpcio-tools requests Pillow minio kafka-python protobuf \ + google-cloud-storage \ + torch==2.2.2+cpu torchvision==0.17.2+cpu \ + onnx boxmot ffmpeg-python \ + --extra-index-url https://download.pytorch.org/whl/cpu + + + +# ─── Copy application source ─── +WORKDIR /opt/app +COPY agguard ./agguard +COPY configs ./configs + +# ─── Compile gRPC stubs ─── +RUN /opt/venv/bin/python -m grpc_tools.protoc \ + -I agguard/proto \ + --python_out=agguard/proto \ + --grpc_python_out=agguard/proto \ + agguard/proto/ingest.proto \ + agguard/proto/mask_classifier.proto \ + agguard/proto/mega_detector.proto + +# ─── Patch imports in generated stubs ─── +RUN /opt/venv/bin/python - <<'PY' +from pathlib import Path; import re +for p in Path("agguard/proto").glob("*_pb2_grpc.py"): + s = p.read_text() + s2 = re.sub(r'(?m)^import (\w+_pb2)\b', r'from . import \1', s) + if s != s2: + p.write_text(s2) + print("✅ patched:", p) +PY + + +############################################################ +# Stage 2 — Runtime image (Flink + PyFlink) +############################################################ +FROM flink:1.19.3-scala_2.12-java11 + +USER root + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + OPENCV_OPENCL_RUNTIME=disabled + +# ─── Minimal runtime dependencies ─── +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-venv libglib2.0-0 libgl1 libstdc++6 libgomp1 \ + ffmpeg ca-certificates curl wget && \ + rm -rf /var/lib/apt/lists/* + +# ─── Copy prebuilt environment and app ─── +COPY --from=builder /opt/venv /opt/venv +COPY --from=builder /opt/app /opt/app + +# ─── Environment variables for Flink + Python ─── +ENV PATH="/opt/venv/bin:${PATH}" \ + PYFLINK_PYTHON=/opt/venv/bin/python \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYTHONPATH="/opt/app:/opt/venv/lib/python3.*/site-packages" + +# ─── Kafka connectors (Flink 1.19) ─── +RUN mkdir -p /opt/flink/lib && \ + wget -q --tries=5 --retry-connrefused --waitretry=5 \ + https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -O /opt/flink/lib/kafka-clients-3.7.0.jar && \ + wget -q --tries=5 --retry-connrefused --waitretry=5 \ + https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -O /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +# ─── Runtime environment ─── +ENV KAFKA_BROKERS=kafka:9092 \ + IN_TOPIC=dev-security-images-keys \ + OUT_TOPIC=alerts \ + PIPELINE_CFG=/opt/app/configs/default.yaml \ + PYTHONPATH=/opt/app \ + GRPC_HOST=security:50052 + +EXPOSE 50051 + +USER flink +WORKDIR /opt/app + +CMD ["flink", "run", "-py", "agguard/pipeline/flink_job.py"] + + + diff --git a/AgCloud/services/security/agguard/pipeline/__init__.py b/AgCloud/services/security/agguard/pipeline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/pipeline/flink_job.py b/AgCloud/services/security/agguard/pipeline/flink_job.py new file mode 100644 index 000000000..ce835db7c --- /dev/null +++ b/AgCloud/services/security/agguard/pipeline/flink_job.py @@ -0,0 +1,222 @@ +import os, json, boto3, cv2, numpy as np, yaml, logging + +from pyflink.datastream import StreamExecutionEnvironment, RuntimeContext +from pyflink.datastream.functions import KeyedProcessFunction +from pyflink.datastream.connectors.kafka import ( + KafkaSource, + KafkaSink, + KafkaOffsetsInitializer, + KafkaRecordSerializationSchema, # ✅ add this +) +from pyflink.common import WatermarkStrategy, Types +from pyflink.common.serialization import SimpleStringSchema +from pyflink.datastream.connectors.kafka import DeliveryGuarantee + + + +from agguard.pipeline.flink_manager import FlinkPipelineManager +from agguard.core.events.models import Rule + +log = logging.getLogger("flink") + +# ---- Force all Python logs to stdout so Flink sees them ---- +import logging, sys + +import sys, logging + +# ---- Force all Python logs to go to stdout (captured by Flink) ---- +root = logging.getLogger() +root.setLevel(logging.DEBUG) + +# Remove any default handlers (Beam might pre-configure one that discards logs) +for h in list(root.handlers): + root.removeHandler(h) + +# Create stdout handler +stdout_handler = logging.StreamHandler(sys.stdout) +stdout_handler.setFormatter(logging.Formatter( + "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +)) +root.addHandler(stdout_handler) + +# Make sure every library logger propagates up to root +logging.captureWarnings(True) # also capture warnings.warn() calls +logging.getLogger().propagate = True +logging.getLogger("pyflink").setLevel(logging.DEBUG) +logging.getLogger("agguard").setLevel(logging.DEBUG) +logging.getLogger("botocore").setLevel(logging.WARNING) +logging.getLogger("urllib3").setLevel(logging.WARNING) + +# Optional: sanity check line +logging.getLogger("flink").info("[Init] Global stdout logger initialized.") + + + +class CameraOperator(KeyedProcessFunction): + def open(self, ctx: RuntimeContext): + # Load the same config as gRPC server + cfg_path = os.getenv("PIPELINE_CFG", "/app/configs/default.yaml") + cfg = yaml.safe_load(open(cfg_path, "r", encoding="utf-8")) + + # ---- Setup logging ---- + log_level = cfg.get("logging", {}).get("level", "INFO") + logging.basicConfig(level=getattr(logging, log_level.upper(), logging.INFO)) + + # ---- Create S3 client ---- + from agguard.adapters.s3_client import S3Client, S3Config + s3_cfg = cfg.get("s3", {}) + self.s3 = S3Client(S3Config( + region_name=s3_cfg.get("region_name", "us-east-1"), + aws_access_key_id=s3_cfg.get("aws_access_key_id"), + aws_secret_access_key=s3_cfg.get("aws_secret_access_key"), + endpoint_url=s3_cfg.get("endpoint_url"), + connect_timeout=float(s3_cfg.get("connect_timeout", 3.0)), + read_timeout=float(s3_cfg.get("read_timeout", 10.0)), + max_attempts=int(s3_cfg.get("max_attempts", 3)), + )) + + # ---- Rules (same as in grpc_server) ---- + from agguard.core.events.models import Rule + rules = [ + Rule( + name="intruding animal", + target_cls="animal", + target_cls_id=1, + match_classes=[ "fox","red_fox","kit_fox", "grey_fox", "brown_bear","bear", "American_black_bear", "wild_boar"], + severity=3, + min_conf=0.25, + min_consec=2, + cooldown=10, + ), + Rule( + name="climbing_fence", + target_cls="animal", + target_cls_id=1, + match_classes=["fox climbing a fence", "bear climbing a fence"], + severity=2, + min_conf=0.5, + min_consec=2, + cooldown=10, + ), + Rule( + name="masked_person", + target_cls="person", + target_cls_id=2, + match_classes=["mask"], + min_conf=0.5, + severity=6, + min_consec=2, + cooldown=6, + ), + + + ] + + + # ---- Create pipeline manager ---- + from agguard.pipeline.flink_manager import FlinkPipelineManager + self.pm = FlinkPipelineManager(cfg, self.s3, rules) + + log.info("[Flink] Initialized CameraOperator with %d rules and S3 at %s", + len(rules), s3_cfg.get("endpoint_url")) + + + def process_element(self, msg, ctx): + data = json.loads(msg) + + full_key = data["key"] # e.g. "security/imagery/air/2025-10-29/123/drone-01_20251029T093413Z.jpg" + file_name = data["file_name"] # e.g. "drone-01_20251029T093413Z.jpg" + linked_time = data.get("linked_time") + + # Split key → bucket + object path + parts = full_key.split("/", 1) + if len(parts) != 2: + log.error(f"Invalid key format (expected 'bucket/path'): {full_key}") + return + bucket, object_key = parts + + # Derive camera_id from the *first part* of the filename + camera_id = file_name.split("_")[0] # "drone-01" + + # Parse ISO 8601 time → seconds since epoch + from datetime import datetime + ts_sec = 0.0 + if linked_time: + try: + ts_sec = datetime.fromisoformat(linked_time.replace("Z", "+00:00")).timestamp() + except Exception as e: + log.warning(f"Invalid linked_time format: {linked_time} ({e})") + + # Fetch image from S3 + frame = self.s3.fetch_image_bgr(bucket, object_key) + + # Process frame through pipeline + evt = self.pm.process( + camera_id=camera_id, + ts_sec=ts_sec, + frame_idx=0, + frame_bgr=frame, + ) + + # Emit alert if there’s one + if evt: + out_json = json.dumps(evt) + log.info(f"[CameraOperator] Emitting alert for {evt.get('alert_id')} ({file_name})") + yield out_json + else: + log.debug(f"[CameraOperator] No alert emitted for {file_name}") + + + + + +def main(): + bootstrap = os.getenv("KAFKA_BROKERS", "kafka:9092") + topic_in = os.getenv("IN_TOPIC", "image_new_security_connections") + topic_out = os.getenv("OUT_TOPIC", "alerts") + + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(1) + + source = ( + KafkaSource.builder() + .set_bootstrap_servers(bootstrap) + .set_topics(topic_in) + .set_group_id("flink-camera-pipeline") + .set_starting_offsets(KafkaOffsetsInitializer.latest()) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + from pyflink.datastream.connectors.kafka import KafkaRecordSerializationSchema + + sink = ( + KafkaSink.builder() + .set_bootstrap_servers(bootstrap) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(topic_out) # static topic, from OUT_TOPIC + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .set_delivery_guarantee(DeliveryGuarantee.AT_LEAST_ONCE) + .build() +) + + + + stream = ( + env.from_source(source, WatermarkStrategy.no_watermarks(), "CameraFrames") + .key_by(lambda m: json.loads(m)["file_name"].split("_")[0]) + .process(CameraOperator(), output_type=Types.STRING()) + .sink_to(sink) +) + + + + env.execute("AgGuard Flink Pipeline") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/security/agguard/pipeline/flink_manager.py b/AgCloud/services/security/agguard/pipeline/flink_manager.py new file mode 100644 index 000000000..3f22b9a0a --- /dev/null +++ b/AgCloud/services/security/agguard/pipeline/flink_manager.py @@ -0,0 +1,152 @@ +from __future__ import annotations +import time, logging +from typing import Dict, Any, List, Optional, Tuple +import numpy as np + +from agguard.core.roi import Roi +from agguard.core.motion import MotionGate +from agguard.specialists.clients.megadetector import MegaDetectorClient +from agguard.core.tracker import BoxMOTWrapper +from agguard.specialists.dispatch import ClassDispatch +from agguard.core.events.aggregator import IncidentAggregator +from agguard.core.events.models import Rule + +log = logging.getLogger(__name__) + + + +class FlinkPipelineManager: + """ + Stateful per-camera pipeline for Flink. + Does NOT talk to DB or S3; emits events to be sent to Kafka. + """ + def __init__(self, cfg: Dict[str, Any],s3, rules: List[Rule]): + self.cfg = cfg + self.rules = rules + self.det = MegaDetectorClient(cfg.get("detector", {})) + self.router = ClassDispatch(cfg.get("specialists", [])) + self.change_thresh = float(cfg.get("change_thresh", 0.02)) + self._states: Dict[str, Dict[str, Any]] = {} + #added + self.s3=s3 + + + def _get_or_create(self, camera_id: str, frame_shape) -> Dict[str, Any]: + if camera_id in self._states: + return self._states[camera_id] + h, w = frame_shape[:2] + roi_poly = Roi.from_normalized([(0,0),(1,0),(1,1),(0,1)], (w,h)) + gate = MotionGate(roi_poly) + trk = BoxMOTWrapper() + + video_bucket = self.cfg.get("video_bucket", "imagery") + media_base = self.cfg.get("media_base", "http://media-proxy:8080") + + aggregator = IncidentAggregator( + rules=self.rules, + camera_id=camera_id, + s3=self.s3, # ✅ your S3 client (already passed into manager) + video_bucket=video_bucket, # ✅ bucket for uploads + video_prefix="security/incidents", + media_base=media_base, # ✅ Base URL for HLS/VOD + ) + + self._states[camera_id] = { + "roi": roi_poly, "gate": gate, "trk": trk, + "aggregator": aggregator, "fps_ema": None, "prev": time.perf_counter() + } + return self._states[camera_id] + + def process(self, camera_id: str, ts_sec: float, + frame_idx: Optional[int], frame_bgr: np.ndarray) -> Optional[Dict[str,Any]]: + + # Start timing for the whole frame + start_time = time.perf_counter() + log.info("[FlinkPipeline] ▶ START frame_idx=%s cam=%s", frame_idx, camera_id) + + p = self._get_or_create(camera_id, frame_bgr.shape) + gate, trk, aggregator = p["gate"], p["trk"], p["aggregator"] + + # ---- 1. Motion gate check ---- + reading = gate.update(frame_bgr) + if reading.score < self.change_thresh: + log.debug("[FlinkPipeline] frame_idx=%s cam=%s — skipped (static frame, score=%.4f)", + frame_idx, camera_id, reading.score) + return None + + # ---- 2. Detection ---- + t_det = time.perf_counter() + dets = self.det.detect(frame_bgr) + log.debug("[FlinkPipeline] frame_idx=%s cam=%s — detected %d objects in %.3fs", + frame_idx, camera_id, len(dets), time.perf_counter() - t_det) + + # ---- 3. Tracking ---- + t_trk = time.perf_counter() + tracks = trk.update(dets, frame_bgr) + # tracks = trk.update([(d.cls, d.conf, d.bbox) for d in dets], frame_bgr) + log.debug("[FlinkPipeline] frame_idx=%s cam=%s — tracker updated %d tracks in %.3fs", + frame_idx, camera_id, len(tracks), time.perf_counter() - t_trk) + + # ---- 4. Specialists (dispatchers) ---- + t_spec = time.perf_counter() + outs = self.router.run(frame_bgr, dets) + log.debug("[FlinkPipeline] frame_idx=%s cam=%s — all specialists done in %.3fs, outputs=%s", + frame_idx, camera_id, time.perf_counter() - t_spec, + {k: len(v) for k, v in outs.items()}) + + # ---- 5. Aggregation ---- + t_agg = time.perf_counter() + evt = aggregator.update(frame_idx, ts_sec, frame_bgr, tracks, outs) + log.debug("[FlinkPipeline] frame_idx=%s cam=%s — aggregator done in %.3fs", + frame_idx, camera_id, time.perf_counter() - t_agg) + + total_time = time.perf_counter() - start_time + log.info("[FlinkPipeline] ✅ DONE frame_idx=%s cam=%s total=%.3fs evt=%s", + frame_idx, camera_id, total_time, + "none" if evt is None else ("open" if evt.opened_incident_id else "close" if evt.closed_incident_id else "other")) + + + + if not evt: + return None + + if not (evt.opened_incident_id or evt.closed_incident_id): + return None + # Prefer closed_data (has more fields), else opened_data + data = evt.closed_data or evt.opened_data or {} + + # Build alert dictionary (only non-None fields) + alert = {} + + # Core identifiers + alert_id = evt.opened_incident_id or evt.closed_incident_id + if alert_id: + alert["alert_id"] = alert_id + else: + alert["alert_id"] = f"alert-{int(time.time()*1000)}" + + # Conditionally add non-empty fields + def put_if_value(key, value): + if value is not None: + alert[key] = value + + put_if_value("alert_type", data.get("kind")) + put_if_value("device_id", camera_id) + put_if_value("started_at", data.get("ts_iso")) + put_if_value("ended_at", data.get("ended_at_iso")) + put_if_value("severity", data.get("severity")) + put_if_value("vod", data.get("vod")) + put_if_value("hls", data.get("hls")) + # Optionally extend later: + + + meta = {"category": "security"} + subject = data.get("subject") + if subject: + meta["subject"] = subject + alert["meta"] = meta + print(alert) + return alert # return dict, not JSON string + + + diff --git a/AgCloud/services/security/agguard/proto/__init__.py b/AgCloud/services/security/agguard/proto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/proto/ingest.proto b/AgCloud/services/security/agguard/proto/ingest.proto new file mode 100644 index 000000000..4685d4160 --- /dev/null +++ b/AgCloud/services/security/agguard/proto/ingest.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; +package agguard.ingest; + +// Basic detection box for optional echo/telemetry +message BBox { + int32 x1 = 1; + int32 y1 = 2; + int32 x2 = 3; + int32 y2 = 4; + string cls = 5; + float conf = 6; + int32 track_id = 7; // -1 if not tracked +} + +message ProcessImageRequest { + // Source location (REQUIRED): the image must already be in S3/MinIO + string s3_bucket = 10; + string s3_key = 11; + + // Metadata (used to keep per-camera state + DB persistence) + string camera_id = 2; // required for tracking continuity + int64 ts_millis = 3; // event time (ms since epoch) + int64 frame_idx = 4; // monotonically increasing per camera, if known + + // Optional toggles + bool return_detections = 20; // echo detections in response +} + +message ProcessImageResponse { + // Quick telemetry for the caller + string camera_id = 1; + int64 frame_idx = 2; + double change_score = 3; // MotionGate score used for gating + int32 num_detections = 4; + int32 num_tracks = 5; + double fps_estimate = 6; + + // Incident state (if IncidentAggregator opened/updated/closed an incident) + // These fields are best-effort signals; you still persist via DbRepository. + string opened_incident_id = 20; // empty if none + string updated_incident_id = 21; // empty if none + string closed_incident_id = 22; // empty if none + + // Optional detailed output (only if requested) + repeated BBox boxes = 30; +} + +service ImageIngestor { + rpc ProcessImage (ProcessImageRequest) returns (ProcessImageResponse); +} diff --git a/AgCloud/services/security/agguard/proto/mask_classifier.proto b/AgCloud/services/security/agguard/proto/mask_classifier.proto new file mode 100644 index 000000000..134eaa5f6 --- /dev/null +++ b/AgCloud/services/security/agguard/proto/mask_classifier.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; +package agguard.classifiers; + +option go_package = "agguard/classifiers"; +option java_multiple_files = true; + +message Crop { + // JPEG-encoded RGB crop for one detected face/person-region. + bytes jpeg = 1; + // Original box on the full frame, for traceability. + int32 x1 = 2; + int32 y1 = 3; + int32 x2 = 4; + int32 y2 = 5; + string subject = 6; +} + +message ClassifyRequest { + // For future classifiers (e.g., "mask", "helmet", "balaclava") + string model_name = 1; + repeated Crop crops = 2; +} + +message Prediction { + int32 x1 = 1; + int32 y1 = 2; + int32 x2 = 3; + int32 y2 = 4; + string label = 5; // lowercased label + float confidence = 6; // 0..1 +} + +message ClassifyResponse { + repeated Prediction preds = 1; +} + +service ClassifierService { + rpc Classify(ClassifyRequest) returns (ClassifyResponse); +} diff --git a/AgCloud/services/security/agguard/proto/mega_detector.proto b/AgCloud/services/security/agguard/proto/mega_detector.proto new file mode 100644 index 000000000..4477399d9 --- /dev/null +++ b/AgCloud/services/security/agguard/proto/mega_detector.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package agguard.proto; + +message ImageRequest { + string image_path = 1; // path in mounted volume, e.g., /data/frames/dev_a/frame_00116.jpg + bytes image_bytes = 2; // optional inline bytes +} + +message Detection { + string cls = 1; + float conf = 2; + float x1 = 3; + float y1 = 4; + float x2 = 5; + float y2 = 6; +} + +message DetectionResponse { + repeated Detection detections = 1; + double inference_time = 2; +} + +service MegaDetector { + rpc Detect (ImageRequest) returns (DetectionResponse); +} diff --git a/AgCloud/services/security/agguard/proto_gen/__init__.py b/AgCloud/services/security/agguard/proto_gen/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/specialists/__init__.py b/AgCloud/services/security/agguard/specialists/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/specialists/animal_service/Dockerfile.animal-classifier b/AgCloud/services/security/agguard/specialists/animal_service/Dockerfile.animal-classifier new file mode 100644 index 000000000..a4e70176e --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/animal_service/Dockerfile.animal-classifier @@ -0,0 +1,97 @@ +############################ +# Animal Classifier — YOLO-CLS gRPC microservice +# Auto-builds gRPC stubs (reusing mask-classifier.proto) +############################ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + OPENCV_OPENCL_RUNTIME=disabled + +# ──────────────────────────────── +# System libs for OpenCV / PyTorch +# ──────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 \ + libgl1 \ + libstdc++6 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# ──────────────────────────────── +# Python dependencies +# ──────────────────────────────── +# Your requirements.txt should include: +# torch, ultralytics, opencv-python-headless, pillow, +# grpcio, grpcio-tools, protobuf, numpy, etc. +COPY agguard/specialists/animal_service/requirements.txt . + +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --upgrade pip==25.0.1 && \ + pip install --no-cache-dir -r requirements.txt \ + --extra-index-url https://download.pytorch.org/whl/cpu + + + +# ──────────────────────────────── +# App source code +# ──────────────────────────────── +COPY agguard ./agguard + +# ──────────────────────────────── +# Weights (YOLO-CLS) +# ──────────────────────────────── +RUN mkdir -p /app/weights +# Optional: copy pretrained YOLO-CLS weights if stored locally +COPY weights/yolov8n-cls.pt /app/weights/yolov8n-cls.pt +RUN test -f /app/weights/yolov8n-cls.pt && echo "✅ YOLO-CLS weights present" || echo "ℹ️ YOLO-CLS weights will be mounted at runtime" + +# ──────────────────────────────── +# gRPC stub generation +# ──────────────────────────────── +RUN mkdir -p agguard/proto && touch agguard/proto/__init__.py + +# Generate stubs from mask-classifier.proto (shared proto) +RUN python -m grpc_tools.protoc \ + -I agguard/proto \ + --python_out=agguard/proto \ + --grpc_python_out=agguard/proto \ + mask_classifier.proto + +# Patch imports to be package-relative (so "from . import X_pb2") +RUN python - <<'PY' +from pathlib import Path +import re +p = Path("agguard/proto/mask_classifier_pb2_grpc.py") +if p.exists(): + s = p.read_text(encoding="utf-8") + s2 = re.sub(r'(?m)^import (\w+_pb2)\b', r'from . import \1', s) + if s2 != s: + p.write_text(s2, encoding="utf-8") + print("patched", p) + else: + print("no patch needed", p) +else: + print("mask_classifier_pb2_grpc.py not found; proto may be missing") +PY + +# ──────────────────────────────── +# Service defaults +# ──────────────────────────────── +ENV PORT=50064 \ + MODEL_PATH=/app/weights/yolov8n-cls.pt \ + DEVICE=cpu \ + MODEL_NAME=yolo-cls + +EXPOSE 50064 + +# ──────────────────────────────── +# Entrypoint +# ──────────────────────────────── +CMD ["python", "-m", "agguard.specialists.animal_service.server"] diff --git a/AgCloud/services/security/agguard/specialists/animal_service/requirements.txt b/AgCloud/services/security/agguard/specialists/animal_service/requirements.txt new file mode 100644 index 000000000..fdeb34e04 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/animal_service/requirements.txt @@ -0,0 +1,15 @@ +# ─── Core deep learning and vision ─── +torch==2.2.2+cpu +torchvision==0.17.2+cpu +ultralytics==8.3.0 +pillow==10.4.0 + +# ─── gRPC and protobuf ─── +grpcio==1.62.2 +grpcio-tools==1.62.2 +protobuf==4.25.3 + +prometheus_client +psutil + + diff --git a/AgCloud/services/security/agguard/specialists/animal_service/server.py b/AgCloud/services/security/agguard/specialists/animal_service/server.py new file mode 100644 index 000000000..2a2782df5 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/animal_service/server.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Animal Classifier microservice — reusing mask-classifier.proto. +Maps model class names (e.g., "american_black_bear", "sloth_bear") +to unified labels (e.g., "bear"). Unrecognized classes → "other". +Now includes Prometheus metrics and system monitoring. +""" + +from __future__ import annotations +import io, os, time, grpc +from concurrent import futures +from PIL import Image +from ultralytics import YOLO +from agguard.proto import mask_classifier_pb2 as pb2 +from agguard.proto import mask_classifier_pb2_grpc as pb2_grpc + +# ───────────────────────────────────────────── +# Prometheus metrics import +# ───────────────────────────────────────────── +from agguard.metrics.monitoring import ( + start_metrics_server, + INFER_REQUESTS, INFER_ERRORS, INFER_LATENCY, MODEL_LOAD_SEC +) + + +class AnimalClassifierServicer(pb2_grpc.ClassifierServiceServicer): + def __init__(self): + model_path = os.getenv("MODEL_PATH", "/app/weights/yolov8n-cls.pt") + print(f"[AnimalClassifier] 🔹 Loading {model_path} ...") + t0 = time.time() + + self.model = YOLO(model_path) + load_time = time.time() - t0 + MODEL_LOAD_SEC.labels(service="animal_classifier").set(load_time) + print(f"[AnimalClassifier] ✅ Model loaded in {load_time:.1f}s") + + # ───────────────────────────────────────────── + # Class mapping + # ───────────────────────────────────────────── + self.label_map = { + # Bears + "american_black_bear": "bear", + "sloth_bear": "bear", + "brown_bear": "bear", + "gibbon": "bear", + "siamang": "bear", + "velvet": "bear", + "colobus": "bear", + "indri": "bear", + "howler_monkey": "bear", + "capuchin":"bear", + # Foxes + "red_fox": "fox", + "grey_fox": "fox", + "kit_fox": "fox", + "white_wolf": "fox", + "kuvasz": "fox", + "dugong": "fox", + "arctic_fox": "fox", + "fox_squirrel": "fox", + "hog": "fox", + "tusker": "fox", + "mink": "fox", + "jay": "fox", + # Others + "wild_boar": "boar", + "wolf": "wolf", + "deer": "deer", + "rabbit": "rabbit", + } + self.intruding = set(self.label_map.values()) + + def _predict(self, jpeg: bytes): + img = Image.open(io.BytesIO(jpeg)).convert("RGB") + res = self.model.predict(img, verbose=False)[0] + idx = res.probs.top1 + conf = float(res.probs.top1conf.item()) + raw_label = res.names[idx].lower().strip() + + label = self.label_map.get(raw_label, "other") + if label not in self.intruding: + label = "other" + + print(f"[AnimalClassifier] raw={raw_label}, mapped={label}, conf={conf:.3f}") + return label, conf + + def Classify(self, request, context): + INFER_REQUESTS.labels(service="animal_classifier").inc() + t0 = time.time() + preds = [] + try: + for crop in request.crops: + try: + label, conf = self._predict(crop.jpeg) + except Exception as e: + INFER_ERRORS.labels(service="animal_classifier").inc() + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details(f"Failed to classify crop: {e}") + return pb2.ClassifyResponse() + preds.append(pb2.Prediction( + label=label, + confidence=conf, + x1=crop.x1, y1=crop.y1, x2=crop.x2, y2=crop.y2, + )) + except Exception as e: + INFER_ERRORS.labels(service="animal_classifier").inc() + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details(str(e)) + return pb2.ClassifyResponse() + + dt = time.time() - t0 + INFER_LATENCY.labels(service="animal_classifier").observe(dt) + + print(f"[AnimalClassifier] → {len(preds)} predictions in {dt:.2f}s") + return pb2.ClassifyResponse(preds=preds) + + +def serve(): + port = int(os.getenv("PORT", "50064")) + metrics_port = int(os.getenv("METRICS_PORT", "8008")) + + start_metrics_server() + print(f"[AnimalClassifier] 📊 Metrics exposed on :{metrics_port}/metrics") + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=2)) + pb2_grpc.add_ClassifierServiceServicer_to_server(AnimalClassifierServicer(), server) + server.add_insecure_port(f"[::]:{port}") + print(f"[AnimalClassifier] 🚀 gRPC server running on port {port}") + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() diff --git a/AgCloud/services/security/agguard/specialists/anomalies_service/Dockerfile.anomalies-classifier b/AgCloud/services/security/agguard/specialists/anomalies_service/Dockerfile.anomalies-classifier new file mode 100644 index 000000000..a92b67060 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/anomalies_service/Dockerfile.anomalies-classifier @@ -0,0 +1,72 @@ +# Dockerfile.anomalies-classifier + +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + OMP_NUM_THREADS=1 + +WORKDIR /app + +# System libs (OpenCV runtime deps) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libglib2.0-0 libgl1 \ + && rm -rf /var/lib/apt/lists/* + +# --- Python deps --- +# 1) Pin NumPy to 1.x BEFORE torch/open_clip to avoid ABI mismatch +# 2) Then base libs, then torch CPU wheels, then open_clip_torch +# 3) FINAL GUARD: force-reinstall numpy==1.26.4 and fail build if not 1.26.x +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --upgrade pip && \ + pip install --no-cache-dir "numpy==1.26.4" && \ + pip install --no-cache-dir \ + grpcio grpcio-tools "protobuf>=4.24" \ + pillow \ + opencv-python-headless \ + psutil prometheus-client && \ + pip install --no-cache-dir \ + torch==2.2.2+cpu torchvision==0.17.2+cpu --index-url https://download.pytorch.org/whl/cpu && \ + pip install --no-cache-dir open_clip_torch && \ + pip install --no-cache-dir --upgrade --force-reinstall "numpy==1.26.4" && \ + python -c "import numpy as np, sys; print('NumPy(final)=', np.__version__); sys.exit(0 if np.__version__.startswith('1.26.') else 1)" + +# --- App code & proto sources --- +COPY agguard agguard +COPY agguard/proto agguard/proto + +# Ensure packages are importable +RUN mkdir -p agguard && touch agguard/__init__.py && touch agguard/proto/__init__.py + +# Compile stubs using the EXISTING generic proto (shared Crop→Prediction schema) +RUN python -m grpc_tools.protoc \ + -I agguard/proto \ + --python_out=agguard/proto \ + --grpc_python_out=agguard/proto \ + agguard/proto/mask_classifier.proto && \ + python -c "from pathlib import Path; import re; p=Path('agguard/proto/mask_classifier_pb2_grpc.py'); s=p.read_text(); s2=re.sub(r'(?m)^import (\\w+_pb2)\\b', r'from . import \\1', s); p.write_text(s2)" + +# --- Service configuration (override in docker-compose if desired) --- +# --- Service configuration (override in docker-compose if desired) --- +ENV PORT=50062 \ + DEVICE=cpu \ + CLIP_MODEL=RN50 \ + CLIP_PRETRAINED=openai \ + CLIP_INPUT_SIZE=224 \ + CLIP_BATCH=32 \ + CLIP_AGGREGATE=meanmax \ + CLIP_LABELS="normal activity,climbing a fence" \ + CLIP_TEMPLATES="a CCTV frame of {},a surveillance video of {},a low-resolution photo of {},a person {},an action of {},a person is {}" \ + CLIP_THRESHOLDS="normal activity:0.30,climbing a fence:0.50" \ + ENABLE_MKLDNN=1 \ + NUM_THREADS=6 \ + CHANNELS_LAST=1 \ + PREPROCESS_CLAHE=1 \ + PREPROCESS_UNSHARP=1 + + +EXPOSE 50062 + +# Entry point (matches the copied server file) +CMD ["python", "-m", "agguard.specialists.anomalies_service.server"] diff --git a/AgCloud/services/security/agguard/specialists/anomalies_service/server.py b/AgCloud/services/security/agguard/specialists/anomalies_service/server.py new file mode 100644 index 000000000..3af07421a --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/anomalies_service/server.py @@ -0,0 +1,197 @@ + + +from __future__ import annotations +import os, logging, warnings, time +from concurrent import futures +from typing import List, Tuple, Dict + +import numpy as np +import cv2 +import grpc +from PIL import Image +import torch +import open_clip + +from agguard.proto import mask_classifier_pb2 as pb +from agguard.proto import mask_classifier_pb2_grpc as pbrpc + +# ───────────────────────────────────────────── +# Prometheus metrics import +# ───────────────────────────────────────────── +from agguard.metrics.monitoring import ( + start_metrics_server, + INFER_REQUESTS, INFER_ERRORS, INFER_LATENCY, MODEL_LOAD_SEC +) + +log = logging.getLogger(__name__) +logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO")) + +Box = Tuple[int, int, int, int] + +# --- tolerate either service name in generated stubs (ClassifierService|Classifier) --- +ServicerBase = getattr(pbrpc, "ClassifierServiceServicer", + getattr(pbrpc, "ClassifierServicer", None)) +if ServicerBase is None: + raise ImportError("No Classifier{Service}Servicer in mask_classifier_pb2_grpc.py") + +add_servicer = getattr(pbrpc, "add_ClassifierServiceServicer_to_server", + getattr(pbrpc, "add_ClassifierServicer_to_server", None)) +if add_servicer is None: + raise ImportError("No add_Classifier{Service}Servicer_to_server in mask_classifier_pb2_grpc.py") + + +def _jpeg_to_pil(jpeg_bytes: bytes, size: int) -> Image.Image: + """Decode JPEG -> RGB PIL image resized to model input.""" + arr = np.frombuffer(jpeg_bytes, dtype=np.uint8) + bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR) + if bgr is None: + raise ValueError("Failed to decode JPEG") + bgr = cv2.resize(bgr, (size, size), interpolation=cv2.INTER_AREA) + rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + return Image.fromarray(rgb) + + +class ClipClassifierServicer(ServicerBase): + """ + gRPC service using OpenCLIP RN50 to estimate the probability that a given subject is climbing a fence. + Includes Prometheus monitoring. + """ + + def __init__(self): + service_name = "clip_classifier" + + # --- Environment configuration --- + self.model_name = os.environ.get("CLIP_MODEL", "RN50") + self.pretrained = os.environ.get("CLIP_PRETRAINED", "openai") + self.input_size = int(os.environ.get("CLIP_INPUT_SIZE", "224")) + self.temperature = float(os.environ.get("CLIP_TEMPERATURE", "100.0")) + self.batch_size = int(os.environ.get("CLIP_BATCH", "32")) + + device_env = os.environ.get("DEVICE") + self.device = torch.device(device_env if device_env else ("cuda" if torch.cuda.is_available() else "cpu")) + + # Optional performance flags + enable_mkldnn = os.environ.get("ENABLE_MKLDNN", "1").lower() not in ("0", "false") + num_threads = os.environ.get("NUM_THREADS") + if enable_mkldnn and self.device.type == "cpu": + torch.backends.mkldnn.enabled = True + if num_threads and self.device.type == "cpu": + torch.set_num_threads(int(num_threads)) + + warnings.filterwarnings("ignore", message="QuickGELU mismatch.*") + + # --- Load CLIP model --- + log.info("[CLIPClassifier] 🔹 Loading OpenCLIP model %s/%s ...", self.model_name, self.pretrained) + t0 = time.time() + self.model, _, self.preprocess = open_clip.create_model_and_transforms( + self.model_name, pretrained=self.pretrained + ) + self.tokenizer = open_clip.get_tokenizer(self.model_name) + self.model = self.model.to(self.device).eval() + load_time = time.time() - t0 + MODEL_LOAD_SEC.labels(service=service_name).set(load_time) + log.info("[CLIPClassifier] ✅ Model loaded in %.1fs on %s", load_time, self.device) + + self._emb_dim = getattr(self.model.visual, "output_dim", 1024) + self.service_name = service_name + + # ===================================================== + # 🔹 Encode helper + # ===================================================== + def _encode_images(self, images: List[Image.Image]) -> torch.Tensor: + if not images: + return torch.empty(0, self._emb_dim, device=self.device) + xs = [self.preprocess(im).unsqueeze(0) for im in images] + x = torch.cat(xs, dim=0).to(self.device) + with torch.inference_mode(): + feats = [] + for i in range(0, len(x), self.batch_size): + z = self.model.encode_image(x[i:i + self.batch_size]) + z = z / z.norm(dim=-1, keepdim=True) + feats.append(z) + return torch.cat(feats, dim=0) + + # ===================================================== + # 🔹 Classification logic + # ===================================================== + def _classify_one(self, image_emb: torch.Tensor, subject: str) -> float: + subj = (subject or "object").strip().lower() + positive_prompt = f"a {subj} climbing a fence" + negative_prompt = f"an image of a {subj} not climbing a fence" + + text_tokens = self.tokenizer([positive_prompt, negative_prompt]).to(self.device) + with torch.inference_mode(): + text_features = self.model.encode_text(text_tokens) + text_features /= text_features.norm(dim=-1, keepdim=True) + logits = (self.temperature * image_emb @ text_features.T).softmax(dim=-1) + prob_climbing = float(logits[0, 0].item()) + return prob_climbing + + # ===================================================== + # 🔹 gRPC entrypoint + # ===================================================== + def Classify(self, request: pb.ClassifyRequest, context) -> pb.ClassifyResponse: + INFER_REQUESTS.labels(service=self.service_name).inc() + t0 = time.time() + + try: + images: List[Image.Image] = [] + boxes: List[Box] = [] + subjects: List[str] = [] + + for c in request.crops: + try: + im = _jpeg_to_pil(c.jpeg, self.input_size) + images.append(im) + boxes.append((c.x1, c.y1, c.x2, c.y2)) + subj = getattr(c, "subject", "") or "object" + subjects.append(subj) + except Exception as e: + log.warning("[CLIPClassifier] Failed to decode crop: %s", e) + + resp = pb.ClassifyResponse() + if not images: + return resp + + img_emb = self._encode_images(images) + + for j, box in enumerate(boxes): + prob = self._classify_one(img_emb[j:j+1], subjects[j]) + label = f"{subjects[j]} climbing a fence" + resp.preds.add( + x1=box[0], y1=box[1], x2=box[2], y2=box[3], + label=label, + confidence=prob + ) + + dt = time.time() - t0 + INFER_LATENCY.labels(service=self.service_name).observe(dt) + log.info("[CLIPClassifier] → %d predictions in %.2fs", len(resp.preds), dt) + return resp + + except Exception as e: + INFER_ERRORS.labels(service=self.service_name).inc() + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details(str(e)) + log.exception("[CLIPClassifier] Inference failed: %s", e) + return pb.ClassifyResponse() + + +def serve(): + port = int(os.environ.get("PORT", "50062")) + metrics_port = int(os.environ.get("METRICS_PORT", "8011")) + + # Start Prometheus metrics server + start_metrics_server() + log.info("[CLIPClassifier] 📊 Prometheus metrics exposed on :%d/metrics", metrics_port) + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=2)) + add_servicer(ClipClassifierServicer(), server) + server.add_insecure_port(f"[::]:{port}") + log.info("[CLIPClassifier] 🚀 gRPC server listening on :%d", port) + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() diff --git a/AgCloud/services/security/agguard/specialists/clients/animal.py b/AgCloud/services/security/agguard/specialists/clients/animal.py new file mode 100644 index 000000000..042e8ab19 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/clients/animal.py @@ -0,0 +1,114 @@ +from __future__ import annotations +import logging +from typing import List, Tuple, Optional +import cv2, numpy as np, grpc + +from agguard.proto import mask_classifier_pb2 as pb +from agguard.proto import mask_classifier_pb2_grpc as pbrpc + +log = logging.getLogger(__name__) +Box = Tuple[int, int, int, int] + +StubClass = getattr(pbrpc, "ClassifierServiceStub", + getattr(pbrpc, "ClassifierStub", None)) +if StubClass is None: + raise ImportError("mask_classifier_pb2_grpc missing ClassifierServiceStub/ClassifierStub") + + +class GrpcAnimalClassifierClient: + def __init__(self, address: str, model_name: str = "yolo-cls", + timeout_sec: float = 12.0, jpeg_quality: int = 85): + self.address = address + self.model_name = model_name + self.timeout = float(timeout_sec) + self.jpeg_quality = int(jpeg_quality) + + self._chan = grpc.insecure_channel(self.address, options=[ + ("grpc.max_send_message_length", 32 * 1024 * 1024), + ("grpc.max_receive_message_length", 32 * 1024 * 1024), + ]) + self._stub = StubClass(self._chan) + + log.info("GrpcAnimalClassifierClient -> %s (model=%s)", + self.address, self.model_name) + + # ─────────────────────────────────────────────── + # Padded crop extractor (20% padding) + # ─────────────────────────────────────────────── + @staticmethod + def _safe_crop(frame_bgr: np.ndarray, box: Box, + pad_ratio: float = 0.2) -> Optional[np.ndarray]: + """ + Return the crop with padding around the box (20% by default). + Clamps to frame boundaries. + """ + H, W = frame_bgr.shape[:2] + x1, y1, x2, y2 = map(int, box) + + # Original width/height + w = max(0, x2 - x1) + h = max(0, y2 - y1) + if w < 1 or h < 1: + return None + + # Compute padding + pad_x = int(w * pad_ratio) + pad_y = int(h * pad_ratio) + + # Expand bounding box + nx1 = max(0, x1 - pad_x) + ny1 = max(0, y1 - pad_y) + nx2 = min(W, x2 + pad_x) + ny2 = min(H, y2 + pad_y) + + if nx2 <= nx1 or ny2 <= ny1: + return None + + return frame_bgr[ny1:ny2, nx1:nx2] + + # ─────────────────────────────────────────────── + # Classify crops + # ─────────────────────────────────────────────── + def classify(self, frame_bgr: np.ndarray, boxes: List[Box]): + crops: List[pb.Crop] = [] + + for b in boxes: + # Use padded crop + crop = self._safe_crop(frame_bgr, b, pad_ratio=0.2) + if crop is None: + continue + + rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) + ok, buf = cv2.imencode(".jpg", rgb, + [int(cv2.IMWRITE_JPEG_QUALITY), + self.jpeg_quality]) + if not ok: + continue + + # IMPORTANT: send original box (not padded box) + crops.append(pb.Crop( + jpeg=buf.tobytes(), + x1=b[0], y1=b[1], x2=b[2], y2=b[3] + )) + + if not crops: + return [] + + req = pb.ClassifyRequest(model_name=self.model_name, crops=crops) + + try: + resp = self._stub.Classify(req, timeout=self.timeout) + except Exception as e: + log.warning("gRPC classify failed (%s): %s", self.address, e) + return [] + + out = [] + for p in resp.preds: + out.append({ + "box": (p.x1, p.y1, p.x2, p.y2), + "label": p.label, + "confidence": p.confidence, + }) + + return out + diff --git a/AgCloud/services/security/agguard/specialists/clients/anomalies.py b/AgCloud/services/security/agguard/specialists/clients/anomalies.py new file mode 100644 index 000000000..025a9619a --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/clients/anomalies.py @@ -0,0 +1,109 @@ + +from __future__ import annotations +from typing import List, Tuple, Optional +import cv2 +import grpc +from agguard.proto import mask_classifier_pb2 as pb +from agguard.proto import mask_classifier_pb2_grpc as pbrpc + +BBox = Tuple[int, int, int, int] + + +class GrpcClipClassifierClient: + """ + Client for the CLIP climbing classifier microservice. + + classify(frame_bgr, boxes, subjects=None) -> List[pb.Pred] + """ + + def __init__( + self, + address: str, + timeout_sec: float = 1.5, + jpeg_quality: int = 85, + padding_ratio: float = 0.8, # <── added padding support + ): + self.address = address + self.timeout = float(timeout_sec) + self.jpeg_quality = int(jpeg_quality) + self.padding_ratio = float(padding_ratio) + + self.channel = grpc.insecure_channel(address) + self.stub = ( + pbrpc.ClassifierServiceStub(self.channel) + if hasattr(pbrpc, "ClassifierServiceStub") + else pbrpc.ClassifierStub(self.channel) + ) + + # ------------------------------------------------------------- + # 🔹 Encode crop with proportional padding + # ------------------------------------------------------------- + def _encode_crop(self, frame_bgr, box: BBox) -> bytes: + h, w = frame_bgr.shape[:2] + x1, y1, x2, y2 = box + + bw = x2 - x1 + bh = y2 - y1 + + # proportional padding + pad_w = int(bw * self.padding_ratio) + pad_h = int(bh * self.padding_ratio) + + # padded box + px1 = max(0, x1 - pad_w) + py1 = max(0, y1 - pad_h) + px2 = min(w, x2 + pad_w) + py2 = min(h, y2 + pad_h) + + crop = frame_bgr[py1:py2, px1:px2] + if crop.size == 0: + return b"" + + ok, buf = cv2.imencode( + ".jpg", crop, + [int(cv2.IMWRITE_JPEG_QUALITY), self.jpeg_quality] + ) + return bytes(buf) if ok else b"" + + # ------------------------------------------------------------- + # 🔹 gRPC call wrapper + # ------------------------------------------------------------- + def classify(self, frame_bgr, boxes: List[BBox], subjects: Optional[List[str]] = None): + req = pb.ClassifyRequest() + subs = list(subjects) if subjects else ["object"] * len(boxes) + + # ensure equal length + if len(subs) < len(boxes): + subs += ["object"] * (len(boxes) - len(subs)) + elif len(subs) > len(boxes): + subs = subs[:len(boxes)] + + for i, b in enumerate(boxes): + jpeg = self._encode_crop(frame_bgr, b) + if not jpeg: + continue + + c = req.crops.add( + x1=int(b[0]), y1=int(b[1]), + x2=int(b[2]), y2=int(b[3]), + jpeg=jpeg + ) + + if hasattr(c, "subject"): + setattr(c, "subject", subs[i]) + + resp = self.stub.Classify(req, timeout=self.timeout) + + # tolerate any response field name + if hasattr(resp, "detections"): + preds = list(resp.detections) + elif hasattr(resp, "preds"): + preds = list(resp.preds) + elif hasattr(resp, "results"): + preds = list(resp.results) + else: + import logging + logging.warning(f"[GrpcClipClassifierClient] Unknown response fields: {resp}") + preds = [] + + return preds diff --git a/AgCloud/services/security/agguard/specialists/clients/mask.py b/AgCloud/services/security/agguard/specialists/clients/mask.py new file mode 100644 index 000000000..3d016766d --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/clients/mask.py @@ -0,0 +1,74 @@ +from __future__ import annotations +import logging +from typing import List, Tuple, Optional, Any, Dict +from dataclasses import dataclass +import cv2, numpy as np, grpc + +from agguard.proto import mask_classifier_pb2 as pb +from agguard.proto import mask_classifier_pb2_grpc as pbrpc +from agguard.specialists.mask_service.mask_classifier import MaskPrediction + +log = logging.getLogger(__name__) +Box = Tuple[int, int, int, int] + + +# tolerate either service name in compiled stubs +StubClass = getattr(pbrpc, "ClassifierServiceStub", getattr(pbrpc, "ClassifierStub", None)) +if StubClass is None: + raise ImportError("mask_classifier_pb2_grpc missing ClassifierServiceStub/ClassifierStub") + +class GrpcMaskClassifierClient: + def __init__(self, address: str, model_name: str = "mask", + timeout_sec: float = 1.5, jpeg_quality: int = 85): + self.address = address + self.model_name = model_name + self.timeout = float(timeout_sec) + self.jpeg_quality = int(jpeg_quality) + self._chan = grpc.insecure_channel(self.address, options=[ + ("grpc.max_send_message_length", 32 * 1024 * 1024), + ("grpc.max_receive_message_length", 32 * 1024 * 1024), + ]) + self._stub = StubClass(self._chan) + log.info("GrpcMaskClassifierClient -> %s (model=%s)", self.address, self.model_name) + + @staticmethod + def _safe_crop(frame_bgr: np.ndarray, box: Box) -> Optional[np.ndarray]: + h, w = frame_bgr.shape[:2] + x1, y1, x2, y2 = map(int, box) + x1 = max(0, min(w-1, x1)); x2 = max(0, min(w-1, x2)) + y1 = max(0, min(h-1, y1)); y2 = max(0, min(h-1, y2)) + if x2 <= x1 or y2 <= y1: + return None + return frame_bgr[y1:y2, x1:x2] + + def classify(self, frame_bgr: np.ndarray, boxes: List[Box]) -> List[MaskPrediction]: + crops: List[pb.Crop] = [] + for b in boxes: + crop = self._safe_crop(frame_bgr, b) + if crop is None: + continue + rgb = cv2.cvtColor(crop, cv2.COLOR_BGR2RGB) + ok, buf = cv2.imencode(".jpg", rgb, [int(cv2.IMWRITE_JPEG_QUALITY), self.jpeg_quality]) + if not ok: + continue + crops.append(pb.Crop(jpeg=buf.tobytes(), x1=b[0], y1=b[1], x2=b[2], y2=b[3])) + + if not crops: + return [] + + req = pb.ClassifyRequest(model_name=self.model_name, crops=crops) + try: + resp = self._stub.Classify(req, timeout=self.timeout) + except Exception as e: + log.warning("gRPC classify failed (%s): %s", self.address, e) + return [] + + out: List[MaskPrediction] = [] + for p in resp.preds: + out.append(MaskPrediction( + box=(p.x1, p.y1, p.x2, p.y2), + label=p.label.lower().strip(), + confidence=float(p.confidence), + raw={} + )) + return out diff --git a/AgCloud/services/security/agguard/specialists/clients/megadetector.py b/AgCloud/services/security/agguard/specialists/clients/megadetector.py new file mode 100644 index 000000000..b41ad5cd7 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/clients/megadetector.py @@ -0,0 +1,38 @@ +from __future__ import annotations +import time, grpc, cv2 +import numpy as np +from agguard.core.types import Detection +from agguard.proto import mega_detector_pb2 as pb2 +from agguard.proto import mega_detector_pb2_grpc as pb2_grpc + + +class MegaDetectorClient: + def __init__(self, cfg: dict): + self.host = cfg.get("host", "mega-detector:50063") + self.timeout = float(cfg.get("timeout", 30.0)) + self.channel = grpc.insecure_channel(self.host) + self.stub = pb2_grpc.MegaDetectorStub(self.channel) + print(f"[MegaDetectorClient] Connected to {self.host}") + + def detect(self, frame_bgr: np.ndarray, roi_mask: np.ndarray | None = None): + if roi_mask is not None: + frame_bgr = cv2.bitwise_and(frame_bgr, frame_bgr, mask=roi_mask.astype(np.uint8)) + ok, buf = cv2.imencode(".jpg", frame_bgr) + if not ok: + return [] + + req = pb2.ImageRequest(image_bytes=buf.tobytes()) + try: + t0 = time.time() + resp = self.stub.Detect(req, timeout=self.timeout) + dt = time.time() - t0 + except grpc.RpcError as e: + print(f"[MegaDetectorClient] gRPC failed: {e.code().name} - {e.details()}") + return [] + + dets = [ + Detection(d.cls, d.conf, (int(d.x1), int(d.y1), int(d.x2), int(d.y2))) + for d in resp.detections + ] + print(f"[MegaDetectorClient] {len(dets)} detections in {dt:.2f}s") + return dets diff --git a/AgCloud/services/security/agguard/specialists/dispatch.py b/AgCloud/services/security/agguard/specialists/dispatch.py new file mode 100644 index 000000000..a01c70e68 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/dispatch.py @@ -0,0 +1,158 @@ +from __future__ import annotations +from typing import Dict, List, Callable, Any +import importlib +import logging + +from agguard.core.types import Detection, BBox + +log = logging.getLogger(__name__) + +# (frame_bgr, boxes, subjects=None) -> predictions +Specialist = Callable[[object, List[BBox], List[str] | None], object] + + +def _load(dotted_path: str): + mod, obj = dotted_path.rsplit(".", 1) + return getattr(importlib.import_module(mod), obj) + + +class ClassDispatch: + """ + Map class name -> [specialist callables]. + Each class can have multiple specialists (gRPC or local). + The dispatcher will call them all and unify their predictions. + """ + + def __init__(self, specs_cfg: List[dict]): + self._by_class: Dict[str, List[Specialist]] = {} + + for spec in specs_cfg or []: + cls_name = str(spec["for_class"]).lower() + + if "grpc" in spec: + grpc_cfg = spec["grpc"] or {} + kind = str(grpc_cfg.get("kind", "mask")).lower().strip() + address = grpc_cfg.get("address", "127.0.0.1:50061") + timeout = float(grpc_cfg.get("timeout_sec", 1.5)) + jpeg_q = int(grpc_cfg.get("jpeg_quality", 85)) + + if kind == "anomalies": + from agguard.specialists.clients.anomalies import GrpcClipClassifierClient + client = GrpcClipClassifierClient( + address=address, + timeout_sec=timeout, + jpeg_quality=jpeg_q, + ) + fn: Specialist = lambda frame, boxes, subjects=None, _c=client, _cls=cls_name: \ + _c.classify(frame, boxes, subjects or ([_cls])) + elif kind == "mask": + from agguard.specialists.clients.mask import GrpcMaskClassifierClient + client = GrpcMaskClassifierClient( + address=address, + model_name="mask", + timeout_sec=timeout, + jpeg_quality=jpeg_q, + ) + fn: Specialist = lambda frame, boxes, subjects=None, _c=client: \ + _c.classify(frame, boxes) + elif kind == "animal": + from agguard.specialists.clients.animal import GrpcAnimalClassifierClient + client = GrpcAnimalClassifierClient( + address=address, + model_name="yolo-cls", + timeout_sec=timeout, + jpeg_quality=jpeg_q, + ) + fn: Specialist = lambda frame, boxes, subjects=None, _c=client: \ + _c.classify(frame, boxes) + + self._by_class.setdefault(cls_name, []).append(fn) + log.info( + "Registered gRPC specialist for class '%s' -> %s (%s)", + cls_name, address, kind + ) + + + + else: + dotted = spec["dotted_path"] + ctor = _load(dotted) + inst = ctor(**(spec.get("kwargs") or {})) + + def _call(frame, boxes, subjects=None, _inst=inst): + try: + return _inst.classify(frame, boxes, subjects=subjects) + except TypeError: + return _inst.classify(frame, boxes) + + self._by_class.setdefault(cls_name, []).append(_call) + log.info( + "Registered local specialist for class '%s' -> %s", + cls_name, dotted + ) + + def run(self, frame_bgr, dets: List[Detection]) -> Dict[str, object]: + """ + For each detection class: + - Group boxes by class + - Call all specialists registered for that class + - Merge all their outputs into a unified list + """ + buckets: Dict[str, List[BBox]] = {} + for d in dets: + key = str(d.cls).lower() + if key in self._by_class: + buckets.setdefault(key, []).append(d.bbox) + + outputs: Dict[str, Any] = {} + for key, boxes in buckets.items(): + merged = [] + subjects = None#[key] * len(boxes) + + for fn in self._by_class.get(key, []): + try: + preds = fn(frame_bgr, boxes, subjects) or [] + if isinstance(preds, list): + merged.extend(preds) + else: + merged.append(preds) + except Exception as e: + log.exception("Specialist for '%s' failed: %s", key, e) + + # Unified merged predictions for that class + outputs[key] = merged + + + # ───────────────────────────────────────────── + # 🧩 CONDITIONAL SECOND-STAGE CALL + # ───────────────────────────────────────────── + if "animal" in outputs and outputs["animal"]: + intruding_preds = [ + p for p in outputs["animal"] + if isinstance(p, dict) and p.get("label") != "other" + ] + if intruding_preds: + subjects = [p["label"] for p in intruding_preds] + log.info("[ClassDispatch] Intruding animal(s): %s → checking climbing_fence", subjects) + + # 🧠 Call CLIP classifier only if configured + if "intruding animal" in self._by_class: + for fn in self._by_class["intruding animal"]: + try: + preds = fn(frame_bgr, [p["box"] for p in intruding_preds], subjects) + if preds: + # Store CLIP results under 'climbing_fence' + outputs["climbing_fence"] = preds + # Also propagate subjects so aggregator knows which animal climbed + outputs["_subject"] = subjects + log.info("[ClassDispatch] climbing_fence predictions: %s", preds) + except Exception as e: + log.exception("intruding-animal specialist failed: %s", e) + else: + log.debug("[ClassDispatch] No 'intruding animal' specialist configured.") + + + + return outputs + + diff --git a/AgCloud/services/security/agguard/specialists/mask_service/Dockerfile.mask-classifier b/AgCloud/services/security/agguard/specialists/mask_service/Dockerfile.mask-classifier new file mode 100644 index 000000000..a3e8e52e8 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/mask_service/Dockerfile.mask-classifier @@ -0,0 +1,82 @@ + +############################ +# Single-stage runtime (like your security Dockerfile's Stage 2) +############################ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + OPENCV_OPENCL_RUNTIME=disabled + +# System libs for OpenCV +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 \ + libgl1 \ + libstdc++6 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# -------- Python deps -------- +# Your requirements.txt should include: onnxruntime, opencv-python-headless, numpy, +# grpcio, grpcio-tools, protobuf, etc. (same file as security uses) +COPY agguard/specialists/mask_service/requirements.txt . +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --upgrade pip && \ + python -m pip install --no-cache-dir -r requirements.txt + +# -------- App sources -------- +COPY agguard agguard + +# -------- Weights (optional copy; you can also mount via compose) -------- +RUN mkdir -p /app/weights +# If you keep your mask model here in the repo, copy it in (adjust filename as needed) +# Otherwise, this line can stay and simply be ignored if the file doesn't exist at build time. +# To fail build when missing, keep the COPY; to ignore, comment it out. +# Example assumes you keep: weights/mask_yolov8.onnx +COPY weights/mask_yolov8.onnx /app/weights/mask_yolov8.onnx +# Optional build-time sanity check (won't fail if file absent; comment out '|| true' to enforce) +RUN test -f /app/weights/mask_yolov8.onnx && echo "✅ mask ONNX present" || echo "ℹ️ mask ONNX will be mounted at runtime" + +# -------- gRPC stubs (proto -> agguard/proto) -------- +RUN mkdir -p agguard/proto && touch agguard/proto/__init__.py + +# Generate stubs INTO the same package (like your security image) +RUN python -m grpc_tools.protoc \ + -I agguard/proto \ + --python_out=agguard/proto \ + --grpc_python_out=agguard/proto \ + mask_classifier.proto + +# Patch generated imports to be package-relative (X_pb2_grpc -> .X_pb2) +RUN python - <<'PY' +from pathlib import Path +import re +p = Path("agguard/proto/mask_classifier_pb2_grpc.py") +s = p.read_text(encoding="utf-8") +s2 = re.sub(r'(?m)^import (\w+_pb2)\b', r'from . import \1', s) +if s2 != s: + p.write_text(s2, encoding="utf-8") + print("patched", p) +else: + print("no patch needed", p) +PY + +# -------- Service defaults -------- +# Match server.py env usage; you can override via docker-compose +ENV PORT=50061 \ + BACKEND=onnx \ + MODEL_PATH=/app/weights/mask_yolov8.onnx \ + CLASSES="no_mask,mask" \ + IMGSZ=224 \ + DEVICE=cpu + +EXPOSE 50061 + +# Run the gRPC server (module path style, like your security CMD) +CMD ["python", "-m", "agguard.specialists.mask_service.server"] diff --git a/AgCloud/services/security/agguard/specialists/mask_service/mask_classifier.py b/AgCloud/services/security/agguard/specialists/mask_service/mask_classifier.py new file mode 100644 index 000000000..3298cbc48 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/mask_service/mask_classifier.py @@ -0,0 +1,260 @@ +# agguard/specialists/mask_service/mask_classifier.py +from __future__ import annotations +from dataclasses import dataclass +from typing import List, Tuple, Optional, Dict, Any +import os +import logging +import numpy as np +import cv2 + +log = logging.getLogger(__name__) + +Box = Tuple[int, int, int, int] + + +@dataclass(frozen=True) +class MaskPrediction: + box: Box + label: str # lowercased class name from the model + confidence: float # probability of that class + raw: Dict[str, Any] # backend-specific details (e.g., full probs) + + +def _ensure_rgb(bgr: np.ndarray) -> np.ndarray: + if bgr.ndim == 2: + bgr = cv2.cvtColor(bgr, cv2.COLOR_GRAY2BGR) + return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + + +def _resize_square(img: np.ndarray, size: int) -> np.ndarray: + return cv2.resize(img, (size, size), interpolation=cv2.INTER_LINEAR) + + +def _softmax(x: np.ndarray, axis: int = -1) -> np.ndarray: + x = x - np.max(x, axis=axis, keepdims=True) + e = np.exp(x) + s = np.sum(e, axis=axis, keepdims=True) + return e / np.maximum(s, 1e-12) + + +class FaceMaskClassifier: + """ + Per-person classifier using a YOLOv8-cls model. + + Backends (must be specified explicitly): + - backend="torch": uses Ultralytics YOLO (expects *.pt) + - backend="onnx": uses onnxruntime (expects *.onnx) + + REQUIRED kwargs: + - model_path: str + - backend: "torch" | "onnx" + + OPTIONAL kwargs: + - imgsz: int (default 224) + - device: str (default "cpu") + - class_names: List[str] (REQUIRED for ONNX unless your app supplies a sidecar) + For torch, names are read from the YOLO model. + + API: + classify(frame_bgr, boxes) -> List[MaskPrediction] + """ + + def __init__( + self, + model_path: str, + backend: str, + imgsz: int = 224, + device: str = "cpu", + class_names: Optional[List[str]] = None, + ): + self.model_path = model_path + self.backend = backend.strip().lower() + self.imgsz = int(imgsz) + self.device = device + self._torch_model = None + self._onnx_session = None + self._onnx_input_name = None + self._class_names = class_names # For ONNX, you must pass this (or ship a sidecar & adjust code) + + if self.backend == "torch": + self._init_torch() + elif self.backend == "onnx": + self._init_onnx() + else: + raise ValueError("backend must be 'torch' or 'onnx' (no auto).") + + if not self._class_names or len(self._class_names) < 2: + raise ValueError( + "class_names must be resolvable. " + "For torch, they come from the Ultralytics model; " + "for onnx, pass class_names explicitly." + ) + + # Normalize class names to lowercase for consistent matching in Aggregator + self._class_names = [str(n).lower() for n in self._class_names] + log.info("FaceMaskClassifier initialized (backend=%s, classes=%s)", self.backend, self._class_names) + + # ----------------- backends ----------------- + + def _init_torch(self): + try: + from ultralytics import YOLO + except Exception as e: + raise RuntimeError("Ultralytics not available. Install with: pip install ultralytics") from e + + if not os.path.isfile(self.model_path): + raise FileNotFoundError(f"Model not found: {self.model_path}") + + self._torch_model = YOLO(self.model_path) + try: + names = getattr(self._torch_model, "names", None) + if isinstance(names, dict) and len(names) > 0: + self._class_names = [names[i] for i in range(len(names))] + except Exception: + pass + + # Try to place on device (Ultralytics handles fallback) + try: + self._torch_model.to(self.device) + except Exception: + pass + + def _init_onnx(self): + try: + import onnxruntime as ort + except Exception as e: + raise RuntimeError("onnxruntime not available. Install with: pip install onnxruntime") from e + + if not os.path.isfile(self.model_path): + raise FileNotFoundError(f"Model not found: {self.model_path}") + + self._onnx_session = ort.InferenceSession(self.model_path, providers=["CPUExecutionProvider"]) + inp = self._onnx_session.get_inputs()[0] + self._onnx_input_name = inp.name + + # For ONNX we require class_names from kwargs (keeps strict) + if not self._class_names: + raise ValueError("ONNX backend requires 'class_names' list (e.g., ['no_mask','mask']).") + + # Detect if the exported ONNX has a static batch size of 1 + shape = inp.shape # e.g. [1, 3, 224, 224] or [None, 3, 224, 224] + self._onnx_static_batch1 = False + if isinstance(shape, (list, tuple)) and len(shape) >= 1: + bdim = shape[0] + if isinstance(bdim, int) and bdim == 1: + self._onnx_static_batch1 = True + + # ----------------- public API ----------------- + + def classify(self, frame_bgr: np.ndarray, boxes: List[Box]) -> List[MaskPrediction]: + if not boxes: + return [] + + H, W = frame_bgr.shape[:2] + crops_rgb: List[np.ndarray] = [] + kept: List[Box] = [] + + for (x1, y1, x2, y2) in boxes: + X1 = max(0, min(W - 1, int(x1))) + Y1 = max(0, min(H - 1, int(y1))) + X2 = max(0, min(W - 1, int(x2))) + Y2 = max(0, min(H - 1, int(y2))) + if X2 <= X1 or Y2 <= Y1: + continue + crop = frame_bgr[Y1:Y2, X1:X2] + rgb = _ensure_rgb(crop) + rgb = _resize_square(rgb, self.imgsz) + crops_rgb.append(rgb) + kept.append((X1, Y1, X2, Y2)) + + if not crops_rgb: + return [] + + if self.backend == "torch": + return self._infer_torch(crops_rgb, kept) + else: + return self._infer_onnx(crops_rgb, kept) + + # ----------------- inference helpers ----------------- + + def _infer_torch(self, crops_rgb: List[np.ndarray], boxes: List[Box]) -> List[MaskPrediction]: + results = self._torch_model.predict( + source=crops_rgb, + imgsz=self.imgsz, + verbose=False, + device=self.device if hasattr(self._torch_model, "overrides") else None + ) + names = self._class_names + preds: List[MaskPrediction] = [] + + for b, r in zip(boxes, results): + if getattr(r, "probs", None) is None: + # Highly unlikely for YOLO-cls; fallback to uniform + probs = np.ones((len(names),), dtype=np.float32) / max(len(names), 1) + top1 = int(np.argmax(probs)) + conf = float(probs[top1]) + else: + probs = np.asarray(r.probs.data, dtype=np.float32) # (C,) + top1 = int(getattr(r.probs, "top1", int(np.argmax(probs)))) + conf = float(getattr(r.probs, "top1conf", probs[top1])) + + label = names[top1] if 0 <= top1 < len(names) else str(top1) + preds.append(MaskPrediction(box=b, label=label.lower(), confidence=conf, raw={ + "probs": probs.tolist(), + "top1": top1 + })) + return preds + + def _infer_onnx(self, crops_rgb: List[np.ndarray], boxes: List[Box]) -> List[MaskPrediction]: + import numpy as np + import onnxruntime as ort + + def _to_nchw(arr: np.ndarray) -> np.ndarray: + # (H, W, 3) RGB -> (1, 3, H, W) float32 in [0,1] + x = (arr.astype(np.float32) / 255.0).transpose(2, 0, 1)[None, ...] + return x + + names = self._class_names + preds: List[MaskPrediction] = [] + + if getattr(self, "_onnx_static_batch1", False): + # Run one-by-one (model expects batch dimension == 1) + for rgb, b in zip(crops_rgb, boxes): + X = _to_nchw(rgb) + outputs = self._onnx_session.run(None, {self._onnx_input_name: X}) + logits = outputs[0] + if logits.ndim == 1: + logits = logits[None, ...] # (1, C) + p = _softmax(logits, axis=-1)[0] + top1 = int(np.argmax(p)) + conf = float(p[top1]) + label = names[top1] if 0 <= top1 < len(names) else str(top1) + preds.append(MaskPrediction(box=b, label=label.lower(), confidence=conf, raw={ + "probs": p.tolist(), + "top1": top1 + })) + return preds + + # Vectorized path (dynamic batch models) + X = np.stack([ + np.transpose((c.astype(np.float32) / 255.0), (2, 0, 1)) + for c in crops_rgb + ], axis=0) # (N, 3, H, W) + + outputs = self._onnx_session.run(None, {self._onnx_input_name: X}) + logits = outputs[0] + if logits.ndim == 1: + logits = logits[None, ...] + probs = _softmax(logits, axis=-1) + + for i, b in enumerate(boxes): + p = probs[i] + top1 = int(np.argmax(p)) + conf = float(p[top1]) + label = names[top1] if 0 <= top1 < len(names) else str(top1) + preds.append(MaskPrediction(box=b, label=label.lower(), confidence=conf, raw={ + "probs": p.tolist(), + "top1": top1 + })) + return preds + diff --git a/AgCloud/services/security/agguard/specialists/mask_service/requirements.txt b/AgCloud/services/security/agguard/specialists/mask_service/requirements.txt new file mode 100644 index 000000000..90663ea1d --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/mask_service/requirements.txt @@ -0,0 +1,7 @@ +grpcio>=1.64.0 +grpcio-tools>=1.64.0 +opencv-python-headless>=4.8.0.76 +numpy>=1.24.0 +onnxruntime>=1.16.0 +prometheus_client +psutil \ No newline at end of file diff --git a/AgCloud/services/security/agguard/specialists/mask_service/server.py b/AgCloud/services/security/agguard/specialists/mask_service/server.py new file mode 100644 index 000000000..10fefeeca --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/mask_service/server.py @@ -0,0 +1,136 @@ +# agguard/specialists/mask_service/server.py +from __future__ import annotations +import os, logging, time +from concurrent import futures +from typing import List + +import cv2 +import numpy as np +import grpc + +from agguard.proto import mask_classifier_pb2 as pb +from agguard.proto import mask_classifier_pb2_grpc as pbrpc + +from agguard.specialists.mask_service.mask_classifier import FaceMaskClassifier, MaskPrediction + +# ───────────────────────────────────────────── +# Prometheus metrics import +# ───────────────────────────────────────────── +from agguard.metrics.monitoring import ( + start_metrics_server, + INFER_REQUESTS, INFER_ERRORS, INFER_LATENCY, MODEL_LOAD_SEC +) + +log = logging.getLogger(__name__) +logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO")) + + +def _jpeg_to_rgb(j: bytes) -> np.ndarray: + arr = np.frombuffer(j, dtype=np.uint8) + img_bgr = cv2.imdecode(arr, cv2.IMREAD_COLOR) + if img_bgr is None: + raise ValueError("Failed to decode JPEG") + return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + + +def _resize_square(rgb: np.ndarray, size: int) -> np.ndarray: + return cv2.resize(rgb, (size, size), interpolation=cv2.INTER_LINEAR) + + +# ───────────────────────────────────────────── +# tolerate either service name in generated stubs +# ───────────────────────────────────────────── +ServicerBase = getattr(pbrpc, "ClassifierServiceServicer", + getattr(pbrpc, "ClassifierServicer", None)) +if ServicerBase is None: + raise ImportError("No Classifier{Service}Servicer in mask_classifier_pb2_grpc.py") + +add_servicer = getattr(pbrpc, "add_ClassifierServiceServicer_to_server", + getattr(pbrpc, "add_ClassifierServicer_to_server", None)) +if add_servicer is None: + raise ImportError("No add_Classifier{Service}Servicer_to_server in mask_classifier_pb2_grpc.py") + + +class ClassifierService(ServicerBase): + def __init__(self, model: FaceMaskClassifier): + self.model = model + + def Classify(self, request: pb.ClassifyRequest, context) -> pb.ClassifyResponse: + service_name = "mask_classifier" + INFER_REQUESTS.labels(service=service_name).inc() + t0 = time.time() + + crops_rgb: List[np.ndarray] = [] + boxes = [] + for c in request.crops: + try: + rgb = _jpeg_to_rgb(c.jpeg) + rgb = _resize_square(rgb, self.model.imgsz) + crops_rgb.append(rgb) + boxes.append((c.x1, c.y1, c.x2, c.y2)) + except Exception as e: + log.warning("Failed to decode crop: %s", e) + + preds: List[MaskPrediction] = [] + try: + if crops_rgb: + if self.model.backend == "torch": + preds = self.model._infer_torch(crops_rgb, boxes) + else: + preds = self.model._infer_onnx(crops_rgb, boxes) + except Exception as e: + INFER_ERRORS.labels(service=service_name).inc() + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details(str(e)) + return pb.ClassifyResponse() + + # Record latency metric + dt = time.time() - t0 + INFER_LATENCY.labels(service=service_name).observe(dt) + log.info("[MaskClassifier] → %d predictions in %.2fs", len(preds), dt) + + out = pb.ClassifyResponse() + for p in preds: + out.preds.add( + x1=p.box[0], y1=p.box[1], x2=p.box[2], y2=p.box[3], + label=p.label.lower(), confidence=float(p.confidence) + ) + return out + + +def serve(): + backend = os.environ.get("BACKEND", "onnx").lower() + model_path = os.environ.get("MODEL_PATH", "/app/weights/mask_yolov8.onnx") + imgsz = int(os.environ.get("IMGSZ", "224")) + device = os.environ.get("DEVICE", "cpu") + classes = os.environ.get("CLASSES") + class_names = [s.strip() for s in classes.split(",")] if classes else None + + log.info("[MaskClassifier] 🔹 Loading model: %s", model_path) + t0 = time.time() + model = FaceMaskClassifier( + model_path=model_path, backend=backend, imgsz=imgsz, device=device, class_names=class_names + ) + load_time = time.time() - t0 + MODEL_LOAD_SEC.labels(service="mask_classifier").set(load_time) + log.info("[MaskClassifier] ✅ Model loaded in %.1fs", load_time) + + port = int(os.environ.get("PORT", "50061")) + metrics_port = int(os.environ.get("METRICS_PORT", "8012")) + + # Start Prometheus metrics server + start_metrics_server() + log.info("[MaskClassifier] 📊 Prometheus metrics available at :%d/metrics", metrics_port) + + # Start gRPC server + server = grpc.server(futures.ThreadPoolExecutor(max_workers=2)) + add_servicer(ClassifierService(model), server) + server.add_insecure_port(f"[::]:{port}") + log.info("[MaskClassifier] 🚀 gRPC server listening on :%d", port) + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() + diff --git a/AgCloud/services/security/agguard/specialists/megadetector_service/Dockerfile.mega-detector b/AgCloud/services/security/agguard/specialists/megadetector_service/Dockerfile.mega-detector new file mode 100644 index 000000000..efda2605b --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/megadetector_service/Dockerfile.mega-detector @@ -0,0 +1,73 @@ +############################ +# Single-stage runtime image +############################ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + OPENCV_OPENCL_RUNTIME=disabled + +# System libs (same as others) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 \ + libgl1 \ + libstdc++6 \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# -------- Python deps -------- +COPY agguard/specialists/megadetector_service/requirements.txt . +RUN --mount=type=cache,target=/root/.cache/pip \ + python -m pip install --upgrade pip && \ + python -m pip install --no-cache-dir torch==2.4.1+cpu torchvision==0.19.1+cpu torchaudio==2.4.1+cpu \ + --index-url https://download.pytorch.org/whl/cpu && \ + python -m pip install --no-cache-dir -r requirements.txt && \ + python -m pip install --no-cache-dir megadetector + + + +# -------- App sources -------- +COPY agguard agguard + +# -------- gRPC stubs (proto -> agguard/proto) -------- +RUN mkdir -p agguard/proto && touch agguard/proto/__init__.py +RUN python -m grpc_tools.protoc \ + -I agguard/proto \ + --python_out=agguard/proto \ + --grpc_python_out=agguard/proto \ + mega_detector.proto + +# Patch generated imports to relative (so imports work inside agguard package) +RUN python - <<'PY' +from pathlib import Path; import re +p = Path("agguard/proto/mega_detector_pb2_grpc.py") +if p.exists(): + s = p.read_text(encoding="utf-8") + s2 = re.sub(r'^import (mega_detector_pb2)', r'from . import \1', s, flags=re.M) + if s2 != s: + p.write_text(s2, encoding="utf-8") + print("patched", p) +PY + +# -------- Environment defaults -------- +ENV PORT=50063 \ + MODEL_NAME=MDV5A \ + DEVICE=cpu + +ENV PORT=50063 \ + METRICS_PORT=8007 \ + MODEL_NAME=MDV5A \ + DEVICE=cpu + +EXPOSE 50063 8007 + + +# -------- Launch gRPC service -------- +CMD ["python", "-m", "agguard.specialists.megadetector_service.server"] diff --git a/AgCloud/services/security/agguard/specialists/megadetector_service/__init__.py b/AgCloud/services/security/agguard/specialists/megadetector_service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/security/agguard/specialists/megadetector_service/requirements.txt b/AgCloud/services/security/agguard/specialists/megadetector_service/requirements.txt new file mode 100644 index 000000000..a587197e4 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/megadetector_service/requirements.txt @@ -0,0 +1,10 @@ +numpy==1.26.4 +pillow +grpcio==1.62.2 +grpcio-tools==1.62.2 +protobuf==4.25.3 +requests +PyYAML==6.0.1 +prometheus-client +psutil + diff --git a/AgCloud/services/security/agguard/specialists/megadetector_service/server.py b/AgCloud/services/security/agguard/specialists/megadetector_service/server.py new file mode 100644 index 000000000..3dd50dbf9 --- /dev/null +++ b/AgCloud/services/security/agguard/specialists/megadetector_service/server.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MegaDetector gRPC microservice — using the official Microsoft MDv5A model. + +✅ Prometheus metrics: + - Inference requests, errors, latency, model load time + - System CPU, process CPU/memory, GPU usage (from monitoring.py) +""" + +from __future__ import annotations +import io, os, time, grpc +from concurrent import futures +import numpy as np +from PIL import Image + +# ───────────────────────────────────────────── +# MegaDetector import (official Microsoft model) +# ───────────────────────────────────────────── +from megadetector.detection import run_detector + +# ───────────────────────────────────────────── +# Proto imports +# ───────────────────────────────────────────── +from agguard.proto import mega_detector_pb2 as pb2 +from agguard.proto import mega_detector_pb2_grpc as pb2_grpc + +# ───────────────────────────────────────────── +# Prometheus metrics (using your monitoring.py) +# ───────────────────────────────────────────── +from agguard.metrics.monitoring import ( + start_metrics_server, + INFER_REQUESTS, + INFER_ERRORS, + INFER_LATENCY, + MODEL_LOAD_SEC, +) + + +# ───────────────────────────────────────────── +# MegaDetector wrapper +# ───────────────────────────────────────────── +class SimpleMegaDetector: + """Wrapper around official MegaDetector (v5A or v6A).""" + + CATEGORY_MAP = {"1": "animal", "2": "person", "3": "vehicle"} + + def __init__(self, model_name: str = "MDV5A", conf: float = 0.2): + print(f"[MegaDetector] 🔹 Loading {model_name} ...") + t0 = time.time() + + # Load model (downloads automatically if needed) + self.model = run_detector.load_detector(model_name) + self.conf = conf + + load_time = time.time() - t0 + MODEL_LOAD_SEC.labels(service="megadetector").set(load_time) + print(f"[MegaDetector] ✅ Model loaded in {load_time:.1f}s") + + def detect(self, img: Image.Image) -> list[dict]: + """Run detection on a PIL image.""" + image_np = np.array(img) + result = self.model.generate_detections_one_image(image_np) + detections = [] + + for d in result.get("detections", []): + if d.get("conf", 0) < self.conf: + continue + bbox = d["bbox"] # normalized [x, y, w, h] + detections.append({ + "category": self.CATEGORY_MAP.get(str(d["category"]), str(d["category"])), + "conf": float(d["conf"]), + "bbox": bbox + }) + return detections + + +# ───────────────────────────────────────────── +# gRPC Servicer +# ───────────────────────────────────────────── +class MegaDetectorServicer(pb2_grpc.MegaDetectorServicer): + """gRPC servicer for MegaDetector.""" + + def __init__(self): + model_name = os.getenv("MODEL_NAME", "MDV5A") + conf_thresh = float(os.getenv("CONF_THRESH", "0.2")) + self.detector = SimpleMegaDetector(model_name=model_name, conf=conf_thresh) + + def Detect(self, request, context): + INFER_REQUESTS.labels(service="megadetector").inc() + t0 = time.time() + + # Load image + try: + if request.image_bytes: + img = Image.open(io.BytesIO(request.image_bytes)).convert("RGB") + elif request.image_path: + img = Image.open(request.image_path).convert("RGB") + else: + raise ValueError("No image provided") + except Exception as e: + INFER_ERRORS.labels(service="megadetector").inc() + context.set_code(grpc.StatusCode.INVALID_ARGUMENT) + context.set_details(f"Failed to load image: {e}") + return pb2.DetectionResponse() + + # Run inference + try: + detections_raw = self.detector.detect(img) + except Exception as e: + INFER_ERRORS.labels(service="megadetector").inc() + context.set_code(grpc.StatusCode.INTERNAL) + context.set_details(f"Inference failed: {e}") + return pb2.DetectionResponse() + + dt = time.time() - t0 + INFER_LATENCY.labels(service="megadetector").observe(dt) + + # Convert normalized → absolute coordinates + w, h = img.size + detections = [] + for det in detections_raw: + x, y, bw, bh = det["bbox"] + detections.append( + pb2.Detection( + cls=det["category"], + conf=det["conf"], + x1=x * w, y1=y * h, + x2=(x + bw) * w, y2=(y + bh) * h, + ) + ) + + print(f"[MegaDetector] → {len(detections)} detections in {dt:.2f}s") + return pb2.DetectionResponse(detections=detections, inference_time=dt) + + +# ───────────────────────────────────────────── +# Entrypoint +# ───────────────────────────────────────────── +def serve(): + port = int(os.getenv("PORT", "50063")) + metrics_port = int(os.getenv("METRICS_PORT", "8000")) + + # Start Prometheus metrics server (handles system/GPU collection) + start_metrics_server() + + print(f"[MegaDetector] 📊 Metrics exposed on :{metrics_port}/metrics") + + # Start gRPC server + server = grpc.server(futures.ThreadPoolExecutor(max_workers=2)) + pb2_grpc.add_MegaDetectorServicer_to_server(MegaDetectorServicer(), server) + server.add_insecure_port(f"[::]:{port}") + print(f"[MegaDetector] 🚀 gRPC server running on port {port}") + server.start() + server.wait_for_termination() + + +if __name__ == "__main__": + serve() + diff --git a/AgCloud/services/security/configs/default.yaml b/AgCloud/services/security/configs/default.yaml new file mode 100644 index 000000000..2460eabb7 --- /dev/null +++ b/AgCloud/services/security/configs/default.yaml @@ -0,0 +1,91 @@ + + +# video: "D:/p9.mp4" +#"C:/Users/yehud/Downloads/cars.mp4" +fit: "1280x720" # or null to keep native +roi: "0,0;1,0;1,1;0,1" # normalized polygon; or "full" +change_thresh: 0 +min_blob_area: 200 +morph_open: 3 +show_mask: false + +# used to construct clickable HLS URLs inside alerts +media_base: "http://media-proxy:8080" # or your public host +media_auth_token: "CHANGE_ME" + + +video: + bucket: imagery + prefix: security/incidents + fps: 9 + hls_segment_time: 0.5 + hls_list_size: 4000 + hls_use_cmaf: False + draw_thickness: 3 + +logging: + level: "INFO" # DEBUG|INFO|WARNING|ERROR + file: null # e.g., "agguard.log" + +detector: + backend: "onnx" + onnx: "weights/yolov8n.onnx" # we copied it here at build-time + conf: 0.4 + imgsz: 640 + roi_pad: 16 + names: + 1: animal # <- add at least the classes you route + 2: person + 3: vehicle + + + + +tracker: + iou_thresh: 0.2 + max_miss: 20 + ema: 0.8 + min_hits: 5 + high_conf: 0.6 + appearance_alpha: 0.8 + center_blend: 0.15 + + +specialists: + - for_class: "person" + grpc: + address: "mask-classifier:50061" + kind: "mask" + timeout_sec: 5 + + - for_class: "animal" + grpc: + address: "animal-classifier:50064" + kind: "animal" + timeout_sec: 6 + + - for_class: "intruding animal" + grpc: + address: "anomalies-classifier:50062" + kind: "anomalies" + timeout_sec: 6 + + + +minio: + endpoint: "localhost:9001" + access_key: "minioadmin" + secret_key: "minioadmin123" + secure: false + default_bucket: "imagery" + +s3: + # For AWS: set only region_name; credentials come from env/role by default + endpoint_url: "http://minio-hot:9000" + region_name: "us-east-1" + aws_access_key_id: "minioadmin" + aws_secret_access_key: "minioadmin123" + default_bucket: "imagery" + connect_timeout: 3.0 + read_timeout: 10.0 + max_attempts: 3 diff --git a/AgCloud/services/sensorAnomalyPro/.gitignore b/AgCloud/services/sensorAnomalyPro/.gitignore new file mode 100644 index 000000000..3f591f5ad --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/.gitignore @@ -0,0 +1,64 @@ +# Logs +*.log +*.crt +sensorAnomalyPro/reports/ +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +build/ +dist/ +*.egg-info/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ +.conda/ +*.conda +*.ipynb_checkpoints + +# PyInstaller / py2exe artefacts +*.manifest +*.spec + +# Unit test / coverage reports +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ +.tox/ +.nox/ +.mypy_cache/ +.dmypy.json +.pyre/ +.pytype/ + +# Logs +*.log + +# IDE / editor settings +.vscode/ +.idea/ +*.swp +*.swo + +# OS generated files +.DS_Store +Thumbs.db +desktop.ini + +# Jupyter notebooks (if you create some later) +.ipynb_checkpoints/ + +# Temporary files +*.tmp +*.bak +*.orig + +reports/ +sensorAnomalyPro/reports/ +**/reports/ diff --git a/AgCloud/services/sensorAnomalyPro/Dockerfile.flink b/AgCloud/services/sensorAnomalyPro/Dockerfile.flink new file mode 100644 index 000000000..801086691 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/Dockerfile.flink @@ -0,0 +1,54 @@ +FROM flink:1.19.3-scala_2.12-java11 + + +# ---------- Root setup ---------- +USER root +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +# ---------- Install Python + tools ---------- +RUN apt-get update && apt-get install -y \ + python3 python3-venv python3-pip curl \ + && rm -rf /var/lib/apt/lists/* + +# ---------- Add Flink Kafka connectors ---------- +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar && \ + curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +# ---------- Python virtual environment ---------- +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# ---------- Workdir ---------- +WORKDIR /opt/app + +# ---------- Copy dependencies first (for caching) ---------- +COPY requirements.txt /opt/app/requirements.txt + +# ---------- Install dependencies ---------- +RUN pip install --upgrade pip certifi \ + && pip install --prefer-binary apache-flink protobuf grpcio \ + && pip install -r /opt/app/requirements.txt + +# ---------- Copy application code ---------- +COPY . /opt/app/ + +RUN mkdir -p /opt/app/sensorAnomalyPro/reports/models && chown -R flink:flink /opt/app/sensorAnomalyPro && chmod -R 777 /opt/app/sensorAnomalyPro + + +# ---------- Flink environment variables ---------- +ENV PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python +ENV PYFLINK_PYTHON=/opt/venv/bin/python +ENV PYTHONPATH="/opt/app/sensorAnomalyPro:/opt/app:$PYTHONPATH" +ENV KAFKA_BROKERS=kafka:9092 +ENV IN_TOPIC=sensor-telemetry +ENV OUT_TOPIC=sensor_anomalies + +# ---------- Default command ---------- +CMD ["job-cluster", "--job-classname", "org.apache.flink.client.python.PythonDriver", "-py", "/opt/app/sensorAnomalyPro/app.py"] diff --git a/AgCloud/services/sensorAnomalyPro/README.md b/AgCloud/services/sensorAnomalyPro/README.md new file mode 100644 index 000000000..e41cfba47 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/README.md @@ -0,0 +1,204 @@ +# Sensor Anomaly Detection +Real-time anomaly detection in environmental sensor data using Flink + Kafka + Python. + +This project continuously monitors IoT sensor streams (e.g., Soil Moisture, Ambient Temperature, Humidity) and detects anomalies using statistical baselines and streaming analytics. + +## Tech Stack +Python · Apache Flink · Apache Kafka · Docker · Pandas · Statsmodels + +## Features +- Cleans and validates incoming sensor data. +- Builds statistical baselines: + - Daily (hour of day) + - Weekly (day of week + hour) + - Seasonal (day of year + hour) +- Detects anomalies: + - Band — value outside baseline ± 2×std + - Spike — sudden jump between consecutive values + - Break — sustained deviation from baseline +- Exports results to: + - reports/anomalies_report.csv + - reports/plots/ + - reports/models/ (saved baseline profiles) + +## Project Structure +sensor-anomaly-pro/ +├─ analyze_sensors.py # Batch analysis on CSV data +├─ profiles_runtime.py # Baseline export & real-time scoring +├─ app.py # Flink streaming job (Kafka → detection → Kafka) +├─ requirements.txt +├─ Dockerfile +├─ data/ +│ └─ plant_health_data.csv +└─ tests/ + +## Environment Variables +| Variable | Description | Default | +|-----------|-------------|----------| +| KAFKA_BROKERS | Kafka broker address | kafka:9092 | +| IN_TOPIC | Input topic for raw telemetry | sensor-telemetry | +| OUT_TOPIC | Output topic for anomalies | sensor_anomalies | +| PYTHONPATH | Application base path | /opt/app/sensorAnomalyPro | + +## Local (Batch Mode) +py -3.12 -m venv .venv +.\.venv\Scripts\Activate.ps1 +python -m pip install -U pip setuptools wheel +python -m pip install -r requirements.txt + +Run analysis: +set DATA_PATH=./data/plant_health_data.csv # Windows +export DATA_PATH=./data/plant_health_data.csv # Linux/macOS +python analyze_sensors.py + +Run tests: +python -m pytest -q --maxfail=1 --disable-warnings --cov=. --cov-report=term-missing + +## Docker +docker build -t sensor-anomaly-pro . +docker run --rm -e DATA_PATH="/app/data/plant_health_data.csv" -v "${PWD}/data:/app/data" -v "${PWD}/reports:/app/reports" sensor-anomaly-pro python analyze_sensors.py + +## Real-Time Flow (Kafka + Flink) +The real-time pipeline connects Kafka and Flink for continuous anomaly detection. + +| Component | Description | +|------------|-------------| +| Kafka Broker (kafka) | Handles telemetry input and anomaly output topics. | +| Flink JobManager / TaskManager | Executes app.py (the streaming anomaly detection job). | +| sensorAnomalyPro/app.py | Python Flink job that consumes, processes, and produces results. | + +### Step-by-Step + +#### 1️ Build and Start Services +```bash +docker-compose up -d --build +``` + +#### 2️ Submit the Flink Job +```bash +docker exec -it flink-jobmanager flink run -py /opt/app/sensorAnomalyPro/app.py + +``` + +#### 3️ Send Sample Sensor Data +```bash +docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +``` +Paste this: +```json + +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +PS C:\Users\user1\Desktop\AgCloud> docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +PS C:\Users\user1\Desktop\AgCloud> docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +PS C:\Users\user1\Desktop\AgCloud> docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +PS C:\Users\user1\Desktop\AgCloud> docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +PS C:\Users\user1\Desktop\AgCloud> docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +PS C:\Users\user1\Desktop\AgCloud> docker exec -i kafka kafka-console-producer.sh --bootstrap-server localhost:9092 --topic dev-robot-telemetry-raw +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-10-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2025-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":55555555555588888888,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":555555555555555,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":555555555555555,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":37.2,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":555555555555555,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Ambient_Temperature","value":555555555555555,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":555555555555555,"lat":32.055,"lon":34.845} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Ambient_Temperature","value":55,"lat":32.045,"lon":34.835} +{"ts":"2024-11-02T12:00:00Z","plant_id":1,"sensor":"Soil_Moisture","value":32.1,"lat":32.048,"lon":34.836} +{"ts":"2024-11-02T12:00:05Z","plant_id":1,"sensor":"Ambient_Temperature","value":28.5,"lat":32.048,"lon":34.836} +{"ts":"2024-11-02T12:00:08Z","plant_id":2,"sensor":"Humidity","value":71.4,"lat":32.020,"lon":34.800} +{"ts":"2024-11-02T12:00:10Z","plant_id":2,"sensor":"Soil_Moisture","value":18.2,"lat":32.020,"lon":34.800} +{"ts":"2024-11-02T12:00:15Z","plant_id":3,"sensor":"Ambient_Temperature","value":44.9,"lat":32.090,"lon":34.910} +``` + +#### 4️ View Detected Anomalies +```bash +docker exec -it kafka kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic sensor_zone_stats --from-beginning +docker exec -it kafka kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic sensor_anomalies --from-beginning +``` +Example output: +```json +{ + "plant_id": 1, + "sensor": "Soil_Moisture", + "ts": "2025-09-21T12:34:56Z", + "value": 37.2, + "result": { + "ok": true, + "is_anomaly": true, + "bl_type": "seasonal", + "baseline": 23.36, + "adaptive_baseline": 23.39, + "bias": 0.05, + "lower": 7.60, + "upper": 39.18, + "band_std": 7.89, + "flags": {"band": false, "spike": false, "break": true}, + "ema_abs_res": 13.83, + "ts": "2025-09-21 12:34:56+00:00" + } +} + +``` + +--- + +## Data Flow Diagram +``` + ┌──────────────┐ ┌────────────┐ ┌────────────────────────┐ + │ Sensors / IoT│ ---> │ Kafka Topic│ ---> │ Flink (app.py) detects │ + │ telemetry │ │sensor-telemetry│ │ anomalies │ + └──────────────┘ └────────────┘ └─────────────┬──────────┘ + │ + ▼ + Kafka Topic: sensor_anomalies +``` + +--- + +## Notes +- **IN_TOPIC** = `sensor-telemetry` +- **OUT_TOPIC** = `sensor_anomalies` +- Kafka + Flink must share the same network (e.g., `agcloud_mesh`). +- Rebuild after modifying `app.py`: + ```bash + docker-compose build jobmanager taskmanager + ``` + +--- + +## Troubleshooting +- Create topics manually if missing: + ```bash + docker exec -it kafka-single kafka-topics.sh --create --topic sensor-telemetry --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1 + ``` +- View logs: + ```bash + docker logs flink-jobmanager + ``` + +--- + +## License +MIT +branch shiffi-flink-sensor-connection \ No newline at end of file diff --git a/AgCloud/services/sensorAnomalyPro/docker-compose.yml b/AgCloud/services/sensorAnomalyPro/docker-compose.yml new file mode 100644 index 000000000..bf4ac0fd4 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/docker-compose.yml @@ -0,0 +1,85 @@ + + + + +version: "3.9" + +services: + sensor_anomaly_pro: + build: + context: ./sensorAnomalyPro + dockerfile: Dockerfile + container_name: sensor-anomaly-pro + volumes: + - ./sensorAnomalyPro/data:/app/data + - ./sensorAnomalyPro/reports:/app/reports + environment: + - DATA_PATH=/app/data/plant_health_data.csv + command: > + python analyze_sensors.py + networks: + - ag_cloud + + jobmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-jobmanager + command: > + bash -c " + /docker-entrypoint.sh jobmanager & + echo '⏳ Waiting for Flink JobManager startup...' && + sleep 10 && + echo '🕓 Waiting for reports to be generated...' && + while [ ! -d /opt/app/sensorAnomalyPro/reports ] || [ -z \"$(ls -A /opt/app/sensorAnomalyPro/reports 2>/dev/null)\" ]; do + echo ' ↳ reports directory empty, waiting...'; + sleep 5; + done && + echo '✅ Reports ready, submitting Flink job...' && + flink run -m localhost:8081 -py /opt/app/sensorAnomalyPro/app.py && + tail -f /dev/null" + depends_on: + - sensor_anomaly_pro + ports: + - "8081:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=dev-robot-telemetry-raw + - OUT_TOPIC=sensor_anomalies + - ZONE_TOPIC=sensor_zone_stats + volumes: + - ./sensorAnomalyPro/reports:/opt/app/sensorAnomalyPro/reports:rw + restart: unless-stopped + networks: + - flink-net + - ag_cloud + + taskmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + depends_on: + - jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - IN_TOPIC=dev-robot-telemetry-raw + - OUT_TOPIC=sensor_anomalies + - ZONE_TOPIC=sensor_zone_stats + - taskmanager.numberOfTaskSlots=4 + volumes: + - ./sensorAnomalyPro/reports:/opt/app/sensorAnomalyPro/reports:rw + restart: unless-stopped + networks: + - flink-net + - ag_cloud + +networks: + flink-net: + driver: bridge + ag_cloud: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/services/sensorAnomalyPro/requirements.txt b/AgCloud/services/sensorAnomalyPro/requirements.txt new file mode 100644 index 000000000..12ce1818c --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/requirements.txt @@ -0,0 +1,15 @@ +pandas==2.2.2 +numpy==1.26.4 +matplotlib==3.8.4 +statsmodels==0.14.2 +scipy==1.11.4 +#fastapi==0.110.0 + + + +uvicorn==0.29.0 +scikit-learn==1.3.2 +joblib==1.3.2 +protobuf>=3.20.3,<5 +grpcio>=1.54.0 +shapely diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/Dockerfile b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/Dockerfile new file mode 100644 index 000000000..0600f3286 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/Dockerfile @@ -0,0 +1,30 @@ +# Dockerfile +FROM python:3.11-slim + +WORKDIR /app + +COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt + +RUN chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy source +COPY analyze_sensors.py ./ +COPY profiles_runtime.py ./ + +COPY app.py ./ +COPY data/ ./data/ +COPY zones.geojson ./ +RUN mkdir -p /app/reports/plots + + +EXPOSE 8000 + +CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/__init__.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/analyze_sensors.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/analyze_sensors.py new file mode 100644 index 000000000..329dbd11a --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/analyze_sensors.py @@ -0,0 +1,264 @@ +# analyze_sensors.py — robust version (no stuck/gap rules, improved safety) + +from pathlib import Path +import os +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt +from statsmodels.tsa.holtwinters import ExponentialSmoothing +from scipy.stats import zscore +from profiles_runtime import export_profiles + +# ---------------- Config ---------------- +# Default to the uploaded dataset location; can be overridden by env var. +DATA_PATH = Path(os.getenv("DATA_PATH", "/mnt/data/plant_health_data.csv")) +OUT_DIR = Path("reports") +PLOTS_DIR = OUT_DIR / "plots" +PLOTS_DIR.mkdir(parents=True, exist_ok=True) + +SENSORS = ["Soil_Moisture", "Ambient_Temperature", "Humidity"] +VALID_RANGES = { + "Soil_Moisture": (0, 100), + "Humidity": (0, 100), + "Ambient_Temperature": (-30, 60), +} + +EPS_STD = 1e-6 + +# -------------- Helpers ----------------- +def _as_numeric(df: pd.DataFrame, col: str) -> pd.Series: + s = pd.to_numeric(df[col], errors="coerce") + return s + + +def read_and_clean(path: Path) -> pd.DataFrame: + if not path.exists(): + raise FileNotFoundError(f"Data file not found: {path}") + df = pd.read_csv(path) + + if "Timestamp" not in df.columns: + raise ValueError("CSV must contain a 'Timestamp' column") + + df["Timestamp"] = pd.to_datetime(df["Timestamp"], errors="coerce") + df = df.dropna(subset=["Timestamp"]) + + if "Plant_ID" not in df.columns: + df["Plant_ID"] = 1 + + for c, (lo, hi) in VALID_RANGES.items(): + if c in df.columns: + df[c] = _as_numeric(df, c) + df.loc[(df[c] < lo) | (df[c] > hi), c] = np.nan + + df = df.sort_values(["Plant_ID", "Timestamp"]).reset_index(drop=True) + return df + + +def _safe_std_by(grouped: pd.core.groupby.SeriesGroupBy) -> pd.Series: + std = grouped.std() + if std.notna().any(): + m = std.mean(skipna=True) + std = std.fillna(m if pd.notna(m) else 0.0) + else: + std = std.fillna(0.0) + std = std.clip(lower=EPS_STD) + return std + +# --------- Baselines --------- +def baseline_daily(d: pd.DataFrame, col: str) -> pd.DataFrame: + x = d[["Timestamp", col]].copy() + x["hod"] = x["Timestamp"].dt.hour + prof = x.groupby("hod")[col].median().reindex(range(24)).interpolate(limit_direction="both") + std = _safe_std_by(x.groupby("hod")[col]).reindex(range(24)).fillna(method="ffill").fillna(method="bfill") + out = x.copy() + out["baseline"] = out["hod"].map(prof) + out["band_std"] = out["hod"].map(std) + out["lower"] = out["baseline"] - 2 * out["band_std"] + out["upper"] = out["baseline"] + 2 * out["band_std"] + out = out.drop(columns=["hod"]) + out["bl_type"] = "daily" + return out + +def baseline_weekly(d: pd.DataFrame, col: str) -> pd.DataFrame: + x = d[["Timestamp", col]].copy() + x["dow"] = x["Timestamp"].dt.dayofweek # 0..6 + x["hod"] = x["Timestamp"].dt.hour # 0..23 + prof = x.groupby(["dow","hod"])[col].median().unstack() + prof = prof.reindex(index=range(7), columns=range(24)) + prof = prof.interpolate(axis=0, limit_direction="both").interpolate(axis=1, limit_direction="both") + std = _safe_std_by(x.groupby(["dow","hod"])[col]).unstack() + std = std.reindex(index=range(7), columns=range(24)) + std = std.fillna(method="ffill").fillna(method="bfill").fillna(EPS_STD) + + out = x.copy() + + dow_idx = out["dow"].to_numpy(dtype=int) + hod_idx = out["hod"].to_numpy(dtype=int) + prof_np = prof.to_numpy() + std_np = std.to_numpy() + out["baseline"] = prof_np[dow_idx, hod_idx] + out["band_std"] = std_np[dow_idx, hod_idx] + out["lower"] = out["baseline"] - 2 * out["band_std"] + out["upper"] = out["baseline"] + 2 * out["band_std"] + out = out.drop(columns=["dow","hod"]) + out["bl_type"] = "weekly" + return out + +def baseline_seasonal(d: pd.DataFrame, col: str) -> pd.DataFrame: + x = d[["Timestamp", col]].copy() + x["doy"] = x["Timestamp"].dt.dayofyear # 1..366 + x["hod"] = x["Timestamp"].dt.hour # 0..23 + doy_prof = x.groupby("doy")[col].median().reindex(range(1, 367)).interpolate(limit_direction="both") + hod_prof = x.groupby("hod")[col].median().reindex(range(24)).interpolate(limit_direction="both") + std_hod = ( + _safe_std_by(x.groupby("hod")[col]) + .reindex(range(24)) + .ffill() + .bfill() + ).clip(lower=EPS_STD) + out = x.copy() + out["baseline"] = 0.5 * out["doy"].map(doy_prof) + 0.5 * out["hod"].map(hod_prof) + out["band_std"] = out["hod"].map(std_hod) + out["lower"] = out["baseline"] - 2 * out["band_std"] + out["upper"] = out["baseline"] + 2 * out["band_std"] + out = out.drop(columns=["doy","hod"]) + out["bl_type"] = "seasonal" + return out + +# ---------------- Forecasts ---------------- +def short_forecast(series: pd.Series): + s = series.dropna() + if len(s) < 48: + return None + try: + if isinstance(s.index, pd.DatetimeIndex): + s = s.asfreq("H") + else: + s.index = pd.date_range(start=pd.Timestamp.now().floor("H") - pd.Timedelta(hours=len(s)-1), + periods=len(s), freq="H") + except Exception: + pass + + sp = 168 if len(s) >= 336 else 24 + try: + model = ExponentialSmoothing( + s, trend="add", seasonal="add", seasonal_periods=sp, initialization_method="estimated" + ).fit() + return model.forecast(sp) + except Exception: + return None + + +# ---------------- Anomalies ---------------- +def detect_flags(dfb: pd.DataFrame, col: str) -> pd.DataFrame: + y = dfb.copy() + + valid = y[col].notna() + y["residual"] = np.where(valid, y[col] - y["baseline"], np.nan) + + y["flag_band"] = np.where(valid, (y[col] < y["lower"]) | (y[col] > y["upper"]), False) + + diff = y[col].diff() + z = zscore(diff.fillna(0.0).to_numpy(), nan_policy="omit") + y["diff"] = diff + y["flag_spike"] = False + if z is not None and len(z) == len(y): + y["flag_spike"] = np.abs(pd.Series(z, index=y.index).fillna(0.0)) > 3.0 + + ma = y["residual"].abs().rolling(24, min_periods=12).mean() + + band_std = y["band_std"].fillna(EPS_STD).clip(lower=EPS_STD) + y["flag_break"] = (ma > (1.5 * band_std)).fillna(False) + + y["is_anomaly"] = y[["flag_band","flag_spike","flag_break"]].any(axis=1) + return y + +# ---------------- Plotting ---------------- +def plot_one(dfp: pd.DataFrame, col: str, plant, label: str): + + if dfp[col].notna().sum() == 0: + return + + low = dfp["lower"].fillna(dfp["baseline"]) + up = dfp["upper"].fillna(dfp["baseline"]) + + plt.figure(figsize=(12, 5)) + plt.plot(dfp["Timestamp"], dfp[col], label=col) + plt.plot(dfp["Timestamp"], dfp["baseline"], label=f"baseline ({label})", linewidth=2) + plt.fill_between(dfp["Timestamp"], low, up, alpha=0.2, label="band") + anom = dfp[dfp["is_anomaly"] & dfp[col].notna()] + if not anom.empty: + plt.scatter(anom["Timestamp"], anom[col], marker="x", s=36, label="anomaly") + plt.title(f"Plant {plant} — {col} — {label}") + plt.xlabel("Time"); plt.ylabel(col) + plt.legend(); plt.tight_layout() + out = PLOTS_DIR / f"plant{plant}_{col}_{label}.png" + plt.savefig(out); plt.close() + +# ---------------- Main ---------------- +def main(): + print(f"Reading data from: {DATA_PATH}") + df = read_and_clean(DATA_PATH) + export_profiles(df) + plants = df["Plant_ID"].dropna().unique().tolist() + if not plants: + print(" No Plant_ID values found.") + return + print(f"Found {len(plants)} plants/sensors: {plants[:10]}{'...' if len(plants)>10 else ''}") + out_rows = [] + + if os.getenv("EXPORT_PROFILES", "0") == "1": + try: + + export_profiles(df) + print(" Exported runtime profiles to reports/models/") + except Exception as e: + print(f" Failed to export profiles: {e}") + + for plant in plants: + d0 = df[df["Plant_ID"] == plant].copy() + for col in SENSORS: + if col not in d0.columns: + continue + d0[col] = _as_numeric(d0, col) + d = d0[["Timestamp", col]].dropna().copy() + if d.empty: + continue + + bl_daily = baseline_daily(d, col) + bl_weekly = baseline_weekly(d, col) if len(d) >= 24*7 else None + bl_season = baseline_seasonal(d, col) if d["Timestamp"].dt.year.nunique() >= 2 or len(d) >= 24*60 else None + + _ = short_forecast(d[col]) + + for bl, name in ((bl_daily, "daily"), (bl_weekly, "weekly"), (bl_season, "seasonal")): + if bl is None: + continue + det = detect_flags(bl, col) + det["Plant_ID"] = plant + det["Sensor"] = col + det["BaselineType"] = name + out_rows.append(det) + try: + plot_one(det, col, plant, name) + except Exception as e: + print(f" plot error Plant={plant} Sensor={col} Type={name}: {e}") + + if out_rows: + out = pd.concat(out_rows, ignore_index=True) + keep = ["Timestamp","Plant_ID","Sensor","BaselineType"] + \ + [c for c in SENSORS if c in out.columns] + \ + ["baseline","lower","upper","residual","flag_band","flag_spike","flag_break","is_anomaly"] + out = out[keep].sort_values(["Plant_ID","Sensor","BaselineType","Timestamp"]) + OUT_DIR.mkdir(exist_ok=True, parents=True) + out_csv = OUT_DIR / "anomalies_report.csv" + out.to_csv(out_csv, index=False) + + n_anom = int(out["is_anomaly"].sum()) + print(f"Anomalies report: {out_csv} (total anomalies: {n_anom})") + print(f" Plots saved in: {PLOTS_DIR}") + else: + print(" No results — maybe no suitable columns/data.") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/app.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/app.py new file mode 100644 index 000000000..42be1cc4e --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/app.py @@ -0,0 +1,271 @@ +import os, json, math, logging +import pandas as pd +from statistics import mean, median, stdev +from pyflink.common import Types, Time +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import KafkaSource, KafkaSink, KafkaRecordSerializationSchema +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy +from pyflink.datastream.window import SlidingProcessingTimeWindows +from pyflink.datastream.functions import WindowFunction +from shapely.geometry import shape, Point +from pathlib import Path +from typing import Optional +from sensorAnomalyPro.profiles_runtime import load_profiles, StreamingState, score_new_point +from datetime import datetime, timezone +import json +from statistics import mean, median, stdev + + + +# --- Config --- +OUT_TOPIC = os.getenv("OUT_TOPIC", "sensor_anomalies") +ZONE_TOPIC = os.getenv("ZONE_TOPIC", "sensor-zone-stats") +ZONES_PATH = Path(__file__).resolve().parent / "zones.geojson" +AGG_INTERVAL = int(os.getenv("ZONE_AGG_INTERVAL_SEC", "300")) +SLIDE_INTERVAL = int(os.getenv("ZONE_SLIDE_INTERVAL_SEC", "60")) + +# --- Logging --- +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +log = logging.getLogger("sensor-anomaly") + +# --- Zones loading --- +_zones = [] +def load_zones(): + global _zones + try: + p = Path(ZONES_PATH) + if not p.exists(): + log.warning(f"zones file not found: {ZONES_PATH}") + return + gj = json.loads(p.read_text(encoding="utf-8")) + feats = gj.get("features", []) + _zones = [(shape(f["geometry"]), f["properties"].get("name", f"zone_{i}")) for i, f in enumerate(feats)] + log.info(f"Loaded {len(_zones)} zones from {ZONES_PATH}") + except Exception as e: + log.warning(f"zones disabled: {e}") + _zones = [] + +load_zones() + + +def resolve_zone(lat, lon) -> Optional[str]: + if not _zones or lat is None or lon is None: + return None + try: + pt = Point(lon, lat) + for poly, name in _zones: + if poly.contains(pt): + return name + return None + except Exception: + return None + + +# --- Helpers --- +_profiles_cache = {} +_states = {} + +def _norm_float(x: Optional[float]) -> Optional[float]: + try: + fx = float(x) + return fx if math.isfinite(fx) else None + except Exception: + return None + +def _finite_or_none(x): + try: + fx = float(x) + return fx if math.isfinite(fx) else None + except Exception: + return None + +def _valid_latlon(lat: Optional[float], lon: Optional[float]) -> bool: + return lat is not None and lon is not None and -90.0 <= lat <= 90.0 and -180.0 <= lon <= 180.0 + +def _classify_condition(sensor: str, value: float, lower: float, upper: float) -> str: + if sensor == "Soil_Moisture": + if value < lower: return "dry" + elif value > upper: return "wet" + elif sensor == "Ambient_Temperature": + if value < lower: return "cold" + elif value > upper: return "hot" + elif sensor == "Humidity": + if value < lower: return "low_humidity" + elif value > upper: return "high_humidity" + return "normal" + + +# --- Anomaly detection --- +def detect_anomaly(evt: dict) -> dict: + plant_id, sensor = evt.get("plant_id"), evt.get("sensor") + key = (plant_id, sensor) + + if evt.get("value") is None: + return {"ok": False, "reason": "no_value"} + + prof = _profiles_cache.get(key) + if prof is None: + prof = load_profiles(plant_id, sensor) + if not prof: + return {"ok": False, "reason": "no_profiles"} + _profiles_cache[key] = prof + + state = _states.get(key) + if state is None: + state = StreamingState(alpha=0.08) + _states[key] = state + + try: + ts_str = evt.get("timestamp") or evt.get("ts") + ts = pd.Timestamp(ts_str) + except Exception: + return {"ok": False, "reason": "bad_ts"} + + res = score_new_point(ts=ts, value=evt.get("value"), + profiles=prof, state=state, + k_band=2.0, spike_z_like=3.0, break_mult=1.5) + if not res.get("ok", False): + return {"ok": False, "reason": res.get("reason", "unknown")} + + condition = _classify_condition(sensor, evt.get("value"), res["lower"], res["upper"]) + return { + "ok": True, + "is_anomaly": bool(res["is_anomaly"]), + "bl_type": str(res["bl_type"]), + "baseline": _finite_or_none(res["baseline"]), + "adaptive_baseline": _finite_or_none(res.get("adaptive_baseline")), + "bias": _finite_or_none(res.get("bias")), + "lower": _finite_or_none(res["lower"]), + "upper": _finite_or_none(res["upper"]), + "band_std": _finite_or_none(res["band_std"]), + "flags": {k: bool(v) for k, v in res["flags"].items()}, + "ema_abs_res": _finite_or_none(res.get("ema_abs_res")), + "ts": str(res["ts"]), + "condition": condition + } + + +# --- Map events --- +def process_event(raw: str): + try: + evt = json.loads(raw) + except json.JSONDecodeError: + return None + + lat = _norm_float(evt.get("lat")) + lon = _norm_float(evt.get("lon")) + if not _valid_latlon(lat, lon): + lat, lon = None, None + + zone_name = resolve_zone(lat, lon) + res = detect_anomaly(evt) + if not res.get("ok", True): + return None + + out = { + "idsensor": evt.get("id"), + "plant_id": evt.get("plant_id"), + "sensor": evt.get("sensor_name"), + "ts": evt.get("timestamp") or evt.get("ts"), + "value": evt.get("value"), + "lat": lat, + "lon": lon, + "zone": zone_name, + "result": res + } + return json.dumps(out) + + +# --- Zone window aggregator (new API) --- + + + +class ZoneAggregator(WindowFunction): + def apply(self, key, window, inputs): + values = [] + anomalies = 0 + + for e in inputs: + evt = json.loads(e) + if evt.get("value") is not None: + values.append(evt["value"]) + if evt["result"].get("is_anomaly"): + anomalies += 1 + + if not values: + return [] + + window_start = datetime.fromtimestamp(window.start / 1000, tz=timezone.utc).isoformat() + window_end = datetime.fromtimestamp(window.end / 1000, tz=timezone.utc).isoformat() + + result = { + "zone": key, + "window_start": window_start, + "window_end": window_end, + "count": len(values), + "mean": mean(values), + "median": median(values), + "min": min(values), + "max": max(values), + "std": stdev(values) if len(values) > 1 else 0.0, + "anomalies": anomalies + } + + log.info(f"[ZoneAggregator] zone={key}, count={len(values)}, anomalies={anomalies}") + return [json.dumps(result)] + +# --- Main --- +def main(): + brokers = os.getenv("KAFKA_BROKERS", "kafka:9092") + in_topic = os.getenv("IN_TOPIC", "sensor-telemetry") + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(int(os.getenv("FLINK_PARALLELISM", "2"))) + + source = KafkaSource.builder() \ + .set_bootstrap_servers(brokers) \ + .set_topics(in_topic) \ + .set_group_id("flink-anomaly-detector") \ + .set_value_only_deserializer(SimpleStringSchema()) \ + .build() + + sink_anomalies = KafkaSink.builder() \ + .set_bootstrap_servers(brokers) \ + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(OUT_TOPIC) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ).build() + + sink_zones = KafkaSink.builder() \ + .set_bootstrap_servers(brokers) \ + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(ZONE_TOPIC) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ).build() + + ds = env.from_source(source, WatermarkStrategy.no_watermarks(), "kafka-in") + + processed = ds.map(process_event, output_type=Types.STRING()) \ + .filter(lambda x: x is not None) + + processed.sink_to(sink_anomalies) + + zone_summary = processed \ + .filter(lambda s: json.loads(s).get("zone") is not None) \ + .key_by(lambda s: json.loads(s)["zone"]) \ + .window(SlidingProcessingTimeWindows.of(Time.seconds(AGG_INTERVAL), + Time.seconds(SLIDE_INTERVAL))) \ + .apply(ZoneAggregator(), output_type=Types.STRING()) + + zone_summary.sink_to(sink_zones) + + env.execute("sensor-anomaly-job") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/data/plant_health_data.csv b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/data/plant_health_data.csv new file mode 100644 index 000000000..59c68ee4e --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/data/plant_health_data.csv @@ -0,0 +1,21601 @@ +Timestamp,Plant_ID,Soil_Moisture,Ambient_Temperature,Humidity +2024-08-04 05:00:00,1,29.03792107887181,23.573387484527874 ,56.107581735896275 +2024-08-04 06:00:00,1,20.7937301586471,23.148506017008554,52.77336185849758 +2024-08-04 07:00:00,1,26.249896374270108,23.396942444999862,42.8909336156407 +2024-08-04 08:00:00,1,12.801455337953044,21.81770590700519,53.030565981651804 +2024-08-04 09:00:00,1,40.049966952512094,22.12141037783682,50.6679930979592 +2024-08-04 10:00:00,1,38.76573172875861,21.46343403894607,57.1600781852107 +2024-08-04 11:00:00,1,19.165179382414934,23.60832781399697,54.748267265455226 +2024-08-04 12:00:00,1,30.06284187961404,18.32241496973967,60.104933525057234 +2024-08-04 13:00:00,1,25.786085731779977,22.112152297869507,59.35711148068796 +2024-08-04 14:00:00,1,18.5598389984081,28.79280022745888,50.58788191278758 +2024-08-04 15:00:00,1,11.616864194002959,26.40087467866423,61.34107267171695 +2024-08-04 16:00:00,1,24.345118137331525,28.75345758556594,61.20777237602485 +2024-08-04 17:00:00,1,24.624126858123336,24.071913170037966,64.2769608549775 +2024-08-04 18:00:00,1,22.36789518131109,20.702254109940842,71.34481932318306 +2024-08-04 19:00:00,1,26.36413278169565,19.387621742715776,58.489527541928666 +2024-08-04 20:00:00,1,24.440180322694435,22.259076404009242,58.31717884017964 +2024-08-04 21:00:00,1,31.17359684463433,22.2673633313296,53.394320093368854 +2024-08-04 22:00:00,1,38.13919869127476,21.407318960928496,49.25426344330619 +2024-08-04 23:00:00,1,29.85101698120235,22.76323170058206,65.83435482085665 +2024-08-05 00:00:00,1,13.002956696173525,24.03712297686338,58.45701392990615 +2024-08-05 01:00:00,1,24.068516931030487,23.024700029531477,37.14383432835665 +2024-08-05 02:00:00,1,22.391500807153264,27.513119414114776,52.864647034847955 +2024-08-05 03:00:00,1,34.92254001369813,26.99600736855183,55.86805175494804 +2024-08-05 04:00:00,1,26.08780838873746,20.735591623990693,56.924332195004894 +2024-08-05 05:00:00,1,33.70408760070097,17.823021687900187,35.95358909559083 +2024-08-05 06:00:00,1,14.261906016682843,22.906090242175864,50.66439537388072 +2024-08-05 07:00:00,1,24.144018154670807,30.921739023925852,66.28747125992986 +2024-08-05 08:00:00,1,26.328270480700613,25.30239302443358,43.09180981137082 +2024-08-05 09:00:00,1,1.369831929703217,21.83538687803042,51.630869058506015 +2024-08-05 10:00:00,1,12.481283001799051,32.07124980744914,74.7973756337099 +2024-08-05 11:00:00,1,30.83518687231192,20.62947497139846,64.4498434252002 +2024-08-05 12:00:00,1,19.105036690229618,20.18875380353395,51.392103785434756 +2024-08-05 13:00:00,1,29.122529689076213,29.76707039604705,52.60126020050412 +2024-08-05 14:00:00,1,10.993425128465324,18.176493163667164,41.603732401514634 +2024-08-05 15:00:00,1,21.319367989570132,22.682907851151608,59.8031125882528 +2024-08-05 16:00:00,1,22.920640788500194,24.841323677522524,61.95888509646855 +2024-08-05 17:00:00,1,21.613433865403344,21.484801217065638,62.01218601198336 +2024-08-05 18:00:00,1,26.01606313213528,17.859890894363403,38.09009591168761 +2024-08-05 19:00:00,1,30.32051562069969,27.818747705458822,70.19285189107718 +2024-08-05 20:00:00,1,28.740030092053715,28.354751740743396,62.218024717439356 +2024-08-05 21:00:00,1,34.304025754896486,17.019317981753318,57.07381709798209 +2024-08-05 22:00:00,1,16.007378574527777,18.006525945252584,58.658642462721595 +2024-08-05 23:00:00,1,19.15686293912383,24.02331044622648,64.71985338052085 +2024-08-06 00:00:00,1,21.590832261588226,24.319841993462695,58.30331431903475 +2024-08-06 01:00:00,1,31.389398409104206,21.66047568406254,47.8841076182756 +2024-08-06 02:00:00,1,30.177986789826306,23.92924986844953,53.03029363548693 +2024-08-06 03:00:00,1,20.48595276840432,23.66021878321665,52.171566327115535 +2024-08-06 04:00:00,1,14.17446484938706,25.87586259282552,70.46043041126279 +2024-08-06 05:00:00,1,23.57020451549026,19.42727539590657,63.001589316087795 +2024-08-06 06:00:00,1,27.69591034031552,22.8235405086489,61.691537615277674 +2024-08-06 07:00:00,1,25.900560906752908,20.038630751789277,62.66310051387477 +2024-08-06 08:00:00,1,29.093632935710275,22.315961281070557,53.64915656119684 +2024-08-06 09:00:00,1,24.344899689881544,20.741546434982705,54.11118306616836 +2024-08-06 10:00:00,1,18.736111118499863,29.646106121456064,45.8488532471804 +2024-08-06 11:00:00,1,15.463513814385365,22.70578469832199,44.601201943915044 +2024-08-06 12:00:00,1,21.552598143318246,27.52948855487989,38.17797701494691 +2024-08-06 13:00:00,1,36.70512162258067,27.849326969351154,53.625755244132364 +2024-08-06 14:00:00,1,34.377091871088496,21.50846156894393,60.29736351806127 +2024-08-06 15:00:00,1,27.059034163715395,23.271996328166082,40.75897495696404 +2024-08-06 16:00:00,1,24.30793449259112,20.63719080450713,60.728584498775994 +2024-08-06 17:00:00,1,16.7760078464269,19.486248029525242,48.30229594352289 +2024-08-06 18:00:00,1,25.709147653792126,24.587419887753455,51.0885250818052 +2024-08-06 19:00:00,1,19.540803851186983,19.783991691465072,59.46375678190696 +2024-08-06 20:00:00,1,16.79411756695974,23.563442542541083,49.045955484917286 +2024-08-06 21:00:00,1,18.100758318804875,27.90414999350916,65.93509243326763 +2024-08-06 22:00:00,1,36.18318848828369,21.53997453392087,65.43524895724987 +2024-08-06 23:00:00,1,31.085043454292318,29.30420676497796,47.35418247972106 +2024-08-07 00:00:00,1,20.701318574971097,25.5717496513562,67.04722874028097 +2024-08-07 01:00:00,1,15.28295800430178,26.490146933503983,50.469376139307506 +2024-08-07 02:00:00,1,14.867758303033543,23.424615677847584,64.39847533698659 +2024-08-07 03:00:00,1,21.27879458329413,28.97297600666812,56.595889654800246 +2024-08-07 04:00:00,1,49.997565333246,27.914899451489077,60.19369080129482 +2024-08-07 05:00:00,1,24.00487193461947,28.20079310416107,44.30086051505175 +2024-08-07 06:00:00,1,29.067740905622667,23.695957136684008,71.2349840501619 +2024-08-07 07:00:00,1,14.7552174737506,22.750720820143876,50.94160666629146 +2024-08-07 08:00:00,1,22.386513637173753,28.307094223114277,66.70910139501837 +2024-08-07 09:00:00,1,30.75541726701294,22.47428008369506,56.74800485190205 +2024-08-07 10:00:00,1,23.533566477375697,23.97910028809586,55.836653218798226 +2024-08-07 11:00:00,1,32.88412840183008,23.716460831790144,54.76217844851262 +2024-08-07 12:00:00,1,24.696082268243224,28.03172533096563,50.844321136914715 +2024-08-07 13:00:00,1,16.919453059074876,26.980391137905617,45.17765292115797 +2024-08-07 14:00:00,1,31.51873848376229,21.776514695155004,54.487438065688224 +2024-08-07 15:00:00,1,15.045203088680115,23.656650908889073,47.02227752262357 +2024-08-07 16:00:00,1,34.22455517240376,25.97941412057818,48.14138493771829 +2024-08-07 17:00:00,1,24.086881597525927,23.1426392953469,55.62257259906953 +2024-08-07 18:00:00,1,11.014253352593506,23.442823884485758,52.23116388276159 +2024-08-07 19:00:00,1,23.57093556416888,20.125423784779358,48.02039799644682 +2024-08-07 20:00:00,1,22.774669223715183,18.82053745916016,50.767709796931264 +2024-08-07 21:00:00,1,25.744140063814243,26.215044906048238,57.820850186204744 +2024-08-07 22:00:00,1,18.80651803600042,21.22473083772917,41.977547195003694 +2024-08-07 23:00:00,1,24.99483594853717,22.47606596529061,58.01517753480812 +2024-08-08 00:00:00,1,26.143196205630293,21.835673344098765,53.66243956504842 +2024-08-08 01:00:00,1,5.723390050686035,22.625635665475752,68.71487802349084 +2024-08-08 02:00:00,1,32.26975113762001,20.99192280869735,59.605790621148145 +2024-08-08 03:00:00,1,31.93395219803308,21.13911668833784,43.254251092738016 +2024-08-08 04:00:00,1,36.07770129771384,23.310380316223366,48.47951478278834 +2024-08-08 05:00:00,1,19.46590492889235,26.54938851905578,63.09037930650789 +2024-08-08 06:00:00,1,18.817993129700124,24.87588127262694,46.29261826319586 +2024-08-08 07:00:00,1,28.243639563863752,26.474966736903944,44.63679270228984 +2024-08-08 08:00:00,1,8.42311412625614,23.206491400423378,60.82534965573644 +2024-08-08 09:00:00,1,18.161300728029758,24.01894675788393,50.718235344652854 +2024-08-08 10:00:00,1,28.535954429827438,25.9216105716598,60.637809085084314 +2024-08-08 11:00:00,1,28.918625906200738,29.307932913213676,53.63390365723796 +2024-08-08 12:00:00,1,21.069297631149418,21.98531996883054,53.23564834920744 +2024-08-08 13:00:00,1,27.805552131777354,28.029387486773174,45.745066509196256 +2024-08-08 14:00:00,1,20.214173007070904,22.363433552939682,59.03570683373275 +2024-08-08 15:00:00,1,22.4022055341601,24.480613108988365,50.36506388261638 +2024-08-08 16:00:00,1,15.55286418439921,18.038460912984807,54.74727594725833 +2024-08-08 17:00:00,1,32.31422446197462,26.396342745009008,65.16554798283282 +2024-08-08 18:00:00,1,35.18769609800852,17.061321056186802,58.342194294909355 +2024-08-08 19:00:00,1,22.651811784222822,22.469391267206984,62.85962417039482 +2024-08-08 20:00:00,1,22.702895944311056,27.04576998208161,61.12600504165067 +2024-08-08 21:00:00,1,27.283692602499944,22.15318187059173,59.08528328723269 +2024-08-08 22:00:00,1,24.927053494024058,12.557932405280813,56.17494540554156 +2024-08-08 23:00:00,1,20.133005057283274,25.180122569559842,73.83355804175582 +2024-08-09 00:00:00,1,17.789086109170924,28.74066444366791,54.822502545856075 +2024-08-09 01:00:00,1,19.510015091881776,28.510404345044392,51.03510644914082 +2024-08-09 02:00:00,1,22.68152783703081,25.266381701356146,57.85337709993771 +2024-08-09 03:00:00,1,19.970175856222564,19.57883972837025,54.62304436523847 +2024-08-09 04:00:00,1,26.539214830818807,20.67797643444635,56.83699936485823 +2024-08-09 05:00:00,1,23.253572150177526,22.779621228814175,55.87641997591526 +2024-08-09 06:00:00,1,25.593356757458608,26.95097925650602,53.25921558465467 +2024-08-09 07:00:00,1,24.541393994569464,24.305559651279488,45.73232524173864 +2024-08-09 08:00:00,1,32.96474451491539,33.49997931097541,60.78355389217283 +2024-08-09 09:00:00,1,35.91983378940968,24.981038619763865,48.123177873472905 +2024-08-09 10:00:00,1,15.587210833093891,23.96214196213429,51.83334309813915 +2024-08-09 11:00:00,1,25.475726790661824,20.953305408596623,50.38859882342891 +2024-08-09 12:00:00,1,10.608772436276414,22.585263139213286,64.69110158630272 +2024-08-09 13:00:00,1,8.160621009934946,24.783997338304996,53.08920919398362 +2024-08-09 14:00:00,1,16.82512990711145,28.56534153403914,58.42788508437367 +2024-08-09 15:00:00,1,26.896662776108016,23.864291485121058,54.71214002949025 +2024-08-09 16:00:00,1,31.683401451534756,24.107891215310353,56.66046503869033 +2024-08-09 17:00:00,1,47.410804418266025,19.672147393384385,57.903825147379884 +2024-08-09 18:00:00,1,21.25623486843901,28.5671987717351,63.136192278233686 +2024-08-09 19:00:00,1,26.48703017673055,30.69642822956248,65.26722390874772 +2024-08-09 20:00:00,1,26.498104605975534,21.816674142061068,65.95227843199444 +2024-08-09 21:00:00,1,23.61890655307722,22.344314579312375,43.23416867962196 +2024-08-09 22:00:00,1,30.347767949597248,24.908101614842874,60.15896218889472 +2024-08-09 23:00:00,1,19.831049510151786,21.693435399907386,62.424253487556605 +2024-08-10 00:00:00,1,26.062467127826086,21.985517277542133,42.40013048407231 +2024-08-10 01:00:00,1,19.887054288845086,20.186671184727974,48.08960688419361 +2024-08-10 02:00:00,1,36.32001523541008,24.73064067100903,41.73834446289872 +2024-08-10 03:00:00,1,28.679948581983634,26.291242864388245,37.50203178671903 +2024-08-10 04:00:00,1,31.055014149112466,23.52703626103902,58.46558549124263 +2024-08-10 05:00:00,1,29.649341334018754,20.651163366249044,47.41020305303531 +2024-08-10 06:00:00,1,24.627529233832703,25.70191916129173,62.84619946608325 +2024-08-10 07:00:00,1,31.808845289365024,20.06679177831329,47.04128390368767 +2024-08-10 08:00:00,1,20.098886103580156,21.55157739998235,56.0120663693764 +2024-08-10 09:00:00,1,21.730874024141997,24.026098665834606,46.34167766946287 +2024-08-10 10:00:00,1,30.527582480402284,26.31002437100542,47.40532235915731 +2024-08-10 11:00:00,1,24.684558968445966,24.668218792207497,62.54881768669644 +2024-08-10 12:00:00,1,11.466437494350112,24.94631327251831,67.58631931714247 +2024-08-10 13:00:00,1,33.40151548814566,20.822849339527295,86.85193584191119 +2024-08-10 14:00:00,1,22.17791654445329,18.555302684171547,54.21374606692678 +2024-08-10 15:00:00,1,20.627855006063136,21.92100328230694,43.541769242591066 +2024-08-10 16:00:00,1,18.91329844497722,24.369166602869583,46.91049796358208 +2024-08-10 17:00:00,1,30.442211084303842,25.1760390902754,47.644479077452054 +2024-08-10 18:00:00,1,15.515219567606128,26.97681843304421,59.00614284871665 +2024-08-10 19:00:00,1,26.119170576464985,21.64580029470436,72.58451098942291 +2024-08-10 20:00:00,1,28.69609641733643,22.891264695746127,43.97534930084333 +2024-08-10 21:00:00,1,21.90190721075277,25.27717287358349,50.12383303325551 +2024-08-10 22:00:00,1,43.55312289377753,18.711517314576643,54.61812722423021 +2024-08-10 23:00:00,1,32.14892374722172,24.167811935316323,63.47922580508802 +2024-08-11 00:00:00,1,24.005830338247144,24.38429845703099,43.60500648850343 +2024-08-11 01:00:00,1,24.28068656084972,23.818537100938915,56.206407897327146 +2024-08-11 02:00:00,1,24.681853388365916,20.175280431572446,45.21349291475316 +2024-08-11 03:00:00,1,23.211445642940994,22.82777435616102,50.42288367160017 +2024-08-11 04:00:00,1,31.534230868777943,21.371465356255907,48.18088221069013 +2024-08-11 05:00:00,1,24.613561272885562,28.265538783385622,49.62164498632458 +2024-08-11 06:00:00,1,15.751091765543622,23.278829939449288,42.753412050266036 +2024-08-11 07:00:00,1,33.396612516536834,23.902166926622787,59.899405403854395 +2024-08-11 08:00:00,1,24.348534771085482,20.398324166949436,54.15768151877945 +2024-08-11 09:00:00,1,33.97864922572324,26.188237942583214,54.538023455911606 +2024-08-11 10:00:00,1,27.86164332067719,28.17543292755813,62.40250391922073 +2024-08-11 11:00:00,1,15.614142779174792,18.25354327796746,60.20240560750708 +2024-08-11 12:00:00,1,41.81764022144607,30.48796914458427,61.056617192098685 +2024-08-11 13:00:00,1,20.55833430858864,24.923818503765762,63.334967802495584 +2024-08-11 14:00:00,1,28.431708486366055,27.57078840249242,60.60921378376799 +2024-08-11 15:00:00,1,32.60961126368253,22.5737113895822,57.000906960426114 +2024-08-11 16:00:00,1,34.63036361976733,25.022639246270593,63.04850235732737 +2024-08-11 17:00:00,1,9.323492190602394,19.015177058234983,67.03265787128659 +2024-08-11 18:00:00,1,20.540614282406022,22.695036580280213,54.455452458817305 +2024-08-11 19:00:00,1,31.940676566969234,24.722106611451178,49.93357015235049 +2024-08-11 20:00:00,1,18.621527670347373,24.955653534498985,68.26840378734732 +2024-08-11 21:00:00,1,38.84364371956337,22.54865633938028,50.29673801068816 +2024-08-11 22:00:00,1,20.01703369875395,18.16114667002307,63.29236621896645 +2024-08-11 23:00:00,1,25.289180188008547,24.928142139830687,63.85830462289534 +2024-08-12 00:00:00,1,22.94499168104483,25.871801048239835,57.04711635941641 +2024-08-12 01:00:00,1,25.854056976843275,29.29594158836087,47.009618764040006 +2024-08-12 02:00:00,1,27.107915361048402,25.403150353434945,64.5842914626226 +2024-08-12 03:00:00,1,23.67014063252804,29.170596900123236,66.17978948699033 +2024-08-12 04:00:00,1,32.51918713834955,16.345758270307652,60.18279015104852 +2024-08-12 05:00:00,1,23.079959574461114,19.58282983105013,32.35880529857722 +2024-08-12 06:00:00,1,6.161057148858738,25.411739713171468,48.97097603265974 +2024-08-12 07:00:00,1,28.078906062357504,16.597043820448654,60.49164116443895 +2024-08-12 08:00:00,1,11.272576492307104,21.53825114140654,55.18634742870894 +2024-08-12 09:00:00,1,23.204019294018966,20.824144666961523,52.00970018241158 +2024-08-12 10:00:00,1,35.2055561034819,23.526596879788336,51.09544944622644 +2024-08-12 11:00:00,1,21.186176708070796,23.744122051631905,62.16656266857938 +2024-08-12 12:00:00,1,21.37066891328868,18.8130651324862,46.367063544167294 +2024-08-12 13:00:00,1,21.595619238279898,25.604272549388927,58.54298519398688 +2024-08-12 14:00:00,1,19.73108746854416,25.64437516227936,57.98465400122003 +2024-08-12 15:00:00,1,32.28846460073194,20.67025512991195,62.6906870801326 +2024-08-12 16:00:00,1,23.45241604371244,24.470083542997077,69.99764824710896 +2024-08-12 17:00:00,1,29.31856123032987,24.4159292240245,48.68820923269702 +2024-08-12 18:00:00,1,29.206751120878742,19.879959386033597,68.3676755506363 +2024-08-12 19:00:00,1,25.690663734206588,25.289484249694283,62.169035442687 +2024-08-12 20:00:00,1,23.473233073762696,18.68996152296916,50.296568944745424 +2024-08-12 21:00:00,1,24.37547996613924,26.31123965218005,39.61906747024591 +2024-08-12 22:00:00,1,21.94677871089107,19.217229301096673,56.77958619854796 +2024-08-12 23:00:00,1,22.467666090217538,21.79470726223322,69.05712791582879 +2024-08-13 00:00:00,1,33.45110227102606,23.89219335719629,47.37897900756112 +2024-08-13 01:00:00,1,37.96114818301405,22.564607887954914,65.97796611204501 +2024-08-13 02:00:00,1,22.51505636123333,32.39664880011,61.45138552663906 +2024-08-13 03:00:00,1,27.35670033095173,23.707801500351277,73.66914302314362 +2024-08-13 04:00:00,1,17.46106630567893,26.38827383274228,70.27085461056795 +2024-08-13 05:00:00,1,26.146643098337094,24.60849669148868,35.98387794105295 +2024-08-13 06:00:00,1,15.882402740528518,23.115372888204263,62.93426178812197 +2024-08-13 07:00:00,1,36.335072947703665,27.775210774774276,59.22928413546545 +2024-08-13 08:00:00,1,20.3756301174952,25.147603729946592,48.15318466139277 +2024-08-13 09:00:00,1,13.882183735880583,26.29087466167092,67.76362713768955 +2024-08-13 10:00:00,1,28.351473068140184,19.286850680039887,58.33892852217556 +2024-08-13 11:00:00,1,17.493287732969137,22.959052647260307,56.838645801694305 +2024-08-13 12:00:00,1,28.987219266794717,26.184129677125966,51.89922969136804 +2024-08-13 13:00:00,1,28.64853913960471,29.155122192691636,47.0885270665205 +2024-08-13 14:00:00,1,19.36679704005926,28.650293777284574,57.02557861044067 +2024-08-13 15:00:00,1,21.728769614415828,21.55547785350024,55.36622647693502 +2024-08-13 16:00:00,1,25.325072513976234,25.494428971389812,49.81497446637675 +2024-08-13 17:00:00,1,26.864381625676614,26.909387187309694,46.52619303588763 +2024-08-13 18:00:00,1,30.794736689643383,27.60124853759673,62.3610148006654 +2024-08-13 19:00:00,1,19.456156377870595,23.61911498458421,60.11090830615611 +2024-08-13 20:00:00,1,10.700535189032758,28.901382427991592,71.82066180362375 +2024-08-13 21:00:00,1,27.21570516192804,20.37338664180927,53.85257580712667 +2024-08-13 22:00:00,1,22.559690166849965,22.224772080895374,53.30620908041038 +2024-08-13 23:00:00,1,36.6116863447329,15.68594862392297,56.36520985208815 +2024-08-14 00:00:00,1,17.710027195476286,28.902954342806307,65.69702177233854 +2024-08-14 01:00:00,1,20.145147725350107,21.89315685308086,51.431750542376626 +2024-08-14 02:00:00,1,36.14475551594042,20.627491796187737,65.93301674212958 +2024-08-14 03:00:00,1,20.97012611559817,26.263164150764613,51.241581248891514 +2024-08-14 04:00:00,1,23.203906100116537,21.47342109832056,50.513763929199 +2024-08-14 05:00:00,1,30.010314466141587,28.435164561786817,49.35108865832345 +2024-08-14 06:00:00,1,11.913733914989125,27.516249815003945,65.00803828033777 +2024-08-14 07:00:00,1,22.417163831361325,22.936424834758142,53.684525347776656 +2024-08-14 08:00:00,1,19.72628753928226,26.307942153608877,62.083349781984424 +2024-08-14 09:00:00,1,20.693588671702248,22.738078719668298,40.51303064581589 +2024-08-14 10:00:00,1,20.091108712437848,20.422848320551626,55.61062587412802 +2024-08-14 11:00:00,1,28.018013244048568,20.594726595468074,47.561170561154654 +2024-08-14 12:00:00,1,18.031267581181062,28.09833422718735,49.79088332103953 +2024-08-14 13:00:00,1,14.682877763333495,24.89705304425873,61.662784164246624 +2024-08-14 14:00:00,1,25.140837357849833,18.022448507683592,46.414690477654005 +2024-08-14 15:00:00,1,30.290717523973804,23.161757072699228,41.78116985176407 +2024-08-14 16:00:00,1,20.43490525403666,24.043717208904344,61.7725135466673 +2024-08-14 17:00:00,1,28.090021076433935,25.813470461933612,46.24333770863467 +2024-08-14 18:00:00,1,24.597012786393954,24.458956759102847,45.66714067159978 +2024-08-14 19:00:00,1,23.603018146816968,21.123325528606383,75.36161680185162 +2024-08-14 20:00:00,1,26.39970565430313,22.407845869952958,61.42915489376911 +2024-08-14 21:00:00,1,27.589821915943446,20.526803828306317,52.80388078357513 +2024-08-14 22:00:00,1,32.80305610627279,20.3814941250504,71.68499271135401 +2024-08-14 23:00:00,1,22.92205978311288,24.260609291481344,75.49143711384818 +2024-08-15 00:00:00,1,23.245998629987966,25.27267600479886,46.44670316789419 +2024-08-15 01:00:00,1,22.502675710829394,18.17984642339084,59.25218525823272 +2024-08-15 02:00:00,1,48.691876613054234,28.609487658258832,47.831627873587436 +2024-08-15 03:00:00,1,19.202894151290465,19.233502186703102,63.17401244330383 +2024-08-15 04:00:00,1,39.60460091119064,31.286738543350864,56.165419937732224 +2024-08-15 05:00:00,1,26.625134159612685,23.813220134175765,72.52941920225399 +2024-08-15 06:00:00,1,36.87570096049372,23.77917603895901,50.78353200434632 +2024-08-15 07:00:00,1,15.7882266516685,22.520966270100722,55.29337924467305 +2024-08-15 08:00:00,1,27.3215227682822,29.388092927209,67.61982694721048 +2024-08-15 09:00:00,1,26.437293440757294,18.285690019638743,42.23571235999292 +2024-08-15 10:00:00,1,40.26887210582137,21.11160655967099,61.55206634231517 +2024-08-15 11:00:00,1,24.353888458512937,20.871683954841796,50.00195746438892 +2024-08-15 12:00:00,1,24.921783226523004,27.108887891050912,70.12255591619754 +2024-08-15 13:00:00,1,23.273880183950848,21.19281654636314,60.78535318354514 +2024-08-15 14:00:00,1,30.496636892463233,25.324614868195155,58.78493679561214 +2024-08-15 15:00:00,1,25.533718878078297,16.913908578785897,43.14456337573895 +2024-08-15 16:00:00,1,23.80945826098455,19.97706192281271,55.55996731346151 +2024-08-15 17:00:00,1,15.951921273138822,24.70176279422168,51.999450507240006 +2024-08-15 18:00:00,1,23.297104406613947,21.84443183345048,66.6113625893336 +2024-08-15 19:00:00,1,33.20696865014612,24.81105545024426,68.38057893927088 +2024-08-15 20:00:00,1,18.267714094076464,18.28804215012749,58.07786078877173 +2024-08-15 21:00:00,1,33.38570918189045,22.422121784768837,53.3581365518219 +2024-08-15 22:00:00,1,5.607276373178948,26.775043673114105,51.10369173289229 +2024-08-15 23:00:00,1,29.95798734304707,22.202646899966886,53.21901301314396 +2024-08-16 00:00:00,1,31.88388560398748,30.900766065412277,51.96933176366968 +2024-08-16 01:00:00,1,38.803595359031405,23.886057260197468,60.23278819168847 +2024-08-16 02:00:00,1,30.03625146337906,21.917110888254356,71.96743758774932 +2024-08-16 03:00:00,1,16.731342178419556,19.492866454959305,54.45681430302303 +2024-08-16 04:00:00,1,38.58088714686281,24.284714414319033,59.77222829370597 +2024-08-16 05:00:00,1,16.86670686284066,25.17831695581336,57.10756642090507 +2024-08-16 06:00:00,1,19.781299763907096,23.522568928786946,33.054272127499615 +2024-08-16 07:00:00,1,28.22843056081258,20.849512640860066,41.60636692528054 +2024-08-16 08:00:00,1,19.81240528528816,22.55827750150246,56.022577083231134 +2024-08-16 09:00:00,1,30.17538034331342,20.206113066010857,54.75350542519164 +2024-08-16 10:00:00,1,23.863210155806822,21.138440820005147,34.15163693414905 +2024-08-16 11:00:00,1,37.064343274911664,27.21349541523828,78.74302500773588 +2024-08-16 12:00:00,1,19.637508681303455,26.28713109399786,49.940753608367345 +2024-08-16 13:00:00,1,20.61155390088912,22.19818418142539,47.09368360131924 +2024-08-16 14:00:00,1,23.71233542888667,27.839904315831944,50.5092252417423 +2024-08-16 15:00:00,1,31.085057923972553,26.161040280789503,44.898293855414195 +2024-08-16 16:00:00,1,27.280307890901664,23.513622542760764,69.75572496663129 +2024-08-16 17:00:00,1,25.04583468421768,23.308601751518314,38.49656985954222 +2024-08-16 18:00:00,1,25.199936224051,21.502475922381716,64.16118163860615 +2024-08-16 19:00:00,1,25.33843335230036,26.778733643342726,44.4116497719977 +2024-08-16 20:00:00,1,30.24229239718541,24.386224510678563,60.06464182586097 +2024-08-16 21:00:00,1,20.819551817066277,24.657738259620622,60.23556644346082 +2024-08-16 22:00:00,1,33.82635552894646,17.624278557492996,55.419852417143986 +2024-08-16 23:00:00,1,14.094414044244322,19.464832207040793,54.52646258013882 +2024-08-17 00:00:00,1,22.1804453530663,25.455005375730416,56.849450942982685 +2024-08-17 01:00:00,1,23.427178113801247,23.550679947494892,60.24625421728555 +2024-08-17 02:00:00,1,38.69836872076069,27.397989241493615,60.388015603904165 +2024-08-17 03:00:00,1,23.114679619413728,21.67527438304415,54.49275219945433 +2024-08-17 04:00:00,1,20.690195877463573,26.265425830024263,52.936465615665746 +2024-08-17 05:00:00,1,15.16195078654758,24.687846273632008,34.98136296737772 +2024-08-17 06:00:00,1,26.56727521861463,22.514407680560957,57.77226911789136 +2024-08-17 07:00:00,1,22.5171883386319,28.331163422073484,37.85955198513352 +2024-08-17 08:00:00,1,29.515439153880422,22.67953427797084,52.901761913224746 +2024-08-17 09:00:00,1,10.704769877683379,29.96673370012453,52.699630529192824 +2024-08-17 10:00:00,1,28.652615828338288,20.476677658441965,81.77459867970992 +2024-08-17 11:00:00,1,18.694886946661633,25.894741996857437,45.11471211890126 +2024-08-17 12:00:00,1,29.642810197051382,22.376268273583616,59.11816478405825 +2024-08-17 13:00:00,1,16.07155685111496,16.833263180411944,45.85272804949294 +2024-08-17 14:00:00,1,31.696593769452313,21.133967567217883,50.68647337578948 +2024-08-17 15:00:00,1,31.442622106988566,19.145262183192184,61.70440776686854 +2024-08-17 16:00:00,1,23.412641199164735,19.887583110072455,55.39229168035501 +2024-08-17 17:00:00,1,29.331350595416673,29.198006875138997,50.36745143100132 +2024-08-17 18:00:00,1,36.04806147511677,23.31695057978011,56.24554258969669 +2024-08-17 19:00:00,1,18.436896405296302,32.41188564785023,56.87503844610381 +2024-08-17 20:00:00,1,30.677791907266506,27.71034635925233,55.370189419332235 +2024-08-17 21:00:00,1,8.551568431820996,30.752906230735277,49.86602902642668 +2024-08-17 22:00:00,1,22.70881985343459,27.280206222252808,60.74911768321514 +2024-08-17 23:00:00,1,19.734192872801476,17.900447927161043,51.781363702304354 +2024-08-18 00:00:00,1,17.42985518193329,24.500951166227985,66.16652649002705 +2024-08-18 01:00:00,1,26.028065646505596,26.303656082123787,63.07463688994363 +2024-08-18 02:00:00,1,23.580682967023783,23.197158172065087,60.522845814242565 +2024-08-18 03:00:00,1,17.87481701650026,24.844598572290543,67.62770389361054 +2024-08-18 04:00:00,1,29.446951659724526,24.65011912346607,60.497377130017405 +2024-08-18 05:00:00,1,40.526917711775965,19.2935117736994,47.38850509382536 +2024-08-18 06:00:00,1,19.87943117675397,28.446249076295295,53.267596331175625 +2024-08-18 07:00:00,1,21.63379930782241,25.446529830193832,54.31758500726631 +2024-08-18 08:00:00,1,35.65237059886378,28.120792453074607,41.58321191720788 +2024-08-18 09:00:00,1,26.350177184425895,24.328600878971653,41.56884187572181 +2024-08-18 10:00:00,1,19.23877446157767,23.490824489508285,70.80675913881414 +2024-08-18 11:00:00,1,25.23276712232272,19.90599222331455,64.98367640865675 +2024-08-18 12:00:00,1,19.28095125016112,20.699005063254308,45.49585447467958 +2024-08-18 13:00:00,1,29.13799656206917,21.804825357568316,54.07666192849569 +2024-08-18 14:00:00,1,25.909705021708366,25.922908556702893,38.51107043923403 +2024-08-18 15:00:00,1,21.6930242267948,21.762882302177992,43.17547163272016 +2024-08-18 16:00:00,1,22.12470673654093,29.741846670879976,47.97663696875601 +2024-08-18 17:00:00,1,34.2167427934332,24.182508446180886,51.02109949640626 +2024-08-18 18:00:00,1,42.740880334960664,22.667951576551285,58.97387054725044 +2024-08-18 19:00:00,1,17.224117025688393,27.915441252227446,61.23090622582129 +2024-08-18 20:00:00,1,26.549993377110255,20.42600450033453,47.67446944108192 +2024-08-18 21:00:00,1,22.246240752019652,23.625500963445017,57.351648757531834 +2024-08-18 22:00:00,1,15.169708323530969,24.91118069508375,59.71360040760895 +2024-08-18 23:00:00,1,32.2289540826298,18.806566858163517,64.51746570962281 +2024-08-19 00:00:00,1,26.350750587785534,16.726547540705678,59.69970301678922 +2024-08-19 01:00:00,1,26.85309309973989,24.984324507360338,48.137078285815896 +2024-08-19 02:00:00,1,33.570733080379355,30.250998591448376,62.905597365129154 +2024-08-19 03:00:00,1,27.106375726525332,19.323184118255202,51.85269632273984 +2024-08-19 04:00:00,1,14.821921546682162,26.412717932005595,59.46942596899558 +2024-08-19 05:00:00,1,39.07343966338005,21.50739874456685,47.991463811244785 +2024-08-19 06:00:00,1,22.65651340798334,31.309348445199262,49.92563312065376 +2024-08-19 07:00:00,1,24.890693707699416,22.035785272983617,48.58573833585201 +2024-08-19 08:00:00,1,19.48328544620259,28.129999353727573,60.055346666622555 +2024-08-19 09:00:00,1,34.444026134992214,20.16325740517131,43.80061056113311 +2024-08-19 10:00:00,1,3.223912353284785,23.871648359276357,66.337578189378 +2024-08-19 11:00:00,1,21.774363240092338,22.447924818685387,58.543398695119464 +2024-08-19 12:00:00,1,25.5225259892714,21.971293749705264,54.44158866263852 +2024-08-19 13:00:00,1,12.152929151369266,24.69390091182964,45.517674592526106 +2024-08-19 14:00:00,1,18.221588526922854,25.998478862968433,63.41028118442293 +2024-08-19 15:00:00,1,33.403820622340206,27.059489834287522,59.38123363434896 +2024-08-19 16:00:00,1,27.465014814016673,23.666453754229575,55.79749277593674 +2024-08-19 17:00:00,1,23.956854501928916,26.481854208909805,69.69738806702023 +2024-08-19 18:00:00,1,24.884165158460444,20.489568210685885,52.38226247932418 +2024-08-19 19:00:00,1,26.26451578603094,21.84926317104776,46.985439675884024 +2024-08-19 20:00:00,1,15.392289860914431,21.10763699055725,61.134775512132876 +2024-08-19 21:00:00,1,30.27358305485125,19.847174784729432,68.57708161262252 +2024-08-19 22:00:00,1,32.554419888031205,21.89166692440454,41.74318566993142 +2024-08-19 23:00:00,1,31.562484261752843,29.632715334680412,64.53606379297418 +2024-08-20 00:00:00,1,20.397410493294394,24.26568623720507,44.74476730008667 +2024-08-20 01:00:00,1,28.125786627893934,25.33257991008629,52.04107683150318 +2024-08-20 02:00:00,1,22.39035123657827,26.99481016604191,55.98160697615326 +2024-08-20 03:00:00,1,45.140012829800504,21.18466760742701,56.36349666189708 +2024-08-20 04:00:00,1,23.09968072714839,24.38063743500733,52.60139208485082 +2024-08-20 05:00:00,1,19.62080265774544,25.900172662270695,46.41942815472 +2024-08-20 06:00:00,1,8.743699970365608,30.26228366513613,56.689518344343945 +2024-08-20 07:00:00,1,14.516480776765983,18.28001778747781,38.7295694923199 +2024-08-20 08:00:00,1,42.35570095483983,22.428621882779616,41.57883897211115 +2024-08-20 09:00:00,1,29.521909931120135,20.982302925026122,54.42558026917862 +2024-08-20 10:00:00,1,17.310160387256843,22.03962018310997,54.13024084392361 +2024-08-20 11:00:00,1,22.308828750757485,23.686753762221887,58.58641928190078 +2024-08-20 12:00:00,1,31.802944352999226,29.39187119426509,66.29252782699623 +2024-08-20 13:00:00,1,20.68338297013616,24.85698982022912,57.22471642638459 +2024-08-20 14:00:00,1,15.582480826896523,23.073665972141903,56.217275923391924 +2024-08-20 15:00:00,1,23.578736286847743,18.68963501968303,62.90556417965958 +2024-08-20 16:00:00,1,20.173305675155753,20.2060322689929,59.71792434090714 +2024-08-20 17:00:00,1,8.631157696370991,24.152810400577994,74.24755807205295 +2024-08-20 18:00:00,1,20.275756409440117,24.38779501602143,57.169101435416444 +2024-08-20 19:00:00,1,26.4248480461718,20.841114859341467,57.224652349063405 +2024-08-20 20:00:00,1,28.100293401708658,18.644144402719242,57.90233183969072 +2024-08-20 21:00:00,1,27.245219493987236,22.963438097248044,55.77823460471824 +2024-08-20 22:00:00,1,20.901555078396942,20.607894141733986,69.93241690946164 +2024-08-20 23:00:00,1,23.96875963717808,17.866123020063682,64.17775599968712 +2024-08-21 00:00:00,1,42.39695144199382,31.91486821518328,65.77239112299095 +2024-08-21 01:00:00,1,27.849577731734566,27.732068475576245,64.0469226847846 +2024-08-21 02:00:00,1,32.02493736401118,21.993117870243807,49.02336008589115 +2024-08-21 03:00:00,1,23.747992076481893,17.963895986518637,50.68797589945025 +2024-08-21 04:00:00,1,16.691054343050176,21.647839172895516,52.02471034349742 +2024-08-21 05:00:00,1,21.078346328226374,31.896358090049116,37.60867922545397 +2024-08-21 06:00:00,1,18.126702806029765,21.463878589970133,47.76992029064759 +2024-08-21 07:00:00,1,21.748019789335842,27.501841258922532,63.29129471284688 +2024-08-21 08:00:00,1,33.350840570153096,21.598054376913396,54.45461873978615 +2024-08-21 09:00:00,1,23.384189817856665,16.747470012283777,54.0865074260643 +2024-08-21 10:00:00,1,26.232483152166342,22.178772664309665,48.621950954498345 +2024-08-21 11:00:00,1,21.219400714641974,22.465879599770226,44.40062345053919 +2024-08-21 12:00:00,1,18.949835289430304,28.74872851140315,67.8512719198022 +2024-08-21 13:00:00,1,18.25674930032227,18.780562222087415,52.11363292616504 +2024-08-21 14:00:00,1,29.657252586654874,30.756986509189172,62.840518063734955 +2024-08-21 15:00:00,1,18.939443371193573,21.397137812187143,67.59696058365078 +2024-08-21 16:00:00,1,44.37382558731929,27.063526785153933,66.64928679310856 +2024-08-21 17:00:00,1,28.876179969730835,21.674398030323726,54.91897169074201 +2024-08-21 18:00:00,1,0.21252500805298524,18.144399704420643,46.372496296684105 +2024-08-21 19:00:00,1,14.863996216192678,28.805793998979066,55.38143123861954 +2024-08-21 20:00:00,1,37.77156105962813,21.69340481808051,68.94465513946781 +2024-08-21 21:00:00,1,25.784632533954728,19.174558871548196,59.91092834724356 +2024-08-21 22:00:00,1,11.065137598215689,26.04942678796564,60.27604150410657 +2024-08-21 23:00:00,1,29.72947540511533,27.65868360654881,57.906534819388604 +2024-08-22 00:00:00,1,21.06969568804105,18.63137743880393,38.84949427223816 +2024-08-22 01:00:00,1,16.484056814819763,21.163754777848734,55.22100140528919 +2024-08-22 02:00:00,1,34.735101485087036,26.311139863092023,47.57455285953833 +2024-08-22 03:00:00,1,20.388019975484106,23.509621307564437,58.640323423295904 +2024-08-22 04:00:00,1,23.2385081443745,23.529492243942375,54.525552139033486 +2024-08-22 05:00:00,1,29.90250361279096,19.130008193915668,44.15077938175777 +2024-08-22 06:00:00,1,33.443508235658626,22.129678275828653,58.41311943308348 +2024-08-22 07:00:00,1,23.758565334080775,28.847068033467814,55.474569787751584 +2024-08-22 08:00:00,1,23.00401427888607,24.406493672902243,46.67364251003285 +2024-08-22 09:00:00,1,33.60282031354249,26.33695124233107,63.448237674274196 +2024-08-22 10:00:00,1,25.38173270268514,28.413752826137824,59.02975040043862 +2024-08-22 11:00:00,1,26.90468955694385,24.96431091551075,58.04229946309515 +2024-08-22 12:00:00,1,27.298010403697276,23.390443493895834,58.82673765459411 +2024-08-22 13:00:00,1,30.00457523907255,22.432111185323386,64.9384555152914 +2024-08-22 14:00:00,1,31.23879566420555,22.382774887666123,47.00330982648761 +2024-08-22 15:00:00,1,27.166777039401016,23.47926327540875,55.97141584242278 +2024-08-22 16:00:00,1,35.123610494399664,22.76592950647086,63.79767896545952 +2024-08-22 17:00:00,1,30.550860283510765,23.33280824989452,51.92215474780424 +2024-08-22 18:00:00,1,26.55430279769785,24.740514582168053,59.65535011154602 +2024-08-22 19:00:00,1,37.11283196431907,18.705126381491393,51.55930439845127 +2024-08-22 20:00:00,1,20.325633217247013,22.27365924854276,56.33729843475222 +2024-08-22 21:00:00,1,28.716220242852994,23.978523238614976,51.46392724466457 +2024-08-22 22:00:00,1,29.455357100346326,26.105364820977062,46.51843971365325 +2024-08-22 23:00:00,1,38.695346608811946,26.561861611708828,56.12633761761826 +2024-08-23 00:00:00,1,25.101171067800312,26.26791852502955,47.58185493421756 +2024-08-23 01:00:00,1,19.64237421445701,26.480048146398595,45.866304802436616 +2024-08-23 02:00:00,1,19.41632751432057,20.88148882689013,59.222391493915836 +2024-08-23 03:00:00,1,33.29862124931968,24.67368715681722,57.31093314346681 +2024-08-23 04:00:00,1,22.06073062506484,16.387064355939426,68.24364999741914 +2024-08-23 05:00:00,1,25.577845368460927,26.28637535402741,53.00749888604909 +2024-08-23 06:00:00,1,18.775359590999976,26.927703666887293,62.37355541173869 +2024-08-23 07:00:00,1,24.5517019084297,22.869818256666804,51.29718165620782 +2024-08-23 08:00:00,1,25.80560103629177,22.302309077416933,48.32780115861754 +2024-08-23 09:00:00,1,7.741198286762881,28.290147548765496,60.63741533466108 +2024-08-23 10:00:00,1,16.733118031238472,21.069955778885685,56.885015623617804 +2024-08-23 11:00:00,1,26.803027089152256,25.814648505487362,65.02929014165761 +2024-08-23 12:00:00,1,49.56444371742323,19.853352433828615,48.79044419458528 +2024-08-23 13:00:00,1,11.196845183533053,22.352388327100396,51.23367785118195 +2024-08-23 14:00:00,1,32.09944895876461,20.970684813080215,64.4188522882002 +2024-08-23 15:00:00,1,36.293288659388814,19.939402219559206,34.084431793781775 +2024-08-23 16:00:00,1,23.920351445080964,25.303713529414708,51.128924140111 +2024-08-23 17:00:00,1,6.757378479353321,25.99126111934935,61.66696634269046 +2024-08-23 18:00:00,1,24.583256450111644,24.30069883319484,68.13133452087193 +2024-08-23 19:00:00,1,36.01964432701088,27.73479979679997,55.468637631171596 +2024-08-23 20:00:00,1,27.640317239690056,16.52487578093189,64.13790751132201 +2024-08-23 21:00:00,1,13.16998755735571,20.159996836272068,54.524325145558194 +2024-08-23 22:00:00,1,33.59806161448488,27.613973171989116,39.12402786030786 +2024-08-23 23:00:00,1,26.682242323643024,27.843462646270613,73.68974019460774 +2024-08-24 00:00:00,1,17.74464255601966,20.960158679088675,64.33507903125661 +2024-08-24 01:00:00,1,32.86359925906178,25.616569362081226,59.669910961927854 +2024-08-24 02:00:00,1,10.271006441902731,25.150903803909987,40.27265860805463 +2024-08-24 03:00:00,1,19.846444562920247,28.346764824933764,49.80302982922765 +2024-08-24 04:00:00,1,29.442051067013132,31.43437749665318,52.814049876451755 +2024-08-24 05:00:00,1,22.83050957369964,19.117766043417102,62.59440054731458 +2024-08-24 06:00:00,1,37.300755781851464,21.095952736159514,62.39810366823828 +2024-08-24 07:00:00,1,23.521665173009822,20.65282830881277,59.48590039435472 +2024-08-24 08:00:00,1,17.33859190555171,24.296622409391468,56.81774382123701 +2024-08-24 09:00:00,1,36.19684737172382,26.355486578398676,61.64541457196313 +2024-08-24 10:00:00,1,12.69283638866691,25.083600310181385,54.17143040119886 +2024-08-24 11:00:00,1,24.041503742826503,18.56232501348839,59.476364004341306 +2024-08-24 12:00:00,1,18.107796786782806,24.498380364523307,58.89518410478921 +2024-08-24 13:00:00,1,25.279868730309783,23.42053882607065,70.04851262410651 +2024-08-24 14:00:00,1,20.30824547419826,20.430319445026456,56.637175490966676 +2024-08-24 15:00:00,1,21.30867055777855,21.138473830726674,53.59564808848417 +2024-08-24 16:00:00,1,16.2628658866187,25.836284714705712,52.82029144624923 +2024-08-24 17:00:00,1,24.397099985501853,18.561976142209602,58.52982319315057 +2024-08-24 18:00:00,1,20.040997196328775,18.35625903742377,63.44192408755953 +2024-08-24 19:00:00,1,23.4862389994603,19.090239687226887,48.28830602888851 +2024-08-24 20:00:00,1,20.60008099772238,23.628453014793916,57.634381751180904 +2024-08-24 21:00:00,1,38.7872572106186,20.531721217623605,52.63846490409673 +2024-08-24 22:00:00,1,29.215352740544212,21.43865010948479,49.094501512140766 +2024-08-24 23:00:00,1,20.20687587383297,29.229814357074478,50.612688740527155 +2024-08-25 00:00:00,1,28.475254824964964,26.583913013257195,60.72211724073165 +2024-08-25 01:00:00,1,25.156837432531862,30.633101390620148,54.9256765599052 +2024-08-25 02:00:00,1,32.844857383808986,26.349020402195915,50.085497069281445 +2024-08-25 03:00:00,1,24.161856734063583,20.886448410837133,47.409626416655655 +2024-08-25 04:00:00,1,31.852676747716583,23.93288053451798,65.30172775432727 +2024-08-25 05:00:00,1,11.685523324937183,24.326450536343877,44.696665282748974 +2024-08-25 06:00:00,1,27.67322543476817,22.5130503744447,53.475051776344884 +2024-08-25 07:00:00,1,21.368126563729536,24.043376371290066,50.52284439631909 +2024-08-25 08:00:00,1,30.11018144579698,20.32601155135639,49.709047941195216 +2024-08-25 09:00:00,1,23.90036236690563,28.186977816476887,79.3060980964289 +2024-08-25 10:00:00,1,20.749692577852958,22.913458552955483,60.49305549917211 +2024-08-25 11:00:00,1,16.636759917394805,17.700698236688776,72.78027450958594 +2024-08-25 12:00:00,1,18.6753004909829,20.741608740776943,38.89396820134268 +2024-08-25 13:00:00,1,17.102546907444236,23.369892131703818,67.86723837253847 +2024-08-25 14:00:00,1,21.519953742604084,25.647083610945675,46.86426123227406 +2024-08-25 15:00:00,1,20.318290369236117,22.328208769753726,63.626182701921834 +2024-08-25 16:00:00,1,20.816428569047122,24.506784750933843,56.4170943525135 +2024-08-25 17:00:00,1,35.746197012461835,17.26724456423088,52.4790906208768 +2024-08-25 18:00:00,1,27.24479319624408,31.387011832271504,58.74473663064278 +2024-08-25 19:00:00,1,26.878994742057635,24.071570297478537,56.64718936084834 +2024-08-25 20:00:00,1,24.908426347054178,23.193290802901217,62.19321350589091 +2024-08-25 21:00:00,1,38.200063237454856,24.113721880410413,56.919464890445575 +2024-08-25 22:00:00,1,31.170706772582207,24.532306323137014,67.10309121144549 +2024-08-25 23:00:00,1,23.462021248607186,19.088976084349156,46.94138786650453 +2024-08-26 00:00:00,1,32.73629540990236,18.280725096909137,61.84600362272572 +2024-08-26 01:00:00,1,24.665342877205994,20.666762915366498,51.729434154940904 +2024-08-26 02:00:00,1,24.679963920011996,20.918541821892678,48.8311149521123 +2024-08-26 03:00:00,1,15.466463910423846,29.075861564799613,53.89045482630588 +2024-08-26 04:00:00,1,29.50816999592457,21.278869042648136,53.461046395033975 +2024-08-26 05:00:00,1,24.705363625662887,28.413051054810545,54.39585828659483 +2024-08-26 06:00:00,1,30.024014539737852,25.715221230962822,60.9701155108522 +2024-08-26 07:00:00,1,26.32786752947027,24.620874853836536,45.39541498304109 +2024-08-26 08:00:00,1,25.375905455249665,23.118718332484338,53.102836144302955 +2024-08-26 09:00:00,1,9.277181275069902,20.654495595664667,41.323561800425914 +2024-08-26 10:00:00,1,23.212364218800836,18.011229475959347,55.468494829017935 +2024-08-26 11:00:00,1,24.73657400047832,21.922874388670603,49.839963859410695 +2024-08-26 12:00:00,1,14.696105181084661,25.63635884879479,47.47708375288483 +2024-08-26 13:00:00,1,30.233612633095873,20.92335202218907,56.93092856074207 +2024-08-26 14:00:00,1,22.071409178395587,26.551108456141897,64.1668191133652 +2024-08-26 15:00:00,1,16.799887574633555,22.97745129436444,51.58966696353286 +2024-08-26 16:00:00,1,27.99033162430089,21.213842171774495,38.43017730499411 +2024-08-26 17:00:00,1,25.919869424529594,22.789058710210853,58.51904255668801 +2024-08-26 18:00:00,1,22.54741289075841,24.702217295460205,52.71598922150883 +2024-08-26 19:00:00,1,25.013572207539553,23.38194269477769,51.18847785615455 +2024-08-26 20:00:00,1,27.919263778732468,18.84649049963966,69.69029473636586 +2024-08-26 21:00:00,1,25.90639818488396,29.64435785463887,62.14938651727732 +2024-08-26 22:00:00,1,23.14487710836218,30.12009740673138,56.150931757964756 +2024-08-26 23:00:00,1,18.405909183518386,16.963892838437737,76.87913880838528 +2024-08-27 00:00:00,1,43.472291733905095,26.146847456091358,48.91835822263345 +2024-08-27 01:00:00,1,31.242295068963067,28.023407943916613,42.00109482317794 +2024-08-27 02:00:00,1,30.76315158427126,25.28013598367644,45.08133233499891 +2024-08-27 03:00:00,1,33.204816626793,22.95869413508148,50.22045770470245 +2024-08-27 04:00:00,1,24.0831961746786,24.739873549195103,59.178343317254594 +2024-08-27 05:00:00,1,22.406544528858255,29.981574569266186,58.554698386257456 +2024-08-27 06:00:00,1,28.985662452196188,23.973182410229793,60.7401957207718 +2024-08-27 07:00:00,1,27.232952915583418,22.50054368054157,44.81260517111017 +2024-08-27 08:00:00,1,26.169801182465072,21.763165531800105,70.1904871914528 +2024-08-27 09:00:00,1,27.487334805520508,21.764155890218117,53.776516947851725 +2024-08-27 10:00:00,1,13.234355023228588,20.98720896552122,68.11855734395034 +2024-08-27 11:00:00,1,33.60492621706125,24.21468990855927,42.649719484827514 +2024-08-27 12:00:00,1,14.278917676151297,20.068222827450548,52.89251827739797 +2024-08-27 13:00:00,1,26.69431717953589,23.970789075697624,61.89734011318821 +2024-08-27 14:00:00,1,24.908571417624206,24.173685106883113,44.602985541083946 +2024-08-27 15:00:00,1,27.16667256194997,24.628610959545135,45.464221730787514 +2024-08-27 16:00:00,1,33.31742049309803,22.242928355273595,61.52074366715614 +2024-08-27 17:00:00,1,34.225188666366506,22.43243099077296,59.26710752150909 +2024-08-27 18:00:00,1,31.729093932367405,24.99211175304536,47.99439534566787 +2024-08-27 19:00:00,1,25.31901811787603,25.695452776167553,55.9170050782249 +2024-08-27 20:00:00,1,24.469297770786824,19.342801330149065,63.77217421219552 +2024-08-27 21:00:00,1,23.29288150256467,18.577774496542183,48.55907898180794 +2024-08-27 22:00:00,1,18.366905538712473,25.719638860881755,58.428141492946985 +2024-08-27 23:00:00,1,21.98457598456573,25.675046202002036,60.58454652507128 +2024-08-28 00:00:00,1,29.98662802428436,28.70243369599958,61.213436219589525 +2024-08-28 01:00:00,1,19.50328805335628,23.86752934614161,71.18595967761293 +2024-08-28 02:00:00,1,37.6534096208114,17.755762815719017,71.75869269387432 +2024-08-28 03:00:00,1,22.39015798150536,26.467110841488754,42.27577030594339 +2024-08-28 04:00:00,1,12.739241912957459,27.155901628961498,53.395318813144186 +2024-08-28 05:00:00,1,37.685254355293004,20.93063505465483,69.38938398206608 +2024-08-28 06:00:00,1,33.997556124147465,25.00591715067439,48.4764012997337 +2024-08-28 07:00:00,1,26.444329974749298,19.59372972752715,64.58469829493964 +2024-08-28 08:00:00,1,19.32518304478825,27.386406232498185,50.94386493470176 +2024-08-28 09:00:00,1,38.17071754887181,28.801843073466486,37.70165710479344 +2024-08-28 10:00:00,1,12.4813646790042,23.33300058747239,49.49335052095326 +2024-08-28 11:00:00,1,35.573263970860566,18.483977276813846,55.896702165531956 +2024-08-28 12:00:00,1,31.597676307682363,19.007875050418306,71.16346044026528 +2024-08-28 13:00:00,1,19.342788503336834,19.25057582927533,54.659874117162886 +2024-08-28 14:00:00,1,23.407750325276606,25.005337658131165,47.215418291339134 +2024-08-28 15:00:00,1,21.014325139563958,25.48195358668795,72.90956434111739 +2024-08-28 16:00:00,1,27.495888617589756,22.88078524903478,47.432430374105785 +2024-08-28 17:00:00,1,25.09596603662852,30.79827675923958,34.81610078596326 +2024-08-28 18:00:00,1,48.41388410758371,25.497706101441427,53.980182919775906 +2024-08-28 19:00:00,1,29.079166674490047,24.17454349305903,55.716285391781 +2024-08-28 20:00:00,1,24.01859115317909,20.290000578529586,56.00312100505559 +2024-08-28 21:00:00,1,28.801010519837888,20.967989108934816,73.58508786596336 +2024-08-28 22:00:00,1,14.11699339785987,19.943403219929458,67.77230332399697 +2024-08-28 23:00:00,1,24.05281925529469,19.74247656806412,44.1360695214532 +2024-08-29 00:00:00,1,22.619300386204447,24.11646827735293,60.639989621830466 +2024-08-29 01:00:00,1,26.008839078631837,27.557438620289023,50.54780070540924 +2024-08-29 02:00:00,1,14.664085785672972,21.335586014524882,64.73122278928209 +2024-08-29 03:00:00,1,26.746460712932237,25.850879319877738,58.891948328359156 +2024-08-29 04:00:00,1,32.90043665752391,26.592233514222134,57.36696229964091 +2024-08-29 05:00:00,1,21.14454752860579,31.017001817181693,47.81201602704195 +2024-08-29 06:00:00,1,40.11152068041236,23.380303368660677,54.56157185406949 +2024-08-29 07:00:00,1,36.12430766942917,19.666687714943233,51.24640738383902 +2024-08-29 08:00:00,1,17.26030802456885,27.29598167508436,53.3791655552068 +2024-08-29 09:00:00,1,27.044405682774794,22.1189415979316,69.83961013193309 +2024-08-29 10:00:00,1,36.119669263906175,28.133834104323636,38.623625225959046 +2024-08-29 11:00:00,1,9.010748721052094,20.753930652088677,53.66531102457384 +2024-08-29 12:00:00,1,33.931268784266166,23.05206465564699,55.99384233793376 +2024-08-29 13:00:00,1,12.809821051322439,20.703370412130397,44.11836905641004 +2024-08-29 14:00:00,1,29.6840807165806,24.41523611069955,60.44337990628447 +2024-08-29 15:00:00,1,31.71983692626555,22.053430367148227,48.93072357188852 +2024-08-29 16:00:00,1,30.91292575777559,26.388103649908228,56.20888173463538 +2024-08-29 17:00:00,1,22.110421363340894,26.087253562192835,46.5385373087627 +2024-08-29 18:00:00,1,29.5801967676796,21.445417443699576,44.65442701205321 +2024-08-29 19:00:00,1,33.43999146992094,24.430278901741772,55.58131177426812 +2024-08-29 20:00:00,1,21.382954988345773,20.74853064410049,47.35216341984134 +2024-08-29 21:00:00,1,34.92102152609469,19.583813007756582,62.12325970716953 +2024-08-29 22:00:00,1,16.42022742623896,17.18470365217303,58.9731552784153 +2024-08-29 23:00:00,1,27.29778172572032,24.11923614391467,59.10641488754468 +2024-08-30 00:00:00,1,12.627195200633395,24.778539802005394,46.256399846481315 +2024-08-30 01:00:00,1,19.428333356996138,28.5027290429812,66.68055782247271 +2024-08-30 02:00:00,1,30.421993054643664,18.222645824621534,52.54616772381569 +2024-08-30 03:00:00,1,20.95648099934826,29.135281566699714,43.832525275671074 +2024-08-30 04:00:00,1,37.60809051207923,29.046706991989602,58.08232446378047 +2024-08-30 05:00:00,1,28.087283143775686,22.00938267768806,60.841744433684156 +2024-08-30 06:00:00,1,26.150602644981777,27.55400374828859,57.91417919727675 +2024-08-30 07:00:00,1,13.373618791860126,23.412950180992915,53.591425061293464 +2024-08-30 08:00:00,1,31.249489775936606,23.18807712194743,50.413041889781354 +2024-08-30 09:00:00,1,32.30414349658584,22.91763571168245,56.19593681738518 +2024-08-30 10:00:00,1,28.121132322914953,23.84757700101369,58.373962889160914 +2024-08-30 11:00:00,1,31.827683624522436,25.458536042459958,45.801036155511255 +2024-08-30 12:00:00,1,14.949407827967134,25.708881135857975,62.903871284639116 +2024-08-30 13:00:00,1,11.35903088948721,20.963551050688352,57.49663238566788 +2024-08-30 14:00:00,1,28.82843759671214,23.322511948699375,46.035231454773985 +2024-08-30 15:00:00,1,17.366693344072655,23.065484083359667,53.075834928029764 +2024-08-30 16:00:00,1,37.766768498732006,29.097984996009927,49.381298072900165 +2024-08-30 17:00:00,1,28.21526529905992,23.774204087959994,52.380367313622926 +2024-08-30 18:00:00,1,12.84775144468431,24.02168409164299,43.32667979858265 +2024-08-30 19:00:00,1,24.06425814064645,24.02837096324443,51.78058098771949 +2024-08-30 20:00:00,1,23.968617328710593,28.046751522811903,58.802869299138194 +2024-08-30 21:00:00,1,25.649978528707432,15.405209374221645,63.65588968954788 +2024-08-30 22:00:00,1,15.358496825633793,19.85789986190556,57.726525084323654 +2024-08-30 23:00:00,1,2.221821952772814,20.690156561924166,63.89033971236623 +2024-08-31 00:00:00,1,21.72313093999551,30.48453264420039,54.605691594669466 +2024-08-31 01:00:00,1,29.745080885561237,22.698346882182825,57.32378674073471 +2024-08-31 02:00:00,1,19.398013026360026,28.583111067020152,60.411770712724305 +2024-08-31 03:00:00,1,26.685640428571126,29.3298043175645,50.71658690626232 +2024-08-31 04:00:00,1,25.196601788832652,29.59272296352829,58.361445403411174 +2024-08-31 05:00:00,1,16.850086175082467,34.58469137550496,59.0446482970928 +2024-08-31 06:00:00,1,34.9756446348011,21.69120942438063,45.482303187575894 +2024-08-31 07:00:00,1,24.880281587825085,21.65286942926046,41.53918689163598 +2024-08-31 08:00:00,1,33.84023216373572,26.028701953657524,57.0354713747819 +2024-08-31 09:00:00,1,10.235065228874227,26.222060439913395,65.41965648913026 +2024-08-31 10:00:00,1,25.426659753052235,26.70243780032015,49.98052320438876 +2024-08-31 11:00:00,1,25.955434588230347,21.23583712662896,66.53749180574408 +2024-08-31 12:00:00,1,4.415632571864894,17.897121695134008,42.80164803975176 +2024-08-31 13:00:00,1,38.79848760316652,26.414188532997972,55.86646051574827 +2024-08-31 14:00:00,1,24.829032839196792,30.35690343989244,59.48920873071758 +2024-08-31 15:00:00,1,29.702044397498224,20.900787903290304,52.5669622126877 +2024-08-31 16:00:00,1,21.48175889271358,24.423088893972864,49.57466363552469 +2024-08-31 17:00:00,1,23.680056399140312,22.532540952823048,63.28067113205416 +2024-08-31 18:00:00,1,28.989327800101375,21.54353692736433,46.71875402187769 +2024-08-31 19:00:00,1,20.31397724630086,24.93241557188687,63.97971558499608 +2024-08-31 20:00:00,1,25.055490749413433,22.16397052536628,47.05525782087455 +2024-08-31 21:00:00,1,27.11395731995489,23.21860456452054,49.542940583338314 +2024-08-31 22:00:00,1,21.29413660790648,21.96920694575429,53.32612383814402 +2024-08-31 23:00:00,1,26.776419538616913,20.73613686684235,43.74808552391982 +2024-09-01 00:00:00,1,27.324972839075922,25.797058219713275,40.77071132866672 +2024-09-01 01:00:00,1,26.811304095199375,26.342614025761605,47.517541686796704 +2024-09-01 02:00:00,1,22.84990333933953,16.854201131322274,56.30423893416525 +2024-09-01 03:00:00,1,15.980717369208351,27.059136860571602,41.71290119962263 +2024-09-01 04:00:00,1,18.2040606601316,26.749168396490994,52.18002392896628 +2024-09-01 05:00:00,1,26.44860919790508,20.221352861931525,56.840865978925414 +2024-09-01 06:00:00,1,14.217828783894385,20.871676922734427,65.52872683842298 +2024-09-01 07:00:00,1,20.201025235078873,27.995015815036737,62.39282662481021 +2024-09-01 08:00:00,1,27.59491890937846,24.839787618896096,54.14975849843496 +2024-09-01 09:00:00,1,29.437444112883735,27.913172464677938,64.0571416891652 +2024-09-01 10:00:00,1,19.484873261087827,22.95665597242909,46.02915406305664 +2024-09-01 11:00:00,1,39.8457272890418,21.188519345847556,42.465883027098506 +2024-09-01 12:00:00,1,25.62670787604282,24.965958206596472,56.0007191428581 +2024-09-01 13:00:00,1,28.822217133785575,20.98454054231999,61.24991777122398 +2024-09-01 14:00:00,1,34.505058452062386,21.49513690381233,61.90487015999341 +2024-09-01 15:00:00,1,31.201649018771917,15.179202743970592,63.29547991592053 +2024-09-01 16:00:00,1,24.81854615710424,20.575785935955594,51.71854584836173 +2024-09-01 17:00:00,1,35.24025397583821,24.61806843017376,70.11514637228656 +2024-09-01 18:00:00,1,26.369527249691032,21.228635821174763,38.5437104152753 +2024-09-01 19:00:00,1,37.248844361436596,27.122514821045804,57.6641933358432 +2024-09-01 20:00:00,1,21.41222637075383,27.614652266959624,53.824966368386704 +2024-09-01 21:00:00,1,22.3901825171203,23.44889259965726,54.07599964289359 +2024-09-01 22:00:00,1,33.16586530744534,16.858731673341374,57.52217629329477 +2024-09-01 23:00:00,1,13.484880511419648,23.311727239786705,47.72061904188385 +2024-09-02 00:00:00,1,27.850606464693612,28.07186817305113,65.39318537720011 +2024-09-02 01:00:00,1,33.65489590114168,21.015200272564798,46.47796164746728 +2024-09-02 02:00:00,1,29.26240155545663,32.30930419953772,60.056715007193475 +2024-09-02 03:00:00,1,37.00619651405969,20.908824398226365,55.868403410120784 +2024-09-02 04:00:00,1,40.766034028311225,27.27093896132793,49.43151071251734 +2024-09-02 05:00:00,1,21.45470872531225,16.53883846729644,50.37492990811625 +2024-09-02 06:00:00,1,3.2252029495090824,22.65569971960501,69.82744754925605 +2024-09-02 07:00:00,1,44.35530650111081,21.681547885129824,54.82827106555678 +2024-09-02 08:00:00,1,50.68132431248324,22.79122542057217,57.72510894574002 +2024-09-02 09:00:00,1,27.186644375175085,30.965572979254652,67.6132122467523 +2024-09-02 10:00:00,1,16.424489082652222,30.014117861100317,56.325233861587634 +2024-09-02 11:00:00,1,18.403534086455696,22.061939627079823,42.76201014452578 +2024-09-02 12:00:00,1,24.26511619921017,21.186177383337313,48.27827428032415 +2024-09-02 13:00:00,1,10.888163263698175,26.055012808746355,71.66757543005164 +2024-09-02 14:00:00,1,24.825580205763256,20.49040184283439,57.55314730570377 +2024-09-02 15:00:00,1,19.91998449215923,18.45144379892616,59.798068694753475 +2024-09-02 16:00:00,1,25.797672386600567,22.998456672452402,41.774034988085475 +2024-09-02 17:00:00,1,26.23887942279837,22.669762680511145,40.28303876872735 +2024-09-02 18:00:00,1,22.467611349433795,24.472009893805357,60.19958210123219 +2024-09-02 19:00:00,1,35.65717747167897,20.900238360489304,45.32254433069578 +2024-09-02 20:00:00,1,27.63014578822823,27.842120665221813,65.40365404233052 +2024-09-02 21:00:00,1,17.242501137886265,16.70089480872703,50.4856428396065 +2024-09-02 22:00:00,1,27.79155055767065,21.280844021217227,47.769217654971065 +2024-09-02 23:00:00,1,25.704127761094355,25.3118223559893,47.864117100279294 +2024-09-03 00:00:00,1,32.059590904393424,25.8289108983529,45.45091247995327 +2024-09-03 01:00:00,1,27.63546707295258,24.385159235490406,65.47469551389865 +2024-09-03 02:00:00,1,20.496699517076387,27.284370246199202,59.59158355171083 +2024-09-03 03:00:00,1,31.09499574876147,28.217745798731595,63.598414083289825 +2024-09-03 04:00:00,1,31.110330670911132,21.822212406683335,67.7418241988329 +2024-09-03 05:00:00,1,16.65120260486936,19.981527062526652,42.014396822827464 +2024-09-03 06:00:00,1,47.0447738722699,25.982106862017027,45.46319227991754 +2024-09-03 07:00:00,1,11.499997067534975,23.000858406939457,35.42346081661079 +2024-09-03 08:00:00,1,39.42704984137656,22.855604645708283,60.050132276905565 +2024-09-03 09:00:00,1,28.854221402990134,27.596945117722502,69.16691337667046 +2024-09-03 10:00:00,1,34.286651545275035,25.88180553931687,60.38865060120685 +2024-09-03 11:00:00,1,29.759428432815476,25.60899402374053,44.33212723085593 +2024-09-03 12:00:00,1,36.21102676413119,15.976707064689993,51.55098675228631 +2024-09-03 13:00:00,1,26.322396948736465,22.830823201951723,46.51841854065428 +2024-09-03 14:00:00,1,24.94218051395357,22.554221131399036,65.52410413842497 +2024-09-03 15:00:00,1,21.012036835898932,19.361401253524622,45.65921847164344 +2024-09-03 16:00:00,1,1.5444916849549877,28.88221896315357,72.15529320119944 +2024-09-03 17:00:00,1,23.696821235089935,21.793924383194035,56.85572376467065 +2024-09-03 18:00:00,1,40.85154405318252,22.39600028040347,72.44892492336999 +2024-09-03 19:00:00,1,7.592831943365933,19.035131147438086,50.1572712320954 +2024-09-03 20:00:00,1,24.655704396760562,23.93723935744649,68.09095326814712 +2024-09-03 21:00:00,1,16.575534918230886,24.894704300469975,59.57351216957542 +2024-09-03 22:00:00,1,30.099683250548228,17.267695079096768,50.98675109161651 +2024-09-03 23:00:00,1,18.62438251870559,19.68626198490299,60.68947018729735 +2024-09-04 00:00:00,1,16.634179256361,25.190323710967398,48.552557109547045 +2024-09-04 01:00:00,1,28.21956646749621,27.636627983261363,57.935544752724226 +2024-09-04 02:00:00,1,34.61268753906873,24.267149702945765,67.23683976269334 +2024-09-04 03:00:00,1,12.742621242511438,32.18898814194573,59.06554472252211 +2024-09-04 04:00:00,1,31.20807564813186,24.588191635659676,47.75377178003692 +2024-09-04 05:00:00,1,26.03754843414886,25.779576787316756,67.18732139840478 +2024-09-04 06:00:00,1,27.604406124894176,24.438261444310577,63.40202115242019 +2024-09-04 07:00:00,1,29.05660291314295,21.55030774646632,40.664545525198996 +2024-09-04 08:00:00,1,28.868511994706132,20.601393426905187,48.65124617456307 +2024-09-04 09:00:00,1,33.645852867890994,19.045768803680573,56.2303863444794 +2024-09-04 10:00:00,1,43.41952370454368,17.917984455173972,53.5391304344769 +2024-09-04 11:00:00,1,20.606605111527838,22.280528739243515,68.16490978033474 +2024-09-04 12:00:00,1,18.833819386130727,21.189220125906154,65.25561911422913 +2024-09-04 13:00:00,1,20.841011351873348,27.74877451423237,60.16928626552411 +2024-09-04 14:00:00,1,18.827599897389526,20.247295497050608,57.04876696806859 +2024-09-04 15:00:00,1,38.41510215845858,23.30248432611464,58.58882591676541 +2024-09-04 16:00:00,1,16.34061501781879,20.247025644773135,54.634870620607124 +2024-09-04 17:00:00,1,14.96903244692263,25.763793038380918,72.26977153570786 +2024-09-04 18:00:00,1,16.976208823065022,23.417507428784532,53.05141147574622 +2024-09-04 19:00:00,1,15.224516742068367,20.171322909858524,41.104037795035026 +2024-09-04 20:00:00,1,16.789092659919874,27.801711872825326,56.071048411589906 +2024-09-04 21:00:00,1,26.165671484901203,21.669674047196924,52.94038808991575 +2024-09-04 22:00:00,1,16.750323361202263,16.022611576482237,52.333386634626116 +2024-09-04 23:00:00,1,21.421340073778467,19.688221345607406,62.87443041932816 +2024-09-05 00:00:00,1,33.98827175434155,22.76234502538297,62.12441107005173 +2024-09-05 01:00:00,1,32.0440630742377,22.533978177817147,59.83240916176654 +2024-09-05 02:00:00,1,30.009538654912387,24.292658111822526,49.13924027525364 +2024-09-05 03:00:00,1,23.70295045245908,26.973629791683805,59.91575598264099 +2024-09-05 04:00:00,1,24.406728865062952,27.613806731699718,70.21127693037698 +2024-09-05 05:00:00,1,19.538252197810195,20.55022444477759,61.08518168992678 +2024-09-05 06:00:00,1,2.8149371525032763,19.21357540910577,65.43369973449569 +2024-09-05 07:00:00,1,19.58706315413211,21.008559371342848,54.750101664611144 +2024-09-05 08:00:00,1,20.20711650639673,28.98955624666363,72.34863326352954 +2024-09-05 09:00:00,1,29.254779407294738,25.786113810857316,52.81942145760198 +2024-09-05 10:00:00,1,18.274425853390078,22.150605509369225,66.60155843128662 +2024-09-05 11:00:00,1,26.76657530040778,31.87260758829656,51.55641898490201 +2024-09-05 12:00:00,1,19.965367672024293,24.403187369724133,50.30826054548102 +2024-09-05 13:00:00,1,9.251890135573161,25.583410926662058,54.16251872686668 +2024-09-05 14:00:00,1,40.942088987156076,28.918843928120296,63.813373028127 +2024-09-05 15:00:00,1,13.792072431247,26.40986350564373,69.7766942333169 +2024-09-05 16:00:00,1,16.323771654429315,25.196511388361593,58.64946951947928 +2024-09-05 17:00:00,1,19.472639925932388,25.730972041664987,66.14456911815788 +2024-09-05 18:00:00,1,25.48466532780433,18.370347431920294,64.59737738812679 +2024-09-05 19:00:00,1,24.07828493681274,23.28602304221484,50.85590274614671 +2024-09-05 20:00:00,1,21.553788722552806,20.229940385844536,45.80021912884848 +2024-09-05 21:00:00,1,22.49008794084177,25.536016799559498,68.11823758698692 +2024-09-05 22:00:00,1,22.064115860285792,17.18497833388399,65.04573885824988 +2024-09-05 23:00:00,1,21.92997354663933,24.505995482513235,61.545270033148334 +2024-09-06 00:00:00,1,38.79928633259084,30.132208254337897,49.616820722405386 +2024-09-06 01:00:00,1,38.25500210496216,25.0077308976032,54.055750414149905 +2024-09-06 02:00:00,1,37.65969947459419,26.536621966029266,40.50668464301952 +2024-09-06 03:00:00,1,26.258681456728723,27.081337319264694,75.81256060002781 +2024-09-06 04:00:00,1,29.599766994370867,25.770481260491284,54.659281366079526 +2024-09-06 05:00:00,1,25.82430895102564,16.41431376597074,76.12471511221305 +2024-09-06 06:00:00,1,30.185231023771326,15.688969273227197,44.00016652381693 +2024-09-06 07:00:00,1,29.26616369905006,20.980845350027263,59.74874696350926 +2024-09-06 08:00:00,1,18.263369087034384,23.91825296303199,78.12828019198685 +2024-09-06 09:00:00,1,21.19644658343283,21.619657862597933,73.41002745975847 +2024-09-06 10:00:00,1,12.984114474635716,22.283080946798364,51.47157463781193 +2024-09-06 11:00:00,1,9.85301488413537,27.045786206146747,55.12714511998582 +2024-09-06 12:00:00,1,18.811398049166662,26.072243839010078,46.91310411415296 +2024-09-06 13:00:00,1,12.798237374410142,23.892132550372935,54.26465985285767 +2024-09-06 14:00:00,1,27.188755893972225,27.98688636413574,57.96693966670056 +2024-09-06 15:00:00,1,19.940316982576277,24.329768015186897,69.1634218309138 +2024-09-06 16:00:00,1,16.226145836013117,28.196982788484867,64.20970408472519 +2024-09-06 17:00:00,1,15.446995107387421,21.667703423381866,56.24315152819787 +2024-09-06 18:00:00,1,30.745480738243256,23.711323375235114,71.45626081110055 +2024-09-06 19:00:00,1,18.253610629536443,26.476805257838308,66.07814718155966 +2024-09-06 20:00:00,1,23.785094252463175,29.20491009270557,71.966366220071 +2024-09-06 21:00:00,1,28.92102458993123,17.61981334507885,48.14541056694261 +2024-09-06 22:00:00,1,28.258528580455213,16.31199471519929,59.16644406689751 +2024-09-06 23:00:00,1,7.684947604604311,20.253457546226386,59.07826941806848 +2024-09-07 00:00:00,1,19.849215639425076,17.283203563258365,63.98254326302932 +2024-09-07 01:00:00,1,19.863211356136972,26.12190559277712,49.48174560731844 +2024-09-07 02:00:00,1,21.798866122939078,20.55255094279629,77.39618052187551 +2024-09-07 03:00:00,1,21.845861933093524,27.086527178448826,60.38248707326036 +2024-09-07 04:00:00,1,17.99275958714689,25.02177579320014,43.50373642483902 +2024-09-07 05:00:00,1,23.294212703428236,25.630418630671656,51.40684618999777 +2024-09-07 06:00:00,1,29.441599597107096,24.06455181633903,77.27901426118028 +2024-09-07 07:00:00,1,38.23384393409676,21.49805865002704,51.24272334513691 +2024-09-07 08:00:00,1,33.94169589752658,23.80012080973076,37.729149326530845 +2024-09-07 09:00:00,1,37.73556136926999,24.779533772048076,67.30069181347547 +2024-09-07 10:00:00,1,9.535343415080423,24.156335295662686,50.21491468093081 +2024-09-07 11:00:00,1,35.40881645708281,25.035873789235147,63.22018740250364 +2024-09-07 12:00:00,1,9.534491009752216,23.089888997775166,48.14247027574974 +2024-09-07 13:00:00,1,9.339353494252574,19.65386453716438,51.89727291312447 +2024-09-07 14:00:00,1,13.886520848927162,29.32122518716345,54.49653717024697 +2024-09-07 15:00:00,1,47.49401194931577,18.53978270700276,57.74965411353277 +2024-09-07 16:00:00,1,25.70698468401829,26.176112765774683,60.380094936997615 +2024-09-07 17:00:00,1,21.555248951166405,31.07272381471993,56.92523256521042 +2024-09-07 18:00:00,1,25.164257934237952,21.27322623970324,71.02347475574861 +2024-09-07 19:00:00,1,32.95270721687166,26.512186619806368,50.66516642934984 +2024-09-07 20:00:00,1,21.291311261688115,24.238418039401655,53.94077737601252 +2024-09-07 21:00:00,1,16.86464661808619,26.288810374454023,54.37562026824225 +2024-09-07 22:00:00,1,2.1775439533113996,26.57606321649981,46.398867026155266 +2024-09-07 23:00:00,1,17.98052629858022,20.508321544749904,39.417030491675234 +2024-09-08 00:00:00,1,22.92666786696594,22.354422150099754,62.46771639415575 +2024-09-08 01:00:00,1,39.90230663838891,25.93754996755624,64.5822868153658 +2024-09-08 02:00:00,1,29.05983177047679,29.405784993047412,62.63691815346665 +2024-09-08 03:00:00,1,33.922543892967624,23.35666605706312,48.49136996605942 +2024-09-08 04:00:00,1,31.447741265635912,18.88698970209967,57.000554349080524 +2024-09-08 05:00:00,1,24.22334851748583,23.42892456206201,71.02810484542081 +2024-09-08 06:00:00,1,15.257616232681093,24.929367705998448,60.539633900051 +2024-09-08 07:00:00,1,15.261872960155268,18.374623450798317,64.26561386493108 +2024-09-08 08:00:00,1,22.06816395189581,19.306307857507182,46.403034712822596 +2024-09-08 09:00:00,1,14.203973601353649,24.140083356436158,44.40478466581612 +2024-09-08 10:00:00,1,20.84708772227149,25.394589403131082,69.09267760285461 +2024-09-08 11:00:00,1,31.898078935219495,16.96548780819974,67.99438760766228 +2024-09-08 12:00:00,1,19.29101185417114,25.82371268265237,55.70621509960838 +2024-09-08 13:00:00,1,18.508020069998093,19.940241462613898,51.29399655243277 +2024-09-08 14:00:00,1,26.654090240391618,25.36611509581357,38.2936439612206 +2024-09-08 15:00:00,1,25.13676279764244,25.34206398369654,47.0337231853366 +2024-09-08 16:00:00,1,16.912216663641725,21.58322127715238,49.5876118062903 +2024-09-08 17:00:00,1,16.674775071989973,23.508445086341403,51.92489026488604 +2024-09-08 18:00:00,1,20.53690431019524,25.9615349175418,64.74229757767354 +2024-09-08 19:00:00,1,14.890955561914293,22.71000418659076,64.06458384572959 +2024-09-08 20:00:00,1,17.998877816004565,21.620507343632866,75.91436716171472 +2024-09-08 21:00:00,1,29.37103542147295,22.034421048014856,52.986394704560155 +2024-09-08 22:00:00,1,29.76526654704091,25.139246433128633,49.222034027575695 +2024-09-08 23:00:00,1,26.1398880196603,17.15687713177575,67.44615699297344 +2024-09-09 00:00:00,1,25.427366805819663,21.773738933069613,60.159755927391 +2024-09-09 01:00:00,1,28.372024342228134,19.09046892209158,67.9713674090967 +2024-09-09 02:00:00,1,38.47860206435507,23.337918911937283,61.679964132864995 +2024-09-09 03:00:00,1,18.281096832408288,19.565832448581595,41.81232399694568 +2024-09-09 04:00:00,1,34.01094086106505,26.830560775841647,60.16707951996497 +2024-09-09 05:00:00,1,22.051259364177923,25.84056653866426,52.1889029731623 +2024-09-09 06:00:00,1,37.57816526893413,19.906475530787716,66.92402667417855 +2024-09-09 07:00:00,1,18.868223885466755,22.691035387212867,69.82758237229697 +2024-09-09 08:00:00,1,20.438643611492463,18.964478282771964,50.62525485178534 +2024-09-09 09:00:00,1,14.001324786021966,25.17208584140558,42.258174040716 +2024-09-09 10:00:00,1,17.25879503546748,22.943171046134445,59.1755213031328 +2024-09-09 11:00:00,1,44.210284168977786,23.814702982964253,47.27879978534446 +2024-09-09 12:00:00,1,21.930507903576185,20.767522495876737,53.551054194321935 +2024-09-09 13:00:00,1,39.401215656450056,16.545635114828485,44.23689149586457 +2024-09-09 14:00:00,1,30.624141824310534,25.246264631784133,61.35169924622886 +2024-09-09 15:00:00,1,27.422808224987065,20.79305810256979,46.503009023186486 +2024-09-09 16:00:00,1,23.985266915934197,24.486543291376112,53.83606435590397 +2024-09-09 17:00:00,1,27.36551354469963,29.715691250829536,55.780339944725625 +2024-09-09 18:00:00,1,20.60162345910487,25.424621081605213,48.210890447049 +2024-09-09 19:00:00,1,13.95088795453217,20.41165207064247,56.02518773000946 +2024-09-09 20:00:00,1,15.704381323284492,29.077395517594773,70.56431628626696 +2024-09-09 21:00:00,1,20.687955239791297,27.767579450751136,60.69899571624785 +2024-09-09 22:00:00,1,24.279850181584333,20.390671814140305,39.45210771508617 +2024-09-09 23:00:00,1,18.657132905570094,23.09347440797225,51.7937200986275 +2024-09-10 00:00:00,1,21.846504716776707,31.025789319578234,55.46486715793034 +2024-09-10 01:00:00,1,33.43617672720588,20.57788811465775,47.82400915804542 +2024-09-10 02:00:00,1,7.6563160799969125,27.43582108908634,60.34568824204847 +2024-09-10 03:00:00,1,20.847605683643422,18.594405313725055,67.07761070167842 +2024-09-10 04:00:00,1,23.860825467006826,25.993924549636503,55.80946197528201 +2024-09-10 05:00:00,1,31.50798012051852,20.558695824675016,51.37617398529984 +2024-09-10 06:00:00,1,39.467777629389474,30.7508326386571,63.490005813397616 +2024-09-10 07:00:00,1,20.879892161541,26.203989914571704,51.156267473782364 +2024-09-10 08:00:00,1,16.88972372080633,27.701399635197575,43.15858708254454 +2024-09-10 09:00:00,1,27.323761178309162,25.313800163353246,50.603631167724444 +2024-09-10 10:00:00,1,18.524273566370123,24.20193883575836,52.9479578021174 +2024-09-10 11:00:00,1,7.208964897328126,22.59334760594941,62.57470803557663 +2024-09-10 12:00:00,1,13.044756532780548,22.040211898076723,34.041384700199096 +2024-09-10 13:00:00,1,14.106537570643091,27.833427796162674,60.28194775164973 +2024-09-10 14:00:00,1,23.211437554164398,26.877337005859918,46.414944499428856 +2024-09-10 15:00:00,1,11.94084710535454,22.013796819942378,41.20337927311344 +2024-09-10 16:00:00,1,30.109686709491704,25.356339663122192,49.514453060969814 +2024-09-10 17:00:00,1,25.87716649313176,22.79289670826073,64.43661186756765 +2024-09-10 18:00:00,1,13.671982309529561,21.60283588060643,74.98898470503148 +2024-09-10 19:00:00,1,35.46497275988128,30.2629262236502,60.01725324068222 +2024-09-10 20:00:00,1,32.93271399650724,23.550348949436525,46.41122072369185 +2024-09-10 21:00:00,1,24.617643906858856,21.382127853245798,48.919605831979176 +2024-09-10 22:00:00,1,30.498198944300867,23.360447719425295,43.26333665012884 +2024-09-10 23:00:00,1,18.21156895155204,24.01065386808842,65.24724922747976 +2024-09-11 00:00:00,1,43.630129704452784,28.887694616130823,63.57855617548916 +2024-09-11 01:00:00,1,32.236970875900624,25.497538640310093,45.98965097928834 +2024-09-11 02:00:00,1,20.82184364541531,17.611237141099373,56.13181868023311 +2024-09-11 03:00:00,1,32.12529396106254,25.83352944276831,42.57193252555954 +2024-09-11 04:00:00,1,36.85833284994794,19.33982588040038,57.114206574798715 +2024-09-11 05:00:00,1,20.708289977488064,27.326724174440635,58.34992081236141 +2024-09-11 06:00:00,1,15.957729179290366,21.107146502669558,45.12528032147128 +2024-09-11 07:00:00,1,24.443051488538227,21.888245147050327,51.10237684321822 +2024-09-11 08:00:00,1,21.529154737242777,19.18240391558581,40.80378890413046 +2024-09-11 09:00:00,1,24.62623529457405,21.97548396682345,56.8195158525269 +2024-09-11 10:00:00,1,17.14473302218501,20.8382253913222,60.0235956385619 +2024-09-11 11:00:00,1,18.908018232976048,23.68866401077555,55.50920935184009 +2024-09-11 12:00:00,1,21.96899513850905,26.027367507077447,42.66832443159295 +2024-09-11 13:00:00,1,24.741310310338836,21.02616135507066,59.66978508717799 +2024-09-11 14:00:00,1,27.37222547743783,27.27006903388241,55.399909786433234 +2024-09-11 15:00:00,1,30.530643968663142,24.26483140772083,53.190526710979434 +2024-09-11 16:00:00,1,22.320982637514973,24.422489158394555,38.352400971517994 +2024-09-11 17:00:00,1,38.70518078520202,26.242032995009133,44.524849915230845 +2024-09-11 18:00:00,1,19.43010549366983,27.151394225623918,33.04791478056406 +2024-09-11 19:00:00,1,23.217804768101544,23.789394935159102,53.05510727558917 +2024-09-11 20:00:00,1,21.513395756762687,21.004602333032114,64.05821518809616 +2024-09-11 21:00:00,1,31.256226088073117,27.655333184941693,51.76330997487393 +2024-09-11 22:00:00,1,20.146663113566994,23.04436332550553,74.8151456058296 +2024-09-11 23:00:00,1,35.15027274097837,19.545567157525785,73.65045555747857 +2024-09-12 00:00:00,1,24.836349111995368,27.20338545523556,55.76309349057809 +2024-09-12 01:00:00,1,24.947086610040362,25.778783969345522,43.09270595673285 +2024-09-12 02:00:00,1,31.781291488420727,21.18814865223763,68.96916194717951 +2024-09-12 03:00:00,1,0.0,28.77663510181965,54.72301839712038 +2024-09-12 04:00:00,1,19.610921830314815,20.837385443687673,64.94892618430463 +2024-09-12 05:00:00,1,32.508566224047925,30.254521678400277,58.49263175005236 +2024-09-12 06:00:00,1,20.058393553986313,25.50753688833484,41.42797449136863 +2024-09-12 07:00:00,1,22.417479938133702,23.529286188924676,60.070295575080934 +2024-09-12 08:00:00,1,28.64499888334235,24.636433830858895,64.7411133154119 +2024-09-12 09:00:00,1,24.718477505695944,25.27430091971297,58.073424478323545 +2024-09-12 10:00:00,1,20.23878299715345,20.639797018342147,54.686244443960334 +2024-09-12 11:00:00,1,19.211880936603695,27.582066188236602,58.77708465236251 +2024-09-12 12:00:00,1,33.03020043683351,26.740907186329054,61.058222348772325 +2024-09-12 13:00:00,1,12.739819345514325,26.805063863267325,56.80487726807374 +2024-09-12 14:00:00,1,23.046467673141052,31.795776187526503,52.49486223692125 +2024-09-12 15:00:00,1,17.024745223109676,22.585508380937796,43.59831171435497 +2024-09-12 16:00:00,1,28.705353675080353,20.45958219403399,55.97609974345956 +2024-09-12 17:00:00,1,23.98300643641024,19.464711566538718,69.5085330984326 +2024-09-12 18:00:00,1,31.818493337222225,24.46991636494243,51.53201252145108 +2024-09-12 19:00:00,1,19.87133664612835,23.833903942380775,69.07369688708947 +2024-09-12 20:00:00,1,33.27216728460235,24.22612954218653,57.30893626202649 +2024-09-12 21:00:00,1,24.088048492778544,17.851892568946116,36.74840744601824 +2024-09-12 22:00:00,1,24.35588650948006,23.69661021853108,55.02013125058214 +2024-09-12 23:00:00,1,23.679151247710724,23.20033059855742,49.921107708223026 +2024-09-13 00:00:00,1,32.522428323961314,29.68251419333596,42.92006543524039 +2024-09-13 01:00:00,1,14.333766830977408,16.192147836168914,46.277410847885704 +2024-09-13 02:00:00,1,34.949993624067986,23.240775152767508,42.24483574351043 +2024-09-13 03:00:00,1,18.82134000668903,23.007313232031155,46.512131181288495 +2024-09-13 04:00:00,1,12.255629880396882,21.975207612145716,45.44998125122397 +2024-09-13 05:00:00,1,37.068892031500695,23.719671042507994,55.123320752222966 +2024-09-13 06:00:00,1,28.957771116305537,25.929219669104757,44.14975607224942 +2024-09-13 07:00:00,1,24.590273248376477,17.65642323803707,62.262942899090405 +2024-09-13 08:00:00,1,26.13787468141618,22.57597171357633,61.45471554669248 +2024-09-13 09:00:00,1,13.928819373473159,26.503233851784213,62.39129524975952 +2024-09-13 10:00:00,1,20.708995320845368,18.850134340598657,59.43644763288118 +2024-09-13 11:00:00,1,28.705273142047595,25.070929666022277,53.18742111977781 +2024-09-13 12:00:00,1,3.989874745496781,24.40152584089891,65.11293191764021 +2024-09-13 13:00:00,1,27.4639180957095,14.367768107647871,57.086509050183096 +2024-09-13 14:00:00,1,16.796768038419597,22.199053712274218,45.248641936526 +2024-09-13 15:00:00,1,20.1135130100686,19.602988982495624,39.14748942722812 +2024-09-13 16:00:00,1,28.326432322069937,27.453364617966237,59.22880869923608 +2024-09-13 17:00:00,1,34.48078967947841,23.361990990874958,52.45065735149577 +2024-09-13 18:00:00,1,34.616244121033176,19.796757105421705,48.151511760982125 +2024-09-13 19:00:00,1,11.743100515690985,18.772929842886427,59.41296427065821 +2024-09-13 20:00:00,1,26.510523262739984,21.724179786572126,34.39954027101536 +2024-09-13 21:00:00,1,32.35212254310415,23.128477263982315,57.70072875776403 +2024-09-13 22:00:00,1,25.170786567775586,17.798107336376656,61.60294824617456 +2024-09-13 23:00:00,1,15.668293123285999,20.76924248054434,60.01004184707829 +2024-09-14 00:00:00,1,24.40372152367916,25.071072408184982,70.83084465336906 +2024-09-14 01:00:00,1,24.47678577113259,26.27247553608131,49.633699697441656 +2024-09-14 02:00:00,1,32.69446476925995,28.734216537422608,52.304395870413664 +2024-09-14 03:00:00,1,31.74347067649831,26.532404565520157,27.697780635938713 +2024-09-14 04:00:00,1,31.719059337705936,20.163301884458544,61.29798567634161 +2024-09-14 05:00:00,1,21.10521482233564,22.037290810869393,51.609821090719194 +2024-09-14 06:00:00,1,24.166954705318382,24.873147913527454,56.54058154149571 +2024-09-14 07:00:00,1,34.756986084137104,23.73251073241458,45.47613782659101 +2024-09-14 08:00:00,1,10.329301460653054,29.562313827019807,37.05684703962116 +2024-09-14 09:00:00,1,17.138944562072794,26.55030936364374,43.27445488535184 +2024-09-14 10:00:00,1,2.623672514698697,23.56580686900123,59.9609973134216 +2024-09-14 11:00:00,1,39.73937936408909,24.535759014500393,54.295901553481656 +2024-09-14 12:00:00,1,21.224574866853573,27.90895089766061,60.53744083656746 +2024-09-14 13:00:00,1,32.509329223923096,18.14244336210702,40.851833549814586 +2024-09-14 14:00:00,1,27.78178115097799,28.493861178541216,55.23079345691572 +2024-09-14 15:00:00,1,30.05517008354231,24.117216887927544,59.060129832710146 +2024-09-14 16:00:00,1,27.238062096965354,19.680800008371403,42.318331063727975 +2024-09-14 17:00:00,1,30.343970868867142,18.494270289664605,57.385064252520976 +2024-09-14 18:00:00,1,16.026296537351207,29.432503875034367,29.068325061968835 +2024-09-14 19:00:00,1,22.552158889673475,26.10983365888202,46.15742094923946 +2024-09-14 20:00:00,1,31.859005363175356,22.257680965581063,64.55177955570036 +2024-09-14 21:00:00,1,30.160054681975744,19.032436167109893,52.68604621574761 +2024-09-14 22:00:00,1,31.97191336007586,22.811000153556595,53.50445072589605 +2024-09-14 23:00:00,1,18.328856893655,28.043427265650617,61.251141679630315 +2024-09-15 00:00:00,1,30.46886094386172,25.78887040491041,66.49983718937904 +2024-09-15 01:00:00,1,22.387126200718445,26.853689435532534,48.15287696241657 +2024-09-15 02:00:00,1,26.59110571689149,22.033034963007328,45.980892372147565 +2024-09-15 03:00:00,1,34.94378636167217,25.250905864574435,41.735781815014775 +2024-09-15 04:00:00,1,30.071078173025654,19.560248161835148,60.512696531097674 +2024-09-15 05:00:00,1,27.98368948033366,18.753113649909697,54.491767877063 +2024-09-15 06:00:00,1,19.54354220171819,31.332373646922996,58.181416823524515 +2024-09-15 07:00:00,1,20.482664238860572,20.280660261244556,52.48124122205998 +2024-09-15 08:00:00,1,27.33885440684528,21.815744952158674,47.58744870060216 +2024-09-15 09:00:00,1,23.74876941613814,18.66823226158991,52.66782415641154 +2024-09-15 10:00:00,1,8.658001962673183,19.59697009904305,54.75137206461779 +2024-09-15 11:00:00,1,31.539013036350347,27.59521346400513,57.48684772125758 +2024-09-15 12:00:00,1,20.81982469970886,21.143532813211873,55.38682758804191 +2024-09-15 13:00:00,1,38.72417825054377,22.004513414556826,55.50687027408623 +2024-09-15 14:00:00,1,30.97203189637141,23.58915192954692,66.05775652011779 +2024-09-15 15:00:00,1,20.53752038416174,19.761438979670004,55.35824014077321 +2024-09-15 16:00:00,1,27.67847454889006,27.805866384283867,59.88181424629692 +2024-09-15 17:00:00,1,24.27879765915006,25.011125919079195,39.41055213423858 +2024-09-15 18:00:00,1,21.117096962133335,20.16080906633063,66.86200148108054 +2024-09-15 19:00:00,1,40.18193213251487,20.609920149222994,48.45996034034886 +2024-09-15 20:00:00,1,36.62077778329679,19.933849137643303,48.08751873658557 +2024-09-15 21:00:00,1,30.012246522202595,23.159996778240984,43.104011079001 +2024-09-15 22:00:00,1,21.475341802528295,22.857896197202376,57.253021864704024 +2024-09-15 23:00:00,1,28.90655202798421,20.620137886859656,48.49796379508934 +2024-09-16 00:00:00,1,23.25110636222958,18.98433739593171,54.50050596999968 +2024-09-16 01:00:00,1,25.011717777475592,27.22089405632201,53.72481809448704 +2024-09-16 02:00:00,1,20.792778783418925,27.22575384381847,70.88569775600477 +2024-09-16 03:00:00,1,22.2762704657792,25.39190605170254,49.05848834729471 +2024-09-16 04:00:00,1,11.702055970924347,25.329331820425704,56.050537277591296 +2024-09-16 05:00:00,1,27.448192415684957,26.43127839176431,56.061826542996464 +2024-09-16 06:00:00,1,25.24686293724246,22.675816035803383,51.178473030236034 +2024-09-16 07:00:00,1,21.357041617426322,20.34946367352358,51.069417012938295 +2024-09-16 08:00:00,1,22.423465502352986,21.0904424349432,55.38471647436356 +2024-09-16 09:00:00,1,27.75816993591521,22.23279331246381,45.54657050614208 +2024-09-16 10:00:00,1,22.458175898621462,17.596968288940612,61.15490368765926 +2024-09-16 11:00:00,1,16.638158294960242,21.58612252397063,60.719899691795675 +2024-09-16 12:00:00,1,31.64818716192023,22.712993430045685,77.3117601523658 +2024-09-16 13:00:00,1,23.72106646698274,23.307920876412833,52.1441966710876 +2024-09-16 14:00:00,1,18.309666177052588,22.824390184360745,55.52184207535966 +2024-09-16 15:00:00,1,20.477410422418295,20.96422977789777,50.58858570718801 +2024-09-16 16:00:00,1,21.139920418904612,23.88924193067982,70.62198234515779 +2024-09-16 17:00:00,1,23.784243590063337,21.717295888748666,47.52128654244279 +2024-09-16 18:00:00,1,22.657914296574788,19.53117240171075,36.591487368240024 +2024-09-16 19:00:00,1,20.053887248547042,18.948117268814833,77.33189765942231 +2024-09-16 20:00:00,1,43.88410958659652,23.625574020556925,57.53698793588256 +2024-09-16 21:00:00,1,42.361377716885016,25.29188854806064,58.58789942626428 +2024-09-16 22:00:00,1,18.294376396122054,25.852507499023286,57.9443542494771 +2024-09-16 23:00:00,1,27.776571469960082,21.201631157881845,60.08483835895782 +2024-09-17 00:00:00,1,29.429401576019753,23.391844202988285,46.5445585066228 +2024-09-17 01:00:00,1,43.01852697222783,24.826792152864982,51.43532423786845 +2024-09-17 02:00:00,1,19.365658078179617,27.75613561568844,42.29573260813633 +2024-09-17 03:00:00,1,31.25799776435337,26.739334704019598,53.614952471572465 +2024-09-17 04:00:00,1,29.471138531497505,25.485041948948883,50.9186086730721 +2024-09-17 05:00:00,1,38.90072545323797,30.391786531953564,69.72963446726767 +2024-09-17 06:00:00,1,25.918504546383254,19.90097489639463,57.161258822996125 +2024-09-17 07:00:00,1,18.06134653659176,28.31046690302423,54.93701213646197 +2024-09-17 08:00:00,1,25.84468089245335,19.338923257165863,37.574167818267256 +2024-09-17 09:00:00,1,29.82545847203105,25.688964901964255,62.45742521170499 +2024-09-17 10:00:00,1,31.21281928711791,23.099848268094956,43.15329702764738 +2024-09-17 11:00:00,1,9.951041990879498,22.542370057057,69.79407448689085 +2024-09-17 12:00:00,1,31.978524811922085,17.250461249821154,59.75234529705524 +2024-09-17 13:00:00,1,5.613055920230778,20.422726310129416,67.23458052122015 +2024-09-17 14:00:00,1,14.150564853690781,21.5086488723078,61.10411451302244 +2024-09-17 15:00:00,1,33.602628170215,25.860214193242587,57.49299532977223 +2024-09-17 16:00:00,1,21.335054716396467,21.87171423796561,58.45725239889329 +2024-09-17 17:00:00,1,28.289030389570748,22.564175313536023,66.2767874480831 +2024-09-17 18:00:00,1,22.876410135952522,19.444998016723,52.01004913608017 +2024-09-17 19:00:00,1,14.633065738719134,19.407452808664484,51.33315406502642 +2024-09-17 20:00:00,1,28.45604834665951,22.243244731256862,66.2589799002439 +2024-09-17 21:00:00,1,28.998893504687747,22.38906431079439,39.854251751537575 +2024-09-17 22:00:00,1,16.14678594175909,22.569515896194538,50.873994570166246 +2024-09-17 23:00:00,1,29.974287451229596,23.527935083800664,59.865998013007356 +2024-09-18 00:00:00,1,21.952041489125648,19.5258967924763,59.163406171683654 +2024-09-18 01:00:00,1,35.03021750872624,25.210114261392267,56.29467206378541 +2024-09-18 02:00:00,1,42.48699862381458,28.13445520624118,75.44161125290573 +2024-09-18 03:00:00,1,19.406801247164022,23.775602027379144,58.681709969040995 +2024-09-18 04:00:00,1,24.349392071896318,15.830285047880588,49.379278697809646 +2024-09-18 05:00:00,1,25.545482879791578,20.69363744124414,62.70026846560688 +2024-09-18 06:00:00,1,21.226818200914174,24.494564371705096,54.12946403124057 +2024-09-18 07:00:00,1,22.512753736741814,26.178959512426793,46.351226820369334 +2024-09-18 08:00:00,1,14.75138626894092,25.25977225879724,69.18237209286752 +2024-09-18 09:00:00,1,20.549607200573277,21.899415225531442,57.52133437284443 +2024-09-18 10:00:00,1,22.348622659536932,25.82797401321022,50.740085036849855 +2024-09-18 11:00:00,1,21.933567320468782,18.800814250726436,54.06268112877158 +2024-09-18 12:00:00,1,26.20709979759728,17.49560402386399,45.20301155565857 +2024-09-18 13:00:00,1,30.061578949586202,29.891514300203998,77.28324937890795 +2024-09-18 14:00:00,1,35.98726946980772,21.971053335009838,64.11997474303033 +2024-09-18 15:00:00,1,41.573920588051166,30.228903240582497,57.84787932542971 +2024-09-18 16:00:00,1,24.550215641940834,23.894503655642932,66.39097323190413 +2024-09-18 17:00:00,1,36.29893971278181,27.70498236625668,59.7033450882461 +2024-09-18 18:00:00,1,31.792597442952903,24.106269616187834,49.65228593554488 +2024-09-18 19:00:00,1,28.243987899157645,27.72261375386064,55.47726740488533 +2024-09-18 20:00:00,1,35.84797382341873,22.166458028854567,66.64465365990108 +2024-09-18 21:00:00,1,28.72079660850627,17.26779828568804,54.750205217984764 +2024-09-18 22:00:00,1,17.352286194464806,19.594413772530732,65.47308498829567 +2024-09-18 23:00:00,1,29.241918374322587,23.08133431740542,55.40166162259166 +2024-09-19 00:00:00,1,10.936128587184612,29.36810287489788,59.94134056496358 +2024-09-19 01:00:00,1,36.78110021995421,23.278581602509966,60.482745625454214 +2024-09-19 02:00:00,1,20.87175646025739,29.098438929052296,41.528097754512004 +2024-09-19 03:00:00,1,32.43582954044137,23.15131591121525,66.22432527455298 +2024-09-19 04:00:00,1,32.906994722422276,25.856661936091324,66.88691171993872 +2024-09-19 05:00:00,1,34.19279309483619,21.910835158282516,48.31967393761578 +2024-09-19 06:00:00,1,35.682195195303336,17.032539585454128,65.08004515341553 +2024-09-19 07:00:00,1,35.02047488315508,28.715744289165873,40.9275747992365 +2024-09-19 08:00:00,1,21.530495332646073,22.021875803147548,73.92259360560735 +2024-09-19 09:00:00,1,32.133269007486845,18.906794282949214,56.2650220480866 +2024-09-19 10:00:00,1,26.86547859125749,21.855083276190324,53.50022776874919 +2024-09-19 11:00:00,1,27.438938972896754,16.886837920200083,50.39995666028248 +2024-09-19 12:00:00,1,22.607657727389224,26.19393354461579,56.83332064262696 +2024-09-19 13:00:00,1,23.296542649131045,29.37406044632969,43.377843913688956 +2024-09-19 14:00:00,1,23.09063398435972,30.53907424353902,59.706658719591694 +2024-09-19 15:00:00,1,29.385828290722834,21.941802244682442,54.89195707761334 +2024-09-19 16:00:00,1,12.768178349342401,25.324067697566488,42.8885272632417 +2024-09-19 17:00:00,1,17.207041572990892,27.404148200577072,57.300148505945664 +2024-09-19 18:00:00,1,26.887444033211484,22.97227575393492,63.92378332903092 +2024-09-19 19:00:00,1,31.031417124074736,24.8012408321304,40.05284182888215 +2024-09-19 20:00:00,1,15.710799857133223,24.39667754187318,67.68861062847392 +2024-09-19 21:00:00,1,18.384102330508682,21.758332076610653,66.97610405556995 +2024-09-19 22:00:00,1,28.675411666491584,20.24048942303929,66.21470486671139 +2024-09-19 23:00:00,1,25.363873367750475,19.096501602168033,63.680531614979245 +2024-09-20 00:00:00,1,29.952507794425202,25.589534190821634,47.3416810699612 +2024-09-20 01:00:00,1,18.862843922055063,22.02452235143561,44.77714118231803 +2024-09-20 02:00:00,1,28.66870959239874,29.118860188924067,46.85676041389969 +2024-09-20 03:00:00,1,28.399510623128506,22.03739346308593,54.591712134653264 +2024-09-20 04:00:00,1,18.33385588491778,22.62429933168812,66.23621045040474 +2024-09-20 05:00:00,1,34.01757251198792,29.286394446463653,67.70826596538694 +2024-09-20 06:00:00,1,31.302666004594993,21.044086133442047,53.98561562775044 +2024-09-20 07:00:00,1,33.02751193172666,16.799656045650956,48.27441562147439 +2024-09-20 08:00:00,1,27.924485959888493,21.632371163684063,43.385846896391726 +2024-09-20 09:00:00,1,35.03519421402765,19.1205305343072,49.94054959434318 +2024-09-20 10:00:00,1,33.259251502093605,24.744655321755715,59.36667244798451 +2024-09-20 11:00:00,1,29.242806693302686,22.49361796199981,57.28843443007159 +2024-09-20 12:00:00,1,26.150298224691376,26.20462703304002,71.18592213413346 +2024-09-20 13:00:00,1,25.635278379551515,22.012701618824366,49.38749893395377 +2024-09-20 14:00:00,1,19.732859809394814,25.807603716488956,65.87941470216997 +2024-09-20 15:00:00,1,10.744493450770863,26.8541645008899,46.55637735807376 +2024-09-20 16:00:00,1,23.363604613808114,30.39929872515874,44.65861816070529 +2024-09-20 17:00:00,1,25.533890774071516,21.316495981552503,45.063677201243884 +2024-09-20 18:00:00,1,20.364353423973995,21.76950826196468,58.58613053530233 +2024-09-20 19:00:00,1,19.324322003656725,20.17504567156115,60.4097036972194 +2024-09-20 20:00:00,1,23.107277275645117,25.081779500262762,48.586416435013504 +2024-09-20 21:00:00,1,19.547596222116383,21.689818034260057,46.412922705899476 +2024-09-20 22:00:00,1,24.822748529000986,22.44017066238764,54.65681574317086 +2024-09-20 23:00:00,1,23.86053543833392,14.156455980168023,47.77704311353251 +2024-09-21 00:00:00,1,25.27166726021011,24.027180838838696,57.402734573002526 +2024-09-21 01:00:00,1,37.52840348436529,27.179001865345782,68.61824928831656 +2024-09-21 02:00:00,1,16.155206442419072,27.009766619056556,60.92561545948214 +2024-09-21 03:00:00,1,29.614721340010263,15.33732290043409,43.324236512687634 +2024-09-21 04:00:00,1,30.987184998343576,23.47309751993782,69.2405366327159 +2024-09-21 05:00:00,1,16.913043990489314,20.110237406685123,63.10186105002552 +2024-09-21 06:00:00,1,38.15560418987617,22.614607480595712,44.28282343759539 +2024-09-21 07:00:00,1,18.099373944371383,22.27912075541026,49.720172783337304 +2024-09-21 08:00:00,1,34.03426219917372,24.746811311724866,46.20877194895541 +2024-09-21 09:00:00,1,20.618871287079312,27.522496108429717,31.24255361578012 +2024-09-21 10:00:00,1,12.878194255922038,22.60352616362792,57.058541058561936 +2024-09-21 11:00:00,1,16.741007754133566,23.824181053936638,46.20700305441182 +2024-09-21 12:00:00,1,19.584528120203906,25.593533176079294,59.9440785177933 +2024-09-21 13:00:00,1,36.37936131554673,25.38490367269503,62.99514567021263 +2024-09-21 14:00:00,1,28.92546315641185,20.779914733131584,54.00844604519515 +2024-09-21 15:00:00,1,22.888651847108157,24.940261808730387,54.20317686463039 +2024-09-21 16:00:00,1,22.684894466871214,24.248229081921245,70.6884610682818 +2024-09-21 17:00:00,1,16.74266583772748,18.87211359910679,48.95458605513941 +2024-09-21 18:00:00,1,12.435985861289062,23.63727688425612,48.00263389574948 +2024-09-21 19:00:00,1,20.5199258488911,26.98287368296726,68.92937223160826 +2024-09-21 20:00:00,1,26.726073914599194,21.464708015363335,58.943276717334825 +2024-09-21 21:00:00,1,24.242889216500206,21.51960811050322,56.297274839695 +2024-09-21 22:00:00,1,37.05527148380476,24.883446916393694,58.7107349250041 +2024-09-21 23:00:00,1,17.019015896073554,21.316967407193914,68.71567010139853 +2024-09-22 00:00:00,1,18.997233138868786,22.149602116591705,55.505024658363475 +2024-09-22 01:00:00,1,26.263585583127014,30.72635041211149,56.657496368337206 +2024-09-22 02:00:00,1,29.008383960257728,26.74404743135998,43.87780033084496 +2024-09-22 03:00:00,1,24.593407530021686,24.03822509546944,49.60009697481365 +2024-09-22 04:00:00,1,28.988451251287046,23.84600827499031,64.32747918196486 +2024-09-22 05:00:00,1,28.48940985297774,23.11233649570161,47.632477358540775 +2024-09-22 06:00:00,1,23.491097512721897,17.450897830458217,64.12032406198823 +2024-09-22 07:00:00,1,21.19822888171409,26.68519865979863,60.72000162415822 +2024-09-22 08:00:00,1,25.216561275603425,23.014493037147922,57.235260090241354 +2024-09-22 09:00:00,1,8.245070932131396,22.808552713659218,64.44196549445525 +2024-09-22 10:00:00,1,15.378276143372837,19.608448801412912,58.19012359012374 +2024-09-22 11:00:00,1,29.588118796463498,27.985899332131993,47.991784175434795 +2024-09-22 12:00:00,1,23.699595919356952,26.194042688142734,65.39290739512171 +2024-09-22 13:00:00,1,26.812437658852584,23.161032898471436,50.24493816378454 +2024-09-22 14:00:00,1,21.336464051615774,19.74789184152276,56.86345122454952 +2024-09-22 15:00:00,1,34.0572402293441,23.25010080807757,50.79564600947059 +2024-09-22 16:00:00,1,35.326248745097864,24.346901542577392,69.31006239988534 +2024-09-22 17:00:00,1,32.2134123901287,19.75462296020527,42.81967747422869 +2024-09-22 18:00:00,1,14.754361090546432,22.397989349489027,52.29042661400599 +2024-09-22 19:00:00,1,27.15797089045683,24.74665195848382,63.028566607949706 +2024-09-22 20:00:00,1,33.09242752442457,22.366947262863466,52.195773141659664 +2024-09-22 21:00:00,1,17.391821020556208,18.540549255929648,60.11184474831731 +2024-09-22 22:00:00,1,9.201439563742143,22.972096601550554,54.304847578691266 +2024-09-22 23:00:00,1,44.53324581168908,20.128157824277498,50.127971912283485 +2024-09-23 00:00:00,1,36.44840712940935,21.14235388574538,56.11975742384693 +2024-09-23 01:00:00,1,29.302855034132875,23.307942661204446,63.52326327005382 +2024-09-23 02:00:00,1,22.181787838739346,26.00074430065272,47.783379612316196 +2024-09-23 03:00:00,1,21.468697201459083,27.27318896662763,70.19161530859175 +2024-09-23 04:00:00,1,26.2941354183899,22.966888352407633,44.7330599789748 +2024-09-23 05:00:00,1,34.36973196047382,28.40332560948897,67.69322680465622 +2024-09-23 06:00:00,1,18.48679514553769,21.82706970661158,57.84491764449618 +2024-09-23 07:00:00,1,37.47976604139645,27.813834489042375,66.0343527573036 +2024-09-23 08:00:00,1,26.405885982687693,23.984182639453227,66.2474120746086 +2024-09-23 09:00:00,1,11.989800758146218,20.45501093845124,63.268316788052644 +2024-09-23 10:00:00,1,31.528252486215894,31.18804364698392,48.14934042613123 +2024-09-23 11:00:00,1,10.499155835173363,20.89635198220539,61.284768098857 +2024-09-23 12:00:00,1,19.192587899201524,29.700893462586553,48.84899251567223 +2024-09-23 13:00:00,1,11.973035838887485,22.853375991241972,64.23435375150899 +2024-09-23 14:00:00,1,30.834353145722144,22.64106591393457,61.410412359193685 +2024-09-23 15:00:00,1,24.278801394946857,19.879375235667013,53.1509965886218 +2024-09-23 16:00:00,1,19.3360874623898,31.098949188157512,58.17073036205562 +2024-09-23 17:00:00,1,27.145240318570483,21.568357984807356,63.21439713788416 +2024-09-23 18:00:00,1,19.532350867632537,28.56822266301447,48.507976914912916 +2024-09-23 19:00:00,1,21.443108247762392,24.996851125997495,72.96605459774601 +2024-09-23 20:00:00,1,26.53116283686376,25.60786573636413,56.19201050017815 +2024-09-23 21:00:00,1,10.604170813470132,20.480394140612212,75.29714633101642 +2024-09-23 22:00:00,1,23.629079560099576,24.858179620457854,55.2743901291288 +2024-09-23 23:00:00,1,11.43460357897406,22.62446866325647,53.66581355461441 +2024-09-24 00:00:00,1,32.82124403927879,22.393700085291215,51.931434853553874 +2024-09-24 01:00:00,1,17.091815889789864,21.899657665850036,42.828854584715664 +2024-09-24 02:00:00,1,20.33108028590027,23.496643308736925,58.12092125194568 +2024-09-24 03:00:00,1,32.33062734962285,19.87273796649333,49.59684531619487 +2024-09-24 04:00:00,1,20.998545227744625,27.501776302023277,50.87054009420632 +2024-09-24 05:00:00,1,18.932371370496853,24.63530319594498,65.48299442118913 +2024-09-24 06:00:00,1,26.083365996300763,22.555096688412135,56.92864866962432 +2024-09-24 07:00:00,1,33.97111591600909,22.775633288666874,45.618987668561445 +2024-09-24 08:00:00,1,23.438820870261406,26.74524573514186,52.90846759633306 +2024-09-24 09:00:00,1,22.421061639698532,26.417181973470715,34.69145939321095 +2024-09-24 10:00:00,1,6.048638422665135,20.526490436609617,68.82053355345 +2024-09-24 11:00:00,1,15.143035828125145,22.912309858949136,64.20458553747514 +2024-09-24 12:00:00,1,16.60019602842757,21.767015767319773,54.881620070371575 +2024-09-24 13:00:00,1,0.8909474013268799,22.020890068946912,59.18900520685581 +2024-09-24 14:00:00,1,23.19691855200985,24.882782129081583,51.230347277935394 +2024-09-24 15:00:00,1,29.29495349470108,27.883848625980907,51.352888648168594 +2024-09-24 16:00:00,1,24.06853148479841,21.8076800566904,71.36650137242844 +2024-09-24 17:00:00,1,26.22090143716701,25.256428107680055,50.080825201333084 +2024-09-24 18:00:00,1,22.10232295006785,22.53710430086722,58.98942861618829 +2024-09-24 19:00:00,1,26.557743500692737,25.743524572915113,65.15930098075991 +2024-09-24 20:00:00,1,37.739972815800115,20.26530446501035,65.63753305482003 +2024-09-24 21:00:00,1,40.86274198031049,27.1188956409391,54.216559463723414 +2024-09-24 22:00:00,1,14.141422259199276,20.801297792833513,41.07167108227358 +2024-09-24 23:00:00,1,35.538129561703606,22.520008651515706,40.165143778969124 +2024-09-25 00:00:00,1,17.534762648500763,22.300829918408514,54.0613109525123 +2024-09-25 01:00:00,1,24.083816546681685,18.209518600697514,51.0759998814626 +2024-09-25 02:00:00,1,38.008477972872726,21.9271219447865,46.78618753186667 +2024-09-25 03:00:00,1,30.866554037937036,23.172395545872448,62.65861520794871 +2024-09-25 04:00:00,1,26.38165477548311,26.878321775267196,63.37443823446598 +2024-09-25 05:00:00,1,37.086733232580976,22.17412728717214,75.48988287285032 +2024-09-25 06:00:00,1,21.502006974941114,24.098461862695785,46.91522369667198 +2024-09-25 07:00:00,1,29.559346733518225,24.820756321700806,57.52246947832942 +2024-09-25 08:00:00,1,40.98325010326127,28.765948561996037,56.00331931173294 +2024-09-25 09:00:00,1,24.08796198340303,25.6564333031145,41.36576753726055 +2024-09-25 10:00:00,1,25.161078505562845,30.72653158109223,42.14772617629728 +2024-09-25 11:00:00,1,22.779246807547068,18.178823402790815,51.794027413181354 +2024-09-25 12:00:00,1,24.442428457578455,29.67390229820261,53.37855598222302 +2024-09-25 13:00:00,1,22.308714128867706,26.304779536525842,41.8985524713985 +2024-09-25 14:00:00,1,31.500414878126836,27.71007970383558,53.46374039592883 +2024-09-25 15:00:00,1,13.773939197674764,15.588626569166866,47.42010071191127 +2024-09-25 16:00:00,1,36.3697355093346,29.048845820259423,61.00093830527998 +2024-09-25 17:00:00,1,16.218928531101728,20.411789770440578,66.57763846993753 +2024-09-25 18:00:00,1,27.137048863292893,28.530509552210244,59.12961366175736 +2024-09-25 19:00:00,1,22.469468718394232,23.751208682721114,55.51233206796416 +2024-09-25 20:00:00,1,34.49729827412318,27.35940567217285,50.90408397083702 +2024-09-25 21:00:00,1,24.488465176964006,21.319601815367363,64.74670408144044 +2024-09-25 22:00:00,1,33.78608847724894,22.02720205967747,67.3363447886012 +2024-09-25 23:00:00,1,22.257386690141182,23.152857856563177,48.52811561557539 +2024-09-26 00:00:00,1,27.993802914089763,20.696991114906584,48.185897445280624 +2024-09-26 01:00:00,1,46.22779272834889,32.53507290284824,49.79016643275663 +2024-09-26 02:00:00,1,27.585597892804607,22.473703186667038,62.88731822154914 +2024-09-26 03:00:00,1,21.872050667216687,22.738154108684068,59.17020846707537 +2024-09-26 04:00:00,1,25.062710523278394,27.342443575299377,56.62680976446249 +2024-09-26 05:00:00,1,29.961270073952438,24.91926196735444,57.03066129474961 +2024-09-26 06:00:00,1,30.915674526344173,31.523587617624663,55.06107780906844 +2024-09-26 07:00:00,1,31.950102491574526,30.806178463639284,49.1350037958785 +2024-09-26 08:00:00,1,45.252706674504324,24.72660451522176,46.57943812115225 +2024-09-26 09:00:00,1,23.103629512595898,29.565465728131326,57.9010900715055 +2024-09-26 10:00:00,1,34.58529992045625,26.196092430873808,53.228434342735476 +2024-09-26 11:00:00,1,33.72134302050483,22.438723785775213,71.27319837852386 +2024-09-26 12:00:00,1,37.47675425943409,31.23053287034033,45.590414980727715 +2024-09-26 13:00:00,1,33.625869825760375,22.08838432971989,57.935493084083824 +2024-09-26 14:00:00,1,24.925250521416597,28.799167597211387,61.74879799568728 +2024-09-26 15:00:00,1,25.046745018642763,30.305603779481846,54.24533229515435 +2024-09-26 16:00:00,1,37.05988554024989,20.568844094614974,50.1183430941356 +2024-09-26 17:00:00,1,12.605286546038133,18.47354596668173,43.4411682728114 +2024-09-26 18:00:00,1,42.46661219977675,19.425865853007004,66.50488288190326 +2024-09-26 19:00:00,1,33.57605033934838,19.383012911902085,60.338072910269474 +2024-09-26 20:00:00,1,21.490133168784812,21.82096839397627,47.22824158188723 +2024-09-26 21:00:00,1,7.705455697049803,25.35561815260007,57.733713508151524 +2024-09-26 22:00:00,1,31.14503303476503,19.651046153756727,55.2913542606439 +2024-09-26 23:00:00,1,21.46957460697603,25.85945194904786,69.12268844543519 +2024-09-27 00:00:00,1,17.2274514473184,20.912870663769638,58.69228653873225 +2024-09-27 01:00:00,1,21.023608215326483,21.580659217293352,50.84731668472658 +2024-09-27 02:00:00,1,37.71366548965262,27.835279871374702,51.457199755799564 +2024-09-27 03:00:00,1,22.172881782963813,22.182857180114976,56.06384408734532 +2024-09-27 04:00:00,1,16.480387979115548,23.79617696825561,52.87763350997481 +2024-09-27 05:00:00,1,25.37323859670242,17.940044420168302,54.77680180649765 +2024-09-27 06:00:00,1,30.18585115342428,20.642901651427,46.51795452937756 +2024-09-27 07:00:00,1,16.350959310092648,21.136818261421688,62.985873747438006 +2024-09-27 08:00:00,1,23.621521965745597,23.985306713486786,60.38708962224211 +2024-09-27 09:00:00,1,36.73772275857091,25.22469196418405,47.70261680060901 +2024-09-27 10:00:00,1,17.0446923067928,18.759077693418742,70.99345468478066 +2024-09-27 11:00:00,1,20.623546354185265,24.60734164959822,44.633179876569 +2024-09-27 12:00:00,1,26.46701323704683,18.55922833527754,55.92788619520047 +2024-09-27 13:00:00,1,19.65146639132601,28.30282724103663,54.52384309188007 +2024-09-27 14:00:00,1,24.494376776497223,24.03331032872058,55.35588837226438 +2024-09-27 15:00:00,1,31.43720755401623,26.1394114095586,60.70381170363773 +2024-09-27 16:00:00,1,30.57246096908636,21.944237525345585,37.06704219812815 +2024-09-27 17:00:00,1,18.182382472669026,29.824892646401,53.21224016671762 +2024-09-27 18:00:00,1,23.083672448695243,27.750854821445422,48.86808765879976 +2024-09-27 19:00:00,1,24.90426258267899,19.436382306187724,66.69445746571401 +2024-09-27 20:00:00,1,20.381485784946427,22.188416863625232,44.983400908564775 +2024-09-27 21:00:00,1,25.648144730397636,20.944586127803046,76.89734813517751 +2024-09-27 22:00:00,1,23.784970226698114,22.340441662450996,48.5797066909033 +2024-09-27 23:00:00,1,17.128121990872614,19.889092025298854,50.06503910780017 +2024-09-28 00:00:00,1,28.98387939281344,27.691956537828283,41.76737875615086 +2024-09-28 01:00:00,1,38.852655151372474,27.819423131923386,60.762085079664 +2024-09-28 02:00:00,1,12.288532347534694,27.42459255149305,64.1513393493163 +2024-09-28 03:00:00,1,34.85337687633441,21.32706936751032,70.24816113619815 +2024-09-28 04:00:00,1,23.593826470796607,24.498628443266796,60.91898515991794 +2024-09-28 05:00:00,1,25.82682496537755,22.79641837697691,69.52533741999552 +2024-09-28 06:00:00,1,17.1100053246051,21.763048819367377,63.675177120840395 +2024-09-28 07:00:00,1,30.835367816097378,29.58162833159374,50.142533902422436 +2024-09-28 08:00:00,1,24.868156573723727,18.789995755716824,58.86796525513857 +2024-09-28 09:00:00,1,18.631665532514454,24.554768085668957,52.99036265021923 +2024-09-28 10:00:00,1,30.59360523702827,22.414455744358712,63.143258734176456 +2024-09-28 11:00:00,1,30.892703847781853,25.243730987995217,65.59353164563896 +2024-09-28 12:00:00,1,31.369949452170527,29.813623972782462,59.90005996375802 +2024-09-28 13:00:00,1,20.38432716011416,22.80839454427254,50.955465564027335 +2024-09-28 14:00:00,1,18.341973208144722,26.31268682994554,65.49766978915736 +2024-09-28 15:00:00,1,24.1235635177252,22.864163465678764,58.812846495470815 +2024-09-28 16:00:00,1,11.413889321061049,22.08247092975375,46.43821578129285 +2024-09-28 17:00:00,1,28.08278409762065,32.17753429788243,59.278840895071724 +2024-09-28 18:00:00,1,13.414636825647937,26.255455746721292,39.956993886177614 +2024-09-28 19:00:00,1,29.13555376951541,22.857648244699778,56.5081966443912 +2024-09-28 20:00:00,1,21.019202144066025,23.668128720102604,60.50439341841954 +2024-09-28 21:00:00,1,24.73486171443889,22.438300200769667,50.41607772064326 +2024-09-28 22:00:00,1,26.781836605383443,30.41793034861904,47.434873121411655 +2024-09-28 23:00:00,1,28.021489231650886,20.221124500945024,57.57982739356361 +2024-09-29 00:00:00,1,33.890079055820216,25.939617450309786,67.04433587307476 +2024-09-29 01:00:00,1,18.63387944112924,26.560493064109224,47.333697308760826 +2024-09-29 02:00:00,1,28.757576027521797,25.74744965834132,47.847678495323805 +2024-09-29 03:00:00,1,40.0664430411356,23.25748661192953,62.03996005308911 +2024-09-29 04:00:00,1,19.52978418548751,27.46436070176975,70.20441772252117 +2024-09-29 05:00:00,1,20.104255529478554,21.52539803593214,48.795464661609024 +2024-09-29 06:00:00,1,30.06188127472235,29.165031819691556,50.10507108764188 +2024-09-29 07:00:00,1,28.42212072119387,28.113882256502762,46.04114881623977 +2024-09-29 08:00:00,1,31.433463917292677,22.548955467396198,58.569890091662934 +2024-09-29 09:00:00,1,16.485398300991875,18.468615915279507,60.27720620371862 +2024-09-29 10:00:00,1,34.6667160884688,23.4189350586576,39.49456445209968 +2024-09-29 11:00:00,1,32.902445379079154,25.02373894854396,42.641464390342804 +2024-09-29 12:00:00,1,16.391366251017907,21.147138083876474,61.58124732282516 +2024-09-29 13:00:00,1,24.53487785008701,24.60818990315834,57.37919601499503 +2024-09-29 14:00:00,1,32.599669930111645,23.73329688722272,54.03142796385952 +2024-09-29 15:00:00,1,24.14121066821465,16.689695360961863,73.38794777666801 +2024-09-29 16:00:00,1,34.47710710428646,26.499694664616595,51.19199363989627 +2024-09-29 17:00:00,1,25.968391552540954,20.960663965422064,58.048068999460305 +2024-09-29 18:00:00,1,17.38601048636093,23.584734752885844,51.45459784610826 +2024-09-29 19:00:00,1,18.763120515126747,23.872770550977812,63.84644049401742 +2024-09-29 20:00:00,1,12.174099552373004,19.14317539894947,70.20652364444048 +2024-09-29 21:00:00,1,23.60861138976622,22.515307296197772,40.713624888134476 +2024-09-29 22:00:00,1,31.992577890370402,32.42611848936549,69.24581728892446 +2024-09-29 23:00:00,1,18.72522182590117,24.498786594230108,59.80968842955309 +2024-09-30 00:00:00,1,30.552008379112984,27.24826383325934,68.07892395200076 +2024-09-30 01:00:00,1,29.25680793407073,18.594158331280113,70.39317312243247 +2024-09-30 02:00:00,1,19.491555694943067,22.147654170317292,38.13023618185207 +2024-09-30 03:00:00,1,31.482987951142636,29.72344886181807,44.68810155003698 +2024-09-30 04:00:00,1,10.46233658509619,21.634734551661758,59.15475958754844 +2024-09-30 05:00:00,1,29.847596928049445,22.92471033490425,55.48928828963179 +2024-09-30 06:00:00,1,26.88047686158244,20.625819023019506,70.19337553221182 +2024-09-30 07:00:00,1,38.48872172919972,21.72837266170875,62.67257686663036 +2024-09-30 08:00:00,1,16.993339624415903,25.204762229631665,54.97124460984242 +2024-09-30 09:00:00,1,22.360464711197167,26.06556878610298,50.132448820660706 +2024-09-30 10:00:00,1,23.705009495765427,21.700884879148543,69.51386290809366 +2024-09-30 11:00:00,1,26.252642771623105,24.96551838246428,60.185406788825965 +2024-09-30 12:00:00,1,14.928805085689438,23.217206306160993,59.41653448826934 +2024-09-30 13:00:00,1,23.197862951744685,25.252802349212608,53.4303951702073 +2024-09-30 14:00:00,1,9.893842916169476,29.23918298843191,70.57972531191987 +2024-09-30 15:00:00,1,32.77288800837135,17.47157596360505,55.18899476978405 +2024-09-30 16:00:00,1,27.340300340453396,26.54934316959116,56.44773030815706 +2024-09-30 17:00:00,1,20.307681184954784,21.42833851982584,58.214633947938 +2024-09-30 18:00:00,1,24.255725436814195,24.47889339823744,51.49327955940467 +2024-09-30 19:00:00,1,26.64940060099376,29.96870825350716,60.08032759065463 +2024-09-30 20:00:00,1,29.188710435260173,20.38802873715018,55.496839937825555 +2024-09-30 21:00:00,1,20.738620066650597,20.049814795745274,72.69739665218071 +2024-09-30 22:00:00,1,14.080638029422111,22.27741876245832,60.20455198202631 +2024-09-30 23:00:00,1,7.0068001002019855,24.058189212023887,55.63815671273677 +2024-10-01 00:00:00,1,30.40852830417067,22.9419490621258,40.54895739958676 +2024-10-01 01:00:00,1,19.365975458229542,22.433848397238545,65.38832193014044 +2024-10-01 02:00:00,1,31.15147563191385,29.32955355668239,59.9154055155587 +2024-10-01 03:00:00,1,35.848586601487625,21.542901045593176,60.76261342264294 +2024-10-01 04:00:00,1,34.92746376546535,22.264172945178288,65.56673485302464 +2024-10-01 05:00:00,1,35.02682165198309,23.78264993092369,67.09986279047851 +2024-10-01 06:00:00,1,18.226160752164574,18.88601049978363,62.6701577140306 +2024-10-01 07:00:00,1,31.512085270651045,17.09190505353184,55.64700597519236 +2024-10-01 08:00:00,1,14.45392816981667,25.241489641349112,38.453334584945466 +2024-10-01 09:00:00,1,33.674436258965144,25.527558657715968,42.742602246852044 +2024-10-01 10:00:00,1,12.961889888652864,21.582765546830384,63.12031751916349 +2024-10-01 11:00:00,1,33.47474057173555,18.112087889767047,43.00005548060637 +2024-10-01 12:00:00,1,15.15752566917587,26.652327809193565,49.629308793290434 +2024-10-01 13:00:00,1,28.604283453557702,24.606761293170322,44.943255108418086 +2024-10-01 14:00:00,1,26.806552104028896,21.240819376881717,55.854639602789234 +2024-10-01 15:00:00,1,12.326369373970962,19.409770201485408,60.968915241026046 +2024-10-01 16:00:00,1,24.354964592813946,22.683368368194113,47.86745293708002 +2024-10-01 17:00:00,1,30.116870714464042,27.530551960527422,60.83658717019692 +2024-10-01 18:00:00,1,23.75703260812139,28.968100710309162,62.76280938796254 +2024-10-01 19:00:00,1,33.44292633194114,24.951326278883165,53.10026434456781 +2024-10-01 20:00:00,1,24.508250835302874,18.57107499330955,64.19127692268307 +2024-10-01 21:00:00,1,26.70524886166256,19.446703169868066,53.79963381533374 +2024-10-01 22:00:00,1,35.76196376311657,19.12605576105427,52.6619371818887 +2024-10-01 23:00:00,1,21.74807745056234,18.91680754345009,44.866750003556604 +2024-10-02 00:00:00,1,38.22681659543622,26.476661726618154,49.01445560502762 +2024-10-02 01:00:00,1,30.804808684268252,19.658373436884254,45.81146664810139 +2024-10-02 02:00:00,1,24.36798434095857,22.412684781124177,56.42294518855845 +2024-10-02 03:00:00,1,16.043715071520076,27.287813056205962,55.038269629578984 +2024-10-02 04:00:00,1,22.63811248623593,21.97439545418149,37.27933189808669 +2024-10-02 05:00:00,1,25.482304258602426,29.92993388325852,60.366005974769216 +2024-10-02 06:00:00,1,20.445039139503002,28.632604978950205,48.37079238297696 +2024-10-02 07:00:00,1,37.77910218237724,25.1946616827699,46.43504402500747 +2024-10-02 08:00:00,1,21.13380966916584,27.187460597051818,55.4846257644944 +2024-10-02 09:00:00,1,16.832454913059063,23.23187481747886,52.44643743258353 +2024-10-02 10:00:00,1,28.886830829132823,19.587107071999238,53.05475677485485 +2024-10-02 11:00:00,1,21.56584763611185,25.636538908963864,56.12334786796157 +2024-10-02 12:00:00,1,19.809272134417757,22.955387175494558,61.42906911470014 +2024-10-02 13:00:00,1,22.139003548028867,25.632044515121706,37.3858863821076 +2024-10-02 14:00:00,1,17.588760459570373,22.73607045835074,48.94066806481969 +2024-10-02 15:00:00,1,22.840289713537512,19.865694548532314,51.373579716106384 +2024-10-02 16:00:00,1,30.252859455052427,24.51673621254783,52.0882962871426 +2024-10-02 17:00:00,1,24.342610434585,22.525866370368934,41.477213279016055 +2024-10-02 18:00:00,1,28.629804457066733,21.101363196725284,50.836454984365574 +2024-10-02 19:00:00,1,12.818847068166297,24.672314817817192,66.03986418939186 +2024-10-02 20:00:00,1,21.93012122674726,16.516529334239372,61.61445398609803 +2024-10-02 21:00:00,1,16.336496213278267,26.538021794450827,66.3422328950481 +2024-10-02 22:00:00,1,20.092177473568835,14.705970268638175,68.85249915627034 +2024-10-02 23:00:00,1,18.311567601104098,27.749109773183502,82.57121588404083 +2024-10-03 00:00:00,1,36.750575604292976,24.136480614747406,57.60857977876731 +2024-10-03 01:00:00,1,39.65067316094797,20.944646863541212,70.66050065121398 +2024-10-03 02:00:00,1,25.304607693819346,22.672012086967168,59.52283796658236 +2024-10-03 03:00:00,1,18.191801791628144,27.280227384487038,56.09646896629889 +2024-10-03 04:00:00,1,34.6193657921913,19.066463028580284,41.99943225304206 +2024-10-03 05:00:00,1,28.86415801534675,20.884715464472368,28.900163314795353 +2024-10-03 06:00:00,1,26.56942905412455,26.24776435829071,52.61468120207398 +2024-10-03 07:00:00,1,33.165560961617295,25.749186817762983,38.97046594735096 +2024-10-03 08:00:00,1,15.602658402477276,20.546770226704982,48.20597787601564 +2024-10-03 09:00:00,1,28.076775045726997,18.95669099302317,45.937947717034014 +2024-10-03 10:00:00,1,7.699388862639454,24.59384237985794,73.33278072379441 +2024-10-03 11:00:00,1,31.640711057098414,22.21505841664277,52.802835776179535 +2024-10-03 12:00:00,1,16.96831160957653,23.321263954007062,65.13976922465184 +2024-10-03 13:00:00,1,18.307894952847704,23.154227087903717,41.0140581926585 +2024-10-03 14:00:00,1,23.023662114046758,27.847341284182576,42.44388233379676 +2024-10-03 15:00:00,1,22.099218789784477,19.743104310628794,48.61225750340717 +2024-10-03 16:00:00,1,32.31033536161683,23.23686373027296,54.58147038632509 +2024-10-03 17:00:00,1,14.11898084401003,26.497842012616662,59.495095481237406 +2024-10-03 18:00:00,1,38.559118113634455,19.48865623742958,59.82181293028399 +2024-10-03 19:00:00,1,30.032732843316914,25.268033911084824,57.66466659938221 +2024-10-03 20:00:00,1,19.968416777942654,24.90510464582265,45.557413756944925 +2024-10-03 21:00:00,1,17.887608596850143,21.23648533339825,43.59873860565949 +2024-10-03 22:00:00,1,15.502887698437032,31.560414298898756,57.76443535026449 +2024-10-03 23:00:00,1,18.046697650417208,17.66391132549181,38.896258797976216 +2024-10-04 00:00:00,1,26.901336194056096,23.168564589638468,52.90700786301155 +2024-10-04 01:00:00,1,17.55811860385616,24.517079251263546,56.3700881888699 +2024-10-04 02:00:00,1,30.601282999279345,23.787764311811536,64.78785506536586 +2024-10-04 03:00:00,1,20.468795159965275,19.568925248724593,64.96836196118974 +2024-10-04 04:00:00,1,29.089939604015694,29.125142874817776,71.63472964394126 +2024-10-04 05:00:00,1,32.803437450893284,20.48862413561612,46.61486970140178 +2024-10-04 06:00:00,1,26.53560203790519,18.843172966223797,43.317075650190255 +2024-10-04 07:00:00,1,17.49566774184658,24.595915675229687,75.72380711244375 +2024-10-04 08:00:00,1,20.711588989775514,19.89765946780276,69.76705813059742 +2024-10-04 09:00:00,1,33.77691224818438,23.618890293633637,40.86722004100125 +2024-10-04 10:00:00,1,33.762956249448145,25.881663033811595,69.20190735809562 +2024-10-04 11:00:00,1,11.56573101602745,29.357585235960894,62.60817100839785 +2024-10-04 12:00:00,1,11.065318483046335,23.201234499037927,45.45515153067698 +2024-10-04 13:00:00,1,24.830891634589943,24.532248590238503,50.23722831479787 +2024-10-04 14:00:00,1,14.538813403692238,21.55743732923598,55.71847385175532 +2024-10-04 15:00:00,1,34.36341341559355,20.618591778106197,65.67105407579467 +2024-10-04 16:00:00,1,8.934111548175931,23.251050100060215,64.64321685882351 +2024-10-04 17:00:00,1,26.33903915239377,25.249019528360158,56.510035890508064 +2024-10-04 18:00:00,1,22.534001253388787,20.193959833603977,53.56917920474914 +2024-10-04 19:00:00,1,37.91672687305352,22.79061485892105,61.815730914095134 +2024-10-04 20:00:00,1,17.90871269765394,25.839473399397992,52.12868432948945 +2024-10-04 21:00:00,1,33.057585627736565,18.146339425275897,61.622333673096136 +2024-10-04 22:00:00,1,40.726034116090716,17.93728109596875,61.693897423100026 +2024-10-04 23:00:00,1,27.817196462607956,19.488395730438953,58.652875310851584 +2024-10-05 00:00:00,1,28.529233387046755,19.455844202561913,57.77127030878533 +2024-10-05 01:00:00,1,30.05882426318878,31.630135353252292,51.258714264488816 +2024-10-05 02:00:00,1,28.96030177102616,19.52302929678172,50.130448016334455 +2024-10-05 03:00:00,1,29.746464886756154,21.27935596704382,55.30270881109973 +2024-10-05 04:00:00,1,36.786393287959775,22.02836450180978,49.57013687472247 +2024-10-05 05:00:00,1,12.634298190258157,27.783140865008058,63.158311728837326 +2024-10-05 06:00:00,1,27.077336891022313,27.08536203056938,63.14375426342515 +2024-10-05 07:00:00,1,19.7281308199876,26.379871183781816,63.445679116367224 +2024-10-05 08:00:00,1,22.640264606960322,23.40262781539492,47.302060156260936 +2024-10-05 09:00:00,1,34.909978913070304,14.611792962050812,54.737027609655776 +2024-10-05 10:00:00,1,15.102541095065595,28.596884325166634,67.77583239243685 +2024-10-05 11:00:00,1,32.08721245103631,26.237490825164475,40.58349647252419 +2024-10-05 12:00:00,1,25.9055981061421,16.24669670568814,60.82646525653714 +2024-10-05 13:00:00,1,29.51944083374221,22.187230942820015,40.8087564239871 +2024-10-05 14:00:00,1,23.84922429346299,19.6375726320566,54.55027987479528 +2024-10-05 15:00:00,1,15.104982453499433,22.971606402684845,55.07299648307301 +2024-10-05 16:00:00,1,20.458995777359824,25.927992946150987,54.58468166098703 +2024-10-05 17:00:00,1,16.74154230530705,23.845969641301163,55.015087417218815 +2024-10-05 18:00:00,1,18.57276184703745,25.609176382975306,44.400841771715896 +2024-10-05 19:00:00,1,7.991922091880522,20.779533844790777,67.24714639266661 +2024-10-05 20:00:00,1,25.953758506411543,22.489071869457227,54.01182591446952 +2024-10-05 21:00:00,1,20.505147082246253,25.183780276497366,68.88404076987513 +2024-10-05 22:00:00,1,22.080196049685185,26.281081852855838,60.458417788044 +2024-10-05 23:00:00,1,25.48076500630416,26.045743020177717,53.80943385099057 +2024-10-06 00:00:00,1,16.687387520718776,31.12493961910883,47.80199685042427 +2024-10-06 01:00:00,1,27.24012668181256,26.508990872315096,54.742440869934754 +2024-10-06 02:00:00,1,28.758121468850195,23.799284216275503,68.00465439087758 +2024-10-06 03:00:00,1,26.08604788799231,24.516826839516387,41.111826821442946 +2024-10-06 04:00:00,1,21.581962072196877,27.005599082752763,60.554617086825296 +2024-10-06 05:00:00,1,17.340708395484153,21.190254939796315,66.49101567316445 +2024-10-06 06:00:00,1,7.679994888349778,24.184135615351757,56.142235108499605 +2024-10-06 07:00:00,1,20.971128922507305,21.20700301960868,55.89739402980915 +2024-10-06 08:00:00,1,4.773079868314966,25.262166249850758,65.50791606308046 +2024-10-06 09:00:00,1,20.60020102879097,22.349908861665426,67.6210492622648 +2024-10-06 10:00:00,1,23.33943230031274,26.54293288515052,55.81548663799198 +2024-10-06 11:00:00,1,10.520394749626679,20.170665243538195,53.562020836052206 +2024-10-06 12:00:00,1,6.450822105252339,24.149840531219137,69.60795975656332 +2024-10-06 13:00:00,1,26.14544869384084,20.610063503431313,42.53551036601925 +2024-10-06 14:00:00,1,17.040102749497272,24.152549577424402,56.07915919614629 +2024-10-06 15:00:00,1,30.816534798118546,25.63743783456757,33.6204333392795 +2024-10-06 16:00:00,1,34.884188380643515,28.290613822944,39.08617276205872 +2024-10-06 17:00:00,1,28.75796306625477,23.943075183814155,63.438982941884895 +2024-10-06 18:00:00,1,30.056169477540273,20.012343463741026,56.5453895936376 +2024-10-06 19:00:00,1,11.482296676091,24.190629055946392,52.59439117368724 +2024-10-06 20:00:00,1,30.584264735348462,19.820900284169088,66.0707277019926 +2024-10-06 21:00:00,1,11.612607862316052,23.690343592623396,60.304474863848874 +2024-10-06 22:00:00,1,17.31071619462393,20.879433821722646,59.92797937723937 +2024-10-06 23:00:00,1,6.0925852696665395,23.674702056063456,64.07429614858884 +2024-10-07 00:00:00,1,22.776517227768874,25.890363177245064,54.75205106052568 +2024-10-07 01:00:00,1,12.804294345985308,22.765107360661148,66.7958317584415 +2024-10-07 02:00:00,1,23.98540147610509,19.39860146233053,62.43093378540924 +2024-10-07 03:00:00,1,33.73911638646363,23.820912452060494,49.709714047232296 +2024-10-07 04:00:00,1,30.27784988978783,25.637526088098934,47.723097797803796 +2024-10-07 05:00:00,1,43.54743724271344,23.193744891231827,35.5754137500831 +2024-10-07 06:00:00,1,26.765547283412275,27.67427019208946,63.86597916055194 +2024-10-07 07:00:00,1,19.162600152250043,20.462032689383097,45.55444142691095 +2024-10-07 08:00:00,1,12.530877604297828,22.611194056837537,59.567124740808566 +2024-10-07 09:00:00,1,33.524696754405355,28.64596932189445,46.125276840348256 +2024-10-07 10:00:00,1,2.0788428020942007,22.219042741756404,49.102083846267774 +2024-10-07 11:00:00,1,31.480911758358097,28.541082908296268,65.49480720988312 +2024-10-07 12:00:00,1,23.307678017239116,22.590516482007455,54.72171176377045 +2024-10-07 13:00:00,1,31.54650115754859,25.82682665977453,66.96631399023198 +2024-10-07 14:00:00,1,21.329306377204034,17.289982378567537,71.5910372337643 +2024-10-07 15:00:00,1,31.79271180390253,20.420175985679318,53.21005754368288 +2024-10-07 16:00:00,1,16.690120426453877,19.61792238952645,62.37699287615216 +2024-10-07 17:00:00,1,11.403681748111543,25.45142176940606,54.1171986824125 +2024-10-07 18:00:00,1,18.273856212912666,25.046876314566337,72.03348509518068 +2024-10-07 19:00:00,1,25.843170458612015,22.4923131045578,49.86137814774145 +2024-10-07 20:00:00,1,24.235466305117924,22.400290965212236,77.69110948548378 +2024-10-07 21:00:00,1,36.90088618515209,18.515324681707845,66.84986316445139 +2024-10-07 22:00:00,1,33.261803320535364,21.0016235322142,62.27167527298192 +2024-10-07 23:00:00,1,17.2440952745499,19.31929804033552,62.56381425162455 +2024-10-08 00:00:00,1,22.78620443922516,30.58208437227841,56.011654027313206 +2024-10-08 01:00:00,1,34.528959519314625,23.26742273205246,48.33218672899304 +2024-10-08 02:00:00,1,24.106703555569357,24.41302533557831,41.32304609661482 +2024-10-08 03:00:00,1,32.31040543322294,24.258965380282387,54.618444959991834 +2024-10-08 04:00:00,1,16.299051990010955,18.80781564807088,72.02912973116219 +2024-10-08 05:00:00,1,33.58153177094834,25.495634043183642,61.04460266665135 +2024-10-08 06:00:00,1,21.715403911232944,22.907221590158134,50.65490989928263 +2024-10-08 07:00:00,1,23.529834016329044,19.91110098460829,63.16782819780693 +2024-10-08 08:00:00,1,0.0,19.832800793799787,53.55194234386615 +2024-10-08 09:00:00,1,20.580977003174198,17.89254351458666,64.52699584423681 +2024-10-08 10:00:00,1,33.825423774655874,23.748295705453398,48.725721927666136 +2024-10-08 11:00:00,1,11.424635494914579,24.545067686006394,43.02377953344133 +2024-10-08 12:00:00,1,18.457495295747922,22.370629804176023,66.45645577447245 +2024-10-08 13:00:00,1,31.243028737380897,22.71781435971494,58.53867006953207 +2024-10-08 14:00:00,1,25.657590044226637,21.69710770080735,49.758439770180594 +2024-10-08 15:00:00,1,33.47260729914572,27.679155118934577,49.66140590053727 +2024-10-08 16:00:00,1,26.05212554271726,23.090052145428146,69.75361222064804 +2024-10-08 17:00:00,1,20.090827054312165,27.30965323228154,42.56089895386776 +2024-10-08 18:00:00,1,24.722101428748875,27.198551932534073,57.91612718719944 +2024-10-08 19:00:00,1,30.788443165378542,21.731250678459535,54.161620912093504 +2024-10-08 20:00:00,1,33.71922913930324,21.336952575388462,53.03492535832491 +2024-10-08 21:00:00,1,15.31550616083145,19.00201726010694,65.04304545018059 +2024-10-08 22:00:00,1,20.957600002260083,27.11259896742368,64.6465677307656 +2024-10-08 23:00:00,1,27.015192841465115,22.292405778209847,63.213208735302935 +2024-10-09 00:00:00,1,34.023917974520714,22.522252325639936,50.695705134639425 +2024-10-09 01:00:00,1,30.0410710244221,25.992664056744456,59.790000675143915 +2024-10-09 02:00:00,1,36.99024499949012,24.909051907328703,66.29312460151037 +2024-10-09 03:00:00,1,25.37904814809528,21.88019740958911,55.08509994545268 +2024-10-09 04:00:00,1,13.449914590409849,23.144641872673375,58.67500109265017 +2024-10-09 05:00:00,1,31.691063880871734,24.180721819148435,65.52132562952849 +2024-10-09 06:00:00,1,12.963270783555691,16.76694689663298,40.61839915789051 +2024-10-09 07:00:00,1,27.13929952299096,22.44403323249942,66.76197080502368 +2024-10-09 08:00:00,1,12.491092923443592,30.724058218596827,64.91941190684116 +2024-10-09 09:00:00,1,19.782507383225536,24.950516867871798,66.61317701274572 +2024-10-09 10:00:00,1,18.45576338529451,24.014152865596134,53.872645810562524 +2024-10-09 11:00:00,1,7.459383221488768,24.84373730376063,54.94716222701438 +2024-10-09 12:00:00,1,34.79083008521266,27.6890518187932,60.761278661436265 +2024-10-09 13:00:00,1,29.455982437110592,24.629303439918232,39.150957280260805 +2024-10-09 14:00:00,1,27.979326775378727,22.116474388788916,55.21388772155508 +2024-10-09 15:00:00,1,27.89895817994978,22.341117112627064,47.29847941405074 +2024-10-09 16:00:00,1,19.488698891126603,24.236026694534228,42.72759356373911 +2024-10-09 17:00:00,1,24.095284617171277,25.254306827251426,50.5664725742451 +2024-10-09 18:00:00,1,47.64839172765738,20.495680020433138,53.76769750961596 +2024-10-09 19:00:00,1,12.282222774099269,24.818381134577248,40.61439342531563 +2024-10-09 20:00:00,1,31.318914763583834,20.90052369836662,64.19934359661936 +2024-10-09 21:00:00,1,23.849991837042122,17.684774617297624,45.338895804224045 +2024-10-09 22:00:00,1,27.314646607535234,20.04940030447303,56.95216765964481 +2024-10-09 23:00:00,1,19.79881470880404,23.735896500764728,63.07293803937657 +2024-10-10 00:00:00,1,20.39970221487155,28.299128553854363,66.51730937295082 +2024-10-10 01:00:00,1,17.843814000223343,23.18561841749459,69.93246112850512 +2024-10-10 02:00:00,1,22.13329671545628,25.34921443170182,50.248406741107104 +2024-10-10 03:00:00,1,20.8405829777766,20.711717361884382,50.179755919661766 +2024-10-10 04:00:00,1,40.88296731534131,19.56215601175207,70.94840583952448 +2024-10-10 05:00:00,1,33.18916857247038,24.59085889195932,55.44593142259494 +2024-10-10 06:00:00,1,28.975707600972964,17.94781963277405,63.0446429826555 +2024-10-10 07:00:00,1,12.065573045911119,20.49684642518036,53.53815537388505 +2024-10-10 08:00:00,1,20.70733465333481,23.355020907037353,50.367725749998755 +2024-10-10 09:00:00,1,5.775645239891528,29.49022078622649,45.68676042254427 +2024-10-10 10:00:00,1,30.125047995431775,25.915565781477135,59.62795019119661 +2024-10-10 11:00:00,1,35.3254880345594,28.341812742495527,44.45091970096196 +2024-10-10 12:00:00,1,25.729901117203905,23.095415730596255,47.54330556445274 +2024-10-10 13:00:00,1,29.827589230251185,28.202888850279326,46.97609813901978 +2024-10-10 14:00:00,1,15.419349986069761,23.256336878212675,52.624276330556434 +2024-10-10 15:00:00,1,19.909283981790075,24.55170259341316,52.97385512333199 +2024-10-10 16:00:00,1,16.964745605365316,26.07488132015872,58.79039169392732 +2024-10-10 17:00:00,1,17.069595677656192,24.935790107019468,49.340764915392796 +2024-10-10 18:00:00,1,39.42341775160593,24.958640301300694,72.57578226461786 +2024-10-10 19:00:00,1,28.235900329451432,16.18763242392664,49.11845083976683 +2024-10-10 20:00:00,1,21.11758352072509,22.813389183951898,42.77923580537664 +2024-10-10 21:00:00,1,30.518317288455425,24.66210351095165,76.61345440210852 +2024-10-10 22:00:00,1,19.900761204953458,22.818884551908475,66.42905799608495 +2024-10-10 23:00:00,1,23.222813285041642,22.832267079110707,44.9591254821145 +2024-10-11 00:00:00,1,19.014055988962134,21.621260315492727,50.129899453334 +2024-10-11 01:00:00,1,7.027031281960085,20.08238474823392,50.121766932323894 +2024-10-11 02:00:00,1,23.79045940250347,20.982288897558654,52.770660466917974 +2024-10-11 03:00:00,1,32.225729394539236,23.544015701053475,48.08918816043452 +2024-10-11 04:00:00,1,26.339981003835252,27.36821158178833,40.29958634874896 +2024-10-11 05:00:00,1,19.16755256507859,21.665164055941503,48.373108096716166 +2024-10-11 06:00:00,1,20.86937294744454,23.392608946018733,52.64930318715471 +2024-10-11 07:00:00,1,25.228888834556628,19.162619754145044,51.57181569077358 +2024-10-11 08:00:00,1,27.226926272452005,16.991795712355284,63.86312409293281 +2024-10-11 09:00:00,1,19.297750374448967,26.266762126021845,56.09441623890621 +2024-10-11 10:00:00,1,26.683235888756872,19.93575533542189,49.88482695013886 +2024-10-11 11:00:00,1,31.11831069718785,24.199655233734678,69.99639114158902 +2024-10-11 12:00:00,1,29.018519388512686,29.464489582516727,58.10141569563153 +2024-10-11 13:00:00,1,20.882336423268598,21.897582764655628,57.708580018689545 +2024-10-11 14:00:00,1,9.872884420860904,24.63478571979557,49.86607267192619 +2024-10-11 15:00:00,1,27.825832272998365,26.702059107839318,43.28436764967446 +2024-10-11 16:00:00,1,19.634893809437997,23.491210740798522,52.518452247034844 +2024-10-11 17:00:00,1,28.749878286719582,25.920178792955852,66.4212081926708 +2024-10-11 18:00:00,1,15.124368503904421,24.815920515850532,71.12699914379266 +2024-10-11 19:00:00,1,17.403290861044397,29.209419905306596,57.25940573424964 +2024-10-11 20:00:00,1,39.189239282647186,25.139139614065645,67.53715482453092 +2024-10-11 21:00:00,1,31.692469192079912,21.292344166673033,53.46736269102036 +2024-10-11 22:00:00,1,33.25740521327462,15.489568858805754,62.69021885961711 +2024-10-11 23:00:00,1,9.56461297612365,18.375766793309086,53.75978566314332 +2024-10-12 00:00:00,1,26.5438956910291,23.87810810532016,50.303440665883464 +2024-10-12 01:00:00,1,38.69882481186691,27.22124186693704,51.69501972485287 +2024-10-12 02:00:00,1,36.309859307723585,24.63274742263994,61.20037242570266 +2024-10-12 03:00:00,1,20.76510377912059,17.727728542634026,56.11055246200181 +2024-10-12 04:00:00,1,24.73734190833624,22.811350722455206,52.92278892432008 +2024-10-12 05:00:00,1,34.92132467355786,26.34677758914983,49.90624651334737 +2024-10-12 06:00:00,1,17.076278546131675,25.412728411858133,72.37239577056891 +2024-10-12 07:00:00,1,28.344604061288173,23.82117124846025,52.271325460342396 +2024-10-12 08:00:00,1,11.584776236328429,26.48376207145803,48.06549514382299 +2024-10-12 09:00:00,1,21.91582288486181,22.35536688449821,54.851769125146646 +2024-10-12 10:00:00,1,11.16187840820001,26.908828448320378,54.464394719559095 +2024-10-12 11:00:00,1,13.7956490706483,25.382211075695924,56.85642344953908 +2024-10-12 12:00:00,1,15.264206860080868,18.30870197573421,64.08924825268817 +2024-10-12 13:00:00,1,25.47848153360061,15.585694518995197,52.14995538249579 +2024-10-12 14:00:00,1,40.57840001700396,17.49025451630392,65.19365644189229 +2024-10-12 15:00:00,1,17.475933473903346,21.32797209159949,55.50807736300158 +2024-10-12 16:00:00,1,48.72643475584809,23.829179358055036,40.07135697041679 +2024-10-12 17:00:00,1,20.173416935739585,23.7739212991164,44.66549971074912 +2024-10-12 18:00:00,1,27.654248839514118,24.10243981428926,37.83098551559338 +2024-10-12 19:00:00,1,18.333128129751454,19.913163743540093,50.41201204178042 +2024-10-12 20:00:00,1,29.911477634389065,30.093457141401917,53.26188132350857 +2024-10-12 21:00:00,1,40.38753747901636,22.237406509588805,52.230027784865456 +2024-10-12 22:00:00,1,32.704682431421304,26.753487584317533,67.1929042393737 +2024-10-12 23:00:00,1,34.93014563626143,21.908320363876236,70.32257163603583 +2024-10-13 00:00:00,1,13.215362467568106,28.909733941585223,41.835075394047635 +2024-10-13 01:00:00,1,34.372574100117625,20.27143699917255,53.28429849783856 +2024-10-13 02:00:00,1,26.42717951450405,22.56778773775331,68.09748632258193 +2024-10-13 03:00:00,1,20.96043060759107,25.48793880127831,62.74703359319713 +2024-10-13 04:00:00,1,30.275457862941515,20.919421033534118,63.602103152105975 +2024-10-13 05:00:00,1,21.72903164683051,20.20664890505694,41.423240390760576 +2024-10-13 06:00:00,1,7.513295738536257,21.605851224035117,49.02589944222015 +2024-10-13 07:00:00,1,26.9726677996761,22.47150676120423,53.70013208071633 +2024-10-13 08:00:00,1,26.461521896652155,22.00177160759733,68.25055178074305 +2024-10-13 09:00:00,1,24.5328828134786,27.4451852178939,59.09979982594213 +2024-10-13 10:00:00,1,17.467364040795225,25.22169628321162,53.93796813220398 +2024-10-13 11:00:00,1,11.659288014881968,17.84538954652186,62.28946502905802 +2024-10-13 12:00:00,1,11.057382847142337,21.34429153913042,60.43653375975985 +2024-10-13 13:00:00,1,24.834304820320693,21.3148903868414,56.93320267085319 +2024-10-13 14:00:00,1,28.828869307626647,24.587542184064763,50.829460597889614 +2024-10-13 15:00:00,1,40.988361830469096,22.90343543595419,52.07399507363064 +2024-10-13 16:00:00,1,24.322199570569772,22.239844992853822,61.02683618087879 +2024-10-13 17:00:00,1,21.603689370768446,23.873494505319,47.31538266728704 +2024-10-13 18:00:00,1,11.326611431863034,25.875712303567028,47.64233099161113 +2024-10-13 19:00:00,1,23.35650917040361,24.302429195304423,71.08641832143613 +2024-10-13 20:00:00,1,7.396847817622103,26.208857222540082,57.80628342595014 +2024-10-13 21:00:00,1,17.869537182772312,24.435044364791516,65.93812130488794 +2024-10-13 22:00:00,1,27.220987549644715,22.270624325902624,63.46401723278669 +2024-10-13 23:00:00,1,12.245557287960116,24.032854744161035,67.94062489554798 +2024-10-14 00:00:00,1,30.18628846708456,20.610076367096568,44.72644293832407 +2024-10-14 01:00:00,1,21.101824379570523,27.507862878746018,50.704675638581044 +2024-10-14 02:00:00,1,35.579144857718916,26.62354020648734,43.04812233425832 +2024-10-14 03:00:00,1,20.496092821441,19.3894260974588,54.57609173209841 +2024-10-14 04:00:00,1,32.40138776494332,26.446509202047807,64.9243109259506 +2024-10-14 05:00:00,1,20.98725706142245,19.346474217227943,56.90586239522759 +2024-10-14 06:00:00,1,15.969055168011787,23.342637427071654,41.9039081238798 +2024-10-14 07:00:00,1,23.19598644693967,31.28779026330845,60.39003305902136 +2024-10-14 08:00:00,1,25.051973497847438,22.880301414537808,58.35717441831298 +2024-10-14 09:00:00,1,27.820360288768164,21.89477273447692,33.461670297941396 +2024-10-14 10:00:00,1,22.740556644763664,25.398159827767106,54.896283794788786 +2024-10-14 11:00:00,1,27.135458191927828,22.172374960362202,60.95684031609204 +2024-10-14 12:00:00,1,15.628012742460857,27.232322011929178,49.853378627871834 +2024-10-14 13:00:00,1,20.147275319079437,17.18844249732227,43.36916568651806 +2024-10-14 14:00:00,1,34.001874455493535,21.240740737272002,55.39221110387789 +2024-10-14 15:00:00,1,35.85927063229366,20.66620267790334,47.01088549176345 +2024-10-14 16:00:00,1,37.19155287764314,25.112864412529067,40.664077544890276 +2024-10-14 17:00:00,1,17.42571202240793,19.180190996647617,64.84197212409963 +2024-10-14 18:00:00,1,21.214613654021246,18.994986058029063,50.224067244844434 +2024-10-14 19:00:00,1,28.271303579904917,17.476396614994496,53.54221174892259 +2024-10-14 20:00:00,1,18.295646868056274,23.818985410621046,47.928676251560276 +2024-10-14 21:00:00,1,23.910633271046557,26.64720363543544,63.690214631444505 +2024-10-14 22:00:00,1,26.38546167458183,27.874184687681698,59.9101778190188 +2024-10-14 23:00:00,1,22.928644168074076,30.228261966426615,54.961334204790894 +2024-10-15 00:00:00,1,28.89820808791448,25.467291621623197,47.70385304623307 +2024-10-15 01:00:00,1,30.43889942885592,19.650638485311564,47.65510925397511 +2024-10-15 02:00:00,1,13.77561938759921,33.57984714006432,48.610085082514175 +2024-10-15 03:00:00,1,14.972948157906682,25.82286798261234,36.42438052114069 +2024-10-15 04:00:00,1,9.659247135934429,23.94427365981278,57.59723600356606 +2024-10-15 05:00:00,1,24.035908508021464,21.281707267631184,43.47893294353545 +2024-10-15 06:00:00,1,14.731360174044024,16.6029029761947,54.36936066170446 +2024-10-15 07:00:00,1,28.486001174521974,25.13537018147982,43.941008820484775 +2024-10-15 08:00:00,1,17.303051120885858,19.48899998518664,70.71948418827111 +2024-10-15 09:00:00,1,21.466409668804705,25.308169338492785,50.39703353100742 +2024-10-15 10:00:00,1,19.13254924401864,23.1658731155268,59.98233018162503 +2024-10-15 11:00:00,1,17.725992028070074,17.724856534002612,61.49442107908691 +2024-10-15 12:00:00,1,28.339615180712837,19.701251616233186,51.37022161381853 +2024-10-15 13:00:00,1,25.642127177386612,25.665289389493044,77.49978576525407 +2024-10-15 14:00:00,1,34.43618027511969,28.347912571110093,48.576183636478504 +2024-10-15 15:00:00,1,26.259782428738593,23.373533916223792,49.13337425309442 +2024-10-15 16:00:00,1,38.52957639833707,20.00555320083187,51.491838498026695 +2024-10-15 17:00:00,1,28.199231041348405,22.392211319065375,50.578158148945164 +2024-10-15 18:00:00,1,40.41685134425787,29.306519563316712,63.96492839983102 +2024-10-15 19:00:00,1,37.35330922094241,25.677766643017236,55.99476015271868 +2024-10-15 20:00:00,1,27.313153214000167,26.663994368639877,57.12255058118505 +2024-10-15 21:00:00,1,26.498299958926573,20.39228685092329,56.79276632034206 +2024-10-15 22:00:00,1,28.644617528890457,14.617058801556368,52.9687216341693 +2024-10-15 23:00:00,1,28.800310746152242,26.785617869759886,70.65076999120018 +2024-10-16 00:00:00,1,30.24509346368774,28.819908557903943,69.94977547794107 +2024-10-16 01:00:00,1,32.869504696461235,25.382676926735105,51.86495476159321 +2024-10-16 02:00:00,1,39.06029466387204,28.59718953773477,46.057566067569084 +2024-10-16 03:00:00,1,21.455975491419466,25.841808979886775,69.02604427955437 +2024-10-16 04:00:00,1,31.390581710226407,29.196769349943764,56.48081835097279 +2024-10-16 05:00:00,1,37.70559295912824,26.051978644764905,48.2393049041227 +2024-10-16 06:00:00,1,16.772146558372533,19.4273317734702,52.59373894884053 +2024-10-16 07:00:00,1,22.920365200241196,26.05146284616611,59.95004511335125 +2024-10-16 08:00:00,1,27.110580159952093,24.476356861486835,54.56611854492525 +2024-10-16 09:00:00,1,19.66235941760147,28.592166692449553,47.113461701619855 +2024-10-16 10:00:00,1,31.210790928822675,27.84228201373376,52.076145651920214 +2024-10-16 11:00:00,1,34.31189278091286,23.536516514661226,52.70886211825075 +2024-10-16 12:00:00,1,26.753466633572085,22.737955778401588,50.915118413542274 +2024-10-16 13:00:00,1,15.6984129845836,27.847806987393692,43.070436527006535 +2024-10-16 14:00:00,1,30.817224570775938,31.247537170932155,64.78707170763904 +2024-10-16 15:00:00,1,27.609509425194155,31.08527067321571,48.0359370076697 +2024-10-16 16:00:00,1,24.102383994862986,28.813874980598086,49.3987955944423 +2024-10-16 17:00:00,1,20.986519957751756,21.91650691433672,62.262021926728984 +2024-10-16 18:00:00,1,34.445493971571025,26.754991013972276,51.41112655989011 +2024-10-16 19:00:00,1,30.396396188484452,23.143123071549315,48.712614106321055 +2024-10-16 20:00:00,1,24.515018121925163,19.058905808445758,65.5673184495393 +2024-10-16 21:00:00,1,15.316765773804711,26.125441360607073,62.008226554504986 +2024-10-16 22:00:00,1,22.890021751585582,22.008056713901578,57.37628244412781 +2024-10-16 23:00:00,1,26.70947782054827,21.035279346816107,51.31948537127679 +2024-10-17 00:00:00,1,21.242380251894495,24.09087232448054,72.63384003375754 +2024-10-17 01:00:00,1,15.594566708985996,23.96337422246755,52.29850106945089 +2024-10-17 02:00:00,1,26.89818036691068,26.802234476265067,57.7796436384506 +2024-10-17 03:00:00,1,25.426549117956164,21.330895121720353,65.17688213374475 +2024-10-17 04:00:00,1,30.836535533440554,23.55249521904281,60.41101200052024 +2024-10-17 05:00:00,1,34.392663115664,21.036641748903577,52.34235899958222 +2024-10-17 06:00:00,1,32.82254863158832,30.42762818745051,58.27453685797479 +2024-10-17 07:00:00,1,40.94328946503062,28.06634893437144,49.397534089910955 +2024-10-17 08:00:00,1,6.89891632886588,19.20643337676125,54.42546575472925 +2024-10-17 09:00:00,1,23.99675721908269,20.403991061004675,46.800417406767735 +2024-10-17 10:00:00,1,16.512648622641404,27.06696131962189,68.25258201293165 +2024-10-17 11:00:00,1,20.831150729499544,16.513372156202053,52.77509964945602 +2024-10-17 12:00:00,1,23.476519720807737,22.500257269489,58.476803481154064 +2024-10-17 13:00:00,1,31.30431990218687,24.848069989454896,50.12606222946746 +2024-10-17 14:00:00,1,21.74936952026197,27.76488625211673,72.16586271353293 +2024-10-17 15:00:00,1,12.106621278165992,24.878294860073456,58.8169359337568 +2024-10-17 16:00:00,1,27.237202645890882,26.114565334942267,48.16333380296457 +2024-10-17 17:00:00,1,29.687220723895457,29.587203872581437,61.56299873310561 +2024-10-17 18:00:00,1,36.739120386109605,17.321754430700082,64.42228949720575 +2024-10-17 19:00:00,1,23.776305475069748,23.166081932750664,45.32507913523128 +2024-10-17 20:00:00,1,22.922267063642963,27.5808733758259,80.20753902412413 +2024-10-17 21:00:00,1,26.57854847402766,18.82980128366287,45.747844036886946 +2024-10-17 22:00:00,1,29.682555851226592,24.307168901742592,61.886176631851725 +2024-10-17 23:00:00,1,7.788088537576094,23.793362028827744,61.898978812945984 +2024-10-18 00:00:00,1,34.170419620021065,25.17974064825268,54.822990397977044 +2024-10-18 01:00:00,1,24.346167884365457,23.90582386227217,47.01960562506838 +2024-10-18 02:00:00,1,19.271886732881214,23.21100429047382,58.182052924386106 +2024-10-18 03:00:00,1,28.079762166475923,22.588237379859095,56.16173898914941 +2024-10-18 04:00:00,1,27.861223782440035,26.121876497030623,51.751998225749745 +2024-10-18 05:00:00,1,22.18378123128477,19.640490490150004,58.03249466954407 +2024-10-18 06:00:00,1,23.223654828848645,26.118912769737552,49.89597581917711 +2024-10-18 07:00:00,1,32.078021210464854,21.547099560673885,49.05712194217482 +2024-10-18 08:00:00,1,33.510911508868205,24.13838705121495,55.685918410507675 +2024-10-18 09:00:00,1,24.82201126912219,22.673233599001712,55.92470548258937 +2024-10-18 10:00:00,1,11.611230499492613,24.684452779983438,62.1899196755199 +2024-10-18 11:00:00,1,25.326686896306782,23.432938671693517,65.74726016983465 +2024-10-18 12:00:00,1,24.687510782304933,27.3792369438624,54.067989844412395 +2024-10-18 13:00:00,1,11.564702587296722,27.796047248074395,74.59469273489695 +2024-10-18 14:00:00,1,46.68705028987321,17.81386507176793,48.82398318283028 +2024-10-18 15:00:00,1,21.802597074959486,28.462772002036818,37.365045684963476 +2024-10-18 16:00:00,1,24.445905230058816,23.088449917184175,41.71241933796287 +2024-10-18 17:00:00,1,25.50825114550963,20.974981567566896,61.92836787103561 +2024-10-18 18:00:00,1,37.11906662480468,27.305074935736478,54.08126072397941 +2024-10-18 19:00:00,1,27.173473010390314,22.77735799324445,55.75675558476259 +2024-10-18 20:00:00,1,29.209179554234204,26.88407406579386,69.42484555984615 +2024-10-18 21:00:00,1,35.46574941369343,21.51673400026679,51.47536977575011 +2024-10-18 22:00:00,1,26.388453865748325,16.369015469636544,52.992103075381536 +2024-10-18 23:00:00,1,13.85379187329988,28.746352326803652,59.77756644201396 +2024-10-19 00:00:00,1,12.511930635495624,25.343057553778465,50.03092664905513 +2024-10-19 01:00:00,1,24.562682695480664,26.488930484228348,57.5342591644177 +2024-10-19 02:00:00,1,24.717730452956832,25.556545869082726,67.77316705681112 +2024-10-19 03:00:00,1,40.78606759832732,20.78241256478141,53.31593744828215 +2024-10-19 04:00:00,1,31.694780234522035,23.572279578486146,66.28827297271911 +2024-10-19 05:00:00,1,19.406096232464996,27.158692389564912,37.27976093991502 +2024-10-19 06:00:00,1,21.743618213907872,26.394279848414694,63.320184083125504 +2024-10-19 07:00:00,1,17.879535895063846,26.87776921884818,41.658071578437976 +2024-10-19 08:00:00,1,28.14118266506828,16.289904054642484,53.40058116837613 +2024-10-19 09:00:00,1,20.880400260805217,21.948266449372543,65.06067181336081 +2024-10-19 10:00:00,1,21.475004879766168,23.69931842544211,55.494746455356875 +2024-10-19 11:00:00,1,36.89109163286439,20.63153138984948,54.88873899524743 +2024-10-19 12:00:00,1,9.29635133842205,23.707332042415473,62.66189991514151 +2024-10-19 13:00:00,1,21.15422277418568,29.164738875309684,59.91165901967338 +2024-10-19 14:00:00,1,26.402869294267386,31.4827784011825,65.43924942368965 +2024-10-19 15:00:00,1,25.58883918810109,19.629018275296797,63.23372891643476 +2024-10-19 16:00:00,1,24.326289485947484,22.17502354778709,59.413144181745025 +2024-10-19 17:00:00,1,24.770977403591555,21.142083246230246,55.562848357974374 +2024-10-19 18:00:00,1,28.776282010055215,27.714416791577044,69.31416402390987 +2024-10-19 19:00:00,1,21.937370750666084,24.11772004936533,66.37387377727111 +2024-10-19 20:00:00,1,30.460879665925287,25.153239197839973,71.31716402760796 +2024-10-19 21:00:00,1,10.732804753906699,31.075867134692594,44.58857888190988 +2024-10-19 22:00:00,1,35.00139993507132,23.88444848843569,58.6552586462827 +2024-10-19 23:00:00,1,19.92114692011911,27.031944828187548,48.917428752237605 +2024-10-20 00:00:00,1,24.999140529058984,17.697411940038798,59.79907243610504 +2024-10-20 01:00:00,1,23.323783691015215,26.990846556363504,48.24818915000279 +2024-10-20 02:00:00,1,31.633332331193312,24.708613409985386,62.725382725591814 +2024-10-20 03:00:00,1,26.771027929472368,22.32926376459418,59.25580169708634 +2024-10-20 04:00:00,1,49.44294699359237,20.606221778308463,52.919285359696254 +2024-10-20 05:00:00,1,23.293467847520592,20.0153273389251,64.3200147885349 +2024-10-20 06:00:00,1,25.176849493438965,22.200166690889972,68.5102032778212 +2024-10-20 07:00:00,1,34.986843341467925,24.282309457186738,51.277128065771365 +2024-10-20 08:00:00,1,22.23586235358861,20.40807048634738,49.740883916031756 +2024-10-20 09:00:00,1,19.0132108288242,19.666441757588544,62.5785790759702 +2024-10-20 10:00:00,1,14.422300688072854,16.68348163876933,62.586909462707936 +2024-10-20 11:00:00,1,11.47018402899026,21.022483483994876,56.70183047291567 +2024-10-20 12:00:00,1,18.489526911171698,30.997157005808386,56.410570659829546 +2024-10-20 13:00:00,1,15.834876492642067,23.703749887363763,66.70650624691324 +2024-10-20 14:00:00,1,31.639990346422998,17.23700305571784,45.839920757528255 +2024-10-20 15:00:00,1,25.903615211985013,33.28088218960076,59.18261448015499 +2024-10-20 16:00:00,1,11.736285748795154,29.89932225964828,65.617634097324 +2024-10-20 17:00:00,1,34.5605946552354,22.651893030367162,47.19721619334992 +2024-10-20 18:00:00,1,22.997962760084754,19.650679494906864,85.84161559389857 +2024-10-20 19:00:00,1,14.047919285180285,26.215873616341323,52.10965411361431 +2024-10-20 20:00:00,1,24.518220036380875,18.27505126756612,74.22049788916287 +2024-10-20 21:00:00,1,22.7099007570905,19.87507588754337,46.579142309486045 +2024-10-20 22:00:00,1,16.283118477326063,26.025823688330664,59.546795380516976 +2024-10-20 23:00:00,1,24.976409156422033,17.428505066385455,55.1543652186421 +2024-10-21 00:00:00,1,30.625954495355867,23.029877840473862,51.970793002376936 +2024-10-21 01:00:00,1,30.91776067631484,21.477197546830865,80.12589069510236 +2024-10-21 02:00:00,1,39.5091408023765,19.3679366616049,50.59566636450589 +2024-10-21 03:00:00,1,37.3381394283325,20.881603647090778,40.96221907938413 +2024-10-21 04:00:00,1,15.337817509935773,25.624814082591207,43.90645497641669 +2024-10-21 05:00:00,1,22.524063561130475,28.726907136181158,55.54223473744595 +2024-10-21 06:00:00,1,23.915621660783625,23.885245192182442,51.51664505406274 +2024-10-21 07:00:00,1,28.377462963488853,20.90542912372785,56.78092806186832 +2024-10-21 08:00:00,1,31.810432179215027,26.19010147506929,55.92766171060768 +2024-10-21 09:00:00,1,24.52472371931752,23.033224502501124,59.374359369546575 +2024-10-21 10:00:00,1,14.89986436863614,19.12550744804149,31.941709577541165 +2024-10-21 11:00:00,1,37.092219672250366,22.21744978787295,43.39565712496555 +2024-10-21 12:00:00,1,27.94482423016472,24.97931143044742,56.531416570006414 +2024-10-21 13:00:00,1,43.352172615793705,27.54281240900685,59.67816746438804 +2024-10-21 14:00:00,1,22.9782434120865,23.870112023034192,59.97433402568776 +2024-10-21 15:00:00,1,27.4795285389268,19.421621553019712,68.35780163232707 +2024-10-21 16:00:00,1,22.772338473836193,20.198631684885957,58.5208968150832 +2024-10-21 17:00:00,1,16.73689503211645,27.177997250654734,46.06868663952349 +2024-10-21 18:00:00,1,28.70504242918387,26.39794104394424,51.62424803952406 +2024-10-21 19:00:00,1,22.703372633139754,22.2625855854871,44.380860933229144 +2024-10-21 20:00:00,1,23.887444809334934,20.287310362545725,68.60952952365366 +2024-10-21 21:00:00,1,20.851871889421147,22.61965203130785,61.08221314928968 +2024-10-21 22:00:00,1,26.638310538689623,20.963159291224475,56.817547050812195 +2024-10-21 23:00:00,1,14.889166771698173,21.719685922365066,47.08143087836295 +2024-10-22 00:00:00,1,28.94701734205625,24.23480325074131,56.32786173973874 +2024-10-22 01:00:00,1,28.366746138048804,26.90236681657145,51.60835531885246 +2024-10-22 02:00:00,1,25.230525338077463,24.570784547475412,59.328079851840094 +2024-10-22 03:00:00,1,24.97943882959476,17.316988118603803,53.044759700617554 +2024-10-22 04:00:00,1,34.24546032296185,28.55455231063223,59.835905768067256 +2024-10-22 05:00:00,1,14.318867820686751,24.13801109076057,61.48213445580749 +2024-10-22 06:00:00,1,24.19693453844229,27.667574185558976,60.2788377396458 +2024-10-22 07:00:00,1,34.30440859825221,22.686631680303385,54.13355209730766 +2024-10-22 08:00:00,1,22.39316077461075,19.64034744227311,40.569510282330675 +2024-10-22 09:00:00,1,34.33326815591019,25.44094414049104,56.56297762576953 +2024-10-22 10:00:00,1,12.35269556836657,21.56252273511238,48.70443035934101 +2024-10-22 11:00:00,1,16.85844241480194,23.92553282891843,60.22619051487701 +2024-10-22 12:00:00,1,22.268561817240087,22.914109629883725,71.98291327448666 +2024-10-22 13:00:00,1,37.18635878262185,27.211391402538656,81.56050770086921 +2024-10-22 14:00:00,1,27.95554494186197,29.83809118426995,55.35265827335046 +2024-10-22 15:00:00,1,26.928702821521952,24.804762333619152,35.24552818537722 +2024-10-22 16:00:00,1,24.368420334089905,25.77571074838668,50.52223341450486 +2024-10-22 17:00:00,1,37.94635625784317,27.262587482240505,52.87155733806412 +2024-10-22 18:00:00,1,24.401677936246422,25.954345604165333,63.38833922233326 +2024-10-22 19:00:00,1,38.04222548009909,21.592781536997073,58.377429821032955 +2024-10-22 20:00:00,1,30.1153859474454,20.61612925901009,63.32269262014588 +2024-10-22 21:00:00,1,32.44469993727837,21.366622675295233,54.4283405564728 +2024-10-22 22:00:00,1,25.23737580290964,23.757767019714123,65.18852881863724 +2024-10-22 23:00:00,1,14.739595474493306,18.784045875723898,54.567573329906665 +2024-10-23 00:00:00,1,38.358079780238725,21.470284459560858,51.14743916052692 +2024-10-23 01:00:00,1,24.605262848603502,28.012585689664405,44.17530863646375 +2024-10-23 02:00:00,1,36.24216694739044,24.657051374896024,25.38668292783506 +2024-10-23 03:00:00,1,29.697647095719944,23.869278853349773,52.683433999123636 +2024-10-23 04:00:00,1,22.194002045066835,22.4131046654369,58.79286062851034 +2024-10-23 05:00:00,1,14.538130553613078,24.409088062668836,69.93650833561571 +2024-10-23 06:00:00,1,19.14030396777614,20.345987496564184,57.753106512420466 +2024-10-23 07:00:00,1,12.164933968076662,22.127580387517632,62.08109507664055 +2024-10-23 08:00:00,1,29.047925964102088,23.767012347679845,38.40384932554114 +2024-10-23 09:00:00,1,22.933149100619286,20.856164436574375,50.38877462703955 +2024-10-23 10:00:00,1,35.56771011494735,26.85978559483371,22.589788093494413 +2024-10-23 11:00:00,1,17.684128049013673,24.568611853778716,41.598995545586334 +2024-10-23 12:00:00,1,12.340954192168011,30.42226302384318,53.83481536231674 +2024-10-23 13:00:00,1,34.28331877054123,29.364391818525284,51.31873711016546 +2024-10-23 14:00:00,1,16.47784349878333,26.15120186195454,44.02713549217155 +2024-10-23 15:00:00,1,26.992364652529005,22.96949386526997,54.22624916499516 +2024-10-23 16:00:00,1,27.088112846537335,24.66460007350648,62.320189515379525 +2024-10-23 17:00:00,1,28.31418627189973,22.726870725373754,56.82859073853722 +2024-10-23 18:00:00,1,20.1037265315293,22.020555470736515,52.760412466052486 +2024-10-23 19:00:00,1,33.95098717830619,22.480203497787556,66.34212371897856 +2024-10-23 20:00:00,1,25.601709238789567,22.067781286212856,66.82486604320665 +2024-10-23 21:00:00,1,34.4873790069616,28.64861295422027,47.86509982291264 +2024-10-23 22:00:00,1,29.96260879875519,22.340693675616077,64.78966360862113 +2024-10-23 23:00:00,1,21.574546621508134,25.586614050587723,61.10239392719647 +2024-10-24 00:00:00,1,27.383707626627018,23.410755948440226,39.092905210749336 +2024-10-24 01:00:00,1,32.358420062490126,25.58227891248407,44.675276491859535 +2024-10-24 02:00:00,1,30.89856082154134,22.705161518931412,60.054808363223565 +2024-10-24 03:00:00,1,28.049724944155095,23.04067790184032,51.48226060405932 +2024-10-24 04:00:00,1,30.634350307391937,21.82707509343186,68.46249831987717 +2024-10-24 05:00:00,1,11.930146139947645,27.726302341844846,44.81435194702866 +2024-10-24 06:00:00,1,22.338896049150016,30.90527863761435,42.79786578100136 +2024-10-24 07:00:00,1,18.754845217802263,20.92515707030511,58.17942891531256 +2024-10-24 08:00:00,1,33.82318392957964,21.33288113092517,66.41616910798363 +2024-10-24 09:00:00,1,23.342809625716296,22.728715086006368,36.285950304084295 +2024-10-24 10:00:00,1,19.00338765950952,21.995450477986374,48.60755317550851 +2024-10-24 11:00:00,1,20.504275164643865,25.29909606010469,58.93717993386119 +2024-10-24 12:00:00,1,15.476758822409373,22.78702524367385,59.45527846273115 +2024-10-24 13:00:00,1,32.419157824385806,29.963014276090693,56.30843069026193 +2024-10-24 14:00:00,1,31.866283632019343,28.28755723202729,52.90365242188169 +2024-10-24 15:00:00,1,21.216044871471116,21.5678792984666,54.35415057459284 +2024-10-24 16:00:00,1,29.2793428251652,20.16215068259192,57.60487764510687 +2024-10-24 17:00:00,1,29.939806481621115,24.481057578970137,57.10669838010704 +2024-10-24 18:00:00,1,27.599154945639633,21.502148774503237,61.589467533856734 +2024-10-24 19:00:00,1,26.659553113548014,24.01412816369365,66.93649249144865 +2024-10-24 20:00:00,1,18.96465242674072,23.006363651298976,75.62921638043665 +2024-10-24 21:00:00,1,21.361441868551303,26.716520414076832,58.65625562093164 +2024-10-24 22:00:00,1,27.322767574765344,24.427344145969496,63.46071379360681 +2024-10-24 23:00:00,1,38.31435260545752,25.469734619957027,41.34359770720308 +2024-10-25 00:00:00,1,31.955929573202255,22.446561037003367,37.092558238299716 +2024-10-25 01:00:00,1,19.481151924896114,24.701046258358904,48.61277866672444 +2024-10-25 02:00:00,1,37.29680846838856,26.18935696912391,48.762356002895686 +2024-10-25 03:00:00,1,28.859831643217742,27.720017095383607,61.35580588323614 +2024-10-25 04:00:00,1,21.95639657161422,24.808172479524963,60.01520329805012 +2024-10-25 05:00:00,1,17.262201233752112,26.596735839115958,60.104583471737094 +2024-10-25 06:00:00,1,34.76709278742159,30.082253300912598,58.08243595473103 +2024-10-25 07:00:00,1,20.211212418740892,21.917926317968586,50.663178882082654 +2024-10-25 08:00:00,1,31.860999221987623,26.35685074578378,62.67961518661447 +2024-10-25 09:00:00,1,20.15275494492252,28.529700008579454,44.46348680551243 +2024-10-25 10:00:00,1,15.518580048031158,24.17743414647268,41.56641138517142 +2024-10-25 11:00:00,1,19.058706275865887,23.596979506111424,45.99682947280522 +2024-10-25 12:00:00,1,23.788088347398705,26.70695728341752,48.66813438971287 +2024-10-25 13:00:00,1,33.15011430987581,24.670926416017068,50.77307505595221 +2024-10-25 14:00:00,1,30.511426510286157,17.617342036807518,71.07327745100766 +2024-10-25 15:00:00,1,30.108103400702923,25.987685655701117,56.28747833436803 +2024-10-25 16:00:00,1,41.978494448849204,25.131243319246565,47.46720765604306 +2024-10-25 17:00:00,1,27.73518672569564,23.986553354702725,61.1526467629686 +2024-10-25 18:00:00,1,30.933339028226154,25.73199644165977,53.092072120244495 +2024-10-25 19:00:00,1,32.98262870316914,27.553903104733585,58.810566496138435 +2024-10-25 20:00:00,1,34.26467433788915,21.926702277839734,68.84722180564272 +2024-10-25 21:00:00,1,22.890102805989763,28.129314446857762,65.57475264804687 +2024-10-25 22:00:00,1,26.836842692715294,23.37805926239158,57.36241776858325 +2024-10-25 23:00:00,1,28.940090219652113,26.869839902036777,46.001766491294916 +2024-10-26 00:00:00,1,36.67127420033326,25.48179039908199,54.18653810192347 +2024-10-26 01:00:00,1,18.841767986130677,24.50144141320874,56.78938204024544 +2024-10-26 02:00:00,1,27.14112773384087,27.191638976129962,55.86099398879824 +2024-10-26 03:00:00,1,28.68326111368308,18.43187639580903,48.393021684148195 +2024-10-26 04:00:00,1,28.93535960271645,25.640829525205227,41.88247785132438 +2024-10-26 05:00:00,1,36.76873041167724,24.490656955220174,56.635415474733094 +2024-10-26 06:00:00,1,30.464115496487093,25.415611123093104,78.41260787719291 +2024-10-26 07:00:00,1,24.93168714436777,24.1870368898921,45.23165232845748 +2024-10-26 08:00:00,1,31.494723020584118,21.16333227333905,48.12419898350147 +2024-10-26 09:00:00,1,19.92903225533376,24.50723751392546,46.9530231690032 +2024-10-26 10:00:00,1,30.385161797652785,20.40449936160877,58.31980005920637 +2024-10-26 11:00:00,1,18.935740313635126,24.79822829985843,64.95007507172576 +2024-10-26 12:00:00,1,24.500766552255826,17.985285366575603,53.35567228199464 +2024-10-26 13:00:00,1,18.23259482755519,22.432258420619927,75.17806965041973 +2024-10-26 14:00:00,1,29.02199874467681,22.652855514590325,47.12819190546618 +2024-10-26 15:00:00,1,22.167247177359307,24.789648497515234,70.40848217478441 +2024-10-26 16:00:00,1,30.630342188380904,24.603814028351334,47.98609339512517 +2024-10-26 17:00:00,1,16.72212687000058,24.696168853818403,58.664424182171174 +2024-10-26 18:00:00,1,30.693818502513686,28.99306057221967,53.04468839091141 +2024-10-26 19:00:00,1,23.17741925761614,22.831070286787114,55.2656071753136 +2024-10-26 20:00:00,1,10.194017640344388,25.209731586981498,49.590923161430936 +2024-10-26 21:00:00,1,33.37303429511269,19.65199120137038,58.21276620314057 +2024-10-26 22:00:00,1,26.051587931857576,26.706195032201915,51.18678635077001 +2024-10-26 23:00:00,1,25.185599676535844,18.239214880435156,48.934741320269026 +2024-10-27 00:00:00,1,10.582431225869788,23.436621354582414,49.008813769136694 +2024-10-27 01:00:00,1,14.15268137580667,27.35867564434183,51.39651014873927 +2024-10-27 02:00:00,1,10.021843675267032,23.54882708095224,68.51725161704435 +2024-10-27 03:00:00,1,19.870844322748628,25.656523711653165,47.35228009475982 +2024-10-27 04:00:00,1,21.17192065960107,26.39596019964308,69.27399688759454 +2024-10-27 05:00:00,1,32.493363559012224,23.52332736812514,54.25472052497873 +2024-10-27 06:00:00,1,21.187890526670852,18.964093918083204,66.77299477806763 +2024-10-27 07:00:00,1,42.67494252432307,15.666088490536477,53.71522371726189 +2024-10-27 08:00:00,1,21.355422264816966,27.39851955116312,66.82893927075312 +2024-10-27 09:00:00,1,7.536097142057542,26.44263980842727,68.99054817987518 +2024-10-27 10:00:00,1,21.055876616424182,21.466281487764594,63.91474608760034 +2024-10-27 11:00:00,1,16.161200770465122,27.12599423156103,49.05064135527307 +2024-10-27 12:00:00,1,20.55742281641296,14.253780061586463,53.48239577555515 +2024-10-27 13:00:00,1,27.43632238802846,21.65099904645781,72.43246771176324 +2024-10-27 14:00:00,1,20.911313056990306,25.92175185894708,48.98429054960481 +2024-10-27 15:00:00,1,36.06618932446341,20.0597147732618,67.79418600780019 +2024-10-27 16:00:00,1,14.52922711479014,23.75266885243158,50.645324271501764 +2024-10-27 17:00:00,1,27.93330677253648,31.30176659214519,43.794525581497616 +2024-10-27 18:00:00,1,30.25816795307722,20.40044496407222,64.16326160129653 +2024-10-27 19:00:00,1,33.28204970705,25.190716601454213,47.7248143835347 +2024-10-27 20:00:00,1,8.490766965931776,22.712435704410144,57.81568330229852 +2024-10-27 21:00:00,1,29.46563907739949,19.393825751861122,59.725462547393505 +2024-10-27 22:00:00,1,42.015676308762636,20.454117976266865,66.04063245569503 +2024-10-27 23:00:00,1,17.486626276713444,26.359051041852744,67.62308291164823 +2024-10-28 00:00:00,1,30.125870520728945,24.680873701291976,57.609893638064406 +2024-10-28 01:00:00,1,30.775703363540387,19.14294567725365,59.71767941384948 +2024-10-28 02:00:00,1,25.300131373786826,30.75841073558572,64.26478622768921 +2024-10-28 03:00:00,1,26.08690851126677,16.587091261477443,59.74201379050323 +2024-10-28 04:00:00,1,30.117659485717738,25.400758891227852,62.064892667336906 +2024-10-28 05:00:00,1,35.68853535228798,21.510979929159337,55.99463153561295 +2024-10-28 06:00:00,1,23.428191276473335,19.034327415921354,53.667961653120074 +2024-10-28 07:00:00,1,20.209610359800912,15.9182704142802,53.687138705351074 +2024-10-28 08:00:00,1,15.26462225592496,15.683052690620753,30.230999112824126 +2024-10-28 09:00:00,1,26.574854040667432,27.067150525868644,78.03509678211175 +2024-10-28 10:00:00,1,17.65275606801318,23.520769451615074,43.39754830894422 +2024-10-28 11:00:00,1,27.16302103417775,24.908561073758694,66.21659028951696 +2024-10-28 12:00:00,1,19.772830097841155,23.85975057254066,48.033188705999805 +2024-10-28 13:00:00,1,26.294570254156906,22.974934920525286,60.5758688462566 +2024-10-28 14:00:00,1,27.829131674310787,21.31414117517012,56.09041592713658 +2024-10-28 15:00:00,1,22.919581565052276,17.625917125425886,61.04885047584823 +2024-10-28 16:00:00,1,34.516328144127606,28.068088987350045,44.1468496507238 +2024-10-28 17:00:00,1,27.026397109558488,26.00130077845813,78.24491193987394 +2024-10-28 18:00:00,1,13.465159145229489,21.64670003266254,48.934727564289076 +2024-10-28 19:00:00,1,16.444487855181713,23.818876493773576,67.33298714044238 +2024-10-28 20:00:00,1,29.244779098449264,21.041570187115347,67.85189591289868 +2024-10-28 21:00:00,1,32.4957127831994,23.821474229872102,55.067985158805975 +2024-10-28 22:00:00,1,19.31753550822893,23.49100306656868,58.40796388216163 +2024-10-28 23:00:00,1,24.545005504630893,23.05387088342966,53.21932321668546 +2024-10-29 00:00:00,1,18.086237421803286,27.1396275370916,42.31428187224781 +2024-10-29 01:00:00,1,30.199469646000644,22.329697126589913,54.1167768822215 +2024-10-29 02:00:00,1,15.160048156245676,17.541552883971264,73.6479515784972 +2024-10-29 03:00:00,1,27.370635983359133,24.732351157192085,58.88598403515465 +2024-10-29 04:00:00,1,22.29452780268984,23.087232995153173,32.10730191348169 +2024-10-29 05:00:00,1,18.545845362537342,27.14118806266991,54.12038734326279 +2024-10-29 06:00:00,1,15.046652253673876,22.905895807593478,68.812982195309 +2024-10-29 07:00:00,1,13.962047013501403,26.459102352431618,58.38116263867429 +2024-10-29 08:00:00,1,25.011082723220326,26.819561195400034,72.1512158973037 +2024-10-29 09:00:00,1,19.58094072255298,23.39591219831149,55.52671079561198 +2024-10-29 10:00:00,1,10.555565211371736,25.13142087312029,48.2014931950057 +2024-10-29 11:00:00,1,21.453222805778413,15.611573386010383,66.33984145162975 +2024-10-29 12:00:00,1,18.517955985555243,24.91042273067073,60.36999420949414 +2024-10-29 13:00:00,1,17.38302883965551,24.691252130438897,58.91470454858671 +2024-10-29 14:00:00,1,18.918416040563688,24.424313793048814,63.61986796005169 +2024-10-29 15:00:00,1,33.476896292175226,25.78355827064527,39.421999141283955 +2024-10-29 16:00:00,1,29.359362736818852,20.203144675628135,48.29074567557173 +2024-10-29 17:00:00,1,37.558538467242215,24.20835350361794,61.21661952723934 +2024-10-29 18:00:00,1,18.036211613895517,27.55477107135531,45.280947003395184 +2024-10-29 19:00:00,1,31.494378710062605,22.800397315974497,56.276757960937694 +2024-10-29 20:00:00,1,34.006061650165876,25.141018807462025,54.96872424599901 +2024-10-29 21:00:00,1,30.72835307190887,26.407737565121497,64.07427571043789 +2024-10-29 22:00:00,1,16.827158898009174,20.496714187400705,60.15896154993677 +2024-10-29 23:00:00,1,6.04553830018612,19.812169692522858,65.18762035247161 +2024-10-30 00:00:00,1,28.834169856893865,24.691010575319538,54.690918139158185 +2024-10-30 01:00:00,1,24.85017028359269,27.34324782881011,67.6192110007969 +2024-10-30 02:00:00,1,33.85057176553039,22.3020871057319,56.02598698592526 +2024-10-30 03:00:00,1,25.651228499566,27.089962007516906,50.60781716922717 +2024-10-30 04:00:00,1,35.80634241835914,20.753281032621796,65.88459658416579 +2024-10-30 05:00:00,1,10.877742588715652,33.16743769541674,50.63272521036052 +2024-10-30 06:00:00,1,30.746606654991382,19.35684813575447,50.31015564254749 +2024-10-30 07:00:00,1,5.777768833493347,13.943972159694004,39.182405188590465 +2024-10-30 08:00:00,1,22.448619898657036,25.728613905193257,66.99337148598457 +2024-10-30 09:00:00,1,24.446280682003735,29.279461430242748,46.70568659947992 +2024-10-30 10:00:00,1,26.10389171898495,22.758029841296324,37.0373169621012 +2024-10-30 11:00:00,1,22.024708705652344,23.198278366912835,50.63694117234505 +2024-10-30 12:00:00,1,22.506525267871275,25.357526446054045,62.46203939161021 +2024-10-30 13:00:00,1,16.830736521003395,25.259633548654755,58.23597385686277 +2024-10-30 14:00:00,1,14.978361382857393,25.200956990040776,59.809534970335356 +2024-10-30 15:00:00,1,1.7539910676899524,20.21899428982294,50.76455400362981 +2024-10-30 16:00:00,1,38.935213950503936,26.187972609360436,50.186925093104634 +2024-10-30 17:00:00,1,26.90403131818065,27.513009660037756,53.58457608384831 +2024-10-30 18:00:00,1,16.25247578726204,24.801429887888478,61.472075545313544 +2024-10-30 19:00:00,1,16.392858006741527,23.90716082859974,49.393060667453106 +2024-10-30 20:00:00,1,28.579215010675778,26.096801299584648,66.45094146956241 +2024-10-30 21:00:00,1,38.0938816688309,20.56457220551982,57.269080891410056 +2024-10-30 22:00:00,1,29.848807187495748,20.128508258381373,60.49436920906416 +2024-10-30 23:00:00,1,9.113840835526798,25.57808812645029,56.56244725594661 +2024-10-31 00:00:00,1,16.648653083719374,23.798178922536156,56.124397900012355 +2024-10-31 01:00:00,1,39.35978614977425,19.72352671947551,51.573283999024916 +2024-10-31 02:00:00,1,30.686356659077315,23.003554547740837,69.90907837147289 +2024-10-31 03:00:00,1,21.26950580129272,27.17870285184918,62.629303434872156 +2024-10-31 04:00:00,1,28.089738412994837,31.327247292394063,59.84659941399096 +2024-10-31 05:00:00,1,15.826561923045196,25.476086887686204,62.765137612452435 +2024-10-31 06:00:00,1,23.907882404987635,21.74208683144223,52.50288062589252 +2024-10-31 07:00:00,1,19.319164483508317,27.396665713322832,53.506294659671404 +2024-10-31 08:00:00,1,32.391513232116125,28.268744927008736,55.98244864295516 +2024-10-31 09:00:00,1,23.80699661374832,26.917087722087768,58.48179689338648 +2024-10-31 10:00:00,1,15.217792544775167,27.77537347668066,41.35778155242215 +2024-10-31 11:00:00,1,25.457993029535185,21.683395154666986,41.85735615511157 +2024-10-31 12:00:00,1,16.913597385946325,29.39708998305784,49.87415269498634 +2024-10-31 13:00:00,1,27.75469359799748,23.836396379349097,66.26737114739826 +2024-10-31 14:00:00,1,18.84699370140696,22.454681177621932,46.35773438781828 +2024-10-31 15:00:00,1,16.594861169754207,18.935386388292148,58.63255057644596 +2024-10-31 16:00:00,1,20.45325104731289,24.828555201500038,50.199285097313904 +2024-10-31 17:00:00,1,29.40826050883989,26.52856519277536,37.79982287011933 +2024-10-31 18:00:00,1,13.752402265434123,22.889322763684543,64.18786100679013 +2024-10-31 19:00:00,1,28.302661036099522,21.423778834822464,68.21505148824991 +2024-10-31 20:00:00,1,16.94663368331818,23.139521195153588,51.55333068974953 +2024-10-31 21:00:00,1,25.387752153777765,19.96538822124537,63.66202131772591 +2024-10-31 22:00:00,1,20.431806723439504,17.477261534871772,60.99009494079978 +2024-10-31 23:00:00,1,32.460172181356356,24.01114211922556,61.40851501194596 +2024-11-01 00:00:00,1,33.971261292529896,26.111423416626312,43.76817853731461 +2024-11-01 01:00:00,1,22.554004000874947,28.488161770562307,47.746786889135684 +2024-11-01 02:00:00,1,20.638389013787922,19.152234047792845,64.29274267637473 +2024-11-01 03:00:00,1,23.673205947476685,22.242581130408812,55.586380281426486 +2024-11-01 04:00:00,1,26.27500019412681,19.262044341132423,64.49638125771713 +2024-11-01 05:00:00,1,21.3332583684023,25.629955640556915,55.523638423704114 +2024-11-01 06:00:00,1,29.74474906064884,30.20818576976633,49.07596648797075 +2024-11-01 07:00:00,1,30.117520626251512,22.47331755500029,50.39396564168196 +2024-11-01 08:00:00,1,25.34242831404731,26.227276954375903,60.63594970771684 +2024-11-01 09:00:00,1,30.29526077497529,25.370707446746337,35.7939733031752 +2024-11-01 10:00:00,1,28.146950236965267,26.121159027703268,61.52956130528556 +2024-11-01 11:00:00,1,19.61311790303252,22.344491294671677,46.16466304994461 +2024-11-01 12:00:00,1,15.909514871013865,22.56899919389828,41.209718530223284 +2024-11-01 13:00:00,1,33.72869500303143,19.265942089539475,63.50722779442248 +2024-11-01 14:00:00,1,22.357483204916623,22.492097253667858,48.49731816323643 +2024-11-01 15:00:00,1,26.718341610578726,25.63171649583391,46.1896233849494 +2024-11-01 16:00:00,1,5.955459330181029,20.306737303041803,58.085397422091575 +2024-11-01 17:00:00,1,10.798497166939953,19.713908353523653,69.20320603284222 +2024-11-01 18:00:00,1,19.658649676968228,27.207142951629322,50.267212409652124 +2024-11-01 19:00:00,1,32.70566845659902,24.64855774466646,44.41675843051207 +2024-11-01 20:00:00,1,19.399057211208465,20.940665283462646,50.97144291455575 +2024-11-01 21:00:00,1,44.48087054916013,27.659060218569252,48.04201774490733 +2024-11-01 22:00:00,1,25.135930021474568,27.86144935455948,42.32733904422221 +2024-11-01 23:00:00,1,28.061113062200096,25.115721983927696,55.799209548231396 +2024-11-02 00:00:00,1,26.458482614168496,27.624841795732525,70.00147304024743 +2024-11-02 01:00:00,1,34.33358347690132,16.41971298370667,50.8447684405978 +2024-11-02 02:00:00,1,33.60103090474811,29.96895160714231,62.864393363731025 +2024-11-02 03:00:00,1,38.45983863699708,26.811038053151155,41.89013132217185 +2024-11-02 04:00:00,1,20.83194727948227,22.498173717670433,57.74541646924558 +2024-08-04 05:00:00,2,25.159263311077424,27.10024238742026,65.11032704097497 +2024-08-04 06:00:00,2,21.203552023093973,30.97878511169887,35.917045114464415 +2024-08-04 07:00:00,2,31.492872545698592,28.82488608544562,48.69754897478913 +2024-08-04 08:00:00,2,30.37841298451656,26.348934366470232,43.01674289180548 +2024-08-04 09:00:00,2,32.06876908643145,29.058398581369552,49.71196650869693 +2024-08-04 10:00:00,2,23.37342041937378,23.192661527522432,50.231680317680464 +2024-08-04 11:00:00,2,15.736759005130153,28.305492523371154,65.06105924539649 +2024-08-04 12:00:00,2,23.21989555907106,21.34982742013364,60.43040604971599 +2024-08-04 13:00:00,2,18.678129031088528,19.71990259871064,62.94400704490884 +2024-08-04 14:00:00,2,7.033026692532367,30.03003960570786,61.22010592700489 +2024-08-04 15:00:00,2,28.479520703644727,23.456594827836923,44.74804311512824 +2024-08-04 16:00:00,2,30.631160709903227,20.914148559527234,47.833517423494236 +2024-08-04 17:00:00,2,22.148282001191617,24.897038625491028,45.16127758341051 +2024-08-04 18:00:00,2,42.73680359907622,31.880257530643995,40.48221304263471 +2024-08-04 19:00:00,2,9.144947223597006,20.185992957209592,54.52706138280959 +2024-08-04 20:00:00,2,32.54540298771463,27.430231165563214,43.03239694178254 +2024-08-04 21:00:00,2,28.423917180377035,20.749901029388763,57.76563512536809 +2024-08-04 22:00:00,2,29.514071472977022,26.895063890449173,56.54807686561151 +2024-08-04 23:00:00,2,29.1131145463081,27.75865917007054,58.854212573321576 +2024-08-05 00:00:00,2,37.823623151540374,24.845718505271012,54.39318489928141 +2024-08-05 01:00:00,2,29.194656487407887,26.832981962707557,46.43752424408811 +2024-08-05 02:00:00,2,27.919350725576617,25.875515145991915,50.535309124112445 +2024-08-05 03:00:00,2,34.60396947885524,25.243204131138025,77.40584217416465 +2024-08-05 04:00:00,2,38.29629185365275,22.94902781659459,44.01901687166203 +2024-08-05 05:00:00,2,42.97367338135454,26.23511016593336,55.07703084183282 +2024-08-05 06:00:00,2,13.920694486317787,17.941830591255787,46.65023006818642 +2024-08-05 07:00:00,2,29.89650638712663,23.274039739893055,47.93545460755723 +2024-08-05 08:00:00,2,39.24623750240955,28.09030105252848,46.81822194590722 +2024-08-05 09:00:00,2,21.927174727272888,29.07285346495845,35.48880092035262 +2024-08-05 10:00:00,2,32.521908157233916,20.9577577651094,50.63473445116144 +2024-08-05 11:00:00,2,26.897159187426094,28.25894608957377,35.23500626172941 +2024-08-05 12:00:00,2,20.788893945861926,19.056111993888834,54.099625496259236 +2024-08-05 13:00:00,2,21.177431562116897,25.62908996528135,56.439146458101334 +2024-08-05 14:00:00,2,20.700551068416246,28.33210206871806,66.72085571365812 +2024-08-05 15:00:00,2,29.122998778315697,29.602982450664523,56.97687435079756 +2024-08-05 16:00:00,2,33.881204990624454,25.26223099372028,59.962677008017735 +2024-08-05 17:00:00,2,26.516380007635473,25.47147014044028,51.38940103390361 +2024-08-05 18:00:00,2,37.843585961704754,25.220395224192067,69.22128898245239 +2024-08-05 19:00:00,2,17.03135474770729,26.64914936722366,49.66565348388413 +2024-08-05 20:00:00,2,23.418380208719274,26.320749764283516,54.008691503005835 +2024-08-05 21:00:00,2,35.67089438417112,24.898909058558292,52.07441226384147 +2024-08-05 22:00:00,2,28.751288457978063,22.077623766217293,39.790769392653615 +2024-08-05 23:00:00,2,21.38669012982069,22.810111628881536,54.40789482606623 +2024-08-06 00:00:00,2,16.182196620231544,21.522847994674162,64.54283645302253 +2024-08-06 01:00:00,2,27.19854989001458,19.855071541229336,57.44619445725253 +2024-08-06 02:00:00,2,35.49438793578275,22.35471371833281,51.23194611379991 +2024-08-06 03:00:00,2,30.989409117810755,22.708539673649003,57.21506483518393 +2024-08-06 04:00:00,2,37.50716192321702,24.781651895682927,54.85683009776637 +2024-08-06 05:00:00,2,46.3964982821564,20.779313414506202,68.56534473865682 +2024-08-06 06:00:00,2,34.591833500626784,21.159770034763465,47.17061488166644 +2024-08-06 07:00:00,2,20.27256018068118,22.229311850198922,43.194833923723465 +2024-08-06 08:00:00,2,23.876756261698596,22.784126627345564,44.36181288327666 +2024-08-06 09:00:00,2,28.206752736987784,25.880838727582297,47.19265251432474 +2024-08-06 10:00:00,2,32.490157984057525,23.56555621285901,46.27090681703733 +2024-08-06 11:00:00,2,37.167875812426075,23.40456319042727,40.3473289271075 +2024-08-06 12:00:00,2,25.892372127691935,26.57422734710176,55.39647496738203 +2024-08-06 13:00:00,2,36.22317820379143,25.513494346260064,60.39790736112025 +2024-08-06 14:00:00,2,37.04838296079069,24.3980728246906,58.480505978410406 +2024-08-06 15:00:00,2,13.421350404056872,21.137297044618524,54.25400341348523 +2024-08-06 16:00:00,2,22.784262843875574,23.78016568660938,46.6590547977377 +2024-08-06 17:00:00,2,29.690662499945,24.183106538601653,52.378059677074155 +2024-08-06 18:00:00,2,18.687493957040644,24.218181747718617,47.98318156330042 +2024-08-06 19:00:00,2,23.994272522744552,25.779191506333287,50.56837254143432 +2024-08-06 20:00:00,2,19.64066390000487,27.167558880797863,56.88732957532582 +2024-08-06 21:00:00,2,28.781234406578054,17.199036771514933,58.737379791070836 +2024-08-06 22:00:00,2,28.171857174585544,24.793321478481833,49.26954704702689 +2024-08-06 23:00:00,2,24.822031253930994,29.250254608115704,59.2438921258123 +2024-08-07 00:00:00,2,27.16181019712507,23.34295363956579,63.98305412584126 +2024-08-07 01:00:00,2,42.74051237253765,23.005508247064064,44.74998515125337 +2024-08-07 02:00:00,2,36.69105844013573,15.586624000945164,58.09166709182236 +2024-08-07 03:00:00,2,29.042058653984583,20.17598354120867,52.99541940170646 +2024-08-07 04:00:00,2,32.46174915680959,20.41147727738229,51.85977920743874 +2024-08-07 05:00:00,2,39.26353463503867,22.050325077318277,54.834711633823574 +2024-08-07 06:00:00,2,31.000046356381826,22.21250267459661,54.56253796643206 +2024-08-07 07:00:00,2,44.77074669794454,24.229473159290986,46.102949851231706 +2024-08-07 08:00:00,2,38.46351097189074,27.948060286637503,59.77148643891649 +2024-08-07 09:00:00,2,26.010605263780363,23.5633437456607,52.88445987768797 +2024-08-07 10:00:00,2,52.728672669299584,30.675979013574164,72.38783495307446 +2024-08-07 11:00:00,2,22.867848872256104,24.78534408200135,50.52433699403947 +2024-08-07 12:00:00,2,34.41272035334839,26.871754794070917,58.395475767397826 +2024-08-07 13:00:00,2,33.9629340981747,23.20557019055203,41.071789803497666 +2024-08-07 14:00:00,2,7.830434486764325,24.61509929381649,67.70263655079054 +2024-08-07 15:00:00,2,21.328392453569062,25.49526059649817,42.960585306902956 +2024-08-07 16:00:00,2,29.18887086071146,24.593288946126496,47.36534064109106 +2024-08-07 17:00:00,2,11.191100390010574,21.91327915380548,52.7633755090755 +2024-08-07 18:00:00,2,27.35654625168667,20.477834157137192,48.56064877981768 +2024-08-07 19:00:00,2,16.07583552965093,25.55408480040421,53.06082129704387 +2024-08-07 20:00:00,2,18.190441026702516,21.524557351186097,43.91666994889057 +2024-08-07 21:00:00,2,30.742212301481736,17.630292135880175,58.40391259223463 +2024-08-07 22:00:00,2,31.090694413927363,27.03673207225558,60.275642257967384 +2024-08-07 23:00:00,2,28.836771181589903,20.841061469666844,47.904929115430015 +2024-08-08 00:00:00,2,23.900779625842695,24.63841425369703,56.05398087869727 +2024-08-08 01:00:00,2,43.967525718864856,16.91058740196961,51.85651614473469 +2024-08-08 02:00:00,2,43.3616038379985,26.412686911724467,53.28008486977965 +2024-08-08 03:00:00,2,50.48824027803555,29.52152249386489,50.07368238698605 +2024-08-08 04:00:00,2,36.4856559019423,24.898415629168973,49.48758399615765 +2024-08-08 05:00:00,2,26.26410787023492,22.523374827426558,45.51964618494037 +2024-08-08 06:00:00,2,21.33921482564626,28.867166345245803,50.3032368976533 +2024-08-08 07:00:00,2,22.0592393767675,22.330618666580868,50.81098467183794 +2024-08-08 08:00:00,2,30.38158081780653,27.92260781593004,44.33650812975947 +2024-08-08 09:00:00,2,12.633439233054197,25.807961064412876,58.63958582758554 +2024-08-08 10:00:00,2,26.448659224199133,30.33751954617372,71.61184897282622 +2024-08-08 11:00:00,2,25.10960104368254,24.088209273344273,34.607275296217985 +2024-08-08 12:00:00,2,27.695867838120776,29.44390216310642,51.08926401770567 +2024-08-08 13:00:00,2,19.015577136918793,22.080708615156862,41.012899081358626 +2024-08-08 14:00:00,2,23.893270837820864,23.432834360023094,70.6047545196586 +2024-08-08 15:00:00,2,27.753810124519553,28.27257166115635,57.92137564900417 +2024-08-08 16:00:00,2,31.83917526586168,29.22791473959964,47.087007690919194 +2024-08-08 17:00:00,2,33.732667982679345,19.064250267823972,58.82239474768336 +2024-08-08 18:00:00,2,18.53751532967911,26.86393335841953,49.96408929654666 +2024-08-08 19:00:00,2,38.57691816782849,22.291753775999425,65.6804466161873 +2024-08-08 20:00:00,2,22.88900646722256,25.794798033813837,74.54453259480727 +2024-08-08 21:00:00,2,34.19181292247211,21.074767508870504,62.96148577015027 +2024-08-08 22:00:00,2,24.7008408559192,23.238626017870427,74.62045917217438 +2024-08-08 23:00:00,2,34.97663105984028,24.315272197937432,51.96504151156273 +2024-08-09 00:00:00,2,27.5031451368623,22.545409563039357,58.72979033325751 +2024-08-09 01:00:00,2,20.05960610271766,26.127373099551924,51.485767732940545 +2024-08-09 02:00:00,2,16.49625953438493,23.70722868892201,78.71030190625495 +2024-08-09 03:00:00,2,23.174384333306744,24.10431074894612,69.57811238933192 +2024-08-09 04:00:00,2,40.10853857002268,27.616563967935505,54.51339640633296 +2024-08-09 05:00:00,2,27.829863338327737,23.498486840103578,55.03205967740842 +2024-08-09 06:00:00,2,40.092798913794674,13.662244708789887,57.745806530982264 +2024-08-09 07:00:00,2,32.28911048021069,24.19148085205495,62.92735020255495 +2024-08-09 08:00:00,2,41.61755224257861,24.808635385635615,59.547603373965 +2024-08-09 09:00:00,2,29.834210566504996,24.193039509869042,49.99725595425746 +2024-08-09 10:00:00,2,40.91041003497595,21.11248783977918,56.41311231336643 +2024-08-09 11:00:00,2,17.077502260298374,27.138760016197676,31.384290581435295 +2024-08-09 12:00:00,2,25.211144708863905,25.68081016332393,46.641708053864406 +2024-08-09 13:00:00,2,14.263554755415125,26.08598089366546,36.51095242878485 +2024-08-09 14:00:00,2,47.82514638530351,28.710566873187105,67.78864429665894 +2024-08-09 15:00:00,2,18.604752619346936,18.466659524087124,58.14188960036409 +2024-08-09 16:00:00,2,27.052634733316935,32.7996283453717,54.55710352793148 +2024-08-09 17:00:00,2,21.98544035634386,20.017518098653074,38.93020975437426 +2024-08-09 18:00:00,2,19.411210659142302,20.94955752542789,76.234335699217 +2024-08-09 19:00:00,2,26.895010519801016,23.760026755229024,51.75636939792793 +2024-08-09 20:00:00,2,29.537665839119512,26.84108185749966,41.22859058441779 +2024-08-09 21:00:00,2,39.690675898840546,30.683331303669807,48.79031505441617 +2024-08-09 22:00:00,2,24.782623877246962,21.770339476991367,56.29098877604724 +2024-08-09 23:00:00,2,33.24119720605444,26.059320132613955,50.67230578060292 +2024-08-10 00:00:00,2,9.312767574972138,21.929266969080675,61.515166522461584 +2024-08-10 01:00:00,2,27.897865552912272,22.956872742771896,51.759521590001675 +2024-08-10 02:00:00,2,26.4345593783624,26.699644396348553,46.35869922594052 +2024-08-10 03:00:00,2,29.33772423876996,26.36527957166678,60.6500865573707 +2024-08-10 04:00:00,2,39.556911614828365,24.80765585935786,57.77048217197156 +2024-08-10 05:00:00,2,23.739052469966364,24.31141626932263,58.04928547035164 +2024-08-10 06:00:00,2,29.352643197168153,18.81007531370329,65.3123895282044 +2024-08-10 07:00:00,2,33.780634405339256,31.519483424559986,57.328390803749684 +2024-08-10 08:00:00,2,38.76518534783364,26.88279046091588,52.10172898658749 +2024-08-10 09:00:00,2,14.556279639531056,29.055494487242072,38.60475121127514 +2024-08-10 10:00:00,2,30.752678744116416,24.899403245186665,52.60686697723368 +2024-08-10 11:00:00,2,27.00013935582726,23.436037810906544,51.4713709000262 +2024-08-10 12:00:00,2,38.18781516366978,28.025913563319712,57.288970803183844 +2024-08-10 13:00:00,2,30.143612186671717,26.495987338542953,69.29149826523062 +2024-08-10 14:00:00,2,27.876091885656315,28.212366809168945,64.32166048975745 +2024-08-10 15:00:00,2,29.403092569426164,29.570645918208147,44.08795169217776 +2024-08-10 16:00:00,2,30.76168619027424,20.74884819840797,48.4715849511204 +2024-08-10 17:00:00,2,27.452365791493897,23.005781207105773,37.54784849492407 +2024-08-10 18:00:00,2,20.21258091357197,22.50178109049918,57.99007725657387 +2024-08-10 19:00:00,2,44.12110728401414,30.823921996765417,49.17338796843808 +2024-08-10 20:00:00,2,32.42781230846518,22.693297346605767,50.166929200602986 +2024-08-10 21:00:00,2,18.994183896943923,21.07947170834392,52.44184063480784 +2024-08-10 22:00:00,2,32.374634250040444,17.32291681893937,10.450155553686578 +2024-08-10 23:00:00,2,32.403796443170485,21.17569322389779,74.69969740965033 +2024-08-11 00:00:00,2,15.6675821779174,29.35832913398943,34.925912860064514 +2024-08-11 01:00:00,2,24.624887029237776,28.6842581763482,42.2129615051846 +2024-08-11 02:00:00,2,28.63282374750302,24.318519864558713,28.431460402743046 +2024-08-11 03:00:00,2,25.96951175178031,24.952515862789213,46.689351480212764 +2024-08-11 04:00:00,2,38.90836806013252,24.995218473452148,48.90930536602414 +2024-08-11 05:00:00,2,19.664921405872665,30.157831684509546,44.981103460349686 +2024-08-11 06:00:00,2,23.266872943527392,25.03940509625175,37.82176749668049 +2024-08-11 07:00:00,2,54.653080505103205,25.682776874456593,44.39694241961966 +2024-08-11 08:00:00,2,21.199223433338382,29.06267391834575,50.83268587484609 +2024-08-11 09:00:00,2,47.11051493615511,29.196352657007733,45.82192546708478 +2024-08-11 10:00:00,2,33.38114002098806,26.84082803104303,47.122863461688894 +2024-08-11 11:00:00,2,28.21247134001093,24.49990292101867,63.51802683838609 +2024-08-11 12:00:00,2,44.51150270453434,27.65974301328711,52.992935175152795 +2024-08-11 13:00:00,2,49.2646456053791,22.433130261250092,49.65163721525793 +2024-08-11 14:00:00,2,18.771054879048336,26.40669619075508,50.79407766525984 +2024-08-11 15:00:00,2,31.01725817045358,23.274980764545045,60.4337348748915 +2024-08-11 16:00:00,2,16.937426651933883,29.766498556183336,55.94245116477602 +2024-08-11 17:00:00,2,16.686454210978326,31.427170804553533,43.22234227299379 +2024-08-11 18:00:00,2,30.986520714810982,25.970171534932586,44.882469425320195 +2024-08-11 19:00:00,2,20.861777007449465,22.99521596171939,65.63816690951529 +2024-08-11 20:00:00,2,18.334393284776212,21.023174305975054,62.72181084644674 +2024-08-11 21:00:00,2,36.45455379438123,29.56912925687281,54.699627379513934 +2024-08-11 22:00:00,2,25.25883759434653,28.97962207399604,51.020358314520834 +2024-08-11 23:00:00,2,28.01856556167168,20.73898665379312,55.33672800089798 +2024-08-12 00:00:00,2,20.262102196888783,24.04715851658202,46.3268174161359 +2024-08-12 01:00:00,2,37.15470088066196,31.268646191894426,49.293735611601385 +2024-08-12 02:00:00,2,43.89194885122516,22.97670549355116,41.55098878693691 +2024-08-12 03:00:00,2,31.20882825932214,23.650532723587787,44.92906807588405 +2024-08-12 04:00:00,2,43.33157430297512,25.781089596947805,54.61718143026477 +2024-08-12 05:00:00,2,38.18629754823088,21.854365599970663,58.565488787487396 +2024-08-12 06:00:00,2,28.711127928503075,25.283974988566918,32.536515167156445 +2024-08-12 07:00:00,2,40.5737884224278,22.153886922941318,46.62614940680401 +2024-08-12 08:00:00,2,26.96228084324298,29.246427593771276,40.18094802047605 +2024-08-12 09:00:00,2,27.770879276485065,23.158169089864906,44.27681665609847 +2024-08-12 10:00:00,2,19.711934849370508,26.71414972222488,42.68161669691959 +2024-08-12 11:00:00,2,30.113854105191617,28.654900631831648,54.70877336590135 +2024-08-12 12:00:00,2,28.943677533669753,18.036323695831776,48.97512492141916 +2024-08-12 13:00:00,2,33.538511600863124,25.09518030700303,41.001083507721646 +2024-08-12 14:00:00,2,29.49420986716409,30.733278194680466,55.97061141052632 +2024-08-12 15:00:00,2,29.187830407790216,25.10347217797962,45.864633445473686 +2024-08-12 16:00:00,2,29.940202818036404,24.181662658901722,44.55085274589726 +2024-08-12 17:00:00,2,16.50348728854781,26.426312809606898,56.424230872295496 +2024-08-12 18:00:00,2,25.210181597990115,19.539735003455405,59.817313977744845 +2024-08-12 19:00:00,2,24.97241685181451,24.854351635037037,42.27195861561859 +2024-08-12 20:00:00,2,30.17264156062805,25.786194643300256,50.367840279218186 +2024-08-12 21:00:00,2,20.1258518809948,25.319123915431963,54.82659722747077 +2024-08-12 22:00:00,2,39.298811547669324,30.69133309166067,60.80413921061849 +2024-08-12 23:00:00,2,30.71196258800257,22.466291910594308,35.03843598725477 +2024-08-13 00:00:00,2,32.224724166312825,18.59189585125243,56.808811080286816 +2024-08-13 01:00:00,2,42.80875975412853,29.978633870863405,39.71285639910323 +2024-08-13 02:00:00,2,19.758165258878115,25.619932855504313,58.06802272905155 +2024-08-13 03:00:00,2,14.384014989976608,23.365545796153633,43.562738850665156 +2024-08-13 04:00:00,2,37.21460810603008,22.786349419169557,64.29960218214178 +2024-08-13 05:00:00,2,22.820698254773625,24.516633380343844,60.47842617825003 +2024-08-13 06:00:00,2,25.129806948400844,27.301844910366448,55.83252722960704 +2024-08-13 07:00:00,2,38.48792789424745,24.905453119813902,55.12332581303057 +2024-08-13 08:00:00,2,26.301634177741477,22.492132634782905,62.61083517015683 +2024-08-13 09:00:00,2,32.08091952230432,31.201908950430493,52.58029728205818 +2024-08-13 10:00:00,2,42.64488692313496,18.999897825963497,53.5753157640452 +2024-08-13 11:00:00,2,23.555870715730812,25.7693377036199,61.22198338442436 +2024-08-13 12:00:00,2,21.061544306665063,28.420761229027853,65.03941764543347 +2024-08-13 13:00:00,2,36.20794487238487,26.572187817896314,52.24618706527974 +2024-08-13 14:00:00,2,49.850170267098726,23.22932048899522,56.02646656929267 +2024-08-13 15:00:00,2,20.9262585449204,22.617331857048992,65.94648456160249 +2024-08-13 16:00:00,2,4.461402491091945,30.591407068969623,62.37558061676777 +2024-08-13 17:00:00,2,12.602026424228622,24.90968699129434,50.55806060479892 +2024-08-13 18:00:00,2,26.795984582416125,26.099651439061013,56.67472132750973 +2024-08-13 19:00:00,2,33.6215192852348,24.678563474912067,55.28240069098856 +2024-08-13 20:00:00,2,31.264540564518434,28.329690393806896,55.42526468590435 +2024-08-13 21:00:00,2,10.575245440792983,19.878211211636724,55.935596452851115 +2024-08-13 22:00:00,2,24.021507939113363,31.058098924433253,51.135839101635355 +2024-08-13 23:00:00,2,16.863173852247044,19.553377994264395,33.647689791978266 +2024-08-14 00:00:00,2,23.006690723437075,20.371501976290002,40.38945385220001 +2024-08-14 01:00:00,2,32.20868482529738,27.978853623256267,47.46974622428991 +2024-08-14 02:00:00,2,39.5520453288446,22.133325456660305,64.88615916969655 +2024-08-14 03:00:00,2,30.562127066303283,19.828544988665282,53.53591267279715 +2024-08-14 04:00:00,2,21.012515959066214,32.273936855292604,52.01279494191524 +2024-08-14 05:00:00,2,33.19205156416956,24.594915304778013,45.38635036843468 +2024-08-14 06:00:00,2,30.1863869385765,22.46481343215221,57.2803147198334 +2024-08-14 07:00:00,2,23.866351056241847,23.885655182294247,47.52467597184874 +2024-08-14 08:00:00,2,38.59071445108823,28.95391887788929,45.84866080484286 +2024-08-14 09:00:00,2,39.38109636948474,32.77892508985148,68.39622987354042 +2024-08-14 10:00:00,2,25.681815329652242,21.218931655538945,42.38625781077587 +2024-08-14 11:00:00,2,17.586320250800824,32.04363543818785,32.93860426117767 +2024-08-14 12:00:00,2,18.410057722713002,24.140694451442393,53.3711179496999 +2024-08-14 13:00:00,2,31.968323378560477,25.44075268207863,36.598368077797296 +2024-08-14 14:00:00,2,34.69989186532622,26.06413733651784,56.16784882503596 +2024-08-14 15:00:00,2,12.74914226089511,23.538125035285795,56.462430722232355 +2024-08-14 16:00:00,2,24.946522280837844,29.410041333273867,52.93239024156817 +2024-08-14 17:00:00,2,39.658740225754975,20.758483007661376,56.36179852113199 +2024-08-14 18:00:00,2,26.78613089310436,31.979419272608986,55.72582563864451 +2024-08-14 19:00:00,2,8.146919011253686,24.44557872885599,60.36413999555127 +2024-08-14 20:00:00,2,20.926948089961073,15.98154734503234,59.572835777785706 +2024-08-14 21:00:00,2,36.720923572719926,23.271651122224284,51.75529606453873 +2024-08-14 22:00:00,2,30.068206271689014,21.337154687176977,60.59920230371868 +2024-08-14 23:00:00,2,33.79185450853592,24.609146535580567,67.62187116659445 +2024-08-15 00:00:00,2,25.136897811131064,30.126853773873624,56.2410216137333 +2024-08-15 01:00:00,2,27.20661201696314,22.37728484193605,41.818305664346624 +2024-08-15 02:00:00,2,31.501941510654103,26.463262295922256,45.481176807910074 +2024-08-15 03:00:00,2,20.75455299650906,18.938408835022567,51.42505399725942 +2024-08-15 04:00:00,2,28.821846902781694,30.21466470242557,59.364330301093524 +2024-08-15 05:00:00,2,28.05734505291677,26.41029247894363,58.1020561596556 +2024-08-15 06:00:00,2,46.1395786895443,28.67003396945846,54.93364647658885 +2024-08-15 07:00:00,2,31.076573414821677,21.798129085336193,65.90213902915518 +2024-08-15 08:00:00,2,27.27925628681446,33.45140490679499,53.114322260413296 +2024-08-15 09:00:00,2,44.12574529427062,19.682454662935218,41.085059903242254 +2024-08-15 10:00:00,2,27.638027788215197,23.11985626345689,51.33871056030454 +2024-08-15 11:00:00,2,38.617470538690306,31.123523890575637,50.60810322226231 +2024-08-15 12:00:00,2,25.928331168261558,22.929540053923464,49.91538928838944 +2024-08-15 13:00:00,2,26.241153762741042,26.606277356275875,63.13198539311572 +2024-08-15 14:00:00,2,25.307393097432687,32.368968009547146,39.75809405796076 +2024-08-15 15:00:00,2,34.322896640759176,28.09251294620357,46.612384503231716 +2024-08-15 16:00:00,2,20.78430666900624,28.20242945123408,65.98127187405667 +2024-08-15 17:00:00,2,8.184418499820401,30.828987353710872,67.02663216792462 +2024-08-15 18:00:00,2,24.957953538303208,25.387053814602993,47.65003199446589 +2024-08-15 19:00:00,2,23.933393115478356,23.40711287853395,51.55925327581242 +2024-08-15 20:00:00,2,19.327827835165937,31.857488483079667,46.21285638924416 +2024-08-15 21:00:00,2,30.117900962169514,24.974328028010195,63.25190666007467 +2024-08-15 22:00:00,2,17.126784586164398,29.088228441676463,57.41167313669722 +2024-08-15 23:00:00,2,21.138552462282973,21.951086976864467,57.9163833102408 +2024-08-16 00:00:00,2,30.528630579767494,24.616565264401032,52.600579456765935 +2024-08-16 01:00:00,2,35.69805191454732,25.620640255176312,55.214235095933674 +2024-08-16 02:00:00,2,32.57229849083295,23.96684063243672,52.74054881173711 +2024-08-16 03:00:00,2,19.19447702928167,26.556798178430647,32.65299385953939 +2024-08-16 04:00:00,2,16.54549962452301,20.222119455974926,45.82943459080592 +2024-08-16 05:00:00,2,36.76653761455257,17.47543869763259,62.14487533400312 +2024-08-16 06:00:00,2,37.572633623267656,27.86300563584308,45.019790689727465 +2024-08-16 07:00:00,2,24.48792922933152,25.36190548655586,56.846741170601646 +2024-08-16 08:00:00,2,19.05954514335822,31.089806959158555,41.39110613175305 +2024-08-16 09:00:00,2,22.528528730394086,24.801548434605657,70.5132225294728 +2024-08-16 10:00:00,2,22.14691406499503,26.33059219845654,35.93389863175171 +2024-08-16 11:00:00,2,22.023096907856555,26.96932831389933,48.74710974299196 +2024-08-16 12:00:00,2,24.41336604729374,27.714174289195846,46.625371029683464 +2024-08-16 13:00:00,2,16.245830777085143,24.548194666472238,52.5564464784508 +2024-08-16 14:00:00,2,29.242956432265853,24.781273717582636,40.41491366866054 +2024-08-16 15:00:00,2,20.7830555934015,31.282320358203847,50.528465409459045 +2024-08-16 16:00:00,2,28.858929946552376,27.266021991488568,40.20593427350696 +2024-08-16 17:00:00,2,25.096595017551095,25.757599921707858,56.72798933269598 +2024-08-16 18:00:00,2,28.610244013854,23.64650743094676,56.349242900001926 +2024-08-16 19:00:00,2,21.361140085069845,22.082642292675523,57.76786856481608 +2024-08-16 20:00:00,2,30.223842947493672,23.122260244525236,45.83966444317822 +2024-08-16 21:00:00,2,27.001105792675165,26.711769065997096,52.02498437451667 +2024-08-16 22:00:00,2,41.856276052871536,20.731449793487922,54.235479669220524 +2024-08-16 23:00:00,2,32.42355104119094,23.349537448998053,42.18342135515445 +2024-08-17 00:00:00,2,24.770265526162994,30.476570709439052,65.05842273534212 +2024-08-17 01:00:00,2,10.255785605137397,27.898956743875313,38.47288704881763 +2024-08-17 02:00:00,2,42.399653725677304,24.85064129317576,53.965670186654535 +2024-08-17 03:00:00,2,31.23893263090162,30.817862819712545,55.35491416300863 +2024-08-17 04:00:00,2,28.546723947616417,19.40391591015475,60.96795311262539 +2024-08-17 05:00:00,2,28.04249281381398,20.50530032520817,62.13824878730054 +2024-08-17 06:00:00,2,36.69193165562313,25.375042686353456,56.36980049997745 +2024-08-17 07:00:00,2,37.615031606436055,22.717807062275973,47.75398915885074 +2024-08-17 08:00:00,2,42.75336175276496,29.48999488281519,54.700858528064614 +2024-08-17 09:00:00,2,41.56064946834481,23.03203358846526,45.90087262103191 +2024-08-17 10:00:00,2,33.01816586564397,25.688260161025767,61.89706453022462 +2024-08-17 11:00:00,2,26.381397502949415,25.59750698463207,55.4927200540819 +2024-08-17 12:00:00,2,28.97794132017899,28.9104848988434,42.20337504714448 +2024-08-17 13:00:00,2,26.637929312741623,23.90493724456836,54.943474927061565 +2024-08-17 14:00:00,2,29.570105711419757,19.804164937235804,53.34134980931796 +2024-08-17 15:00:00,2,45.921980798278454,24.696805528235604,63.84158811985888 +2024-08-17 16:00:00,2,40.49514861733371,27.429921034054328,48.48261604210892 +2024-08-17 17:00:00,2,39.92339928620121,27.31039667358512,40.528500673159684 +2024-08-17 18:00:00,2,15.471760748573308,24.455094698416755,39.518190375067604 +2024-08-17 19:00:00,2,40.83246550429894,21.138456530178082,42.980517775781365 +2024-08-17 20:00:00,2,25.208922899685746,23.177175056453617,47.257249965559126 +2024-08-17 21:00:00,2,28.040149613940525,24.551062951564514,45.96212053293466 +2024-08-17 22:00:00,2,21.862234735657367,22.315689827596998,43.68553992811008 +2024-08-17 23:00:00,2,24.823578330160476,22.35716243762019,56.068366757618314 +2024-08-18 00:00:00,2,5.55291975033208,22.31819341941324,64.72209767612812 +2024-08-18 01:00:00,2,37.509255581046624,20.13272379250691,49.61590157785594 +2024-08-18 02:00:00,2,43.90431179980692,23.598558617992218,54.28372154039285 +2024-08-18 03:00:00,2,36.36772678711476,26.985336647700134,73.58273651306706 +2024-08-18 04:00:00,2,42.08164093606633,22.02483052734093,50.59683774318504 +2024-08-18 05:00:00,2,19.795160213753924,22.075621719071048,50.76950850272929 +2024-08-18 06:00:00,2,17.302855610940185,21.666707314643926,44.16229020150779 +2024-08-18 07:00:00,2,23.836265361904413,25.518594615516292,55.4526859637097 +2024-08-18 08:00:00,2,35.56683253725817,20.01425375603788,43.80294936538709 +2024-08-18 09:00:00,2,27.553229561977176,27.116825490165443,54.77764079544254 +2024-08-18 10:00:00,2,42.23386962686229,30.168364747846592,68.086433970189 +2024-08-18 11:00:00,2,24.824638234451616,30.05799497886933,69.02420667576692 +2024-08-18 12:00:00,2,23.948764053342785,29.87231538429061,56.53777740637091 +2024-08-18 13:00:00,2,12.408380812744031,28.44011591953972,52.0102228249238 +2024-08-18 14:00:00,2,15.631292975506101,30.166492028124114,42.474755803084776 +2024-08-18 15:00:00,2,1.344837881790955,25.408723125671557,46.77151257050714 +2024-08-18 16:00:00,2,25.532607814090692,26.24223952777212,54.03127854361794 +2024-08-18 17:00:00,2,28.436021051553876,25.851370871050037,47.66048683801 +2024-08-18 18:00:00,2,34.10908915078983,26.477074499894286,65.9913539821024 +2024-08-18 19:00:00,2,30.78044402001481,23.706384127534896,64.22745431768747 +2024-08-18 20:00:00,2,44.691867417252865,17.699030533559494,53.36523570259411 +2024-08-18 21:00:00,2,18.1891227234764,29.44474122076666,61.93789660052661 +2024-08-18 22:00:00,2,13.24714093184589,22.96093984122487,57.61959102520529 +2024-08-18 23:00:00,2,19.288677813704528,24.09278747646452,67.76485130769218 +2024-08-19 00:00:00,2,22.946932823350423,21.183161620632607,36.600481478704246 +2024-08-19 01:00:00,2,37.256000792063105,30.138383066938093,57.34558805048644 +2024-08-19 02:00:00,2,29.44782449224714,31.572744075527595,62.25510300146917 +2024-08-19 03:00:00,2,27.192306534594355,21.574921778869665,59.074555419257436 +2024-08-19 04:00:00,2,30.73764946421858,25.44500988653254,47.402311916152975 +2024-08-19 05:00:00,2,40.38223539210057,17.60544398139373,52.55617733232129 +2024-08-19 06:00:00,2,28.334038868271918,16.48426614939048,46.30624619678166 +2024-08-19 07:00:00,2,29.426607288186737,25.82682713500774,50.087269723264924 +2024-08-19 08:00:00,2,38.47621693976612,24.41428815576379,59.513582635094025 +2024-08-19 09:00:00,2,39.39742536870034,26.877157488134287,63.245321527861854 +2024-08-19 10:00:00,2,26.723548300024103,24.57053884916411,50.7423579272893 +2024-08-19 11:00:00,2,19.76566769433439,25.184526551639753,59.0322721211582 +2024-08-19 12:00:00,2,36.54100688647952,26.736040320853796,40.67856236499254 +2024-08-19 13:00:00,2,20.31366715869257,23.937784273811296,36.708469309998705 +2024-08-19 14:00:00,2,20.994483138685162,21.840645198358533,50.39993288607332 +2024-08-19 15:00:00,2,27.10848484256873,24.923837192666582,53.273402710389384 +2024-08-19 16:00:00,2,34.27879362249237,22.823653437360075,51.72775232895926 +2024-08-19 17:00:00,2,29.117265165360873,23.060355582473463,44.68895665421114 +2024-08-19 18:00:00,2,32.518618153314975,24.198349361203743,54.766694950655925 +2024-08-19 19:00:00,2,25.473396298644257,25.259457609429646,71.51520408674797 +2024-08-19 20:00:00,2,21.31363809555154,25.83722035558721,46.564178180886536 +2024-08-19 21:00:00,2,10.79303067826635,26.595839263715373,57.08301830118645 +2024-08-19 22:00:00,2,23.208326166998223,18.703105287295585,65.23106366306422 +2024-08-19 23:00:00,2,41.00584473746681,24.688694778161842,72.84802979816894 +2024-08-20 00:00:00,2,37.65494651534236,18.222939470648946,61.992240320647284 +2024-08-20 01:00:00,2,19.948089448636456,21.1713335723703,54.95736155965223 +2024-08-20 02:00:00,2,24.957478502345353,22.454164930280566,37.406617968182296 +2024-08-20 03:00:00,2,30.673921723782637,22.228107170910693,45.96819353127965 +2024-08-20 04:00:00,2,28.991608664030082,23.234556729845842,52.010572657298624 +2024-08-20 05:00:00,2,23.061205503840394,32.71251719242665,54.397483603364755 +2024-08-20 06:00:00,2,48.467636650064776,17.572815813955916,39.83678347636098 +2024-08-20 07:00:00,2,31.406561014061243,30.503955419391293,47.24790233013883 +2024-08-20 08:00:00,2,54.53328777226124,20.178324493280165,54.08250447491592 +2024-08-20 09:00:00,2,26.714976525981143,25.854895972650823,49.33720032359998 +2024-08-20 10:00:00,2,18.630122064939272,25.030985795084455,47.49332585856172 +2024-08-20 11:00:00,2,31.564018268113696,26.68789070414922,58.19188569966103 +2024-08-20 12:00:00,2,27.651226634470007,31.77837296891323,43.22416846496189 +2024-08-20 13:00:00,2,35.67681204580356,26.82963308896707,53.91890303932432 +2024-08-20 14:00:00,2,20.600218503153634,21.342002082257036,40.14334877998593 +2024-08-20 15:00:00,2,53.77263346528092,25.588580860755947,45.425458919671065 +2024-08-20 16:00:00,2,27.94509350193601,26.14279524956695,41.575725943364134 +2024-08-20 17:00:00,2,24.42408994786766,19.550559501020384,59.80756810680779 +2024-08-20 18:00:00,2,22.2403393215851,20.87474330722432,53.34657714823049 +2024-08-20 19:00:00,2,32.17799262395152,28.379172310234605,63.58202183766615 +2024-08-20 20:00:00,2,27.320439902903214,22.198988956995706,68.195714708116 +2024-08-20 21:00:00,2,32.519263979323796,27.645948286218065,65.84507148092031 +2024-08-20 22:00:00,2,23.656581751968865,24.9483548258515,57.85805782353165 +2024-08-20 23:00:00,2,37.04814698733635,18.87596516027152,44.14622740452055 +2024-08-21 00:00:00,2,36.555112680341786,22.74583300990882,36.67706388031819 +2024-08-21 01:00:00,2,18.354081173598082,22.063191515213987,59.13027621813253 +2024-08-21 02:00:00,2,29.561265766393586,24.284730764678283,55.94012295345768 +2024-08-21 03:00:00,2,28.308996652896777,19.93991527051974,57.08845814590065 +2024-08-21 04:00:00,2,26.965005046416508,26.11650158770874,54.26023536502755 +2024-08-21 05:00:00,2,22.532716326338438,25.74771710876072,62.47688161611587 +2024-08-21 06:00:00,2,11.422269707119217,24.686465884368854,48.35842935632861 +2024-08-21 07:00:00,2,34.671405356719184,25.073964675768522,39.92882871820556 +2024-08-21 08:00:00,2,52.569022474821885,17.97237508271171,70.88551806949982 +2024-08-21 09:00:00,2,55.132437096115936,27.8876442857873,59.7933597292285 +2024-08-21 10:00:00,2,22.74097517817893,26.69388337309183,58.96309333439741 +2024-08-21 11:00:00,2,18.09997791610346,33.806134734016204,51.64724907218202 +2024-08-21 12:00:00,2,36.568504502079165,29.574309412937385,43.71139918390563 +2024-08-21 13:00:00,2,15.327783907683651,28.547368672949805,65.69208926717648 +2024-08-21 14:00:00,2,19.828486168946547,21.812936538898327,41.04697142042185 +2024-08-21 15:00:00,2,28.46752561465656,26.84232192167867,50.61285234600827 +2024-08-21 16:00:00,2,22.929196836760052,21.313597310983166,58.24093566378696 +2024-08-21 17:00:00,2,16.34379693847795,27.339888060182354,69.5266241017012 +2024-08-21 18:00:00,2,21.11353039442698,30.278288555235505,57.129355514551904 +2024-08-21 19:00:00,2,17.058294041184517,23.513966733427214,52.28452181432741 +2024-08-21 20:00:00,2,12.838443251824248,24.078009678830316,60.79335700671732 +2024-08-21 21:00:00,2,34.63827780253766,19.94595930943624,43.82607977146702 +2024-08-21 22:00:00,2,31.1738642468706,15.04077533733975,43.04718292492321 +2024-08-21 23:00:00,2,40.49140290733422,25.22503499186288,53.75721255451496 +2024-08-22 00:00:00,2,37.10626875940959,21.384566592890458,66.67098861715229 +2024-08-22 01:00:00,2,39.05579281759634,20.709319169824028,52.91523653671466 +2024-08-22 02:00:00,2,27.855065053464234,23.929207115553154,50.94676798989304 +2024-08-22 03:00:00,2,31.75515957781608,18.776101013560485,59.53157646154609 +2024-08-22 04:00:00,2,13.313323856485427,21.792488375597177,44.59163693416868 +2024-08-22 05:00:00,2,39.636374244365214,26.91047265825584,53.59370350658898 +2024-08-22 06:00:00,2,41.65236591528257,20.493093873915733,37.05619445762548 +2024-08-22 07:00:00,2,18.253518741467495,27.419700995843424,61.25500238231502 +2024-08-22 08:00:00,2,34.47462893755285,23.910854136314192,49.51178389304792 +2024-08-22 09:00:00,2,29.857450687091173,28.450555007292895,42.279292039757664 +2024-08-22 10:00:00,2,19.440908560976133,25.467022718421223,49.850513560166235 +2024-08-22 11:00:00,2,17.269353510815392,23.39259647419138,48.51606750505466 +2024-08-22 12:00:00,2,22.3584407781881,26.526636472649024,54.440605425636384 +2024-08-22 13:00:00,2,32.01536424384063,27.82256629144399,55.09711301147445 +2024-08-22 14:00:00,2,12.420329940152083,22.956204959933824,39.41705167043485 +2024-08-22 15:00:00,2,32.37282864288962,24.167798401986012,59.21672740508083 +2024-08-22 16:00:00,2,20.871924475202007,23.28602342480498,62.41379454013369 +2024-08-22 17:00:00,2,10.217239428677923,21.005865830135075,49.614547539161435 +2024-08-22 18:00:00,2,18.88716858166145,24.983779853331654,52.13198081525583 +2024-08-22 19:00:00,2,25.75558054221938,22.403028782727347,43.95741437950953 +2024-08-22 20:00:00,2,18.02836169137676,25.518344515228286,54.90739381831032 +2024-08-22 21:00:00,2,20.780425368803208,22.12298254416885,50.38311849772299 +2024-08-22 22:00:00,2,17.75486205426931,24.847464330939413,52.75835758810755 +2024-08-22 23:00:00,2,21.81681165715361,24.988029519539996,50.19140459403013 +2024-08-23 00:00:00,2,26.00654597674911,21.130438399473924,54.700842501437286 +2024-08-23 01:00:00,2,39.41547723773171,20.18956384420293,35.24630341062419 +2024-08-23 02:00:00,2,32.31468687727739,25.838772646181965,39.88099698816453 +2024-08-23 03:00:00,2,30.374002622827703,19.452671814919704,56.92854347803338 +2024-08-23 04:00:00,2,36.589198621004726,22.12044624094659,61.72713385015054 +2024-08-23 05:00:00,2,29.851875269664784,28.292133574180617,88.60634861509453 +2024-08-23 06:00:00,2,35.57488268554102,28.947874617298694,49.67975340803177 +2024-08-23 07:00:00,2,27.4910603800811,22.675398158805883,71.36946084033163 +2024-08-23 08:00:00,2,26.19539061075762,26.883003378251296,70.06332682732157 +2024-08-23 09:00:00,2,25.630328059176144,22.498400621731864,57.797502942410155 +2024-08-23 10:00:00,2,31.360925735624857,26.419268329297573,53.47614865241641 +2024-08-23 11:00:00,2,30.693632605119166,29.96817414490531,31.287455667893948 +2024-08-23 12:00:00,2,16.86561905062429,21.661340084633366,56.37425014116531 +2024-08-23 13:00:00,2,29.792323316199226,27.31469041986786,57.68680659041752 +2024-08-23 14:00:00,2,35.783586455873554,25.769336374083725,65.14009008467251 +2024-08-23 15:00:00,2,28.712205598008588,22.08772766625785,69.35057562634658 +2024-08-23 16:00:00,2,18.35712878942898,26.185778366318214,55.09970967585156 +2024-08-23 17:00:00,2,10.892621671130982,27.828247628005922,50.82216268607807 +2024-08-23 18:00:00,2,31.18384486839132,29.575668385862322,57.50597369467339 +2024-08-23 19:00:00,2,32.05369899218805,26.982344967253123,51.73270048777959 +2024-08-23 20:00:00,2,22.74670785418785,24.054081584327943,52.8653090634036 +2024-08-23 21:00:00,2,36.365585076805914,24.371882894573574,66.01233756002027 +2024-08-23 22:00:00,2,19.219719085228945,25.926754627307837,52.53910159944739 +2024-08-23 23:00:00,2,26.612667259655485,21.562146433707248,37.24002606220543 +2024-08-24 00:00:00,2,48.067834935651376,23.594619694781862,58.897623240013004 +2024-08-24 01:00:00,2,34.48448534220328,21.44482528019674,37.31859631692142 +2024-08-24 02:00:00,2,32.22042922396291,20.560815908733098,34.440752411172205 +2024-08-24 03:00:00,2,16.29568022443999,25.07777753452099,53.46964224068133 +2024-08-24 04:00:00,2,37.47595009111025,24.63464492578103,74.10248575973294 +2024-08-24 05:00:00,2,25.868098941511235,21.590642943573712,46.634566251416736 +2024-08-24 06:00:00,2,27.916283584278883,20.302809103309066,65.50236677924626 +2024-08-24 07:00:00,2,21.81171796073565,25.1043405966328,55.29669686414706 +2024-08-24 08:00:00,2,35.00210349582742,27.557511972175035,53.40531837942642 +2024-08-24 09:00:00,2,38.206333295277126,24.672069557492573,53.88408700766567 +2024-08-24 10:00:00,2,24.491342287286194,24.623270324081563,51.022722650561434 +2024-08-24 11:00:00,2,32.362925628002145,25.296909997487766,27.07401410653469 +2024-08-24 12:00:00,2,35.23588366823085,29.943645448989262,46.694529463138565 +2024-08-24 13:00:00,2,13.661719093325505,26.50104976952021,60.464120037351904 +2024-08-24 14:00:00,2,19.923777703410796,27.983748485587718,40.214845477932464 +2024-08-24 15:00:00,2,4.02452460678111,27.69887064489663,66.59655917021642 +2024-08-24 16:00:00,2,4.896362488659268,31.562045856502035,42.47479927458299 +2024-08-24 17:00:00,2,19.72368650967862,29.120923939022703,51.045687508174424 +2024-08-24 18:00:00,2,13.45660732680118,30.66071988016041,50.706870574955076 +2024-08-24 19:00:00,2,26.159375179434488,19.80850736290716,52.29979566191329 +2024-08-24 20:00:00,2,23.965623567643885,18.07263070889641,45.05844330678056 +2024-08-24 21:00:00,2,17.243731480335903,22.608549241990655,49.880704873730004 +2024-08-24 22:00:00,2,35.6570103036226,25.291892376202565,27.737262782514737 +2024-08-24 23:00:00,2,29.910791978549266,15.971561431749834,47.73055000701273 +2024-08-25 00:00:00,2,27.157574951180372,22.676326307438313,45.689356462346545 +2024-08-25 01:00:00,2,26.29235931429089,22.48322421717359,68.20801054424787 +2024-08-25 02:00:00,2,25.835370659214075,25.73161173905809,57.77431933619047 +2024-08-25 03:00:00,2,40.25818640286929,23.160552155983478,52.90161528529247 +2024-08-25 04:00:00,2,34.470847168786634,25.483068170104325,52.00060234924823 +2024-08-25 05:00:00,2,32.81451313272135,25.9683805408413,68.96187011631415 +2024-08-25 06:00:00,2,38.09372872820101,23.629131999701187,46.010614074039765 +2024-08-25 07:00:00,2,27.269499726761772,25.905454829019305,42.773386948725765 +2024-08-25 08:00:00,2,21.740020554532286,24.054317577536764,57.41685170769995 +2024-08-25 09:00:00,2,19.72061002424988,13.737400411236694,52.19470908776594 +2024-08-25 10:00:00,2,22.388238171492652,22.314672868400425,51.16790160723848 +2024-08-25 11:00:00,2,34.864929095406644,32.036256951620516,59.078648924816925 +2024-08-25 12:00:00,2,11.749701961496037,18.774488569011844,52.11154588525094 +2024-08-25 13:00:00,2,30.3704441933564,26.40060947808249,50.47816443394636 +2024-08-25 14:00:00,2,29.08118447602707,25.58895386350508,56.0091620786902 +2024-08-25 15:00:00,2,26.202181501639462,31.042026446559547,61.039139730509085 +2024-08-25 16:00:00,2,28.70720828710521,18.614073243323766,58.776278583044245 +2024-08-25 17:00:00,2,18.695340693062917,21.85147872839695,59.72569141250279 +2024-08-25 18:00:00,2,16.384551861650575,24.161520265379618,65.16546731804773 +2024-08-25 19:00:00,2,30.726165794409546,26.167857950617574,45.35489676956345 +2024-08-25 20:00:00,2,22.632756639576467,26.322207204401558,59.44139787341476 +2024-08-25 21:00:00,2,27.706394572914807,15.460979916805162,63.39568403981306 +2024-08-25 22:00:00,2,21.785623159693547,17.244585862163746,42.9058392396208 +2024-08-25 23:00:00,2,27.47793263597826,30.631188013788567,51.591248841723505 +2024-08-26 00:00:00,2,33.44200817656734,20.93326509327326,47.122830067430925 +2024-08-26 01:00:00,2,33.16142350697726,23.678041076463742,44.999285454458125 +2024-08-26 02:00:00,2,44.59533048733291,25.633202791629614,54.42258344323806 +2024-08-26 03:00:00,2,26.52764263826209,24.46085056506828,43.101027749030386 +2024-08-26 04:00:00,2,35.69823301175129,21.700780130631113,53.76592795133696 +2024-08-26 05:00:00,2,21.92753764766935,28.00581343821154,62.257079346462845 +2024-08-26 06:00:00,2,23.27242323346799,24.54010806668623,49.72969241170663 +2024-08-26 07:00:00,2,35.32319887368076,19.228568205036762,37.57826790311612 +2024-08-26 08:00:00,2,21.60246031212509,24.49238998745498,49.19367833408797 +2024-08-26 09:00:00,2,40.89192639387735,24.626832213438053,64.10668415973271 +2024-08-26 10:00:00,2,31.705257440293316,27.06425890527517,56.906442225170714 +2024-08-26 11:00:00,2,38.356511393528706,21.758978865407755,51.39517800700767 +2024-08-26 12:00:00,2,31.514847889255265,26.398804589181808,57.41724112238151 +2024-08-26 13:00:00,2,21.5709744655935,24.881077347483593,49.06091792894725 +2024-08-26 14:00:00,2,39.68909267896123,22.03442132987976,59.33864563550111 +2024-08-26 15:00:00,2,19.968254619258005,23.653851519559268,73.76182209047982 +2024-08-26 16:00:00,2,20.731001599961075,30.001296173585537,54.81404497667506 +2024-08-26 17:00:00,2,15.411144976585444,23.905286406832943,60.48647883227327 +2024-08-26 18:00:00,2,40.6740337729294,21.277477365845336,55.87192491436082 +2024-08-26 19:00:00,2,28.496604724949602,29.497791754368254,39.7402162907116 +2024-08-26 20:00:00,2,31.020829090690988,23.527576921588707,38.955372369958106 +2024-08-26 21:00:00,2,20.07993257731974,24.704243025107274,61.725535939353385 +2024-08-26 22:00:00,2,22.68965738798752,20.078683121607916,56.15096125657466 +2024-08-26 23:00:00,2,8.39552035005929,24.34657583783584,53.694113251500674 +2024-08-27 00:00:00,2,7.11483219815371,25.720965803965182,47.41608134176404 +2024-08-27 01:00:00,2,32.47929405705604,22.344733147054143,46.69708161084694 +2024-08-27 02:00:00,2,35.94169270178306,23.257958762335406,54.32055322340338 +2024-08-27 03:00:00,2,19.608879259525043,23.196172286282145,55.21333936251081 +2024-08-27 04:00:00,2,24.29738766015028,21.387963030374852,52.78937197977555 +2024-08-27 05:00:00,2,16.25088232261586,26.530391313353032,53.49911799263715 +2024-08-27 06:00:00,2,28.21376056242733,28.508518568250718,29.39884299336242 +2024-08-27 07:00:00,2,22.6603538904599,27.059948345987795,54.25359266449331 +2024-08-27 08:00:00,2,48.21914827582407,24.245771728556257,47.05357369336341 +2024-08-27 09:00:00,2,26.22996685342757,34.720527705376035,49.69021881814536 +2024-08-27 10:00:00,2,28.513675047483872,30.731453482852466,44.97473927643599 +2024-08-27 11:00:00,2,40.677925935910466,28.45576886045869,60.54167197836132 +2024-08-27 12:00:00,2,36.53485842421712,28.02315449465896,44.653189037388806 +2024-08-27 13:00:00,2,22.49185387503161,23.55501273464652,52.828321283230856 +2024-08-27 14:00:00,2,26.388617665013275,26.00707814198517,45.03883147955714 +2024-08-27 15:00:00,2,26.312047050075865,24.97856118918993,49.820035359969864 +2024-08-27 16:00:00,2,22.8778324091071,24.98597746514465,45.61030304438855 +2024-08-27 17:00:00,2,21.13583748875636,26.980821795924818,56.49915276632462 +2024-08-27 18:00:00,2,13.366090252527185,27.799908731412817,58.3874964464955 +2024-08-27 19:00:00,2,19.190981275213296,24.8957311042056,56.81210888990493 +2024-08-27 20:00:00,2,26.794571518658575,24.070870289333815,51.188562968759285 +2024-08-27 21:00:00,2,26.484660351481,21.351361538329428,70.22829372673552 +2024-08-27 22:00:00,2,28.370122968652275,29.052388294021096,50.12934764294905 +2024-08-27 23:00:00,2,43.41944630549567,17.306029415861527,56.46869741659485 +2024-08-28 00:00:00,2,36.992164326415754,32.4542191751964,66.8965532273418 +2024-08-28 01:00:00,2,33.46346401065295,26.89873086763649,32.92989194613037 +2024-08-28 02:00:00,2,15.897679431904074,17.092160602050654,51.90256755429473 +2024-08-28 03:00:00,2,42.57545983431812,24.03470763967672,55.35714502660442 +2024-08-28 04:00:00,2,22.152116875406612,20.04226813988023,62.23575494787614 +2024-08-28 05:00:00,2,36.78199053445819,26.38338457098542,55.838231587629245 +2024-08-28 06:00:00,2,29.002898885806058,24.379895561226956,68.17962673168235 +2024-08-28 07:00:00,2,33.41161744897067,22.179112770591587,60.93430659489322 +2024-08-28 08:00:00,2,34.245802045520236,24.594596664741577,53.16894358163085 +2024-08-28 09:00:00,2,17.25661750123441,30.670774927317215,54.45933505545078 +2024-08-28 10:00:00,2,24.538023261974793,22.58461632479878,47.63766229170359 +2024-08-28 11:00:00,2,31.892983082998963,32.670477893973235,70.13079739103631 +2024-08-28 12:00:00,2,27.881821588830974,24.32530889682471,40.43483543623856 +2024-08-28 13:00:00,2,43.40696144856912,29.56407331426832,46.67116249842787 +2024-08-28 14:00:00,2,44.82532479763026,28.656185444191966,66.88297525601371 +2024-08-28 15:00:00,2,37.694371227157845,29.98022774366328,74.67436622528251 +2024-08-28 16:00:00,2,24.89363480664745,32.023317715404104,62.387048719743646 +2024-08-28 17:00:00,2,31.58330066361149,24.450659050591266,44.60494254309263 +2024-08-28 18:00:00,2,13.721438931602837,28.634261956531866,44.57772943207745 +2024-08-28 19:00:00,2,26.67401638456543,25.60937695266783,58.99931745160771 +2024-08-28 20:00:00,2,23.62268378346328,19.30210647989516,68.04035128542509 +2024-08-28 21:00:00,2,31.025648211222276,28.067375671052247,46.805353695167604 +2024-08-28 22:00:00,2,37.946523195519006,22.732642761046893,69.99262815900693 +2024-08-28 23:00:00,2,45.2873215499363,22.816220689521813,44.89621579304799 +2024-08-29 00:00:00,2,34.03672806503603,24.28671069446948,54.34560687833091 +2024-08-29 01:00:00,2,20.013713127609662,22.639216487505053,55.27104679963603 +2024-08-29 02:00:00,2,27.33069862573489,26.288837698425468,54.88012672414422 +2024-08-29 03:00:00,2,38.74526689907117,27.04983176145946,48.45068012567553 +2024-08-29 04:00:00,2,35.17340521743101,20.368715886479713,48.302636629965285 +2024-08-29 05:00:00,2,27.876130201455204,22.152007904329057,47.960140001120585 +2024-08-29 06:00:00,2,21.20640441823765,27.872846501179417,55.16272520061531 +2024-08-29 07:00:00,2,24.599725985872425,24.58506681693233,48.06538484530508 +2024-08-29 08:00:00,2,34.06368265371403,20.77556355512841,52.494653375869 +2024-08-29 09:00:00,2,23.591584590685514,20.322663025047653,45.00469365404741 +2024-08-29 10:00:00,2,24.56518083256279,26.029700919222122,54.84069499539619 +2024-08-29 11:00:00,2,35.41399404113511,29.17277644642671,47.50604633637117 +2024-08-29 12:00:00,2,33.106760919499756,29.034044050032765,61.0522151112679 +2024-08-29 13:00:00,2,36.195883934994455,24.800519980459185,47.058076372238176 +2024-08-29 14:00:00,2,21.650727090410157,27.37804334267667,58.89565402345384 +2024-08-29 15:00:00,2,30.625411335123566,30.061568933033392,71.4205335712894 +2024-08-29 16:00:00,2,20.741475685895033,20.599779834278063,53.713651234250676 +2024-08-29 17:00:00,2,35.650267122770344,25.827722113695877,61.0751974021538 +2024-08-29 18:00:00,2,12.04002784843911,24.109526952679094,43.96390814481906 +2024-08-29 19:00:00,2,12.515983565491457,26.929784966074934,37.084462577780016 +2024-08-29 20:00:00,2,21.994286047499173,22.547784361394744,63.67002080950978 +2024-08-29 21:00:00,2,31.292609998583018,26.825554442816866,65.00549733977839 +2024-08-29 22:00:00,2,23.315940884309377,23.276337395874357,64.58869546236946 +2024-08-29 23:00:00,2,25.889292556430682,31.385980974903312,61.361284942908455 +2024-08-30 00:00:00,2,38.891404347911376,28.71699291411891,60.66447035585924 +2024-08-30 01:00:00,2,23.716520251244695,27.97556768441182,37.97346176125168 +2024-08-30 02:00:00,2,15.577470189471816,26.168694211412248,51.986317565149335 +2024-08-30 03:00:00,2,31.065102040277697,20.387754450224033,60.651314917361844 +2024-08-30 04:00:00,2,44.92830983074266,26.546068000629166,47.60921934453866 +2024-08-30 05:00:00,2,28.015709728408243,26.139009476753305,52.71761353987707 +2024-08-30 06:00:00,2,34.20128708255822,27.152392777954763,58.20741609595502 +2024-08-30 07:00:00,2,36.379680421181085,23.32546553918654,50.3220943855566 +2024-08-30 08:00:00,2,32.434480757060975,27.104752394532056,60.69320811341898 +2024-08-30 09:00:00,2,20.69700372207895,23.84189844843039,52.099216424857715 +2024-08-30 10:00:00,2,39.35424051844894,31.860067181301133,62.592233224139825 +2024-08-30 11:00:00,2,47.35123759731875,24.77423020479566,57.42934842426991 +2024-08-30 12:00:00,2,44.34287520649603,31.29348763990629,52.24226671048485 +2024-08-30 13:00:00,2,23.544790511943336,27.466477084068114,62.57286748985452 +2024-08-30 14:00:00,2,12.463794059263359,25.842690666377155,43.85799693333078 +2024-08-30 15:00:00,2,37.20380912174835,25.656490868955544,50.953168288380425 +2024-08-30 16:00:00,2,34.229313558985424,21.53804359285544,57.16554048199245 +2024-08-30 17:00:00,2,38.109749794877,28.469916950654557,43.75316466086744 +2024-08-30 18:00:00,2,19.35019148878001,24.269254217275634,55.411186776325664 +2024-08-30 19:00:00,2,30.54638838579789,25.123547752911204,40.847978750198 +2024-08-30 20:00:00,2,23.27549504912103,30.695904635857914,53.47534103416369 +2024-08-30 21:00:00,2,20.787729154946177,25.35984665533823,51.956493493598174 +2024-08-30 22:00:00,2,19.893621109958442,28.812459136436892,57.754955204575914 +2024-08-30 23:00:00,2,27.636491517826254,28.451556093439738,72.59799121386565 +2024-08-31 00:00:00,2,21.08830694173775,23.460782561496764,47.19944113609844 +2024-08-31 01:00:00,2,18.65620688419373,25.928027384013753,50.500804737273484 +2024-08-31 02:00:00,2,34.00162128029034,23.05871574971396,54.4338701698071 +2024-08-31 03:00:00,2,38.78408906384336,25.461729533525993,48.566899649279335 +2024-08-31 04:00:00,2,24.715043591373266,23.85412200986694,42.57407564046613 +2024-08-31 05:00:00,2,21.53131806834005,18.36018193599733,38.386822113256244 +2024-08-31 06:00:00,2,19.84217965712167,23.12896691125425,51.511228764684724 +2024-08-31 07:00:00,2,41.104837510855944,23.55802798819142,56.05084516091756 +2024-08-31 08:00:00,2,23.441602802014344,26.846709612858767,56.55439803414121 +2024-08-31 09:00:00,2,2.1901460491582547,29.44260938507922,59.71184613935532 +2024-08-31 10:00:00,2,9.974211141600602,30.32296170079181,52.767391135044704 +2024-08-31 11:00:00,2,20.583140675181177,31.593049995767277,51.78763329134669 +2024-08-31 12:00:00,2,26.855883418384504,23.071099274679625,43.00362289985626 +2024-08-31 13:00:00,2,31.39729505178233,24.970512505888152,53.758165412266 +2024-08-31 14:00:00,2,14.804099186462935,18.62936847082746,60.82787456717335 +2024-08-31 15:00:00,2,12.25172135914015,26.513513625142565,39.72521741828646 +2024-08-31 16:00:00,2,15.4088229051869,23.464260935801036,47.089481646073075 +2024-08-31 17:00:00,2,32.314598773414446,21.18296044699763,42.64032097341328 +2024-08-31 18:00:00,2,23.276817075484015,29.511506284648934,47.752261239295834 +2024-08-31 19:00:00,2,25.34888607966902,32.16299221821971,49.517708409666525 +2024-08-31 20:00:00,2,20.34477009893254,20.775256610397513,52.010885204640076 +2024-08-31 21:00:00,2,15.01283820358225,25.37722512691821,54.4031949985554 +2024-08-31 22:00:00,2,32.642806676905344,16.6648895317488,56.41469042203674 +2024-08-31 23:00:00,2,19.800979731068654,22.130184733614833,42.65742642154319 +2024-09-01 00:00:00,2,33.12398258177636,23.238348824364472,48.07610056471217 +2024-09-01 01:00:00,2,29.524697109322275,20.962037786836365,49.84360843506079 +2024-09-01 02:00:00,2,42.127976536783954,19.39724096497767,44.882084112491235 +2024-09-01 03:00:00,2,24.221483383996137,23.01660513463233,61.27968698968469 +2024-09-01 04:00:00,2,31.35905723849991,15.712512942679712,52.23436542098788 +2024-09-01 05:00:00,2,21.205510591067544,25.717273069904397,42.39033104195553 +2024-09-01 06:00:00,2,43.41226237593232,23.936356778483937,43.83813787549544 +2024-09-01 07:00:00,2,23.219573633748052,19.206934839072975,58.09851612106464 +2024-09-01 08:00:00,2,31.598513306113112,21.554206183951685,64.97804491284563 +2024-09-01 09:00:00,2,33.965182407834675,27.88723076842843,64.61834267128037 +2024-09-01 10:00:00,2,24.585089914448197,22.688554103851413,33.62759957910073 +2024-09-01 11:00:00,2,31.00804539677039,24.152404843804806,63.372223758046495 +2024-09-01 12:00:00,2,25.94113906786581,27.647476033407983,49.53927462018736 +2024-09-01 13:00:00,2,28.29839020564067,29.368550486098837,46.269821028679786 +2024-09-01 14:00:00,2,28.079708394643287,26.497008005322886,44.083417795974114 +2024-09-01 15:00:00,2,27.146690093839126,23.656611806981495,61.50205819540247 +2024-09-01 16:00:00,2,2.0230558398030176,25.435677976994178,56.73577269483832 +2024-09-01 17:00:00,2,36.83058430095357,23.76405767415306,48.496803441386085 +2024-09-01 18:00:00,2,35.188240365959906,22.3469688741854,46.611628130306784 +2024-09-01 19:00:00,2,25.515887841274242,25.829572206362744,56.835832427732875 +2024-09-01 20:00:00,2,18.324669632271252,27.303067839337896,60.13929707363423 +2024-09-01 21:00:00,2,41.29713236300957,20.12941050587235,56.2327831957272 +2024-09-01 22:00:00,2,23.724506355756958,28.39969136724989,81.48697470653458 +2024-09-01 23:00:00,2,27.758829396770835,22.70169802044269,37.14844807297035 +2024-09-02 00:00:00,2,26.912177772928207,22.487514928995356,39.494362268064435 +2024-09-02 01:00:00,2,26.752317158253383,30.447115146284975,59.741435481277406 +2024-09-02 02:00:00,2,22.48563592048955,31.79185949049252,50.335676399425445 +2024-09-02 03:00:00,2,14.651850529906739,27.886658265665766,69.40245826661402 +2024-09-02 04:00:00,2,36.23513277626896,24.476140670477204,56.859206702862956 +2024-09-02 05:00:00,2,29.52856098046253,23.881977980528447,56.058490700173664 +2024-09-02 06:00:00,2,33.949858680361444,25.033191260662704,47.93410654104977 +2024-09-02 07:00:00,2,17.146388079513997,28.910560556850804,40.44282858393903 +2024-09-02 08:00:00,2,24.70769760551833,23.610261025297476,52.30418876043183 +2024-09-02 09:00:00,2,39.82120016458765,24.193672247129186,47.87891405823067 +2024-09-02 10:00:00,2,28.805958781692063,31.560191777460943,68.69163524788567 +2024-09-02 11:00:00,2,22.638670647278907,20.585948198742248,56.48427336850124 +2024-09-02 12:00:00,2,35.73264208795833,24.821234731777878,37.32035295671141 +2024-09-02 13:00:00,2,25.240223692777498,21.683857709679103,33.386629294195394 +2024-09-02 14:00:00,2,34.322700312097645,29.17844364285796,40.59387335218466 +2024-09-02 15:00:00,2,14.471577101838763,23.827966482424472,61.054025868737355 +2024-09-02 16:00:00,2,18.986542843413975,28.125207390128537,55.21615288962397 +2024-09-02 17:00:00,2,28.281830840426974,20.676220565812073,63.04909926188488 +2024-09-02 18:00:00,2,23.480849658508884,28.068314921104133,53.981762938951405 +2024-09-02 19:00:00,2,32.73720122114837,16.591883197314562,50.93372678631431 +2024-09-02 20:00:00,2,23.52773120268393,27.276079363562545,51.23139432570043 +2024-09-02 21:00:00,2,24.976903275556385,22.556843302038057,59.131612892691145 +2024-09-02 22:00:00,2,24.5511341402077,18.916793110815213,62.95878073645466 +2024-09-02 23:00:00,2,29.919551155543388,23.28598103549952,61.06844685131054 +2024-09-03 00:00:00,2,20.884532181715734,22.215359358539715,67.67436821193832 +2024-09-03 01:00:00,2,41.35698876504635,25.97914584584033,59.96058405607477 +2024-09-03 02:00:00,2,18.57549276848741,24.188400442824452,50.02753376236635 +2024-09-03 03:00:00,2,37.89236630630114,19.34892743151326,37.32428743536934 +2024-09-03 04:00:00,2,18.86456729668837,21.639767093025817,46.577624736134354 +2024-09-03 05:00:00,2,27.27063574974407,22.095318438941067,41.24960471643146 +2024-09-03 06:00:00,2,39.99315306975742,27.04183572874762,62.79796493480421 +2024-09-03 07:00:00,2,49.07178125914466,28.18765184770838,52.00479881070894 +2024-09-03 08:00:00,2,45.62690137829237,24.986007155231853,60.52304874923023 +2024-09-03 09:00:00,2,25.729414903508488,26.028660437741916,58.777606657579675 +2024-09-03 10:00:00,2,30.33213673037506,30.3751209181982,52.84436751057091 +2024-09-03 11:00:00,2,29.830065254510995,27.438721354752413,57.02387102530553 +2024-09-03 12:00:00,2,15.80791206051375,27.493150160204678,59.279608084407926 +2024-09-03 13:00:00,2,31.298750021633605,28.360753248281412,45.07901404973407 +2024-09-03 14:00:00,2,36.127098748220746,26.832234908233982,48.37924455614383 +2024-09-03 15:00:00,2,12.219501828876892,29.723364631514233,50.011834398543904 +2024-09-03 16:00:00,2,19.213595842732488,27.55822248443415,47.74793510899525 +2024-09-03 17:00:00,2,28.147278761769513,36.26407037034579,62.6948969849133 +2024-09-03 18:00:00,2,8.432953081754404,27.806799868422395,45.397448480110974 +2024-09-03 19:00:00,2,15.91864728871462,21.006541919338765,47.30380223996626 +2024-09-03 20:00:00,2,31.931235168955027,23.36662607855014,49.863191894171734 +2024-09-03 21:00:00,2,19.833510241989046,25.928746340432923,62.22064381040643 +2024-09-03 22:00:00,2,9.87237349028799,20.494926912049593,59.67476765167008 +2024-09-03 23:00:00,2,8.163405028121446,25.288576535069716,56.56793334687285 +2024-09-04 00:00:00,2,36.76471421700741,25.7131901566805,43.1791760593879 +2024-09-04 01:00:00,2,29.969661887536244,22.04116164854438,63.057415266262126 +2024-09-04 02:00:00,2,18.09923081168987,19.059242471629254,56.59923377444672 +2024-09-04 03:00:00,2,15.293695375168383,24.874012076764032,61.86133875837639 +2024-09-04 04:00:00,2,38.82997246062676,20.661910619115936,37.00653804430104 +2024-09-04 05:00:00,2,33.631914834866826,25.517936205574735,41.96881244310421 +2024-09-04 06:00:00,2,11.46845159738708,27.84216355028189,51.52427717043573 +2024-09-04 07:00:00,2,37.788375635062785,22.8189022405018,52.32783808895114 +2024-09-04 08:00:00,2,34.60247905176328,26.073185972695477,72.82972658709735 +2024-09-04 09:00:00,2,33.83558215965776,20.590546429496772,58.89059365619629 +2024-09-04 10:00:00,2,29.629427326306494,29.944508720856504,60.52082116565612 +2024-09-04 11:00:00,2,28.272378295373656,22.08698071777642,50.059453454650665 +2024-09-04 12:00:00,2,26.955232100727212,22.26744024522426,36.560277025752946 +2024-09-04 13:00:00,2,17.457683231843813,26.481815821111333,45.639164730170215 +2024-09-04 14:00:00,2,39.12773348388134,22.305169245937247,49.31896397531383 +2024-09-04 15:00:00,2,16.804695969819274,26.28412379061403,58.847476190815875 +2024-09-04 16:00:00,2,27.31469003067278,23.513963461100676,46.745090292221526 +2024-09-04 17:00:00,2,17.889031150889515,24.199278621419566,42.366056376653546 +2024-09-04 18:00:00,2,8.126615722369024,20.147533479579845,64.2934792892993 +2024-09-04 19:00:00,2,35.99782758738954,25.12903456057192,66.40905411219217 +2024-09-04 20:00:00,2,40.15958624787519,21.066145653882508,61.46764110071145 +2024-09-04 21:00:00,2,47.54861426972218,17.868158399805036,57.123704041708116 +2024-09-04 22:00:00,2,25.62631138712848,16.018700956160103,56.87175699639211 +2024-09-04 23:00:00,2,13.457183862799493,22.480507463537503,54.31930360259859 +2024-09-05 00:00:00,2,29.194692872397674,21.26959827326641,59.34830536690908 +2024-09-05 01:00:00,2,27.149171801931182,18.397180995313015,55.11132189106305 +2024-09-05 02:00:00,2,25.061978072052103,26.326039506538045,55.48240665861183 +2024-09-05 03:00:00,2,29.206890688926727,21.86198919800891,43.70498105196092 +2024-09-05 04:00:00,2,33.578273310193836,24.7156708495504,52.982933064471865 +2024-09-05 05:00:00,2,37.36648041903641,23.432008129039072,53.47583499030655 +2024-09-05 06:00:00,2,24.302643253790894,24.015369693358796,57.613473309641925 +2024-09-05 07:00:00,2,25.914894755116634,23.74438571739902,58.758549609322564 +2024-09-05 08:00:00,2,31.880597290006232,18.860789096138426,68.90959722345559 +2024-09-05 09:00:00,2,34.64243924568775,26.06346768423296,50.46494863810443 +2024-09-05 10:00:00,2,17.2757812491285,28.64376314999032,57.01004558449053 +2024-09-05 11:00:00,2,28.887885086454908,28.61294366719729,63.00631489390588 +2024-09-05 12:00:00,2,29.8807350730499,24.169108765452208,51.81875982203883 +2024-09-05 13:00:00,2,26.421491586497776,25.429116785215463,43.48547049411313 +2024-09-05 14:00:00,2,34.113714850097566,27.932729230232,63.32718942734667 +2024-09-05 15:00:00,2,31.004743837117534,22.74460598575609,56.43842575284448 +2024-09-05 16:00:00,2,9.323981300458826,28.05034246880058,52.068975321778936 +2024-09-05 17:00:00,2,29.25394888025038,20.791211928054715,51.60661573560669 +2024-09-05 18:00:00,2,35.454424179141434,28.617862053504652,66.31689433940996 +2024-09-05 19:00:00,2,34.03227851331188,24.865995663788123,60.54756639617764 +2024-09-05 20:00:00,2,16.776653473994546,19.065658328345148,70.2971215728662 +2024-09-05 21:00:00,2,19.64426609890949,26.525836354259294,49.043369788543224 +2024-09-05 22:00:00,2,23.263904628190268,22.338701973513146,46.8886843999497 +2024-09-05 23:00:00,2,17.75492504138025,19.692847957146096,59.86657544901067 +2024-09-06 00:00:00,2,18.384078448691117,29.75111589556533,36.28197792329607 +2024-09-06 01:00:00,2,22.617437610205528,24.335551268427402,61.1415834599108 +2024-09-06 02:00:00,2,43.328984722346746,22.730525919072832,42.731452644458216 +2024-09-06 03:00:00,2,18.594282270058685,21.87634498834832,37.04001159520247 +2024-09-06 04:00:00,2,6.347925908474757,20.872787393554766,40.08590351505811 +2024-09-06 05:00:00,2,27.429499670445125,27.572882601014957,58.473726322778624 +2024-09-06 06:00:00,2,33.591257789665455,19.55193850488456,45.80266850139123 +2024-09-06 07:00:00,2,37.01757630028711,27.18218811148615,38.36395213978501 +2024-09-06 08:00:00,2,20.13365627211422,25.888523258999356,65.0492629857178 +2024-09-06 09:00:00,2,30.45211625252814,21.579582334962268,52.67538523852675 +2024-09-06 10:00:00,2,31.24493296135147,24.228357756305492,51.073495474354836 +2024-09-06 11:00:00,2,26.89732836001766,27.607282054915526,40.33820889843175 +2024-09-06 12:00:00,2,27.29330513654746,24.426142265030617,49.29363569288624 +2024-09-06 13:00:00,2,36.58988415776179,25.530848380455982,37.79337072706901 +2024-09-06 14:00:00,2,31.468437355520546,25.683225299646903,53.2972974508672 +2024-09-06 15:00:00,2,14.93820122996293,28.462594687045335,56.77800645816625 +2024-09-06 16:00:00,2,22.48772921702765,27.05542177652213,50.738846141196944 +2024-09-06 17:00:00,2,32.2085700778661,31.90624674860952,44.15039862204594 +2024-09-06 18:00:00,2,23.05592845938274,23.384198802889923,53.059074993321005 +2024-09-06 19:00:00,2,30.138329542837457,25.748684797590926,48.12685118801168 +2024-09-06 20:00:00,2,46.73916628075705,25.954417028930344,57.022690402529946 +2024-09-06 21:00:00,2,38.38997043265606,21.465910971143362,62.28388184614771 +2024-09-06 22:00:00,2,34.836212287370145,22.920540729951053,35.788582388472875 +2024-09-06 23:00:00,2,27.6118523976072,24.038027933569936,40.54382338194884 +2024-09-07 00:00:00,2,43.776211050379246,19.106878672080256,46.593116560975176 +2024-09-07 01:00:00,2,36.7127874024208,21.609295098915364,60.82988062009491 +2024-09-07 02:00:00,2,23.42513656391273,22.359051720576,41.953717905543115 +2024-09-07 03:00:00,2,37.65736203837039,27.948466634903824,54.04191490763218 +2024-09-07 04:00:00,2,17.652601199687595,26.42448707777258,53.44479338775749 +2024-09-07 05:00:00,2,24.7379209476228,28.749831570245924,45.41002848027228 +2024-09-07 06:00:00,2,41.557501374762815,23.509751630752376,74.13714104472135 +2024-09-07 07:00:00,2,7.98452048811377,31.396670135953638,62.09865178933078 +2024-09-07 08:00:00,2,41.453596932608455,23.321915616542345,39.89391173705986 +2024-09-07 09:00:00,2,41.573705529935125,26.788387897981888,34.49250986035145 +2024-09-07 10:00:00,2,35.47569952655475,23.50184185034884,51.831697792280714 +2024-09-07 11:00:00,2,20.235754383590464,27.503318357715603,62.185359812376085 +2024-09-07 12:00:00,2,45.54113949551616,25.421337679707012,63.15661257614194 +2024-09-07 13:00:00,2,26.876039260860935,29.364880138071456,53.834568161648185 +2024-09-07 14:00:00,2,38.165901335184316,29.75665987768874,52.305760496221765 +2024-09-07 15:00:00,2,32.42152292846026,26.654008975961997,51.340380222865114 +2024-09-07 16:00:00,2,16.666858799240472,22.393757828365523,52.96500330775533 +2024-09-07 17:00:00,2,23.732555051195888,20.44571849031973,62.45009683230287 +2024-09-07 18:00:00,2,9.130693293651563,24.0249346276446,64.40031788667056 +2024-09-07 19:00:00,2,32.67784049200703,22.59056413491444,48.00846313126331 +2024-09-07 20:00:00,2,17.531621529449822,22.925485038815513,50.368571108456806 +2024-09-07 21:00:00,2,26.99282198317928,27.330503880642514,53.402040089403094 +2024-09-07 22:00:00,2,14.825319127733389,17.876222463950725,44.32449973386305 +2024-09-07 23:00:00,2,28.761809825051746,26.831823385611774,66.40847746159483 +2024-09-08 00:00:00,2,15.913391786617213,23.72224577114718,60.39617785364926 +2024-09-08 01:00:00,2,24.85107967095651,23.56185647748259,38.49016267711097 +2024-09-08 02:00:00,2,37.11549554100662,20.42727084911909,34.40516489874454 +2024-09-08 03:00:00,2,25.34606257719058,21.82622640904517,57.05118922195105 +2024-09-08 04:00:00,2,22.891687610916332,20.70422311949,44.8862525034483 +2024-09-08 05:00:00,2,31.378792829156826,24.015969522208746,50.78354022972435 +2024-09-08 06:00:00,2,21.88340263658283,24.32978133366408,56.8194111359649 +2024-09-08 07:00:00,2,41.68249120650791,27.241819325962513,62.28209475131274 +2024-09-08 08:00:00,2,31.25238233133414,25.485216414060428,79.54949557437496 +2024-09-08 09:00:00,2,17.436034334522994,29.562506443030593,43.528037224318695 +2024-09-08 10:00:00,2,24.64468098735801,22.389746305680507,52.06303625337768 +2024-09-08 11:00:00,2,37.31297472236453,25.533828681369364,61.921110913378286 +2024-09-08 12:00:00,2,18.930984640593714,25.160634839864937,49.29917472533538 +2024-09-08 13:00:00,2,11.892027762497632,25.3754067657346,39.29894609093249 +2024-09-08 14:00:00,2,25.61871189989768,28.649782979776006,41.21491488445968 +2024-09-08 15:00:00,2,2.9382978466692933,22.897747788910088,52.26729976024273 +2024-09-08 16:00:00,2,20.983378462003575,25.757495018087237,51.6185392052857 +2024-09-08 17:00:00,2,37.413566745997635,24.17132276201266,54.03948765582824 +2024-09-08 18:00:00,2,37.39855334321308,22.48185071691821,49.861748189465345 +2024-09-08 19:00:00,2,14.69032749037039,26.097617577002573,55.404368431043295 +2024-09-08 20:00:00,2,8.819678205443712,25.056526276946546,51.00421596283452 +2024-09-08 21:00:00,2,32.97411338597586,27.480585184681107,54.664725214052 +2024-09-08 22:00:00,2,35.93738885650727,23.3738523227203,72.58220733753178 +2024-09-08 23:00:00,2,14.781399874946048,25.263096497089453,58.408172484707244 +2024-09-09 00:00:00,2,16.444710961945887,26.01282388595506,50.95938624483849 +2024-09-09 01:00:00,2,36.30976774473828,22.03733100997984,44.56688774029307 +2024-09-09 02:00:00,2,21.05449535766475,23.486807003453528,63.35109876141709 +2024-09-09 03:00:00,2,31.395239075221376,21.329644508987226,49.597879239570254 +2024-09-09 04:00:00,2,28.420411970249337,24.04864497115781,49.34511913717374 +2024-09-09 05:00:00,2,13.275508372665232,24.246301339151643,48.56716022392094 +2024-09-09 06:00:00,2,36.1256608430797,18.83145505730707,62.79032533493246 +2024-09-09 07:00:00,2,48.46654231608819,25.81538495997398,65.4370952278101 +2024-09-09 08:00:00,2,25.57381527257944,25.75866802206506,37.14376271146136 +2024-09-09 09:00:00,2,16.392126137762233,23.587281216401028,62.39725048975019 +2024-09-09 10:00:00,2,37.086100889471524,28.796672143936984,45.231527464912006 +2024-09-09 11:00:00,2,31.568475608549424,29.543729387939035,71.17213198213925 +2024-09-09 12:00:00,2,28.62501639453868,23.609385305216087,45.39965341230212 +2024-09-09 13:00:00,2,33.20487424471018,25.99571875238303,56.491493593367636 +2024-09-09 14:00:00,2,32.75168841530851,27.019363639745258,57.245962475459265 +2024-09-09 15:00:00,2,14.892212553418466,19.42185976651979,57.12244411392468 +2024-09-09 16:00:00,2,37.65952264484373,27.966308229516528,45.13147477545819 +2024-09-09 17:00:00,2,34.53136263773918,25.91042810183064,57.38851008341424 +2024-09-09 18:00:00,2,37.44502915705273,21.321929384467943,66.21811011063197 +2024-09-09 19:00:00,2,36.819000146158324,21.665413432153006,68.45051754703407 +2024-09-09 20:00:00,2,31.559693051202956,23.174731555303634,80.51636108687373 +2024-09-09 21:00:00,2,31.161915936614555,22.080720381996567,53.12520823299678 +2024-09-09 22:00:00,2,20.829906053356215,22.502798175998223,67.636276036492 +2024-09-09 23:00:00,2,21.02580847475044,24.171252504940952,50.60621115305819 +2024-09-10 00:00:00,2,14.183493646654739,24.874903069781514,43.79238693916426 +2024-09-10 01:00:00,2,34.613154124320204,25.910219785353338,54.439094845262616 +2024-09-10 02:00:00,2,21.081472921697785,21.444859815450624,48.13847502135925 +2024-09-10 03:00:00,2,35.48959473757926,21.372753223566598,51.89541646001339 +2024-09-10 04:00:00,2,20.776222515695515,22.12787912037722,44.5534739009752 +2024-09-10 05:00:00,2,26.020042140429354,26.565459978011212,46.509760788189766 +2024-09-10 06:00:00,2,19.186804266157424,24.45857165161435,34.332253360424225 +2024-09-10 07:00:00,2,39.48216806373714,28.184759242733772,64.61034466711362 +2024-09-10 08:00:00,2,44.25151236023247,28.005986832075944,88.17312437936152 +2024-09-10 09:00:00,2,42.468605695033546,29.171367355871006,42.098350117565836 +2024-09-10 10:00:00,2,40.27061713264859,22.787714743377478,37.20117794414913 +2024-09-10 11:00:00,2,29.006118841709167,24.54874760496248,55.8645239490793 +2024-09-10 12:00:00,2,35.07079155184585,24.935587059058335,40.65498552110107 +2024-09-10 13:00:00,2,21.368545805781316,22.429889995415326,64.09320737294647 +2024-09-10 14:00:00,2,9.447315126375504,24.242175778454353,46.76962040707491 +2024-09-10 15:00:00,2,19.035519120886956,27.427589743945227,52.93739952799063 +2024-09-10 16:00:00,2,11.633416126359837,28.504281227996234,58.81451122327842 +2024-09-10 17:00:00,2,30.408705836540904,24.77019730441609,68.6698269305696 +2024-09-10 18:00:00,2,18.770498213361684,28.145054010141525,59.343312347757475 +2024-09-10 19:00:00,2,34.63049449421454,22.758885078292018,64.26951749604393 +2024-09-10 20:00:00,2,35.01325315027422,21.476266410966534,46.2654654465133 +2024-09-10 21:00:00,2,14.895949008030916,19.380547138517976,36.163645166620725 +2024-09-10 22:00:00,2,22.100167497911432,19.938810202261436,41.32853760029098 +2024-09-10 23:00:00,2,37.57562606388201,20.795155775685423,64.63316270862137 +2024-09-11 00:00:00,2,23.717281368677487,22.23461420934417,47.35967146597382 +2024-09-11 01:00:00,2,42.92104558481491,24.489855856981848,52.36613190204169 +2024-09-11 02:00:00,2,37.28397961002527,18.9763578676911,66.33450609930921 +2024-09-11 03:00:00,2,26.018531234033105,15.661508706565966,49.75337554624584 +2024-09-11 04:00:00,2,38.58694129212893,24.9448476878404,51.955459315955686 +2024-09-11 05:00:00,2,37.91947676002723,23.904488535762734,55.32112496810283 +2024-09-11 06:00:00,2,37.26237666045766,22.987000160828874,63.14999323439059 +2024-09-11 07:00:00,2,32.32535473488588,19.87089384968917,63.66894111782844 +2024-09-11 08:00:00,2,24.790567637857528,21.036200565572926,41.24890412025645 +2024-09-11 09:00:00,2,27.77860913638265,26.428496432821536,46.827407949182515 +2024-09-11 10:00:00,2,26.337601714240314,23.73086052083729,43.9471646026698 +2024-09-11 11:00:00,2,24.151134017335192,21.273639891779197,47.590676900187155 +2024-09-11 12:00:00,2,19.768906212013952,25.138171279108565,62.31926015030786 +2024-09-11 13:00:00,2,12.771566378265796,31.5230240601891,56.52313703635202 +2024-09-11 14:00:00,2,10.888799424677146,27.408597972124962,54.72557271107649 +2024-09-11 15:00:00,2,22.589774376313166,29.29005662128671,67.01152131684562 +2024-09-11 16:00:00,2,22.917963947753154,27.22070909461721,30.90600865540686 +2024-09-11 17:00:00,2,33.52208972510086,19.948182037676823,69.49164067427321 +2024-09-11 18:00:00,2,13.27614829747696,22.046694244743144,64.36605897920677 +2024-09-11 19:00:00,2,22.995032718373235,22.18474576190336,66.81594782086518 +2024-09-11 20:00:00,2,13.117343558943361,23.632416819466584,70.4268913800058 +2024-09-11 21:00:00,2,22.801387964828837,25.059172462688345,58.70565948439929 +2024-09-11 22:00:00,2,22.222016806530828,18.321779508400006,45.5386648941523 +2024-09-11 23:00:00,2,25.431232379735697,20.462920032975106,58.93057558929072 +2024-09-12 00:00:00,2,32.46510391885973,25.631485158908873,41.95393503927252 +2024-09-12 01:00:00,2,20.105264016423952,25.92431723537254,47.59153673654821 +2024-09-12 02:00:00,2,39.708643273545356,22.548020126070615,42.69450171782016 +2024-09-12 03:00:00,2,22.009124152631717,27.453830828366506,56.47910187618027 +2024-09-12 04:00:00,2,25.352437974910544,23.74992637031561,54.253810778714914 +2024-09-12 05:00:00,2,25.648538273165038,36.18204698731013,44.30980194791955 +2024-09-12 06:00:00,2,35.35118449507037,28.150222246688042,28.926938910449667 +2024-09-12 07:00:00,2,35.055462069363195,23.91277562728872,46.28818877086395 +2024-09-12 08:00:00,2,37.07401881664476,18.496816353379764,50.9561512384201 +2024-09-12 09:00:00,2,29.806636219682275,33.96051219150656,63.285922615078164 +2024-09-12 10:00:00,2,44.555919563426244,25.6962671488187,41.205457415237035 +2024-09-12 11:00:00,2,9.653451468570335,30.633464706062647,62.69917517760871 +2024-09-12 12:00:00,2,28.145961779858315,32.00830604518714,49.28944911267647 +2024-09-12 13:00:00,2,21.728447835220834,23.444576325051695,49.6648169808077 +2024-09-12 14:00:00,2,16.179244065895592,27.296692450161473,64.12288828748451 +2024-09-12 15:00:00,2,27.423896747449422,27.732382093511305,50.84533024068489 +2024-09-12 16:00:00,2,0.0,21.178361981544537,42.35114347327339 +2024-09-12 17:00:00,2,21.75803895809898,23.004729082501214,53.851924184898856 +2024-09-12 18:00:00,2,30.460832892736462,26.658430864022797,50.14589658919128 +2024-09-12 19:00:00,2,12.143297749676364,21.101246196779574,47.59055178328027 +2024-09-12 20:00:00,2,25.513816648453204,23.82318436168357,44.62047175223166 +2024-09-12 21:00:00,2,29.16345574881342,27.907627074754004,57.30828889374076 +2024-09-12 22:00:00,2,24.317262449051512,26.02925511412978,66.0477342328602 +2024-09-12 23:00:00,2,31.78924790673727,27.345829562568845,44.73778049900714 +2024-09-13 00:00:00,2,49.397201624706874,24.549206051106665,46.096452173271345 +2024-09-13 01:00:00,2,16.34755648014078,24.592641853142187,39.98586081481101 +2024-09-13 02:00:00,2,29.511134415121404,29.49787139494933,57.3190346564801 +2024-09-13 03:00:00,2,26.70907301925265,26.092289582885005,54.04695034509457 +2024-09-13 04:00:00,2,26.526343823863886,20.526702257922693,49.475113304359496 +2024-09-13 05:00:00,2,7.723280410411913,23.71572028366623,61.952197233598945 +2024-09-13 06:00:00,2,24.794513471985926,16.684518377569944,51.97476437826792 +2024-09-13 07:00:00,2,6.655465627503581,23.069502291808227,49.585091854145624 +2024-09-13 08:00:00,2,46.28314267536901,24.57849136337558,43.835456973417735 +2024-09-13 09:00:00,2,32.1204122711643,29.708858633661038,52.26755040117932 +2024-09-13 10:00:00,2,40.0173143930995,24.14022651914286,55.66726894915212 +2024-09-13 11:00:00,2,19.14792639815026,19.56733348272213,55.74378016624945 +2024-09-13 12:00:00,2,28.70095264399689,22.99371630443615,55.73705121188157 +2024-09-13 13:00:00,2,39.145620641392654,24.62450546723967,54.89238223896927 +2024-09-13 14:00:00,2,18.437297230034414,22.949333607468397,60.64045445258906 +2024-09-13 15:00:00,2,22.20819782170639,23.297914820299468,47.477494820040974 +2024-09-13 16:00:00,2,43.3973054802903,24.68168979244502,62.26319683084453 +2024-09-13 17:00:00,2,33.00499977067628,29.405859225198757,68.20422839085725 +2024-09-13 18:00:00,2,30.65418807869288,26.565317074605442,50.77422794985499 +2024-09-13 19:00:00,2,32.95881523775793,22.124679711083342,49.39287159632597 +2024-09-13 20:00:00,2,28.68874586559969,31.139162400109115,63.21620225711062 +2024-09-13 21:00:00,2,17.331355679400033,23.93516768745558,49.342876200705184 +2024-09-13 22:00:00,2,20.437540650698402,29.591857882290093,63.55489188311274 +2024-09-13 23:00:00,2,32.55905791905403,19.643914129135887,68.97123354532837 +2024-09-14 00:00:00,2,30.737222930216724,20.24487972698433,62.183712092969074 +2024-09-14 01:00:00,2,45.938925378304916,26.140495023083226,49.175786702995346 +2024-09-14 02:00:00,2,41.224180907780706,17.296285288091887,47.369761076331955 +2024-09-14 03:00:00,2,35.19084625268651,22.50868751928494,36.83654470610821 +2024-09-14 04:00:00,2,38.55716305378024,24.472669054800818,32.43492465731593 +2024-09-14 05:00:00,2,6.172570611971157,25.81736358244391,63.33599256454406 +2024-09-14 06:00:00,2,11.617589672837212,26.135853987073155,61.43064687577494 +2024-09-14 07:00:00,2,25.18328463589447,24.7808641482545,50.52687780398148 +2024-09-14 08:00:00,2,21.087920696077113,30.075709011519475,35.51248187824392 +2024-09-14 09:00:00,2,20.9551613342055,31.430072876250833,43.21454762402092 +2024-09-14 10:00:00,2,40.2654822696621,26.31198366042914,49.66552817112624 +2024-09-14 11:00:00,2,25.345307534993818,19.510817165394077,70.38565336069979 +2024-09-14 12:00:00,2,23.23091974967353,28.183366969644055,64.2556536390723 +2024-09-14 13:00:00,2,19.73734014110068,29.596074294688925,54.254227071434855 +2024-09-14 14:00:00,2,18.018200625600052,27.115614504951168,54.35835162344037 +2024-09-14 15:00:00,2,0.0,25.55561627334774,31.19456441655831 +2024-09-14 16:00:00,2,30.446489758713547,18.258271782281454,54.192835181409755 +2024-09-14 17:00:00,2,14.701422684135894,27.952236834001056,53.863672566039945 +2024-09-14 18:00:00,2,34.60747253707942,26.161858316449123,58.0412694137474 +2024-09-14 19:00:00,2,33.0908413858765,21.71570902774844,48.50159032239668 +2024-09-14 20:00:00,2,29.657004054447874,25.787730221807095,44.31152546277724 +2024-09-14 21:00:00,2,23.287216462804633,26.403645649720662,68.17914478221631 +2024-09-14 22:00:00,2,31.569052114771374,21.758140718149754,70.13819146874488 +2024-09-14 23:00:00,2,27.87435777496088,25.483235044767838,48.324506797724986 +2024-09-15 00:00:00,2,18.501874718719066,23.663224232776603,30.986460025900953 +2024-09-15 01:00:00,2,37.93863302739544,16.304334766588013,59.92559749041607 +2024-09-15 02:00:00,2,23.81759162317443,21.011719273772783,57.30307475185115 +2024-09-15 03:00:00,2,28.757319357165414,20.181408872514748,49.3581059600279 +2024-09-15 04:00:00,2,32.639813125235676,23.179965511422054,53.81630765175636 +2024-09-15 05:00:00,2,28.154160989203863,26.022502526877048,50.17991593615835 +2024-09-15 06:00:00,2,30.412898522185113,22.451487559194653,51.153091441021225 +2024-09-15 07:00:00,2,24.340884911524846,25.545551240371022,64.54728650756125 +2024-09-15 08:00:00,2,29.76794817473825,15.821741195075258,67.12459841036775 +2024-09-15 09:00:00,2,34.8439601688,27.82338827823261,56.268204138169274 +2024-09-15 10:00:00,2,35.43280871947971,16.57443907227809,43.263931286272566 +2024-09-15 11:00:00,2,29.66194247060537,26.441907325194002,31.803181658899803 +2024-09-15 12:00:00,2,14.11557283339205,31.6771535075546,58.64380424954286 +2024-09-15 13:00:00,2,27.392473682056618,19.112553133425845,56.60623426809323 +2024-09-15 14:00:00,2,34.09572006392179,29.583864258498323,52.70696248714794 +2024-09-15 15:00:00,2,15.835025687851866,23.581830698994942,48.34637162670781 +2024-09-15 16:00:00,2,30.15096634082841,28.47059087592712,59.85061915673623 +2024-09-15 17:00:00,2,18.395822062025676,24.532397324540504,47.8893783838653 +2024-09-15 18:00:00,2,32.216596428763296,27.55762607203195,64.25936917164604 +2024-09-15 19:00:00,2,30.919874474806573,25.470023590955318,38.62838916460682 +2024-09-15 20:00:00,2,35.29033970064491,30.021273058158677,34.41367176798287 +2024-09-15 21:00:00,2,25.716908146453513,24.52496787705076,50.45967207495817 +2024-09-15 22:00:00,2,23.17796121578669,22.425196816386514,63.05442873362593 +2024-09-15 23:00:00,2,24.565032561072865,28.700777437851823,53.02785712575478 +2024-09-16 00:00:00,2,25.442391560289746,24.52397442571013,46.995775504715965 +2024-09-16 01:00:00,2,19.918950152863285,22.745245383433154,41.056871150284266 +2024-09-16 02:00:00,2,36.16660493095388,27.300795287088565,63.53986238152825 +2024-09-16 03:00:00,2,36.970190414368716,21.046703256918498,39.251553325882924 +2024-09-16 04:00:00,2,17.256614735010068,23.678589377428935,54.995849270576485 +2024-09-16 05:00:00,2,41.51741570153634,18.373583326541272,51.061765031413564 +2024-09-16 06:00:00,2,22.557368278476098,19.365948389655117,52.66679883213681 +2024-09-16 07:00:00,2,28.32547399233268,23.93459935648005,63.04074416529535 +2024-09-16 08:00:00,2,41.02477137535845,26.835379684942332,51.192196115028075 +2024-09-16 09:00:00,2,25.79834044385301,24.181849827643948,63.968366163124024 +2024-09-16 10:00:00,2,41.320060909009385,20.094119735215273,46.71628206229422 +2024-09-16 11:00:00,2,29.12074478092541,27.988159119211133,52.482654520943626 +2024-09-16 12:00:00,2,34.134274883512454,21.63552225572765,46.86441071938569 +2024-09-16 13:00:00,2,53.5112089423335,30.83740373589225,49.90344437298559 +2024-09-16 14:00:00,2,28.372416587181156,24.393484557284797,66.09312348984095 +2024-09-16 15:00:00,2,28.135788509639557,20.02669797653676,40.489027645970154 +2024-09-16 16:00:00,2,6.314184953808617,22.791243557755045,56.42981924459487 +2024-09-16 17:00:00,2,18.357873819481775,26.228749600659842,39.23542576205281 +2024-09-16 18:00:00,2,25.637750238228577,23.429730454641245,69.8683549429013 +2024-09-16 19:00:00,2,9.180900418159796,22.97539672179055,58.39733149217589 +2024-09-16 20:00:00,2,17.64999831770124,18.08473082345664,73.75085764974975 +2024-09-16 21:00:00,2,24.531846033257906,24.315586380375322,59.17198821876209 +2024-09-16 22:00:00,2,17.816741531378792,25.29538351438549,62.27002080759528 +2024-09-16 23:00:00,2,11.530291905406624,19.44606695674164,72.2560206118321 +2024-09-17 00:00:00,2,21.430267690878,23.065298373593222,64.72152756181362 +2024-09-17 01:00:00,2,32.7025088520868,24.77530029241979,53.96769576337034 +2024-09-17 02:00:00,2,31.733396554727115,33.32719086890671,58.6872558576709 +2024-09-17 03:00:00,2,29.28183681129313,19.11653996182404,56.03802799742674 +2024-09-17 04:00:00,2,25.987030624251144,22.139266916738222,43.572465761458524 +2024-09-17 05:00:00,2,43.95014973565568,23.2334840716635,51.513460657633026 +2024-09-17 06:00:00,2,11.312837601036481,28.95688201306733,51.659228607400244 +2024-09-17 07:00:00,2,22.623724413806528,23.967191051609056,41.329057961436376 +2024-09-17 08:00:00,2,32.128163968132846,25.820200508333695,41.20039356528134 +2024-09-17 09:00:00,2,37.93712201692745,27.052610114712152,48.99530294667794 +2024-09-17 10:00:00,2,23.17108706141039,23.254586973067678,60.34386852838072 +2024-09-17 11:00:00,2,33.554561792378095,32.770550126824304,52.722773227611526 +2024-09-17 12:00:00,2,33.401850852781536,28.559233867844256,39.7715604259738 +2024-09-17 13:00:00,2,2.8277350662612903,25.666491101213555,60.711000064694645 +2024-09-17 14:00:00,2,23.618784310594453,22.56729438430321,57.404025980763244 +2024-09-17 15:00:00,2,39.62862313957359,22.242393785383094,52.688687658453674 +2024-09-17 16:00:00,2,7.476685139085834,23.58245397375207,44.432713853851155 +2024-09-17 17:00:00,2,34.1693529658299,26.941001042783633,65.04401722141228 +2024-09-17 18:00:00,2,29.721444260155558,24.72563062180645,53.39033915429724 +2024-09-17 19:00:00,2,23.73838224154554,23.70204145819801,69.13650557422957 +2024-09-17 20:00:00,2,22.0018655004904,23.75933702993846,46.5982585978833 +2024-09-17 21:00:00,2,24.79513663241266,28.52993707204937,62.661249361127965 +2024-09-17 22:00:00,2,29.94042987464016,29.547848913777976,68.96356913706808 +2024-09-17 23:00:00,2,24.42156456229481,30.735281226913457,44.832081145824404 +2024-09-18 00:00:00,2,26.10286535008402,16.418929748246832,54.347038589977835 +2024-09-18 01:00:00,2,27.812576411146143,18.700695440927245,75.24894723920072 +2024-09-18 02:00:00,2,21.852586901266164,26.166135096807178,55.80137358041331 +2024-09-18 03:00:00,2,30.206868559695884,22.60227566962041,58.77256514150939 +2024-09-18 04:00:00,2,35.84088672310336,24.902836567264696,41.69106434569996 +2024-09-18 05:00:00,2,43.64326482046738,25.551224994222387,52.370557117810314 +2024-09-18 06:00:00,2,2.2563093584091725,25.37957729201613,45.37385489682159 +2024-09-18 07:00:00,2,42.081202076851675,25.809117229280208,45.09217589433743 +2024-09-18 08:00:00,2,21.709938043805668,32.16755835784582,47.474171170997494 +2024-09-18 09:00:00,2,37.92561152822853,22.15889304418276,73.06647744280149 +2024-09-18 10:00:00,2,28.378422895176733,25.453599423199133,63.76200061632698 +2024-09-18 11:00:00,2,32.34820666567282,28.792105609913126,49.60727807544701 +2024-09-18 12:00:00,2,43.69674154468344,33.65107166526821,62.48342389153362 +2024-09-18 13:00:00,2,28.564186992875886,24.375036153257582,66.21646945349698 +2024-09-18 14:00:00,2,20.616847434322928,21.80311233271369,48.17504468438455 +2024-09-18 15:00:00,2,20.274315900907304,23.661951832929653,51.308643559589804 +2024-09-18 16:00:00,2,22.591917874777394,32.777727369104916,60.24949214252146 +2024-09-18 17:00:00,2,22.256079589823663,26.89251575763304,55.35446730559517 +2024-09-18 18:00:00,2,24.430634492787764,26.040456649204238,66.19677498266675 +2024-09-18 19:00:00,2,33.59729686895942,24.396935863839786,58.52945842939836 +2024-09-18 20:00:00,2,15.44594112037163,24.895911472267592,57.59420065966612 +2024-09-18 21:00:00,2,18.427136158500947,31.466228427840154,60.507176684607884 +2024-09-18 22:00:00,2,22.14645019937311,19.38871511394025,63.42791491071672 +2024-09-18 23:00:00,2,42.059267561531485,27.718747027825763,48.155138021298626 +2024-09-19 00:00:00,2,33.0373179546201,22.95104570496199,68.14488354398264 +2024-09-19 01:00:00,2,47.97909574799757,26.33465353303371,39.88404892850411 +2024-09-19 02:00:00,2,32.57948744362238,26.813337570375076,73.49628518412104 +2024-09-19 03:00:00,2,10.923644763644521,20.52691761722845,57.05503441640289 +2024-09-19 04:00:00,2,20.110219541888387,27.523184433972222,40.041162394948124 +2024-09-19 05:00:00,2,31.781600836202312,28.35819456641065,58.845317114073026 +2024-09-19 06:00:00,2,19.139942652291737,27.161439275809037,48.9444836888462 +2024-09-19 07:00:00,2,21.30174883685089,25.338945424927964,49.20427494450789 +2024-09-19 08:00:00,2,49.98845916358974,25.127021327495207,53.69861254817052 +2024-09-19 09:00:00,2,37.25057817100974,25.30933391598337,56.16962755900401 +2024-09-19 10:00:00,2,28.639953872696044,25.846628278479574,39.68940397344676 +2024-09-19 11:00:00,2,42.01172483617955,31.322417701214714,43.0094000915529 +2024-09-19 12:00:00,2,29.415060765871278,28.854147164507186,55.957654149126384 +2024-09-19 13:00:00,2,41.12993655715752,25.099121569204463,63.347205384210554 +2024-09-19 14:00:00,2,24.3871971296118,28.83876698158329,50.653416974657084 +2024-09-19 15:00:00,2,29.54524304750249,29.783935378150105,61.05074395884046 +2024-09-19 16:00:00,2,39.97849254670763,20.250247367061057,52.096169448405085 +2024-09-19 17:00:00,2,33.33545307212867,20.340530894437254,46.25425057245002 +2024-09-19 18:00:00,2,30.117717492500315,24.204330987798002,56.50810128334422 +2024-09-19 19:00:00,2,24.159747476964966,32.79917874598671,60.90110879379234 +2024-09-19 20:00:00,2,23.817164424069766,23.097608874391796,62.86863015787605 +2024-09-19 21:00:00,2,11.023932960151836,24.273354940729934,61.398167502489954 +2024-09-19 22:00:00,2,30.602862671503466,25.90221361537739,40.4442840029774 +2024-09-19 23:00:00,2,18.39565256380409,23.722647478128223,53.31141671060012 +2024-09-20 00:00:00,2,44.780674881689244,21.93730662619193,64.92911277778246 +2024-09-20 01:00:00,2,40.68987178100659,19.654966895999554,67.1678304465676 +2024-09-20 02:00:00,2,42.60209114476717,23.129125311593302,57.860680001650714 +2024-09-20 03:00:00,2,30.848238811222963,26.911964579473185,64.36551053349098 +2024-09-20 04:00:00,2,37.74956083788046,25.364533545660976,66.7760315526201 +2024-09-20 05:00:00,2,22.18257423539054,27.42055640561696,41.252992169057705 +2024-09-20 06:00:00,2,38.97849515155109,27.75842310823238,51.02164715588662 +2024-09-20 07:00:00,2,27.429403990997557,20.38449296187492,45.724150169935086 +2024-09-20 08:00:00,2,25.544048249139742,25.89524724694874,60.859536736958574 +2024-09-20 09:00:00,2,36.45599204958157,27.472666906081006,68.36381200381612 +2024-09-20 10:00:00,2,34.51483278263365,26.035932218273572,58.404970989766085 +2024-09-20 11:00:00,2,32.6097142982448,28.059389275960875,37.67340278264393 +2024-09-20 12:00:00,2,27.10136557969657,25.86328237338034,51.42863458497711 +2024-09-20 13:00:00,2,23.384423055015986,27.337783818270793,62.3100322025965 +2024-09-20 14:00:00,2,32.3266047469278,26.48563236896757,52.37066683528819 +2024-09-20 15:00:00,2,20.71609214417772,24.64927698893151,67.275781613167 +2024-09-20 16:00:00,2,9.905300725855238,23.84886868863254,51.298154565970776 +2024-09-20 17:00:00,2,28.87398772886381,21.43652218525884,50.1306597518899 +2024-09-20 18:00:00,2,30.25857364419273,22.29747379948693,69.94863958524067 +2024-09-20 19:00:00,2,20.879853216366804,24.24171578453471,59.99394366178176 +2024-09-20 20:00:00,2,39.314509156835044,26.540081106237363,55.87317136506835 +2024-09-20 21:00:00,2,24.05195326800808,20.155405648718048,39.4309527554803 +2024-09-20 22:00:00,2,25.25999950124225,23.31787721835872,51.535633843856985 +2024-09-20 23:00:00,2,16.04413353621547,21.962776627263224,45.52315446856069 +2024-09-21 00:00:00,2,35.622094027869196,27.28296683118571,50.4910974455934 +2024-09-21 01:00:00,2,35.66606334986856,25.909721026351054,65.01666085489994 +2024-09-21 02:00:00,2,24.41510028477805,25.048055493714042,61.74220662135821 +2024-09-21 03:00:00,2,22.885401839725393,25.920190346360446,65.95561600119844 +2024-09-21 04:00:00,2,40.99140476559354,24.7380206906178,55.690541970392395 +2024-09-21 05:00:00,2,27.179429091667316,24.29236449622064,46.84708303648859 +2024-09-21 06:00:00,2,37.73988828696692,30.57696431960729,62.8488574523519 +2024-09-21 07:00:00,2,28.62471907864404,20.126352021930337,40.485656576960274 +2024-09-21 08:00:00,2,12.09171487119739,23.53511703540273,52.74847858606415 +2024-09-21 09:00:00,2,19.697810469224578,26.88746092868161,30.97453220890751 +2024-09-21 10:00:00,2,24.536220225238736,27.753818516370387,67.3449690736577 +2024-09-21 11:00:00,2,21.354935976826194,25.46866241855932,45.181775885367 +2024-09-21 12:00:00,2,36.99787337838971,26.205912564621812,58.06734934531485 +2024-09-21 13:00:00,2,36.49050207896816,29.5530545610047,60.69922079189129 +2024-09-21 14:00:00,2,19.332001912004074,27.62827281009935,46.083468048444225 +2024-09-21 15:00:00,2,31.83105874854568,32.79822633792821,55.86551128645861 +2024-09-21 16:00:00,2,40.190703157926144,22.880217270297685,46.49110206117389 +2024-09-21 17:00:00,2,28.55481443512293,29.06486888440391,38.44826920749061 +2024-09-21 18:00:00,2,14.184034095846114,25.217758215394106,63.21610897277209 +2024-09-21 19:00:00,2,22.1409545408212,28.97497896594783,53.234441299093824 +2024-09-21 20:00:00,2,19.10073319386785,29.35421328272792,51.32791796048609 +2024-09-21 21:00:00,2,20.31127488389401,24.24193850571538,60.03005632054877 +2024-09-21 22:00:00,2,28.939898766426257,22.246149665097253,63.141142360814854 +2024-09-21 23:00:00,2,16.866655622556586,31.194244896143584,60.54252712093367 +2024-09-22 00:00:00,2,11.716445404785993,23.877464125012857,49.43668135584791 +2024-09-22 01:00:00,2,24.085915506684934,22.06496521633791,48.03088149336397 +2024-09-22 02:00:00,2,23.751572189101125,22.55228971111275,38.16924324132833 +2024-09-22 03:00:00,2,7.827447726621447,23.430914470432555,47.9222269009639 +2024-09-22 04:00:00,2,18.94171336607459,24.687870408032474,58.22745529601762 +2024-09-22 05:00:00,2,25.05893317136196,28.02947167745825,56.499958957328005 +2024-09-22 06:00:00,2,34.50447407478296,26.506490298958543,45.1845232589025 +2024-09-22 07:00:00,2,33.03031150589709,23.331393833931863,64.92197771594314 +2024-09-22 08:00:00,2,24.79457241531851,26.82065478789923,57.91910716863225 +2024-09-22 09:00:00,2,38.23418393498899,23.163470373763378,47.55166722664513 +2024-09-22 10:00:00,2,25.08276687030728,23.305127744260737,35.728107540236586 +2024-09-22 11:00:00,2,43.00129714458217,31.578476733679597,71.41397426721093 +2024-09-22 12:00:00,2,22.857468228169054,23.80087916342899,50.85991894921499 +2024-09-22 13:00:00,2,27.062294874465525,31.532750700134503,37.81488698211899 +2024-09-22 14:00:00,2,33.651839975313685,23.280188908957314,42.376417681684615 +2024-09-22 15:00:00,2,27.198540228171165,28.763675334680734,44.87496602665292 +2024-09-22 16:00:00,2,25.59409735933211,26.411788079245337,53.72519643930831 +2024-09-22 17:00:00,2,18.7448558692135,28.70964632591066,50.51447742872624 +2024-09-22 18:00:00,2,28.76020738238708,25.358939562320312,61.47082238415945 +2024-09-22 19:00:00,2,28.549473494856045,20.428524567839567,63.1263328102817 +2024-09-22 20:00:00,2,27.754562770166785,22.473579511822837,54.443980410592516 +2024-09-22 21:00:00,2,23.579082606190482,28.592692301552972,53.494709853088715 +2024-09-22 22:00:00,2,15.275325199569949,25.685013183410806,66.46391191010883 +2024-09-22 23:00:00,2,27.0176597661361,26.541490971704462,71.1804782547646 +2024-09-23 00:00:00,2,24.543172556740853,25.19380094442127,55.884987839907424 +2024-09-23 01:00:00,2,23.51814616713291,24.396411579419066,58.78914895591795 +2024-09-23 02:00:00,2,29.531830554157725,21.0687274246994,44.349405236233004 +2024-09-23 03:00:00,2,36.864838530324114,23.956624668425864,67.50350594533809 +2024-09-23 04:00:00,2,19.826857187956314,18.691030232659138,53.18255333190537 +2024-09-23 05:00:00,2,23.796967788800515,15.378668826023864,64.55314437133597 +2024-09-23 06:00:00,2,42.73543334207165,28.438613796771087,50.81149168637762 +2024-09-23 07:00:00,2,31.32566600285628,25.923080672260745,35.31295304046071 +2024-09-23 08:00:00,2,28.09058300579085,28.25025926882376,55.23603182310873 +2024-09-23 09:00:00,2,28.053379702106966,27.191536337472623,52.835629089155915 +2024-09-23 10:00:00,2,33.1755857862361,28.864211795186613,47.36536936279652 +2024-09-23 11:00:00,2,16.686883984897424,29.43011035146666,59.73057814701203 +2024-09-23 12:00:00,2,26.142537357218963,28.031771104700184,36.51501998251387 +2024-09-23 13:00:00,2,24.567628702378197,22.966920665366523,50.60008376832059 +2024-09-23 14:00:00,2,17.463364695654757,25.34892126136483,45.866010520985675 +2024-09-23 15:00:00,2,16.92534415701142,22.878212759570403,53.20431002742478 +2024-09-23 16:00:00,2,13.279090108161272,26.367885194668457,48.59265391232793 +2024-09-23 17:00:00,2,18.20018179269144,22.19567919800664,52.97495250716482 +2024-09-23 18:00:00,2,21.49978008248565,33.85593352747225,47.4161688064632 +2024-09-23 19:00:00,2,22.16005192588878,25.458497482210046,54.840784977854256 +2024-09-23 20:00:00,2,27.24031249515676,23.35689490179188,55.8285252125462 +2024-09-23 21:00:00,2,42.40658886975085,20.724227628003103,60.632827649843335 +2024-09-23 22:00:00,2,35.31716250198066,22.51475232242462,30.725178510222804 +2024-09-23 23:00:00,2,29.734985481834677,29.970025254351004,49.231784755260726 +2024-09-24 00:00:00,2,30.63017597478918,23.09089826810857,51.612400219696 +2024-09-24 01:00:00,2,34.97866284502411,21.755559555089178,41.51688934380611 +2024-09-24 02:00:00,2,40.78519172968409,17.988935719304035,39.91914006854378 +2024-09-24 03:00:00,2,30.61511303565647,22.230537818273955,55.31647380613086 +2024-09-24 04:00:00,2,38.065377660750045,22.764299883135784,35.16298760789584 +2024-09-24 05:00:00,2,30.637607047032105,22.180916564924985,59.86427295648236 +2024-09-24 06:00:00,2,27.48688126600048,28.918013195517897,73.84955613826085 +2024-09-24 07:00:00,2,25.86665976638933,24.87041990432191,54.78957702159837 +2024-09-24 08:00:00,2,39.31309347419305,25.692290523459402,41.97170229527768 +2024-09-24 09:00:00,2,24.41405113230932,26.929185920088976,38.55445924616433 +2024-09-24 10:00:00,2,33.07057973473975,32.15402126390207,51.02845923716995 +2024-09-24 11:00:00,2,20.747818266090682,27.472616173915142,51.17690143534888 +2024-09-24 12:00:00,2,25.243275178074914,28.552315661629894,53.991729201334785 +2024-09-24 13:00:00,2,23.865263786775262,31.390080098652138,52.63081120962875 +2024-09-24 14:00:00,2,15.733377695381343,23.259357100133958,52.25285343228638 +2024-09-24 15:00:00,2,20.612291580818336,25.76063179116483,48.96596744256949 +2024-09-24 16:00:00,2,17.500734888592206,19.797368513647985,52.82946582073627 +2024-09-24 17:00:00,2,2.157108444164063,28.88557875375528,65.43630036074617 +2024-09-24 18:00:00,2,17.780992058265575,26.312262471826006,45.713109446351005 +2024-09-24 19:00:00,2,30.703544808309058,27.792705319797985,69.83353883947956 +2024-09-24 20:00:00,2,27.854905294268924,28.573604168304414,67.93047918777054 +2024-09-24 21:00:00,2,26.646387487437533,22.931986703236724,47.00010602841813 +2024-09-24 22:00:00,2,13.791262476457488,24.279110945395743,69.45784383691065 +2024-09-24 23:00:00,2,23.321148347810546,24.00698683878248,36.51018899316568 +2024-09-25 00:00:00,2,32.06460757287421,26.011083853949206,40.93783479369938 +2024-09-25 01:00:00,2,32.06717577313239,29.393394309012095,54.38186094334762 +2024-09-25 02:00:00,2,36.199737447588745,19.605094897677667,37.37506781209992 +2024-09-25 03:00:00,2,31.58810566093237,20.061165394216257,53.1446582863372 +2024-09-25 04:00:00,2,41.161522011061095,30.59553769121053,54.26656315861524 +2024-09-25 05:00:00,2,19.789812356607737,19.212830772898705,48.27546696883303 +2024-09-25 06:00:00,2,32.72939899051568,22.78521091135859,42.7276721418893 +2024-09-25 07:00:00,2,22.289983404116903,27.259211248449457,50.86306825141948 +2024-09-25 08:00:00,2,32.186125022501756,23.587025584962642,52.15797961696869 +2024-09-25 09:00:00,2,40.91395216797367,22.838633932744287,45.8606634442873 +2024-09-25 10:00:00,2,42.796615097792035,20.911494330605656,44.73499293470898 +2024-09-25 11:00:00,2,26.914952996806694,22.05835899458652,41.97194765411172 +2024-09-25 12:00:00,2,26.34413244336247,27.074215974801113,62.4863294639717 +2024-09-25 13:00:00,2,15.013113977502961,23.886069085857475,50.56704793495618 +2024-09-25 14:00:00,2,28.55102413231951,25.21469122815824,51.61423291831554 +2024-09-25 15:00:00,2,21.591158328017357,23.10970167711395,46.27908285343382 +2024-09-25 16:00:00,2,20.388175808709647,23.383500466124755,60.871620627495545 +2024-09-25 17:00:00,2,33.502915330885756,24.719103537772526,41.40910851150179 +2024-09-25 18:00:00,2,28.825989647814804,28.144426213717995,52.33404305802398 +2024-09-25 19:00:00,2,13.901140381055418,22.363305021785422,35.94716695062016 +2024-09-25 20:00:00,2,35.758873248991634,23.970532053236308,52.85914373467521 +2024-09-25 21:00:00,2,32.19826272608328,23.101000740057568,45.598312732454 +2024-09-25 22:00:00,2,29.216273737801675,22.75324889012652,68.18681913296182 +2024-09-25 23:00:00,2,21.858702527526948,28.605877072372472,69.82809100485076 +2024-09-26 00:00:00,2,37.66978091716528,23.43613057244015,40.43180145762868 +2024-09-26 01:00:00,2,24.993929153999247,21.611407215948574,39.133546918876135 +2024-09-26 02:00:00,2,30.08935230805849,22.7061417984645,34.74618421067477 +2024-09-26 03:00:00,2,27.763838029823887,19.73101034657121,46.53057429294351 +2024-09-26 04:00:00,2,39.14983155592558,18.613589014857777,64.51767990364696 +2024-09-26 05:00:00,2,13.201851342421094,20.021071948184655,59.52011410137439 +2024-09-26 06:00:00,2,26.597631765052178,22.848193330175203,50.2420671191402 +2024-09-26 07:00:00,2,15.501913260979315,36.01956839235798,44.216266720755506 +2024-09-26 08:00:00,2,40.801483328325745,27.559853626056157,51.97632470410297 +2024-09-26 09:00:00,2,35.61315960198223,18.234525056050337,63.5039074940816 +2024-09-26 10:00:00,2,40.507264661594576,27.91265422009096,63.14337803337962 +2024-09-26 11:00:00,2,17.99791080053534,27.355564776451672,46.82151435676822 +2024-09-26 12:00:00,2,18.124987677282526,25.41523241588232,68.29105293884179 +2024-09-26 13:00:00,2,37.84673625652027,21.626590363925786,60.57545141303379 +2024-09-26 14:00:00,2,29.595464198135915,25.87813298932534,59.80144310309902 +2024-09-26 15:00:00,2,21.58380367979193,24.963896865701784,52.402240973779925 +2024-09-26 16:00:00,2,22.393863662055296,22.050180165626013,69.59174394468758 +2024-09-26 17:00:00,2,37.696108969791474,33.30647577225555,61.778212941539444 +2024-09-26 18:00:00,2,36.529771002441834,23.81512277204657,47.461070141780205 +2024-09-26 19:00:00,2,5.477442062971985,23.306126524784016,55.09685171518853 +2024-09-26 20:00:00,2,31.27062095518581,26.35520953055734,60.65666955957653 +2024-09-26 21:00:00,2,29.898214570442263,26.225256685913063,56.165176851681196 +2024-09-26 22:00:00,2,36.077362754405335,22.01884573841107,39.432495588919835 +2024-09-26 23:00:00,2,17.734080029692535,23.25195194982668,31.068017083341378 +2024-09-27 00:00:00,2,42.38722057688475,23.06293227359588,48.568882037069606 +2024-09-27 01:00:00,2,33.056273656740416,28.8438907191876,45.74407570994616 +2024-09-27 02:00:00,2,30.07524531315611,25.757361496473834,53.9491464852147 +2024-09-27 03:00:00,2,30.295872324395066,26.807847497196768,43.942171508818674 +2024-09-27 04:00:00,2,11.53296235949205,27.731049712104145,37.447922615056314 +2024-09-27 05:00:00,2,36.59351012575874,15.228336380523146,69.549473902859 +2024-09-27 06:00:00,2,23.916360320264907,24.414959789302213,58.43959560692368 +2024-09-27 07:00:00,2,31.0628190540949,24.847010541774694,44.285696029767 +2024-09-27 08:00:00,2,29.34696878053623,26.058156897201112,51.898701041654675 +2024-09-27 09:00:00,2,51.73852668727006,26.532764405599163,30.07973121594255 +2024-09-27 10:00:00,2,40.86184709136373,27.66780538345887,69.73838041563263 +2024-09-27 11:00:00,2,23.15499118022175,29.031096295645035,58.2610486837967 +2024-09-27 12:00:00,2,24.034887372884818,24.007750664011976,63.63094049921823 +2024-09-27 13:00:00,2,33.460924988718105,23.565986340806052,50.01705834056615 +2024-09-27 14:00:00,2,18.711292839777766,27.97931001034908,56.09081767735483 +2024-09-27 15:00:00,2,8.762044366326263,27.196654366640598,76.25389058101179 +2024-09-27 16:00:00,2,12.090902340581914,35.60966587273896,52.28419477756577 +2024-09-27 17:00:00,2,35.90756864680576,27.440790169441215,61.95820717115319 +2024-09-27 18:00:00,2,34.544819218253565,22.629948787934406,40.01345000966628 +2024-09-27 19:00:00,2,41.76586257823641,24.056515319104776,45.85051015740004 +2024-09-27 20:00:00,2,11.670180906929719,20.80519996430509,65.72703235052106 +2024-09-27 21:00:00,2,25.478656710444636,22.4061483933417,37.933110076397156 +2024-09-27 22:00:00,2,29.290385118594283,20.64519309576656,42.40184704134796 +2024-09-27 23:00:00,2,23.484016791400045,23.37024758025938,44.38027238609707 +2024-09-28 00:00:00,2,31.237298570725915,22.921661894968352,39.730605882810785 +2024-09-28 01:00:00,2,21.473386552638463,31.522874079926382,48.16938436290029 +2024-09-28 02:00:00,2,38.17586933382451,22.271380476009046,55.85433165096051 +2024-09-28 03:00:00,2,30.002525913238507,26.39400798474496,48.998783388933916 +2024-09-28 04:00:00,2,20.606090665611738,23.809974062929992,57.42895391636091 +2024-09-28 05:00:00,2,26.187632350256848,24.4157432034323,46.42724720764952 +2024-09-28 06:00:00,2,30.31838170214335,22.877918632698666,39.72066436238861 +2024-09-28 07:00:00,2,29.427676594636687,26.221567902486175,58.69882673617023 +2024-09-28 08:00:00,2,34.82046330247662,30.282989027147817,59.741333512938176 +2024-09-28 09:00:00,2,26.359416044121957,28.48920600648981,47.223624646706284 +2024-09-28 10:00:00,2,45.68485019278498,26.20399110701287,67.92102613824619 +2024-09-28 11:00:00,2,37.26867368239854,27.47022549530521,50.85822963363374 +2024-09-28 12:00:00,2,25.61978214145348,24.222618942405422,53.89289263302577 +2024-09-28 13:00:00,2,36.77107472931175,24.3689266830867,53.29493209723654 +2024-09-28 14:00:00,2,13.763595317277302,24.798007157934826,66.78725693642387 +2024-09-28 15:00:00,2,16.931099556069306,27.251295974302362,61.83308535707464 +2024-09-28 16:00:00,2,28.267707302610436,35.08600779893986,49.89730964277551 +2024-09-28 17:00:00,2,12.021093842296121,24.878832018686698,31.411512962003982 +2024-09-28 18:00:00,2,15.87890700473815,16.78427953832312,47.769410956551546 +2024-09-28 19:00:00,2,5.0685053477529785,29.495769729031313,57.239273418169304 +2024-09-28 20:00:00,2,29.88348656547804,22.593181364097,42.527731373094724 +2024-09-28 21:00:00,2,28.88432058456351,25.457718722976175,53.54695249478985 +2024-09-28 22:00:00,2,16.77467646410699,17.695870960393812,41.42026716567594 +2024-09-28 23:00:00,2,30.800632682672777,23.63523064475023,56.295039430091165 +2024-09-29 00:00:00,2,18.815901810936737,21.118099771144784,64.7824641568228 +2024-09-29 01:00:00,2,30.146325439513777,22.609830758853374,41.78808082444262 +2024-09-29 02:00:00,2,34.86931317438626,24.842187913489305,63.65582903209309 +2024-09-29 03:00:00,2,34.705591124315625,24.480809836902136,67.17047355572163 +2024-09-29 04:00:00,2,15.065385733869343,24.43359016227993,43.56620870103122 +2024-09-29 05:00:00,2,26.557343810055766,23.67288965594075,54.46175977018369 +2024-09-29 06:00:00,2,26.9588909187299,24.583257747819935,63.353561430430375 +2024-09-29 07:00:00,2,27.58979299070505,25.819047254399244,64.02778066630302 +2024-09-29 08:00:00,2,38.397632550654905,23.281314061300385,48.242037383395875 +2024-09-29 09:00:00,2,22.960067493213057,21.86579295559622,58.03764374965176 +2024-09-29 10:00:00,2,44.9835661908043,28.82625141728347,45.803966577361585 +2024-09-29 11:00:00,2,22.487107975181345,24.467991378491774,56.385522680244556 +2024-09-29 12:00:00,2,20.264598280902867,26.99636042330894,60.525584682332386 +2024-09-29 13:00:00,2,32.238309073520306,23.76980601997184,57.45367750822797 +2024-09-29 14:00:00,2,27.093332603132122,20.92161630907587,58.233456847166615 +2024-09-29 15:00:00,2,22.185244182954403,29.048018590528528,44.70455239271003 +2024-09-29 16:00:00,2,26.943922991901466,22.345697253287984,47.75744189701557 +2024-09-29 17:00:00,2,20.35292132546592,31.171497667051874,46.434151747683714 +2024-09-29 18:00:00,2,19.91456582357992,25.03453189616468,31.11244989335323 +2024-09-29 19:00:00,2,27.05685396493312,31.233530064067818,41.37284321219444 +2024-09-29 20:00:00,2,23.955435518295992,26.23976008383177,42.43781460278248 +2024-09-29 21:00:00,2,7.023144451998903,22.635127012422082,59.27640106907897 +2024-09-29 22:00:00,2,34.0435523160655,20.015961446671877,58.79005029045231 +2024-09-29 23:00:00,2,16.152865006565925,24.705341305048666,47.6810933004468 +2024-09-30 00:00:00,2,44.59676144031507,21.08899427465257,60.52005859456686 +2024-09-30 01:00:00,2,24.13446247512851,24.97576424904876,59.72584803204503 +2024-09-30 02:00:00,2,23.48599636762104,23.46271172581756,63.16004395456807 +2024-09-30 03:00:00,2,32.93964936666505,25.175570608813555,61.8826740988534 +2024-09-30 04:00:00,2,37.58049287147013,29.618407632164498,45.6042696573297 +2024-09-30 05:00:00,2,46.65430878182032,24.349505432705538,51.58845202701903 +2024-09-30 06:00:00,2,41.32252686109838,22.81155334988633,44.00961886203663 +2024-09-30 07:00:00,2,37.704673016027236,26.79154124716516,55.79551081506662 +2024-09-30 08:00:00,2,7.401681654559731,25.74697365131598,60.597054759225244 +2024-09-30 09:00:00,2,21.514498012819008,25.78663205938809,56.498100824363384 +2024-09-30 10:00:00,2,41.60393258141667,23.314546885841494,52.21515187203808 +2024-09-30 11:00:00,2,19.457430335430026,18.4621677123667,52.42207774848052 +2024-09-30 12:00:00,2,31.27592241788586,24.577428216075486,61.96667010578867 +2024-09-30 13:00:00,2,32.28523032468156,33.821005671714545,55.73562926643043 +2024-09-30 14:00:00,2,12.846721394230514,28.375347120224546,50.64997779320905 +2024-09-30 15:00:00,2,16.828155088210856,25.12736578479648,48.21392079058717 +2024-09-30 16:00:00,2,21.307714933103533,25.622844177822046,43.09375793939201 +2024-09-30 17:00:00,2,34.54092169988412,26.04210995056804,46.049667596075786 +2024-09-30 18:00:00,2,15.93999407167943,21.663993114091333,51.668123669522075 +2024-09-30 19:00:00,2,7.531313822338646,24.738499666561943,48.185525060499614 +2024-09-30 20:00:00,2,28.797691069690856,26.125932486836145,58.856719403134136 +2024-09-30 21:00:00,2,20.72250635986024,23.784734310450517,51.114549925998546 +2024-09-30 22:00:00,2,26.851985849744086,25.637998854426467,53.81480447731264 +2024-09-30 23:00:00,2,13.88191295430682,23.47457300758313,43.156406848580126 +2024-10-01 00:00:00,2,29.18811037052324,27.24871245058994,65.35997467439438 +2024-10-01 01:00:00,2,20.39372617874704,25.67207603564881,37.221086087718454 +2024-10-01 02:00:00,2,10.420517625775748,24.278647647557026,56.14503938754901 +2024-10-01 03:00:00,2,24.577211134246582,21.683385390203505,57.666152943062656 +2024-10-01 04:00:00,2,37.71127470040339,20.867483885716155,59.65712249528459 +2024-10-01 05:00:00,2,43.135109181360015,23.48771330024174,51.487515715441056 +2024-10-01 06:00:00,2,26.837678300268475,21.58539216574657,44.818018837571145 +2024-10-01 07:00:00,2,36.56737892960351,29.3941144445817,47.90498443542392 +2024-10-01 08:00:00,2,28.513164922413498,19.31520105700445,44.506636300204875 +2024-10-01 09:00:00,2,32.21623303175149,20.192643784999262,55.88151011609321 +2024-10-01 10:00:00,2,18.056724491840576,30.931539109410934,63.55559746264616 +2024-10-01 11:00:00,2,25.91325233477307,26.13302133816532,48.439055716078144 +2024-10-01 12:00:00,2,38.02372742041956,21.147733468065127,52.375515009838544 +2024-10-01 13:00:00,2,29.957858503237354,26.2963527395624,48.6412235117538 +2024-10-01 14:00:00,2,25.902781367341685,24.976953487846412,46.7221788778128 +2024-10-01 15:00:00,2,10.493422496851977,24.57248658630239,49.882035598986384 +2024-10-01 16:00:00,2,29.986706443476674,26.79793639100982,73.56246866242643 +2024-10-01 17:00:00,2,20.662611090993824,25.901284789911152,61.569678514668134 +2024-10-01 18:00:00,2,15.71334006191018,21.18010423844541,69.99290598987749 +2024-10-01 19:00:00,2,13.061182585705438,21.859000874336253,48.112252495519954 +2024-10-01 20:00:00,2,21.114816449667387,24.082447319626954,39.45955871397613 +2024-10-01 21:00:00,2,5.502330840085271,24.524419134478894,38.347205408067296 +2024-10-01 22:00:00,2,27.388955699792255,27.007795112314678,52.61136542343171 +2024-10-01 23:00:00,2,23.39179455480221,20.02972016495883,51.96454465149245 +2024-10-02 00:00:00,2,43.02416464899647,25.42531572944652,52.5786283302575 +2024-10-02 01:00:00,2,25.484671523030713,21.33594648723757,55.303923895541324 +2024-10-02 02:00:00,2,22.91916295956751,21.44869700475455,40.817587564484285 +2024-10-02 03:00:00,2,41.38902856911088,28.814577615794633,42.32378554234708 +2024-10-02 04:00:00,2,22.33416093290506,21.27692680161531,59.512766592403736 +2024-10-02 05:00:00,2,36.384412029057465,27.61114246115391,27.141475692013234 +2024-10-02 06:00:00,2,22.963714592275583,25.417999326780766,41.22773414415797 +2024-10-02 07:00:00,2,44.07710858731424,28.395237965356685,68.12198480529419 +2024-10-02 08:00:00,2,13.58802216236537,25.093608835615896,65.20547696428783 +2024-10-02 09:00:00,2,37.522577374144035,27.90281391146877,63.54184221227157 +2024-10-02 10:00:00,2,30.284336857836966,27.636872234836,36.89265595517812 +2024-10-02 11:00:00,2,31.81301198787936,28.09694127885456,54.46088496305182 +2024-10-02 12:00:00,2,31.358147645442465,26.779122385987932,53.88164484955097 +2024-10-02 13:00:00,2,33.31043747519817,33.34281117969117,39.87927741517849 +2024-10-02 14:00:00,2,0.0,28.978502575579245,48.69060577977309 +2024-10-02 15:00:00,2,20.515533587740006,19.649157244316978,54.02641281844399 +2024-10-02 16:00:00,2,19.16880695438492,24.026062078184992,49.29044665364965 +2024-10-02 17:00:00,2,31.87286006366842,25.35593774674784,39.1309550029635 +2024-10-02 18:00:00,2,27.223817995201518,22.458729370111605,52.23165813812293 +2024-10-02 19:00:00,2,24.275792977947955,27.200981773504715,61.63626488926005 +2024-10-02 20:00:00,2,21.918815156182113,25.445122996708452,54.39702169453716 +2024-10-02 21:00:00,2,39.56011790421921,25.070634707570253,57.77130188474632 +2024-10-02 22:00:00,2,18.135417053822025,17.200858071081022,62.09398288738757 +2024-10-02 23:00:00,2,27.960079267862092,22.23556626972128,54.661066027361656 +2024-10-03 00:00:00,2,42.41537050073513,23.959290804766663,48.2035847603894 +2024-10-03 01:00:00,2,36.39457022798123,18.642987096152837,62.88137512815595 +2024-10-03 02:00:00,2,27.849155964428235,31.227603955055834,63.956800339987005 +2024-10-03 03:00:00,2,22.27309897227438,27.35673144793498,41.160688441827894 +2024-10-03 04:00:00,2,27.52353827679076,25.56927035139674,42.787071856414485 +2024-10-03 05:00:00,2,12.423909093817443,19.716860358541453,55.71638007162401 +2024-10-03 06:00:00,2,36.02861394662911,30.474300913559844,44.38878905818633 +2024-10-03 07:00:00,2,33.360052438367255,26.645892303117822,54.70667159677435 +2024-10-03 08:00:00,2,22.712293624842438,28.019437668948104,58.05191128290666 +2024-10-03 09:00:00,2,29.927792452666115,26.28801469870884,49.353429419176166 +2024-10-03 10:00:00,2,28.568567732578998,25.79753311429718,38.508680532071885 +2024-10-03 11:00:00,2,27.83941804776045,27.636739919276973,54.2638919714982 +2024-10-03 12:00:00,2,36.524614189791045,27.796254381234956,38.72606399713287 +2024-10-03 13:00:00,2,34.27976814224196,24.036406652886974,49.49621858643805 +2024-10-03 14:00:00,2,36.215989035899426,27.88375618274544,64.46725778883898 +2024-10-03 15:00:00,2,30.220432070399838,22.15856513860922,63.90006384235914 +2024-10-03 16:00:00,2,22.135332047540444,30.12954737564337,58.80301218300683 +2024-10-03 17:00:00,2,28.6419454227499,23.59774015810626,53.53290898142134 +2024-10-03 18:00:00,2,17.76296664419134,28.116585185455087,55.145173895184264 +2024-10-03 19:00:00,2,24.01715714156474,27.991052681006934,44.06375245036886 +2024-10-03 20:00:00,2,25.284837248302562,29.657538063751954,59.33098295794029 +2024-10-03 21:00:00,2,17.056219354543742,21.64693300153131,48.10590970737225 +2024-10-03 22:00:00,2,23.078094588629124,30.075418196699644,52.99674931376531 +2024-10-03 23:00:00,2,22.956130790682813,22.770574477357954,46.91353706584373 +2024-10-04 00:00:00,2,25.777122013303064,27.856800813468865,44.06218176082605 +2024-10-04 01:00:00,2,43.70559588070067,24.31936719004745,47.50855041215001 +2024-10-04 02:00:00,2,29.335662690215653,25.757314016408817,60.89972445554843 +2024-10-04 03:00:00,2,34.36873192272144,20.396223348559694,34.47180902919942 +2024-10-04 04:00:00,2,17.242397492287097,24.47537844958743,57.78171455370843 +2024-10-04 05:00:00,2,34.37771749954191,30.245803538523102,48.424398532173534 +2024-10-04 06:00:00,2,22.845279904850308,28.240468143893498,52.61206285215108 +2024-10-04 07:00:00,2,24.109605605879562,29.81944798597916,40.980351409474125 +2024-10-04 08:00:00,2,25.92009634966207,29.932582765563215,54.10564528248774 +2024-10-04 09:00:00,2,30.880694283225036,27.512371036592427,46.904026793140595 +2024-10-04 10:00:00,2,39.14966153255888,22.655177812260536,47.62763019806369 +2024-10-04 11:00:00,2,25.84606405488515,26.67870335086403,47.38582408699183 +2024-10-04 12:00:00,2,26.902803156104437,32.226454596318895,46.65967147126092 +2024-10-04 13:00:00,2,19.163021063129143,22.514977589185687,73.61030416498026 +2024-10-04 14:00:00,2,36.84071427657992,24.760837284552515,47.367019073772546 +2024-10-04 15:00:00,2,22.104276706247568,21.452551437472273,57.69037064847764 +2024-10-04 16:00:00,2,24.544269253470787,21.417609696313335,58.725935378629 +2024-10-04 17:00:00,2,23.241841650602655,31.51434698837741,67.49005365638772 +2024-10-04 18:00:00,2,10.915168943841593,25.194838318839274,35.616535893576554 +2024-10-04 19:00:00,2,35.413974753264085,22.76025412347791,44.67987525219537 +2024-10-04 20:00:00,2,37.1072208673033,29.545577921940062,46.929710311515684 +2024-10-04 21:00:00,2,24.354955654615278,20.120043875339697,53.45365966423234 +2024-10-04 22:00:00,2,26.73676508715069,22.76796883902867,44.00176355581066 +2024-10-04 23:00:00,2,31.39696448642117,24.214386417191356,42.95292505459414 +2024-10-05 00:00:00,2,27.43540613510898,25.0520826451209,41.93373781572153 +2024-10-05 01:00:00,2,31.66937503739528,24.930753908981412,52.56595335421581 +2024-10-05 02:00:00,2,38.67026213242724,23.56154091098606,42.295301202046794 +2024-10-05 03:00:00,2,11.445474401321082,19.486963310293177,62.31681918424243 +2024-10-05 04:00:00,2,42.00813051331054,22.728038518199245,49.12402699950843 +2024-10-05 05:00:00,2,33.57895676563623,24.16783913039738,55.4840928199345 +2024-10-05 06:00:00,2,30.962545184481733,23.348886646810715,61.44484607055966 +2024-10-05 07:00:00,2,27.555169894894725,26.68833165137223,47.84148080506039 +2024-10-05 08:00:00,2,30.078571232804638,26.681227277908445,41.6937145078649 +2024-10-05 09:00:00,2,29.18731290202121,24.543977209247576,48.18061207456299 +2024-10-05 10:00:00,2,28.755298814708684,29.23045701174829,41.39119940233316 +2024-10-05 11:00:00,2,12.109262691175637,29.30295752293535,65.60114450426057 +2024-10-05 12:00:00,2,43.54445818099649,31.40283443431444,70.8653501116141 +2024-10-05 13:00:00,2,6.0805494391947725,25.28896376594371,50.17382299723779 +2024-10-05 14:00:00,2,35.89881946607363,22.377615453126772,55.77294344568854 +2024-10-05 15:00:00,2,30.30114045226476,30.736927622054225,53.99607088819731 +2024-10-05 16:00:00,2,26.03896838331562,23.56008179970982,60.472390364891496 +2024-10-05 17:00:00,2,18.23701075181056,28.78153349205726,49.972993237403976 +2024-10-05 18:00:00,2,35.1401069026393,19.165961569788898,47.826562562765275 +2024-10-05 19:00:00,2,18.203351572749128,27.14408248333622,38.377578224598835 +2024-10-05 20:00:00,2,24.818166703188737,21.48278555424796,55.51560801415276 +2024-10-05 21:00:00,2,34.89078598544176,22.265222791131496,56.53937271469576 +2024-10-05 22:00:00,2,24.6575237665199,24.124627923581407,42.46362818026617 +2024-10-05 23:00:00,2,19.406576379638413,28.619506931219142,61.042252975249745 +2024-10-06 00:00:00,2,34.153181583429856,22.567692544903966,49.40963059002486 +2024-10-06 01:00:00,2,27.615766700105084,22.085875147872375,59.87253695767068 +2024-10-06 02:00:00,2,17.836393176303584,15.299797807754194,43.433928779408475 +2024-10-06 03:00:00,2,23.231088926140828,25.013283482769705,47.06588228293407 +2024-10-06 04:00:00,2,27.79235307138462,22.39730969770961,42.27946498502017 +2024-10-06 05:00:00,2,25.230429194693027,19.73360396112141,58.30223274337429 +2024-10-06 06:00:00,2,36.088456103903866,22.61980985901947,48.12678682625055 +2024-10-06 07:00:00,2,50.00572953728624,21.63558609596907,45.58342916045537 +2024-10-06 08:00:00,2,10.941354597141519,25.674222003803514,62.57184584572485 +2024-10-06 09:00:00,2,29.12666816374282,23.883115365914332,47.42344144176982 +2024-10-06 10:00:00,2,29.99697927388988,22.24640413299053,50.26947176466585 +2024-10-06 11:00:00,2,22.667810466629053,22.139114650955907,53.55871249425189 +2024-10-06 12:00:00,2,31.502841096304472,24.027358211173137,52.24153058250939 +2024-10-06 13:00:00,2,16.72210113509815,28.233214457633185,40.76938589145555 +2024-10-06 14:00:00,2,30.34703988969084,29.85997417926383,58.957767830079334 +2024-10-06 15:00:00,2,35.74829073087149,23.79934912034036,56.8351848964335 +2024-10-06 16:00:00,2,23.06348162094126,29.52424642936521,41.35389664220874 +2024-10-06 17:00:00,2,32.743723568886246,24.794265254678642,59.45105587040092 +2024-10-06 18:00:00,2,31.396059765761652,28.830339157077596,43.38275711106119 +2024-10-06 19:00:00,2,35.05489666012512,24.807920202639817,44.33725679075181 +2024-10-06 20:00:00,2,12.058460735053547,24.528193989005153,54.44397061631768 +2024-10-06 21:00:00,2,26.851011145186853,28.981556835248682,57.293929776485896 +2024-10-06 22:00:00,2,36.25219070364129,21.975480400767395,33.013790907143715 +2024-10-06 23:00:00,2,11.133782802872469,26.73645713235068,38.88006368491333 +2024-10-07 00:00:00,2,32.44357716971818,23.183102941115166,45.990140749501535 +2024-10-07 01:00:00,2,21.855028367043765,26.264859800130477,58.07126534482174 +2024-10-07 02:00:00,2,38.4460245598348,22.670968945876133,49.988393663245 +2024-10-07 03:00:00,2,27.72304741639723,21.649910679935495,62.327240568751236 +2024-10-07 04:00:00,2,52.41795130190568,19.931349542147768,61.02852382852238 +2024-10-07 05:00:00,2,16.7925269861462,17.659995728146857,58.47699988173966 +2024-10-07 06:00:00,2,42.812927656089904,23.793019711451237,51.1908108423626 +2024-10-07 07:00:00,2,37.70128299930934,22.485253383659188,73.93237814903259 +2024-10-07 08:00:00,2,40.06801180701715,29.581182719989105,59.524808946672366 +2024-10-07 09:00:00,2,36.027598346091125,22.88483958670337,53.39296986111939 +2024-10-07 10:00:00,2,23.390022192758018,24.579973106236338,61.214590389654035 +2024-10-07 11:00:00,2,29.67933094299705,21.75565271129729,47.99905290591124 +2024-10-07 12:00:00,2,25.601177271494766,26.484299919851438,66.85417265998447 +2024-10-07 13:00:00,2,38.35591056809206,20.223311645956386,35.37824875532015 +2024-10-07 14:00:00,2,29.892185674513943,21.142041324447824,51.42709023320873 +2024-10-07 15:00:00,2,11.01594475031266,28.168476537859352,45.34528011490618 +2024-10-07 16:00:00,2,25.978112150981662,25.14435098941009,55.26367668306865 +2024-10-07 17:00:00,2,6.470723067691967,28.81236432976664,50.70577507605326 +2024-10-07 18:00:00,2,20.091486940945053,27.90198850151118,40.29276539512157 +2024-10-07 19:00:00,2,37.52674215448301,22.932119634369172,51.093528156025606 +2024-10-07 20:00:00,2,12.10439689183982,19.114327841667077,48.491381356355575 +2024-10-07 21:00:00,2,25.691979578369782,24.07882755820853,40.678816593046385 +2024-10-07 22:00:00,2,15.386607855799111,21.728827892230964,34.06614681038768 +2024-10-07 23:00:00,2,36.20919322651868,21.830947543981896,60.59047038662846 +2024-10-08 00:00:00,2,28.20964991080684,22.455835894929933,42.99537964468802 +2024-10-08 01:00:00,2,32.640645646921065,31.52280188640798,58.596379087571535 +2024-10-08 02:00:00,2,32.280323219674024,24.36044490475421,40.52805890754432 +2024-10-08 03:00:00,2,34.93697956178072,23.208590760979437,52.95489899446633 +2024-10-08 04:00:00,2,21.320991769357825,26.530821622043323,60.88115546425735 +2024-10-08 05:00:00,2,44.799242528414496,19.04790827450395,47.76952239126014 +2024-10-08 06:00:00,2,17.901880868584445,20.4201070395492,56.38509132422618 +2024-10-08 07:00:00,2,43.93138920868659,19.09275718962549,55.077241389501644 +2024-10-08 08:00:00,2,16.132378867144105,17.393164400340098,46.153402936319715 +2024-10-08 09:00:00,2,36.16076656604974,28.440502479633142,59.737644851573506 +2024-10-08 10:00:00,2,23.11132900827924,27.810306555026425,49.6882501437211 +2024-10-08 11:00:00,2,32.8036831961987,24.412120197438224,42.31537804720656 +2024-10-08 12:00:00,2,6.514728814274186,22.178310491336013,59.77427109039909 +2024-10-08 13:00:00,2,27.769349657176296,30.123577562753887,47.64022183586774 +2024-10-08 14:00:00,2,26.241420458892186,23.68211279705069,48.299860951116514 +2024-10-08 15:00:00,2,32.62128518462882,26.987473915315768,58.25128160259254 +2024-10-08 16:00:00,2,23.698756431547874,31.35046284956715,63.64106180065785 +2024-10-08 17:00:00,2,16.347912723600245,33.89342327583338,63.54509286562738 +2024-10-08 18:00:00,2,21.787870287458208,24.552896003826316,43.6351829589794 +2024-10-08 19:00:00,2,31.478358843445164,31.27648150544214,55.23038313092629 +2024-10-08 20:00:00,2,29.80479866606606,22.947162707779615,49.16846126736024 +2024-10-08 21:00:00,2,29.329793427735286,21.649125042418238,52.7135218840655 +2024-10-08 22:00:00,2,10.316830585616772,21.109017520845104,55.78178659686035 +2024-10-08 23:00:00,2,35.430544898707424,27.64349039741568,50.801200129922556 +2024-10-09 00:00:00,2,30.402890034757196,19.94809029485019,56.596108771202616 +2024-10-09 01:00:00,2,43.956446613002804,24.81324215324521,63.00592065707063 +2024-10-09 02:00:00,2,17.478938920754835,24.348178757574406,60.10696835119441 +2024-10-09 03:00:00,2,27.328601417005842,22.6559015038861,43.34165408637285 +2024-10-09 04:00:00,2,28.24369473578053,21.423577173486937,63.76571082201289 +2024-10-09 05:00:00,2,34.928999355007804,26.237445455977507,52.25760802386392 +2024-10-09 06:00:00,2,24.589919701125325,24.749401362492154,48.85318582964982 +2024-10-09 07:00:00,2,20.942447769749663,24.776615558333813,54.29595140652149 +2024-10-09 08:00:00,2,39.98330963440111,28.638604933898858,50.455349899119696 +2024-10-09 09:00:00,2,27.085299478723805,27.45643554267676,52.68557946578082 +2024-10-09 10:00:00,2,47.57297330308583,24.876082337853727,66.43874214931454 +2024-10-09 11:00:00,2,26.217030675343302,23.029132973636592,56.88550702300982 +2024-10-09 12:00:00,2,20.859898925642305,29.629563194912826,46.74391590249251 +2024-10-09 13:00:00,2,39.57829356334392,33.116886793050234,51.777377044523476 +2024-10-09 14:00:00,2,31.717679390277652,24.138808758529372,54.91658109870653 +2024-10-09 15:00:00,2,19.87169024156089,27.07192099900413,68.82316213428447 +2024-10-09 16:00:00,2,24.304671519354155,31.339731770611607,61.37774767766831 +2024-10-09 17:00:00,2,17.22364918337609,20.16142445534871,33.53108056813136 +2024-10-09 18:00:00,2,7.577156909472208,25.796892125509082,36.81669630930954 +2024-10-09 19:00:00,2,12.760480212841605,22.657725480176158,60.28958388248167 +2024-10-09 20:00:00,2,29.648265464009466,28.945361479670662,65.17584870622116 +2024-10-09 21:00:00,2,17.03587919575425,19.307996140200828,52.73483700715142 +2024-10-09 22:00:00,2,54.979095801580755,22.40017868225121,61.26992637163092 +2024-10-09 23:00:00,2,17.34901849948179,21.105026654885148,56.42613701419063 +2024-10-10 00:00:00,2,25.063384522399495,24.68197145942172,62.503526771830536 +2024-10-10 01:00:00,2,40.39353156485192,20.158108375466348,45.780525341273076 +2024-10-10 02:00:00,2,28.635436802418432,26.053519106835815,48.6860297403165 +2024-10-10 03:00:00,2,30.246861488238277,27.84286712849873,34.616291714196194 +2024-10-10 04:00:00,2,16.856306913227044,18.797496326853565,61.14946154612258 +2024-10-10 05:00:00,2,31.936581653352416,22.521583709068242,49.49372612867596 +2024-10-10 06:00:00,2,26.25384345660643,23.962604123160993,49.15628860315315 +2024-10-10 07:00:00,2,29.99157328874214,26.890063213444144,40.62463173969219 +2024-10-10 08:00:00,2,46.83301614768227,28.530980330204017,50.11488721745901 +2024-10-10 09:00:00,2,27.679536982715014,29.607959653616568,60.53932608735843 +2024-10-10 10:00:00,2,22.66250348170845,31.115022972945795,55.34728406524865 +2024-10-10 11:00:00,2,13.896281372519656,25.77278797168106,42.18691210990481 +2024-10-10 12:00:00,2,27.88082173121882,25.321960541565783,49.292198009034266 +2024-10-10 13:00:00,2,26.127876847919595,21.150932025349892,45.265842931359984 +2024-10-10 14:00:00,2,18.60108256976943,21.13976135377704,80.55866059103315 +2024-10-10 15:00:00,2,35.363431614431185,23.795933404224794,63.99017772066957 +2024-10-10 16:00:00,2,25.93881802790142,28.41387258990197,72.24285155950177 +2024-10-10 17:00:00,2,25.028981624074774,31.79310330391366,44.22426892135732 +2024-10-10 18:00:00,2,19.33602420156153,27.33714588622112,60.72606887501803 +2024-10-10 19:00:00,2,24.614051238657797,21.37891883669669,54.57213185353492 +2024-10-10 20:00:00,2,27.047477585302676,26.520760998174417,44.259768580773176 +2024-10-10 21:00:00,2,27.357585957925373,20.582472474105817,43.982061794713026 +2024-10-10 22:00:00,2,28.56939099233483,21.69386308979356,54.02819809663486 +2024-10-10 23:00:00,2,19.80937515058602,30.77406668109421,60.5629073858385 +2024-10-11 00:00:00,2,32.06101820148139,26.25476692035768,43.81375028651792 +2024-10-11 01:00:00,2,27.041536665003438,18.18857960559965,58.123250972897 +2024-10-11 02:00:00,2,32.265818127826805,18.233866361228657,69.82688692366736 +2024-10-11 03:00:00,2,19.718512407014664,26.464808006378718,51.281810102854905 +2024-10-11 04:00:00,2,31.412920192525508,21.673347684104638,62.10344933602556 +2024-10-11 05:00:00,2,29.827485947367908,20.80754271954646,55.4952210700402 +2024-10-11 06:00:00,2,37.52718405745833,25.808376904021166,37.30052693660578 +2024-10-11 07:00:00,2,32.22547578660641,22.77058571817725,47.75848433733222 +2024-10-11 08:00:00,2,21.125538420614177,25.084900013102303,63.90742047023577 +2024-10-11 09:00:00,2,29.592881501751297,28.203465676914288,54.28108310021554 +2024-10-11 10:00:00,2,29.46722156262359,30.08828566348922,43.78141486741023 +2024-10-11 11:00:00,2,17.32764535710727,22.75232441920014,73.43158405820307 +2024-10-11 12:00:00,2,17.134705944903786,22.32763737803123,49.26573663833906 +2024-10-11 13:00:00,2,21.172003816320867,29.77305796184857,56.22470196172848 +2024-10-11 14:00:00,2,20.331668820495693,28.871053057235724,43.83232594217638 +2024-10-11 15:00:00,2,31.946001344189565,28.30667069126616,61.717599557580876 +2024-10-11 16:00:00,2,27.515599682313404,23.31597992091906,30.546251352169307 +2024-10-11 17:00:00,2,28.26185119552104,19.312262288027668,49.55595560588247 +2024-10-11 18:00:00,2,42.687537224793815,24.343554316749398,60.996106990035216 +2024-10-11 19:00:00,2,22.924619231060586,26.660810856583137,55.182076232689205 +2024-10-11 20:00:00,2,33.05024629067686,25.055787215999906,54.29780109754337 +2024-10-11 21:00:00,2,20.996825666992386,22.49249230898845,41.87281119449915 +2024-10-11 22:00:00,2,28.978458037750535,23.296948214989854,56.891974312874254 +2024-10-11 23:00:00,2,9.610149363512695,23.837477584257105,61.08734827446149 +2024-10-12 00:00:00,2,21.67666162001331,21.51177199171088,39.79201041597256 +2024-10-12 01:00:00,2,19.470889439081894,20.613695461695485,52.090504442057146 +2024-10-12 02:00:00,2,21.34994606683476,31.032427022028006,44.02640801122842 +2024-10-12 03:00:00,2,21.58153056814291,24.491389596241227,49.67444195027689 +2024-10-12 04:00:00,2,28.518601669587756,25.246081779790075,44.88533488989975 +2024-10-12 05:00:00,2,46.11160589536724,18.12279870973483,60.81997631401195 +2024-10-12 06:00:00,2,28.121424834679758,20.729146873460373,44.53309289850435 +2024-10-12 07:00:00,2,25.3037477490873,23.746816506475913,41.17270461218401 +2024-10-12 08:00:00,2,50.854550746826575,26.029051931476005,53.54314910272466 +2024-10-12 09:00:00,2,32.85357837225755,24.983271132447232,58.9991696890426 +2024-10-12 10:00:00,2,32.13583833632013,30.67496960716679,74.08303376534221 +2024-10-12 11:00:00,2,21.50398069521497,25.469513611220457,44.41158621992086 +2024-10-12 12:00:00,2,21.440008646352254,25.139844874391777,60.68269221220005 +2024-10-12 13:00:00,2,33.67756689865066,25.385106850373496,49.491570124241306 +2024-10-12 14:00:00,2,30.010519086091726,23.257661469303148,46.13259233846084 +2024-10-12 15:00:00,2,27.430086963345385,32.45191395015855,53.06615128800197 +2024-10-12 16:00:00,2,17.9365015728944,28.83383372710863,56.11945760226683 +2024-10-12 17:00:00,2,32.05184305004633,23.929621604272047,46.9622624232086 +2024-10-12 18:00:00,2,19.617174070861648,21.413373061728183,56.94213153287212 +2024-10-12 19:00:00,2,18.728634537274136,27.437332346219772,49.27659097247076 +2024-10-12 20:00:00,2,19.824866293031555,24.840650732252037,51.465673880623775 +2024-10-12 21:00:00,2,17.941899267514902,25.29135555848804,61.42641967799034 +2024-10-12 22:00:00,2,23.116968694184756,30.230236908265365,51.81762282761125 +2024-10-12 23:00:00,2,26.454794783849117,30.75064908742933,70.96827974971605 +2024-10-13 00:00:00,2,27.589132670777705,26.99633715429403,49.81992461572897 +2024-10-13 01:00:00,2,30.838823965398785,29.551209346674415,51.59697209508675 +2024-10-13 02:00:00,2,28.503230339473028,24.91559588387016,53.23421108715292 +2024-10-13 03:00:00,2,33.61960579326914,26.58424486140456,49.81225182415977 +2024-10-13 04:00:00,2,29.328403736864484,24.215384061407004,53.69199407470764 +2024-10-13 05:00:00,2,24.58500785140428,27.291006029633202,68.4854511502555 +2024-10-13 06:00:00,2,28.448634300584143,30.111884004744027,75.69573097779927 +2024-10-13 07:00:00,2,30.366855841045034,21.865610254558515,61.45259620915697 +2024-10-13 08:00:00,2,33.38355919254621,27.653459189292402,55.17532172620465 +2024-10-13 09:00:00,2,26.029454996137964,25.38117247661608,48.063368497056736 +2024-10-13 10:00:00,2,17.195673344838887,24.46443771798657,46.52476076523792 +2024-10-13 11:00:00,2,35.438901196380954,26.086236593562184,48.18904675098462 +2024-10-13 12:00:00,2,21.76293765178566,22.502465183837998,44.01021642451594 +2024-10-13 13:00:00,2,26.679921574827,18.74644463444548,39.94461783449913 +2024-10-13 14:00:00,2,13.326647843433735,24.94012274241377,58.90595759906984 +2024-10-13 15:00:00,2,15.143728656819007,25.34547490156572,54.09059146795729 +2024-10-13 16:00:00,2,22.915993006812883,29.696370058884792,54.80336725631421 +2024-10-13 17:00:00,2,31.212883448273665,21.473244917150723,39.67164631530336 +2024-10-13 18:00:00,2,9.022272556920177,28.849883084873813,54.0696975310791 +2024-10-13 19:00:00,2,27.930733795941553,24.89516191457316,33.543749657111825 +2024-10-13 20:00:00,2,16.074401938403717,24.15140873378409,59.082409069474366 +2024-10-13 21:00:00,2,21.7467980533266,20.32997337639425,43.068362038278565 +2024-10-13 22:00:00,2,22.747735723995447,29.65730035371584,47.833924623114065 +2024-10-13 23:00:00,2,23.564722601125514,15.747763566192226,41.45267539710953 +2024-10-14 00:00:00,2,41.89061688014147,25.54931517295855,55.16145953453149 +2024-10-14 01:00:00,2,27.17034417949826,24.070410090773613,58.89797292432592 +2024-10-14 02:00:00,2,16.073568683577093,20.683712202512112,47.69104060547242 +2024-10-14 03:00:00,2,38.21345641137798,28.451639007993766,38.55337769755905 +2024-10-14 04:00:00,2,26.955369019413695,26.784597842705494,50.233294074575184 +2024-10-14 05:00:00,2,29.45721955921629,27.78768462712104,61.65439810107576 +2024-10-14 06:00:00,2,29.28213646944429,24.329485382815275,67.05203683429974 +2024-10-14 07:00:00,2,25.302718974614294,25.31172030311254,43.170954145994706 +2024-10-14 08:00:00,2,24.018947654670708,24.086629667200732,55.024868631616755 +2024-10-14 09:00:00,2,23.805916979248746,30.135523705021757,42.06780850425068 +2024-10-14 10:00:00,2,27.47097638096881,27.249039542816725,29.73806897542876 +2024-10-14 11:00:00,2,25.994299487312475,28.005840599998642,71.43134687783589 +2024-10-14 12:00:00,2,19.043369870015745,23.94195981421765,48.980797737773365 +2024-10-14 13:00:00,2,15.045882995519671,30.025669550513513,56.16660677333118 +2024-10-14 14:00:00,2,29.80236140652976,32.321310903027324,39.896241462088966 +2024-10-14 15:00:00,2,29.87130549379074,22.58617019615444,53.1623854567501 +2024-10-14 16:00:00,2,26.818375344360646,30.098665117443105,55.59366009769055 +2024-10-14 17:00:00,2,35.77059936694684,22.312424615130453,60.76150237732669 +2024-10-14 18:00:00,2,28.59033605183027,23.100093247030184,56.92017898973772 +2024-10-14 19:00:00,2,32.11138129703551,27.929848348316742,54.58271333888336 +2024-10-14 20:00:00,2,13.730778537365154,22.96715453081605,37.680358365899544 +2024-10-14 21:00:00,2,31.11161788728808,23.18440979269226,66.85536383563769 +2024-10-14 22:00:00,2,37.19872343763163,26.497394588136967,57.46232322389806 +2024-10-14 23:00:00,2,38.23513058872787,21.219991590614228,62.311787181160675 +2024-10-15 00:00:00,2,48.80607084499408,22.03963825823452,70.33714600939182 +2024-10-15 01:00:00,2,38.40336564984565,21.609047699343982,42.15084849686505 +2024-10-15 02:00:00,2,30.91059386601083,18.604796986619913,55.556128075054055 +2024-10-15 03:00:00,2,21.69151614763593,27.33457742172718,50.16656674290498 +2024-10-15 04:00:00,2,29.190793013524846,19.718903324938644,49.93018964935346 +2024-10-15 05:00:00,2,22.55555962208215,22.89502718177785,28.088537346679118 +2024-10-15 06:00:00,2,37.04160096260902,25.798554235966805,43.787342094613074 +2024-10-15 07:00:00,2,19.913315893813,21.142766285849873,49.60801279963465 +2024-10-15 08:00:00,2,32.67700475653767,29.668069512780708,42.78962922844546 +2024-10-15 09:00:00,2,16.965010395295472,23.632143595792353,55.730007513714376 +2024-10-15 10:00:00,2,33.094832588268474,21.315412560425905,72.10580319218614 +2024-10-15 11:00:00,2,39.851537689253576,27.500485331322697,53.0047362437065 +2024-10-15 12:00:00,2,23.451047201636925,28.293901567444887,58.01614180741781 +2024-10-15 13:00:00,2,17.10753253069365,22.09172349347073,66.7861619372346 +2024-10-15 14:00:00,2,22.90143047023196,30.029202909161263,53.487349060444316 +2024-10-15 15:00:00,2,15.51536552491196,24.94553070256446,41.73540069803215 +2024-10-15 16:00:00,2,30.378451820269934,31.20582807776543,56.70930176139019 +2024-10-15 17:00:00,2,13.917614465946752,28.112988161117322,54.09088764044366 +2024-10-15 18:00:00,2,28.39979495048737,28.802568521521454,66.21086399842592 +2024-10-15 19:00:00,2,15.453621099622191,28.02580910817219,61.55537114914052 +2024-10-15 20:00:00,2,29.734473732972397,17.552556912548138,59.14855988111874 +2024-10-15 21:00:00,2,24.417354254583117,26.01015344878786,60.150267776405265 +2024-10-15 22:00:00,2,31.044688518435557,23.351172733464992,63.113312690125404 +2024-10-15 23:00:00,2,28.090765780989127,22.92950633048018,45.76591389713248 +2024-10-16 00:00:00,2,29.280153155649767,24.015704325437863,47.537160901309264 +2024-10-16 01:00:00,2,17.754213895968384,26.904464290787885,52.84189525199543 +2024-10-16 02:00:00,2,34.59261181842108,19.946999838838288,47.67968047760651 +2024-10-16 03:00:00,2,32.52252528984976,28.249973018363743,40.67181038895652 +2024-10-16 04:00:00,2,35.38588809240768,21.541475145445165,70.77672203127304 +2024-10-16 05:00:00,2,39.82625152467832,24.5248317178907,52.91739869187866 +2024-10-16 06:00:00,2,28.130801605769992,24.35195974500345,41.68051664964265 +2024-10-16 07:00:00,2,42.86378301153807,26.43017925935094,50.23271204527747 +2024-10-16 08:00:00,2,27.181314332955523,23.144063322380116,38.87548226179922 +2024-10-16 09:00:00,2,34.76214063593616,17.459086509090803,58.0084825246977 +2024-10-16 10:00:00,2,26.36728588080952,24.0588115957005,51.58405928962221 +2024-10-16 11:00:00,2,28.373278641105074,30.800600890577197,40.819977037296006 +2024-10-16 12:00:00,2,34.829437961708805,18.80213512012243,51.90251427557127 +2024-10-16 13:00:00,2,23.953411690305053,25.960049460243596,56.169053861743855 +2024-10-16 14:00:00,2,14.485516539842692,28.235991260660416,59.881479131770085 +2024-10-16 15:00:00,2,19.919179874973587,24.878324843728272,40.5824233573137 +2024-10-16 16:00:00,2,8.11410367114609,25.728029250650682,66.02947988142739 +2024-10-16 17:00:00,2,15.428461382360853,25.531605766695353,60.13078309293554 +2024-10-16 18:00:00,2,26.941061071574463,21.306258983120074,51.95592723675967 +2024-10-16 19:00:00,2,24.432232768923367,19.79748742864256,52.47295745155977 +2024-10-16 20:00:00,2,21.086283074079052,25.05652519073412,62.25582811800451 +2024-10-16 21:00:00,2,27.883226129324484,24.344433679570734,44.82923951393371 +2024-10-16 22:00:00,2,25.786792919980712,24.239654165971068,42.958434722008214 +2024-10-16 23:00:00,2,25.443612266919903,20.85977941808592,39.073392586736254 +2024-10-17 00:00:00,2,34.51790516202066,27.34884641083347,58.293161263155795 +2024-10-17 01:00:00,2,24.34007205077694,23.083552424849046,49.428919546520866 +2024-10-17 02:00:00,2,22.370610509962695,20.58383567896894,41.18335084432564 +2024-10-17 03:00:00,2,31.586266899147727,16.185636725050035,47.84285692190128 +2024-10-17 04:00:00,2,13.455003529020924,26.362835633182513,46.96006728873418 +2024-10-17 05:00:00,2,24.38318575715914,23.381833524043437,53.326885604337164 +2024-10-17 06:00:00,2,38.733204041307864,30.719726461701775,43.81252184581635 +2024-10-17 07:00:00,2,34.39590059036882,29.19623434550525,61.53261349684035 +2024-10-17 08:00:00,2,39.37732936334739,24.484833763137843,46.15681978005437 +2024-10-17 09:00:00,2,40.72582979153921,21.783438803117217,60.899263183233735 +2024-10-17 10:00:00,2,39.74465613406649,27.961753153357932,54.567730475230256 +2024-10-17 11:00:00,2,27.919081920148656,26.17445904359483,44.60260104898733 +2024-10-17 12:00:00,2,26.898603404156734,17.582675047607683,59.28095835273429 +2024-10-17 13:00:00,2,24.514215523521244,22.21120570617596,67.70090439956743 +2024-10-17 14:00:00,2,23.87381192898833,25.09267197820914,50.29664401378485 +2024-10-17 15:00:00,2,28.764836613745654,22.713731132683872,62.673933958345074 +2024-10-17 16:00:00,2,16.960374089689573,20.83568968068135,51.88115306017661 +2024-10-17 17:00:00,2,19.695952410237282,26.595531503609273,61.29609533752409 +2024-10-17 18:00:00,2,27.136701686751355,32.126616860829856,62.023703394205675 +2024-10-17 19:00:00,2,33.83994657994462,25.87064620906434,64.87097676881262 +2024-10-17 20:00:00,2,9.524268829470595,28.87896333206652,48.249966998401646 +2024-10-17 21:00:00,2,32.88224467687432,26.45885809868966,74.90750874756827 +2024-10-17 22:00:00,2,30.87260597129668,21.407028036916742,57.382157225251625 +2024-10-17 23:00:00,2,30.12644710196624,21.132568324986508,42.94199131727923 +2024-10-18 00:00:00,2,19.72996775319953,21.997651886452065,35.45812912616704 +2024-10-18 01:00:00,2,27.39571704902504,24.942304407569903,56.27055973124418 +2024-10-18 02:00:00,2,24.875035350901967,23.762769860549202,49.114935264193974 +2024-10-18 03:00:00,2,42.18948108792266,21.225408380173118,53.238317430119544 +2024-10-18 04:00:00,2,26.595839544518775,27.743380972349115,48.07621092018269 +2024-10-18 05:00:00,2,32.711006552859274,28.95698712648317,51.15675819847848 +2024-10-18 06:00:00,2,38.325334598227016,17.582404988910397,61.66292216732998 +2024-10-18 07:00:00,2,21.462681814501046,24.25624698480555,59.84164100665144 +2024-10-18 08:00:00,2,20.626363667897778,27.80115698786107,58.52458640494412 +2024-10-18 09:00:00,2,33.96532336859207,25.623244423493496,50.31477648128324 +2024-10-18 10:00:00,2,29.484825677515826,28.096260600755176,48.518053850542366 +2024-10-18 11:00:00,2,28.64113637570513,20.338502430231205,57.145699410503546 +2024-10-18 12:00:00,2,31.483612532047218,25.104887650477576,65.6948122420308 +2024-10-18 13:00:00,2,19.166175678225777,19.28288139283016,50.7781656790291 +2024-10-18 14:00:00,2,4.800484449984687,27.718407395614413,50.68938870756915 +2024-10-18 15:00:00,2,20.249913324441962,25.994834544074415,52.670489623999636 +2024-10-18 16:00:00,2,25.850780386519244,24.091057841969608,53.563138371804506 +2024-10-18 17:00:00,2,15.681495550351766,28.056138202116017,57.1337377017923 +2024-10-18 18:00:00,2,31.2948738491794,25.831351209499832,62.6446506793083 +2024-10-18 19:00:00,2,4.6609406900493795,21.68412738014605,54.70083805719704 +2024-10-18 20:00:00,2,8.559531304053841,27.43445893329219,40.13419878302435 +2024-10-18 21:00:00,2,25.299957537792462,27.163533001979722,52.95125501134555 +2024-10-18 22:00:00,2,21.431440180305223,21.856562786574464,37.049444464525024 +2024-10-18 23:00:00,2,30.75524221261522,26.669834105174335,50.14291780149005 +2024-10-19 00:00:00,2,17.81299972663318,19.599142067922386,46.02279815858014 +2024-10-19 01:00:00,2,20.96555155390736,26.82323471756732,42.200703296766825 +2024-10-19 02:00:00,2,28.0770316046805,21.414945723035583,64.53396001094099 +2024-10-19 03:00:00,2,16.021654068972882,17.85748818421655,55.00425195109416 +2024-10-19 04:00:00,2,14.18473433794478,27.92631242579401,54.111647879398646 +2024-10-19 05:00:00,2,23.986577538307536,23.26411408297291,51.77426544661849 +2024-10-19 06:00:00,2,38.726477961094545,23.452702108441773,44.06002242858443 +2024-10-19 07:00:00,2,40.932638542223515,25.091120363791998,39.76505852729868 +2024-10-19 08:00:00,2,34.78824797714388,28.76076247651401,41.345670633577804 +2024-10-19 09:00:00,2,30.422127026304977,16.269161543648554,45.19482962554779 +2024-10-19 10:00:00,2,30.90852998394759,25.300965302605377,43.073361718442925 +2024-10-19 11:00:00,2,14.090509585015438,24.431063698005886,46.6618375372952 +2024-10-19 12:00:00,2,4.807434420581572,26.468534102493884,60.48896243862768 +2024-10-19 13:00:00,2,31.710244428790812,22.175307009684452,86.06482015500163 +2024-10-19 14:00:00,2,31.20697859219142,27.71815653863842,35.490518459949975 +2024-10-19 15:00:00,2,17.89363838793264,34.06009813230214,47.34245031260222 +2024-10-19 16:00:00,2,32.698771696300156,29.7185064862814,52.99062979498363 +2024-10-19 17:00:00,2,35.9195247792347,27.659050592470216,48.69682955245063 +2024-10-19 18:00:00,2,27.88798298999069,26.92369350892002,41.86061710568872 +2024-10-19 19:00:00,2,26.90597720645419,26.580976501580196,57.76467434043278 +2024-10-19 20:00:00,2,15.196716657191192,15.069174432155489,54.20640054336363 +2024-10-19 21:00:00,2,19.098998497340897,23.230490615859434,35.581330683697765 +2024-10-19 22:00:00,2,30.50612691557256,25.044125970739426,57.86694565967669 +2024-10-19 23:00:00,2,30.970149842355546,18.30970580499057,52.911336336849736 +2024-10-20 00:00:00,2,36.19739713436355,22.651515480500898,54.51694060692491 +2024-10-20 01:00:00,2,38.7688835993767,27.950240855481006,65.02185223648813 +2024-10-20 02:00:00,2,20.650179821277888,20.35476568035665,32.37743856994325 +2024-10-20 03:00:00,2,31.99753974807396,27.64123730347048,61.21632855753583 +2024-10-20 04:00:00,2,32.89676232727604,23.410222575417855,49.38591176951117 +2024-10-20 05:00:00,2,32.92490579342761,20.480679877619885,56.44582906221768 +2024-10-20 06:00:00,2,29.683315578872257,23.277659789358314,48.508755327743586 +2024-10-20 07:00:00,2,34.461860803973636,21.10644452896207,43.98427773120745 +2024-10-20 08:00:00,2,29.780914769081924,23.17800584281774,57.88817284146735 +2024-10-20 09:00:00,2,46.47962549408845,21.43804094928627,63.746835058706104 +2024-10-20 10:00:00,2,37.03230894613353,29.937889675443692,67.89413933778722 +2024-10-20 11:00:00,2,28.97319572977551,27.233959084146075,63.60299414660477 +2024-10-20 12:00:00,2,28.297517103603557,24.717281490995713,41.2566188252245 +2024-10-20 13:00:00,2,26.48224391054736,28.14288167496203,37.37754203507147 +2024-10-20 14:00:00,2,11.072580707103574,31.629667695815773,39.53955683074947 +2024-10-20 15:00:00,2,27.010691960993544,24.16180196523465,55.32503520048576 +2024-10-20 16:00:00,2,29.302835270334484,29.168180434239854,47.513611634059416 +2024-10-20 17:00:00,2,27.086174671591838,21.741276127189405,57.13706209210338 +2024-10-20 18:00:00,2,25.58382549146093,30.100099344892236,52.189410003583404 +2024-10-20 19:00:00,2,22.816889267492307,23.622889407428556,52.31332706006038 +2024-10-20 20:00:00,2,16.245882216618604,24.58033555302511,55.61875627254473 +2024-10-20 21:00:00,2,15.146278881485111,24.368142727408987,60.55279717030796 +2024-10-20 22:00:00,2,21.601506291266794,25.552358557907795,62.845473674690155 +2024-10-20 23:00:00,2,37.51700716699562,21.6934421473907,37.799863524566604 +2024-10-21 00:00:00,2,38.64296639663661,27.03964486652735,55.23190554640871 +2024-10-21 01:00:00,2,52.29651443301968,18.02439145344938,59.10447555883528 +2024-10-21 02:00:00,2,22.972322858132095,28.62062142171916,39.33051172103656 +2024-10-21 03:00:00,2,34.74332710318796,24.512390527673492,56.410343867739705 +2024-10-21 04:00:00,2,31.11035937700008,25.10576740932699,35.193705049860796 +2024-10-21 05:00:00,2,6.795587797762085,21.608864754875206,47.52402321771493 +2024-10-21 06:00:00,2,22.460427834530392,26.45567162643994,56.60513447068232 +2024-10-21 07:00:00,2,17.842497140014096,25.728769393426425,48.78695616457619 +2024-10-21 08:00:00,2,32.24461998454943,29.472645824555027,67.0696879651189 +2024-10-21 09:00:00,2,33.617450624108024,23.98236109725988,36.03157986032615 +2024-10-21 10:00:00,2,27.390786401430738,28.399706477407058,54.80364498823998 +2024-10-21 11:00:00,2,7.915201383419376,32.45082107625356,54.79246368339898 +2024-10-21 12:00:00,2,29.921923376646205,26.50767360278319,47.976155050808025 +2024-10-21 13:00:00,2,32.454760990434224,26.742462952838103,53.233215131823286 +2024-10-21 14:00:00,2,26.97233833860279,23.95487778304622,58.26625515153565 +2024-10-21 15:00:00,2,22.858176188367786,29.543956703403147,59.93468947653679 +2024-10-21 16:00:00,2,36.53115333263044,22.918497470248308,43.53225276711008 +2024-10-21 17:00:00,2,29.11233624905993,27.272589347827715,71.2369359866193 +2024-10-21 18:00:00,2,34.263539077215995,26.36856739312536,53.12310484718036 +2024-10-21 19:00:00,2,24.07774761806649,26.56408587390653,42.08659098930313 +2024-10-21 20:00:00,2,35.18396093115789,25.333912459473623,50.35565404418819 +2024-10-21 21:00:00,2,32.33507571516861,24.804761441344755,56.00199555477519 +2024-10-21 22:00:00,2,27.015395410532275,21.66337904706043,48.73761743329431 +2024-10-21 23:00:00,2,25.51553608960812,21.148548506929117,61.02541228594354 +2024-10-22 00:00:00,2,25.70498549401668,21.725538196310108,41.41183575871467 +2024-10-22 01:00:00,2,34.88395044431969,23.17993705611769,31.612622774770426 +2024-10-22 02:00:00,2,28.813852265388626,22.41833781924866,61.45629208127391 +2024-10-22 03:00:00,2,42.83297209313503,26.621900844707213,56.32797708847304 +2024-10-22 04:00:00,2,20.865890502156837,17.680954568584113,54.81321945479641 +2024-10-22 05:00:00,2,21.475842978183266,19.398479261168383,58.832524262491 +2024-10-22 06:00:00,2,50.53929755586704,27.341715586129613,46.03696064261816 +2024-10-22 07:00:00,2,13.58942452328651,28.818890597337276,64.10898844090491 +2024-10-22 08:00:00,2,24.943643389351205,30.476123064256782,50.34346224569225 +2024-10-22 09:00:00,2,32.03059666811515,21.967060859936844,51.92135523155785 +2024-10-22 10:00:00,2,36.732913010013476,27.527992214422863,31.123159581065504 +2024-10-22 11:00:00,2,24.57517349566696,31.14552701371959,43.53388630591661 +2024-10-22 12:00:00,2,11.361816780948253,28.940971266558247,62.698407105475134 +2024-10-22 13:00:00,2,25.248553164654577,22.176926360822808,53.514866745977535 +2024-10-22 14:00:00,2,44.401669029570925,27.29338366262928,48.98499830014348 +2024-10-22 15:00:00,2,23.58860381999353,26.438783702918066,70.35317788592624 +2024-10-22 16:00:00,2,29.134791110381656,20.071480771869627,54.13074588094041 +2024-10-22 17:00:00,2,33.593532585167424,23.78066512445257,67.07725425339993 +2024-10-22 18:00:00,2,27.135623063086804,22.2999659132313,59.89093266356112 +2024-10-22 19:00:00,2,44.04751814770876,23.71438494922871,45.4465951890521 +2024-10-22 20:00:00,2,35.98322897099018,27.617685834629974,35.71829962162843 +2024-10-22 21:00:00,2,38.40091253773604,23.32396306061663,64.45223164151707 +2024-10-22 22:00:00,2,25.457618235904565,28.598202299241954,51.801631184100195 +2024-10-22 23:00:00,2,22.772059521918496,26.166340941913006,68.6083251482075 +2024-10-23 00:00:00,2,31.449897946391708,22.48398332456802,60.47450499362121 +2024-10-23 01:00:00,2,39.15629903781682,24.244350795044774,43.92588352849673 +2024-10-23 02:00:00,2,29.349892211817647,17.320650739780863,58.78295185504629 +2024-10-23 03:00:00,2,19.40906379466177,19.46180211546394,37.488555071929916 +2024-10-23 04:00:00,2,25.87044876228498,23.085069829563498,46.30189730121235 +2024-10-23 05:00:00,2,22.640641702000703,20.0340668876524,59.26396464909256 +2024-10-23 06:00:00,2,30.723120538585995,26.439961944022272,35.09111758708811 +2024-10-23 07:00:00,2,28.80743221088358,26.776071287699022,56.647858166158734 +2024-10-23 08:00:00,2,29.057831683743608,30.15911728370511,61.80006315971002 +2024-10-23 09:00:00,2,41.279802437947154,31.056928066200847,52.18261506982463 +2024-10-23 10:00:00,2,21.462448241003624,23.537921584105558,49.82118243279115 +2024-10-23 11:00:00,2,38.170411553713265,31.263976319302653,63.78014388001907 +2024-10-23 12:00:00,2,35.409123685334315,22.01891267508678,53.220050346457214 +2024-10-23 13:00:00,2,21.751079644920036,25.213411621003786,62.83025630734109 +2024-10-23 14:00:00,2,31.49297006644038,21.7122500118962,55.94556983482434 +2024-10-23 15:00:00,2,30.904362529070326,32.097046808369335,70.23617225843022 +2024-10-23 16:00:00,2,30.721096039062054,30.17074134768652,48.55407942433907 +2024-10-23 17:00:00,2,22.910989653312665,28.673015226806037,57.162736082999515 +2024-10-23 18:00:00,2,18.449020860864614,22.853200717994255,55.146447179797455 +2024-10-23 19:00:00,2,23.29825356142821,32.26084498948485,54.76177276111833 +2024-10-23 20:00:00,2,28.530610025848752,21.440307942098187,60.85497123774946 +2024-10-23 21:00:00,2,39.53642329021871,21.199398283915027,51.56837003856983 +2024-10-23 22:00:00,2,20.004250312634106,18.144115765938658,72.74644887948669 +2024-10-23 23:00:00,2,45.13740395951124,23.0484605945499,57.393457549710895 +2024-10-24 00:00:00,2,23.256476984097368,25.288397320236278,52.54096159871673 +2024-10-24 01:00:00,2,40.51256845303182,20.942757868011483,40.65913091592006 +2024-10-24 02:00:00,2,32.78606607343357,31.312838399148013,41.58941964447271 +2024-10-24 03:00:00,2,25.392319723307754,23.712363089579146,55.91733610465401 +2024-10-24 04:00:00,2,23.112558398073162,24.603045749384087,57.38243629540792 +2024-10-24 05:00:00,2,30.739456893059053,21.19940251020877,33.965071759923134 +2024-10-24 06:00:00,2,28.112036347364963,23.000515133018098,57.47973586299955 +2024-10-24 07:00:00,2,25.7388096389452,27.66660958609265,55.308524257161004 +2024-10-24 08:00:00,2,26.593179594927985,25.045275256133518,47.07761905619107 +2024-10-24 09:00:00,2,40.08454416749788,20.879658898309817,63.45751461937813 +2024-10-24 10:00:00,2,31.639604860900477,29.876081028887814,70.29062420412119 +2024-10-24 11:00:00,2,44.501437786881795,25.236012496070423,52.92108320242809 +2024-10-24 12:00:00,2,15.229298099610066,26.55103550042789,61.814429881301976 +2024-10-24 13:00:00,2,40.80186348650006,26.436409917569314,61.44884435767558 +2024-10-24 14:00:00,2,25.79542032198761,23.782367603490822,46.196158964897954 +2024-10-24 15:00:00,2,28.869680174822427,29.32110525432222,62.8022176976212 +2024-10-24 16:00:00,2,29.54149706767347,26.1997487413774,48.840613037764 +2024-10-24 17:00:00,2,14.536384423217656,23.102601615534013,33.48116042902792 +2024-10-24 18:00:00,2,31.054708845886697,21.278094140045088,53.512867197405335 +2024-10-24 19:00:00,2,10.801950054696762,19.38237768501536,52.28263909926257 +2024-10-24 20:00:00,2,17.804102094123124,25.634030426435878,46.98966560324233 +2024-10-24 21:00:00,2,38.16799218228412,27.718004890181998,77.36210542848195 +2024-10-24 22:00:00,2,18.349937420185597,25.250920048018603,69.16993643761757 +2024-10-24 23:00:00,2,28.481343183668727,21.622690964358487,61.97399885992547 +2024-10-25 00:00:00,2,25.739082193302448,18.249528823115728,38.21643038550849 +2024-10-25 01:00:00,2,30.654969003448212,18.86943019264392,40.80366499603552 +2024-10-25 02:00:00,2,32.98287799403617,29.064163862435848,60.413189188676895 +2024-10-25 03:00:00,2,27.202512564763186,25.6524807400842,44.979188084251575 +2024-10-25 04:00:00,2,6.002212660611743,25.50968185792513,55.591431823551865 +2024-10-25 05:00:00,2,30.126487701261745,17.54723805934453,54.50403120820338 +2024-10-25 06:00:00,2,35.729902814018786,17.767123454712795,54.74556965075701 +2024-10-25 07:00:00,2,16.96967493076659,30.411032878870735,47.32470163266906 +2024-10-25 08:00:00,2,25.025021856665035,27.267559490628134,52.004528267469574 +2024-10-25 09:00:00,2,28.41042865262799,27.67652801690054,48.472848949573944 +2024-10-25 10:00:00,2,27.145307609201918,26.221799301886577,63.101591796522 +2024-10-25 11:00:00,2,23.004623675863886,25.978123459708286,43.478632920550965 +2024-10-25 12:00:00,2,27.312606558046397,29.25744348120348,41.36572523399889 +2024-10-25 13:00:00,2,28.791510576202775,29.040764303878884,42.1052105237266 +2024-10-25 14:00:00,2,25.729854512309732,21.796954921709723,47.32504926903947 +2024-10-25 15:00:00,2,23.094313034635487,24.470970943824483,43.7053350850107 +2024-10-25 16:00:00,2,33.82771069848218,26.2818940373365,49.68594646127656 +2024-10-25 17:00:00,2,36.952394407089166,23.459609656868942,50.91256335648416 +2024-10-25 18:00:00,2,19.286182464106258,21.83461707546012,45.60696208929158 +2024-10-25 19:00:00,2,15.023638013305767,25.314611683926337,42.98227664887476 +2024-10-25 20:00:00,2,13.794399544035176,33.35256808038497,43.53249152821353 +2024-10-25 21:00:00,2,19.324565396141438,17.73702390688276,38.80015374486669 +2024-10-25 22:00:00,2,37.074326015727124,24.89591933328568,57.00304522135515 +2024-10-25 23:00:00,2,36.007618210926864,24.57120459789894,48.085450762786294 +2024-10-26 00:00:00,2,29.30520457248498,21.555960381661624,34.4846989769975 +2024-10-26 01:00:00,2,40.28818697769612,22.977028175717493,44.10935142720012 +2024-10-26 02:00:00,2,44.902991576023005,27.349615163751753,66.20347185609782 +2024-10-26 03:00:00,2,36.90678762451723,20.982015114459273,56.99917117515524 +2024-10-26 04:00:00,2,48.21805605444636,22.098225545556527,69.37048612933071 +2024-10-26 05:00:00,2,21.8183323610451,29.318010142354687,38.13092781772556 +2024-10-26 06:00:00,2,26.89830887612632,20.519419624362985,57.78555737999343 +2024-10-26 07:00:00,2,37.818299962880886,20.371256654435662,54.96308636972205 +2024-10-26 08:00:00,2,29.297395305286724,23.11006392567915,54.745700365118424 +2024-10-26 09:00:00,2,15.381868976144052,23.094763894326235,59.31966491774971 +2024-10-26 10:00:00,2,29.374602175988866,25.05298912663855,55.57430486326963 +2024-10-26 11:00:00,2,26.335327852882354,24.751138935670166,50.46208823197185 +2024-10-26 12:00:00,2,32.53656479477195,24.486181567769425,52.95520405510069 +2024-10-26 13:00:00,2,31.698747194796866,26.06166161683667,44.85440938925962 +2024-10-26 14:00:00,2,14.508162933876976,23.309172901940702,44.946128147068436 +2024-10-26 15:00:00,2,26.38755221763938,26.800920644977506,64.4356601445879 +2024-10-26 16:00:00,2,19.405719639414656,32.584296813653786,49.921433191177435 +2024-10-26 17:00:00,2,24.542336954839737,24.36332586641401,73.49144857500936 +2024-10-26 18:00:00,2,33.893227575768876,21.797627105863604,48.14053384219447 +2024-10-26 19:00:00,2,20.91709566034146,18.08055503702641,49.92061172271178 +2024-10-26 20:00:00,2,32.83820945211607,26.22905808162148,45.26646433296851 +2024-10-26 21:00:00,2,34.13458247506104,24.971072698160558,44.31393779092569 +2024-10-26 22:00:00,2,23.494670056954785,23.425557470192288,61.48888504803452 +2024-10-26 23:00:00,2,20.70185054912576,27.536992692502032,61.71443825410498 +2024-10-27 00:00:00,2,33.913400290264555,24.504887417408238,57.51323888686684 +2024-10-27 01:00:00,2,23.886075188703312,24.265770847085182,46.811436239968856 +2024-10-27 02:00:00,2,41.56871017430444,27.00527297576215,47.103873058245746 +2024-10-27 03:00:00,2,38.03242378683272,20.81201348168795,63.646692647879846 +2024-10-27 04:00:00,2,32.10025226850138,26.637976158972396,39.096474655371274 +2024-10-27 05:00:00,2,35.160506099400116,18.872356223627587,73.28159915682262 +2024-10-27 06:00:00,2,38.334194286330444,27.159814153930654,53.463787130872134 +2024-10-27 07:00:00,2,8.47544902104454,23.22747076434983,69.79977662017833 +2024-10-27 08:00:00,2,40.06345322541513,22.07681084425103,54.99506298839003 +2024-10-27 09:00:00,2,31.159531071182577,33.7562813832247,52.27225035794019 +2024-10-27 10:00:00,2,20.120059241559698,26.22274326256112,48.44161256720507 +2024-10-27 11:00:00,2,42.7329672653716,21.991909542335303,51.00926504034122 +2024-10-27 12:00:00,2,21.73439947236598,26.239177296458262,53.241012961386495 +2024-10-27 13:00:00,2,34.05821472890033,20.424978830797432,57.486027573463765 +2024-10-27 14:00:00,2,31.548975262866556,27.014600350654717,47.402795501287244 +2024-10-27 15:00:00,2,17.07434376809717,23.638288025328798,52.88271924998058 +2024-10-27 16:00:00,2,31.07111942301634,28.99554755780704,46.25280378820472 +2024-10-27 17:00:00,2,21.284525035392242,23.58711267042765,57.87971376472676 +2024-10-27 18:00:00,2,30.776854240381866,22.911508949773882,55.319904977196 +2024-10-27 19:00:00,2,20.344603520263917,26.505964957335372,55.196980574982156 +2024-10-27 20:00:00,2,26.18012640858879,24.74624372302736,49.79578989544821 +2024-10-27 21:00:00,2,24.15596365067677,26.247781356089753,47.41689068054885 +2024-10-27 22:00:00,2,28.271090360670627,30.624365078360228,46.9208902376365 +2024-10-27 23:00:00,2,34.33698351539573,21.3931328862203,60.430780793486804 +2024-10-28 00:00:00,2,19.092644159382857,13.292780184860318,66.00993630858542 +2024-10-28 01:00:00,2,36.354867417482495,30.546085465950135,47.19585042872741 +2024-10-28 02:00:00,2,27.138007094859308,24.877725259277344,49.36330439044647 +2024-10-28 03:00:00,2,33.36032634985952,24.584932450963823,45.146660846380016 +2024-10-28 04:00:00,2,29.2242710700273,23.12853943092617,34.01566629607541 +2024-10-28 05:00:00,2,26.326792960436826,23.51359513541298,54.98958637460909 +2024-10-28 06:00:00,2,23.188148529795818,24.321028718920026,56.315561671286886 +2024-10-28 07:00:00,2,36.744017189245966,25.95937317216304,60.13626244033701 +2024-10-28 08:00:00,2,37.27282956691545,22.065779809383695,60.781544569507766 +2024-10-28 09:00:00,2,28.844522602713493,25.751479522091948,62.58429971282125 +2024-10-28 10:00:00,2,17.822133616594535,25.94410526158676,59.24266679155244 +2024-10-28 11:00:00,2,21.10185709683372,23.36169573122417,57.29137107801376 +2024-10-28 12:00:00,2,48.8012585738427,23.724768706200635,59.12090140942556 +2024-10-28 13:00:00,2,10.815059975498178,24.85832588732429,52.84363875346886 +2024-10-28 14:00:00,2,26.277732620191426,28.54253242713326,69.72505367491243 +2024-10-28 15:00:00,2,30.435284632353437,27.12881677408015,60.11777717009184 +2024-10-28 16:00:00,2,25.509347896941293,18.294325111486287,55.24841071105591 +2024-10-28 17:00:00,2,21.84107224507086,25.20873982888311,59.30930864905344 +2024-10-28 18:00:00,2,24.141906047854683,18.803970878842563,51.828933561719495 +2024-10-28 19:00:00,2,28.923531902271222,26.56673924238665,54.24564468209221 +2024-10-28 20:00:00,2,27.02370090875224,25.67145039818347,58.51953389079196 +2024-10-28 21:00:00,2,18.926932942680665,19.893935774544723,61.083880958105425 +2024-10-28 22:00:00,2,14.759930472707808,20.979279282764377,52.69129668688616 +2024-10-28 23:00:00,2,21.75994014182416,20.651930019656156,51.89644764536679 +2024-10-29 00:00:00,2,40.53344838592288,28.1721600644257,71.23304335802624 +2024-10-29 01:00:00,2,32.602774440528584,24.753977518952603,44.364582364975234 +2024-10-29 02:00:00,2,3.7469822263424923,25.93149790293216,63.963115611488774 +2024-10-29 03:00:00,2,34.072280946024094,28.77779200873932,50.751228200171745 +2024-10-29 04:00:00,2,26.349240027823416,22.39921170946664,52.03318034879232 +2024-10-29 05:00:00,2,22.388525815844062,23.396604468022,48.352787149832025 +2024-10-29 06:00:00,2,19.807794525631497,23.501812250255593,55.734552074635666 +2024-10-29 07:00:00,2,25.41615517017459,23.798820140977565,49.835707338878365 +2024-10-29 08:00:00,2,15.409367109361128,20.00328975419161,33.66024964657663 +2024-10-29 09:00:00,2,16.961285969105564,24.87532378437833,60.20775745250333 +2024-10-29 10:00:00,2,25.640948151740236,22.07607223097409,51.845396400055165 +2024-10-29 11:00:00,2,27.543725428188594,26.20851245212173,55.80206064480996 +2024-10-29 12:00:00,2,30.3459985197147,26.44640935932394,51.78405508501372 +2024-10-29 13:00:00,2,16.767050872946548,31.661482488897278,50.676685996420545 +2024-10-29 14:00:00,2,33.21798223969147,25.481485498557323,50.640657582721246 +2024-10-29 15:00:00,2,30.172775900955124,23.013194118614102,62.58352022851589 +2024-10-29 16:00:00,2,27.272889424187234,19.72224243820892,63.1448948456589 +2024-10-29 17:00:00,2,28.78596437931342,27.561407836277496,55.64544790467232 +2024-10-29 18:00:00,2,23.809637492080885,24.56786841515423,51.96710735633074 +2024-10-29 19:00:00,2,15.330577941622751,26.726092527680606,38.8349654523824 +2024-10-29 20:00:00,2,43.655757058296075,23.904375869634695,58.47633683606964 +2024-10-29 21:00:00,2,0.0,26.527194863652035,50.05360447775351 +2024-10-29 22:00:00,2,27.243647905989633,23.192629698279553,60.7383757578999 +2024-10-29 23:00:00,2,33.62668307428923,25.03687259617493,54.32259044407777 +2024-10-30 00:00:00,2,22.367751985304515,18.08362973693863,48.25789455209714 +2024-10-30 01:00:00,2,31.2828597791138,17.758841537137144,60.31581844263379 +2024-10-30 02:00:00,2,29.882708744135336,26.967091248765495,53.66263944028784 +2024-10-30 03:00:00,2,47.487582676426506,22.18060180090467,58.81223552316339 +2024-10-30 04:00:00,2,46.76787760463493,26.870792031304703,40.322711134987486 +2024-10-30 05:00:00,2,16.484657007033377,21.822410694165757,43.319902809476304 +2024-10-30 06:00:00,2,24.377020822628786,21.49523180900801,60.06268589028593 +2024-10-30 07:00:00,2,11.062640047990865,20.005957426348523,63.609403103210774 +2024-10-30 08:00:00,2,26.352588395357408,25.682685928780455,27.90584474579437 +2024-10-30 09:00:00,2,32.71512134142609,28.006768192009897,71.36326703665407 +2024-10-30 10:00:00,2,37.27933448950031,25.841914084960294,37.09276389152542 +2024-10-30 11:00:00,2,31.331739240426124,30.09139020144776,64.9897435440982 +2024-10-30 12:00:00,2,20.337477242052998,26.12532772000226,53.954798748629926 +2024-10-30 13:00:00,2,16.145902458163803,26.44978983146711,57.57949817017863 +2024-10-30 14:00:00,2,35.04868308680348,23.721252448328737,34.2379982520742 +2024-10-30 15:00:00,2,27.679672629678176,26.04504186547103,56.4599407266423 +2024-10-30 16:00:00,2,15.500858929712228,18.423931936438624,63.84747165575004 +2024-10-30 17:00:00,2,14.199587201902064,27.335823741859677,61.06487875881625 +2024-10-30 18:00:00,2,16.721418809462442,25.11065079093994,50.63592762321158 +2024-10-30 19:00:00,2,24.45923583736839,26.39338192725237,41.5055690412142 +2024-10-30 20:00:00,2,51.30725652705132,22.281467463249133,58.835698527779726 +2024-10-30 21:00:00,2,25.216902278318972,21.015671057855656,46.88255241387288 +2024-10-30 22:00:00,2,33.0855749753694,21.89954137648407,54.27916726615014 +2024-10-30 23:00:00,2,36.64699824998998,26.778198095167202,46.44723693161783 +2024-10-31 00:00:00,2,38.816141447259604,21.945217853322887,50.15450073219261 +2024-10-31 01:00:00,2,16.615491317368658,18.600941244109972,60.23036332086174 +2024-10-31 02:00:00,2,46.92627002403051,29.294789994647576,49.62544283972522 +2024-10-31 03:00:00,2,23.581811749200057,23.4969867657728,61.243975405206356 +2024-10-31 04:00:00,2,21.982047991223002,28.884763098058436,59.60524070679771 +2024-10-31 05:00:00,2,39.956205552612104,21.692028394078207,48.62595236325639 +2024-10-31 06:00:00,2,22.987146305138477,19.572499953380742,43.08650873411789 +2024-10-31 07:00:00,2,35.49301277056276,29.00638844101728,45.65412422301371 +2024-10-31 08:00:00,2,22.271402571038337,20.547219195014012,50.233794287344665 +2024-10-31 09:00:00,2,25.032766952637687,24.174582813574304,39.95277981861571 +2024-10-31 10:00:00,2,39.26891952559507,24.28171697678416,53.93149019737559 +2024-10-31 11:00:00,2,34.24296692754842,24.01479888377466,46.747796533968696 +2024-10-31 12:00:00,2,22.234760706295628,30.75528091127112,39.46658019251838 +2024-10-31 13:00:00,2,34.88438457341517,27.587898956608957,48.482312423178385 +2024-10-31 14:00:00,2,28.27784480472273,17.267342084993963,41.8834705874664 +2024-10-31 15:00:00,2,27.79798875040851,22.605524023786522,68.5580872674694 +2024-10-31 16:00:00,2,45.82382531481337,18.724070345193667,70.95370245916277 +2024-10-31 17:00:00,2,21.73477941944377,31.21070312578659,66.73701564213715 +2024-10-31 18:00:00,2,25.820832096653564,21.8448047947693,58.09754507572579 +2024-10-31 19:00:00,2,17.771514828544078,16.164971620992695,42.63845381747911 +2024-10-31 20:00:00,2,33.99671419447765,27.73300230148448,64.60714472798492 +2024-10-31 21:00:00,2,20.74497819062659,24.02540534080855,56.91128013221894 +2024-10-31 22:00:00,2,13.823675709993633,22.80115688885622,56.92158800658376 +2024-10-31 23:00:00,2,34.808609528868715,27.097636023635953,59.97341715774703 +2024-11-01 00:00:00,2,32.59938168015914,17.6130422166339,55.836891491252864 +2024-11-01 01:00:00,2,36.27380372308532,23.99137045366584,67.21614724699145 +2024-11-01 02:00:00,2,14.51490643304181,21.210914386009378,50.8596958705604 +2024-11-01 03:00:00,2,28.73408494176704,24.164569452036666,60.02342188922772 +2024-11-01 04:00:00,2,39.840941098081565,28.431467486261482,50.535024317268864 +2024-11-01 05:00:00,2,32.49771175384356,28.26423381460607,65.70468559144322 +2024-11-01 06:00:00,2,36.23466351809193,19.689034747296223,60.386051506236946 +2024-11-01 07:00:00,2,29.048618459894907,26.4987668647565,45.65397116018018 +2024-11-01 08:00:00,2,19.92181535271426,26.839152647428712,39.68595020279663 +2024-11-01 09:00:00,2,29.667540970120108,26.697380272816694,52.078282492738516 +2024-11-01 10:00:00,2,33.7546986628866,24.676790257946898,44.833618087069695 +2024-11-01 11:00:00,2,56.201533866641746,23.131261220305593,54.514140344964034 +2024-11-01 12:00:00,2,31.770463618526744,30.512301438170027,63.24519410806789 +2024-11-01 13:00:00,2,28.498757961575805,29.70582637380717,41.86179555459791 +2024-11-01 14:00:00,2,45.21365131837826,30.803593866225828,42.71614030201846 +2024-11-01 15:00:00,2,37.58671066799417,28.948739843778455,41.40134107422808 +2024-11-01 16:00:00,2,27.48181971616923,27.943734690543966,66.78366950999286 +2024-11-01 17:00:00,2,32.70013611510334,25.377162245793045,48.7844696792603 +2024-11-01 18:00:00,2,20.234237128603677,26.854797932038245,56.442475444298324 +2024-11-01 19:00:00,2,32.276431971981886,19.931860164113107,59.399078299613954 +2024-11-01 20:00:00,2,15.45665989666059,28.386597044130003,59.81303620204041 +2024-11-01 21:00:00,2,31.264284531286286,21.189761688887636,55.736291210809085 +2024-11-01 22:00:00,2,30.837880052883158,20.0541228686596,60.79120823851463 +2024-11-01 23:00:00,2,23.952219585152026,25.59741607920837,38.905739798906374 +2024-11-02 00:00:00,2,12.07173689995869,23.29523594500922,39.53154251523446 +2024-11-02 01:00:00,2,20.132445213074273,22.255301168331627,29.767674689732903 +2024-11-02 02:00:00,2,30.473194137969507,28.02809141398651,47.84432076713916 +2024-11-02 03:00:00,2,23.309460368241204,23.94585149894811,43.23530374798399 +2024-11-02 04:00:00,2,28.437803448587193,20.428383878468313,60.9994434368778 +2024-08-04 05:00:00,3,13.736503205526095,24.427635651310077,56.461756631497636 +2024-08-04 06:00:00,3,32.24036556379243,23.019532778955124,50.4173062680538 +2024-08-04 07:00:00,3,19.829946465921974,22.758803496231835,48.256156776221616 +2024-08-04 08:00:00,3,32.991266307436845,33.17958299986773,57.268320593399196 +2024-08-04 09:00:00,3,28.58763707287789,25.200857184050605,49.86286880951184 +2024-08-04 10:00:00,3,30.45924646991654,28.414086374848818,46.4251407196213 +2024-08-04 11:00:00,3,44.94894094768448,26.190234849835285,47.943569167965464 +2024-08-04 12:00:00,3,25.77987933417574,19.220183613237815,50.87770094058346 +2024-08-04 13:00:00,3,23.52520137174741,25.54013834558293,52.23206407768936 +2024-08-04 14:00:00,3,23.84026219506914,23.569612394005294,64.37945667911268 +2024-08-04 15:00:00,3,31.144301141550493,23.807586781224124,54.83470356745749 +2024-08-04 16:00:00,3,6.731473168140752,31.844309751823253,49.248332845012 +2024-08-04 17:00:00,3,26.298704559852993,16.780944994513614,53.54411961891608 +2024-08-04 18:00:00,3,30.7187347486894,24.030739901370563,55.08475172613841 +2024-08-04 19:00:00,3,20.879651058857544,30.413421228689472,41.646650617887424 +2024-08-04 20:00:00,3,23.920667568172654,23.702997426170104,49.5306620013904 +2024-08-04 21:00:00,3,27.90960613669885,19.152262642505903,55.56441646272441 +2024-08-04 22:00:00,3,22.426421850471655,20.965693899143602,51.70600773767562 +2024-08-04 23:00:00,3,21.67536004659877,27.340980762741285,61.00797516398751 +2024-08-05 00:00:00,3,29.308212518458134,24.354574621123717,63.40478874346158 +2024-08-05 01:00:00,3,25.394486029157235,25.793243141643952,58.371695325063925 +2024-08-05 02:00:00,3,19.2116487746635,24.482242982435736,66.18753194216038 +2024-08-05 03:00:00,3,37.24644066516059,25.784014669922588,57.81042440063526 +2024-08-05 04:00:00,3,27.9051506388449,25.69795845138821,62.19831418597903 +2024-08-05 05:00:00,3,17.750292003707255,23.341476886305735,48.8478269615671 +2024-08-05 06:00:00,3,35.18781313708974,17.803283394546042,58.25087134148644 +2024-08-05 07:00:00,3,35.917753729424646,27.087580309659398,60.58255419345063 +2024-08-05 08:00:00,3,33.69473852924972,21.02141800472377,40.64939213918251 +2024-08-05 09:00:00,3,22.02453163920106,21.705746993531076,38.201722309491544 +2024-08-05 10:00:00,3,21.762186430979952,28.10282794701827,65.55477702619518 +2024-08-05 11:00:00,3,28.649025443414008,22.12464617777117,44.37076324528394 +2024-08-05 12:00:00,3,28.808897450589033,26.34271111405205,56.28313439318529 +2024-08-05 13:00:00,3,15.452762464032308,22.22082239151632,59.790599343056655 +2024-08-05 14:00:00,3,45.23643266265376,30.645113160489704,56.312056262675526 +2024-08-05 15:00:00,3,26.118289435948576,24.81253273519872,47.66319657313113 +2024-08-05 16:00:00,3,2.510446357106076,27.695758294677013,54.80064793475107 +2024-08-05 17:00:00,3,37.96569289389412,22.842563814869195,51.313089279098854 +2024-08-05 18:00:00,3,20.52369761560987,27.540959682262567,53.3490969285525 +2024-08-05 19:00:00,3,28.468463536918087,24.488328827660126,61.26136255907178 +2024-08-05 20:00:00,3,31.372423849944887,24.399869357281634,49.31031552266482 +2024-08-05 21:00:00,3,16.000721187663174,21.726361429848204,63.945999560377125 +2024-08-05 22:00:00,3,22.918162464594545,26.008739089242543,56.111008123884524 +2024-08-05 23:00:00,3,27.046313937764086,26.283398894343136,71.32707481310975 +2024-08-06 00:00:00,3,35.59163341224632,23.72384349142345,43.57539041014473 +2024-08-06 01:00:00,3,24.367610375341506,27.460041847474116,69.70790729706438 +2024-08-06 02:00:00,3,35.58120029370763,21.174282501620755,52.76802619495483 +2024-08-06 03:00:00,3,25.023158240735754,20.48874296034054,49.925775271946094 +2024-08-06 04:00:00,3,22.03247059322726,18.68919648033764,56.715647145008084 +2024-08-06 05:00:00,3,16.55851078774675,29.136037818869326,62.02197538045655 +2024-08-06 06:00:00,3,21.741541498509676,27.948933286276638,61.64075357007251 +2024-08-06 07:00:00,3,13.106987097407528,24.15541332933617,56.17327428939424 +2024-08-06 08:00:00,3,24.15261682701798,22.480818904629295,45.42128077343728 +2024-08-06 09:00:00,3,30.627880314772273,19.58630224511943,53.19014556566012 +2024-08-06 10:00:00,3,36.05573898201577,27.671410220830477,60.77653512506414 +2024-08-06 11:00:00,3,38.230304605519116,26.66226388976891,34.96738689975054 +2024-08-06 12:00:00,3,14.39945118253358,28.077025822452676,51.256620114860496 +2024-08-06 13:00:00,3,5.977759152093313,27.194941709120403,64.4332663151916 +2024-08-06 14:00:00,3,31.324858485611372,22.14286783779075,57.95945851334693 +2024-08-06 15:00:00,3,34.09293573132882,19.109859095883415,64.75294110697014 +2024-08-06 16:00:00,3,16.523649155506952,24.570631050906005,57.27257539590724 +2024-08-06 17:00:00,3,28.315057308018233,25.36723471601885,53.36863636797141 +2024-08-06 18:00:00,3,20.590505782335597,30.62758057572383,63.23897095938623 +2024-08-06 19:00:00,3,46.73074145586799,21.82967155509987,44.97731052725149 +2024-08-06 20:00:00,3,22.913661001077422,25.120861996949174,50.25582477485435 +2024-08-06 21:00:00,3,27.123863678541376,21.24958895048039,65.8695397714669 +2024-08-06 22:00:00,3,25.94527122787339,16.716117674975216,62.20578355816446 +2024-08-06 23:00:00,3,27.433476206066143,21.012421481990582,51.44156804136358 +2024-08-07 00:00:00,3,23.729869674987214,24.603698031144173,56.24312512468012 +2024-08-07 01:00:00,3,35.939622155260736,27.09243699005905,56.74359098919959 +2024-08-07 02:00:00,3,23.587866895605025,23.854359586804527,61.68875472909046 +2024-08-07 03:00:00,3,15.42649028825203,28.925828252546186,56.116277337820904 +2024-08-07 04:00:00,3,20.519376599798395,19.42749734107651,58.31427254179895 +2024-08-07 05:00:00,3,17.00935765964683,28.822497094790467,63.11400326672613 +2024-08-07 06:00:00,3,31.10468611336281,22.34218582001133,57.58613918812765 +2024-08-07 07:00:00,3,15.324627669834001,18.0871010397802,47.49708422162255 +2024-08-07 08:00:00,3,28.119876578642266,17.610685040163553,51.329864276970255 +2024-08-07 09:00:00,3,14.265259371943564,20.898640804192897,62.33519571010624 +2024-08-07 10:00:00,3,40.40898011052303,28.329100082705867,59.51845921519924 +2024-08-07 11:00:00,3,21.414440390063305,28.899636360797853,42.91166954874163 +2024-08-07 12:00:00,3,27.247101495519466,27.972991935450942,35.75318433325036 +2024-08-07 13:00:00,3,29.039006633559833,25.411589490360903,49.556079625231426 +2024-08-07 14:00:00,3,18.803094638608254,24.81911100795757,52.259230054190844 +2024-08-07 15:00:00,3,35.231125059898496,35.15387387420126,60.88620911019656 +2024-08-07 16:00:00,3,23.63956123310093,21.862327052374184,53.936889525478215 +2024-08-07 17:00:00,3,16.209563420195966,25.428644309919996,53.94475950544039 +2024-08-07 18:00:00,3,8.795172773207424,27.53702061119887,52.81522151804541 +2024-08-07 19:00:00,3,13.420724843430762,23.920933102506133,42.3128633095068 +2024-08-07 20:00:00,3,20.140030064577832,19.977074837785004,57.62080171352022 +2024-08-07 21:00:00,3,20.409948749394804,22.97645805366133,55.273239966043164 +2024-08-07 22:00:00,3,7.979166229457693,24.824036810832617,63.62288658715729 +2024-08-07 23:00:00,3,9.836360546604087,23.582380387942777,58.867310943995484 +2024-08-08 00:00:00,3,23.406290763506664,22.285421308976613,59.393038501895205 +2024-08-08 01:00:00,3,20.590953273312852,19.699388279818177,51.09855706004496 +2024-08-08 02:00:00,3,33.18545879313969,28.079434529623505,47.44881540163281 +2024-08-08 03:00:00,3,14.925433052221639,20.12057903181993,50.672377739870726 +2024-08-08 04:00:00,3,26.194579300209842,22.95074172378942,60.59134299116021 +2024-08-08 05:00:00,3,33.8578480702979,31.795752103349688,65.19070281238658 +2024-08-08 06:00:00,3,22.715976962829373,23.553217861327457,46.306084017187146 +2024-08-08 07:00:00,3,39.23026869761725,32.60202588763489,61.12988305926176 +2024-08-08 08:00:00,3,21.569927724401193,20.95005756224546,60.694427061348165 +2024-08-08 09:00:00,3,30.919782490477534,23.699704230392953,51.5408050970523 +2024-08-08 10:00:00,3,16.471333815284282,29.08669771962247,40.065670844334285 +2024-08-08 11:00:00,3,45.33714816945515,23.18063415613853,63.880358746213744 +2024-08-08 12:00:00,3,33.567017217315964,24.150177206905205,60.425371785637225 +2024-08-08 13:00:00,3,36.165504307839605,24.703488029577173,44.90595391647762 +2024-08-08 14:00:00,3,28.082901818961524,25.13043688476514,53.41008386986872 +2024-08-08 15:00:00,3,22.061193808181258,22.766783110991447,47.94512848775378 +2024-08-08 16:00:00,3,9.81030113158754,22.147216077726316,59.83456429638597 +2024-08-08 17:00:00,3,14.953775826309167,24.161101565975027,57.42444012020266 +2024-08-08 18:00:00,3,23.73583316757316,26.733123062176194,58.13921416233848 +2024-08-08 19:00:00,3,32.350322255319426,27.82759466402657,52.138018253888625 +2024-08-08 20:00:00,3,25.743590701711376,25.70312645577542,53.48845119360591 +2024-08-08 21:00:00,3,7.024074405021754,27.104375598336976,48.86905414459949 +2024-08-08 22:00:00,3,22.54675809880532,16.51340039971857,64.01439223067601 +2024-08-08 23:00:00,3,17.9778083563016,29.325655803358224,58.20795622986524 +2024-08-09 00:00:00,3,27.149268532862326,28.22546894661294,67.393762833751 +2024-08-09 01:00:00,3,8.053663432090993,23.261058259622033,51.38585283950419 +2024-08-09 02:00:00,3,29.47057155700691,32.27674091691678,62.41668070460574 +2024-08-09 03:00:00,3,23.198649197404997,25.36131963748844,59.70041994169124 +2024-08-09 04:00:00,3,31.829283970234545,23.197657648551814,66.09624954526855 +2024-08-09 05:00:00,3,27.694217443354994,28.07271019487402,52.097942726449794 +2024-08-09 06:00:00,3,42.53354480687609,22.156515777272002,58.81736310981457 +2024-08-09 07:00:00,3,21.799920426479602,29.480231679413208,59.69531743763765 +2024-08-09 08:00:00,3,16.757351227263527,20.560551347611796,57.91726487968335 +2024-08-09 09:00:00,3,21.304771683886443,25.523894130464218,57.964184913690524 +2024-08-09 10:00:00,3,22.226800341350167,25.500971844479857,57.04991510243066 +2024-08-09 11:00:00,3,32.78686967880196,21.91939589878705,55.6059000702552 +2024-08-09 12:00:00,3,26.7169187037372,30.885585570310727,41.46516748477098 +2024-08-09 13:00:00,3,33.6837277305476,24.990970844108833,58.12598268379495 +2024-08-09 14:00:00,3,30.289739363777855,27.998742593605424,66.34899749909228 +2024-08-09 15:00:00,3,24.34882422009728,22.612842553582443,59.52505849251109 +2024-08-09 16:00:00,3,26.106823734668403,22.608064503942675,55.50121164259156 +2024-08-09 17:00:00,3,19.672960168898545,29.83037363890672,59.73760614070064 +2024-08-09 18:00:00,3,32.784350042900044,21.37117962569236,52.18807272946202 +2024-08-09 19:00:00,3,22.86169150547007,23.60621066325119,60.43364415282504 +2024-08-09 20:00:00,3,22.31059490385928,20.177329282162496,54.12988855753263 +2024-08-09 21:00:00,3,30.311856428790833,27.10696060978529,54.814685091002865 +2024-08-09 22:00:00,3,23.642215167055745,18.173849254229594,59.989965201853785 +2024-08-09 23:00:00,3,43.043584113328265,20.720854630994463,46.29375758270381 +2024-08-10 00:00:00,3,23.151727498351377,32.65198985096211,52.3195116383584 +2024-08-10 01:00:00,3,33.98586433126199,24.24619388797372,51.78701285134248 +2024-08-10 02:00:00,3,31.100091187574087,27.63654229744401,47.10665154156066 +2024-08-10 03:00:00,3,20.280631527434956,23.42921799372455,54.14487102559582 +2024-08-10 04:00:00,3,21.516781674421317,25.338521220334062,57.306048748990435 +2024-08-10 05:00:00,3,22.031960143139997,27.665918617338647,64.23697661662182 +2024-08-10 06:00:00,3,30.285301258651216,30.68270828990299,54.972031548219725 +2024-08-10 07:00:00,3,23.66590235690005,24.275364625271045,62.268894885876826 +2024-08-10 08:00:00,3,24.824023015121888,22.804456271094686,59.28513619041324 +2024-08-10 09:00:00,3,16.580747506326986,22.41475696655016,50.47192264521454 +2024-08-10 10:00:00,3,24.656414905905045,23.376974558628216,33.75544558555334 +2024-08-10 11:00:00,3,14.976975721084726,26.196740453664926,51.580644042332445 +2024-08-10 12:00:00,3,28.091440114411704,23.625959197529564,51.873585754028106 +2024-08-10 13:00:00,3,28.506043643343126,29.814717648425628,46.02653486665017 +2024-08-10 14:00:00,3,25.641206890482774,27.44702426729721,42.97827041217337 +2024-08-10 15:00:00,3,30.2817407377301,30.990187124723214,70.52348596629409 +2024-08-10 16:00:00,3,20.974579809599966,28.98784000252791,56.84812270304863 +2024-08-10 17:00:00,3,19.332839727417188,21.254307803746503,49.003537991382 +2024-08-10 18:00:00,3,28.603046593053413,26.200332960996874,63.659255507801575 +2024-08-10 19:00:00,3,5.45349076881503,20.168065882803702,58.769404851561696 +2024-08-10 20:00:00,3,37.71805684854694,22.74734159926021,70.3316041395752 +2024-08-10 21:00:00,3,13.859186860034757,25.233065339982108,46.77141162204852 +2024-08-10 22:00:00,3,22.505801643112793,23.984922380224315,43.76021757003939 +2024-08-10 23:00:00,3,27.23174285650495,20.085874267839525,57.812950105408916 +2024-08-11 00:00:00,3,36.33692912337184,22.337334353223838,68.1181097961626 +2024-08-11 01:00:00,3,36.127035805292806,30.955174116479313,58.77439791848447 +2024-08-11 02:00:00,3,23.54786457937136,24.603152667313875,66.7584318260513 +2024-08-11 03:00:00,3,32.03837560540157,25.603672349011294,39.254104956079544 +2024-08-11 04:00:00,3,37.0037008884628,22.948957861343008,53.49780145449268 +2024-08-11 05:00:00,3,26.541713934561052,20.56338867603281,52.57399179375454 +2024-08-11 06:00:00,3,26.018872045407342,23.9459853775757,55.62119735611815 +2024-08-11 07:00:00,3,30.186046761610353,27.661335858712178,48.58437188870653 +2024-08-11 08:00:00,3,28.531342313411464,26.052829678364468,46.11475514323581 +2024-08-11 09:00:00,3,35.56169412979595,27.649126016311467,54.83088365504478 +2024-08-11 10:00:00,3,4.798123199247431,25.3500231190768,44.37681510712844 +2024-08-11 11:00:00,3,24.694862216250975,18.67114515073405,39.721132497005605 +2024-08-11 12:00:00,3,20.403970563706057,24.75820336179843,41.0783222221119 +2024-08-11 13:00:00,3,28.384558179506094,25.67737965661369,57.55110058081882 +2024-08-11 14:00:00,3,43.06693257061518,25.0694087519848,44.521392676387315 +2024-08-11 15:00:00,3,12.695087048556397,27.150953229991458,52.597891663205935 +2024-08-11 16:00:00,3,12.187904562768301,30.69068138617093,54.273640189342636 +2024-08-11 17:00:00,3,18.056756244444518,25.499943105604576,59.502638998130756 +2024-08-11 18:00:00,3,33.38779873194258,27.299144331280548,61.38343768449902 +2024-08-11 19:00:00,3,32.48173703198519,20.992257982068452,47.98478305271327 +2024-08-11 20:00:00,3,11.190522268904823,25.599998773176583,60.357947261204856 +2024-08-11 21:00:00,3,38.7067839650922,26.629009601928963,51.75361915699031 +2024-08-11 22:00:00,3,30.797398456281403,20.426337826774418,47.08562980386789 +2024-08-11 23:00:00,3,37.48575811752571,25.029082696567635,48.92013615388481 +2024-08-12 00:00:00,3,23.145925074287113,23.323343609324144,61.53331460253491 +2024-08-12 01:00:00,3,20.121029913758612,17.463844405020655,58.20317562817362 +2024-08-12 02:00:00,3,23.652245125587488,21.55245978883966,50.448085508739766 +2024-08-12 03:00:00,3,29.182102881836244,28.041487150820235,48.05872133080192 +2024-08-12 04:00:00,3,28.56183828866662,21.855508637693887,68.02288694347192 +2024-08-12 05:00:00,3,35.04245001920543,30.21169506111088,60.56447934785444 +2024-08-12 06:00:00,3,27.895161389022707,30.29560893531663,70.40396397062837 +2024-08-12 07:00:00,3,44.170850207958196,23.677792982059998,67.21151756307498 +2024-08-12 08:00:00,3,17.982033738099624,26.682039585221023,45.14804958237505 +2024-08-12 09:00:00,3,11.6591198586642,30.685870161072817,60.3772558878989 +2024-08-12 10:00:00,3,24.894813682431135,28.405584618987863,65.94708453156947 +2024-08-12 11:00:00,3,24.666102304064683,22.83049487519646,38.024545157171886 +2024-08-12 12:00:00,3,19.078862319054515,24.92389020287223,52.2145499371354 +2024-08-12 13:00:00,3,31.975833337812862,26.89553229960313,61.520622982858356 +2024-08-12 14:00:00,3,33.15885761151701,24.975842054952007,45.43666290734065 +2024-08-12 15:00:00,3,19.78386282812644,28.2607049626002,52.28888995311833 +2024-08-12 16:00:00,3,30.18213782278468,26.913385885591698,58.029454245409056 +2024-08-12 17:00:00,3,31.14832416818347,19.053044056130503,61.48934720674961 +2024-08-12 18:00:00,3,35.66937353434516,21.985410402442987,57.494767979474126 +2024-08-12 19:00:00,3,15.641746062532963,16.12737513187907,55.888963617463865 +2024-08-12 20:00:00,3,22.026029592980922,19.559718449602506,58.29739584703681 +2024-08-12 21:00:00,3,28.856512419930677,19.31674767020429,57.370137819647965 +2024-08-12 22:00:00,3,26.517680784806394,20.62866031513462,55.52900142520867 +2024-08-12 23:00:00,3,30.782475566897546,22.967966331149377,52.294656709458685 +2024-08-13 00:00:00,3,32.28720436946012,23.740946480777854,53.058203372892706 +2024-08-13 01:00:00,3,35.57957550912236,30.01117760887621,48.89955474305539 +2024-08-13 02:00:00,3,32.21832489269023,22.56995022701798,52.481698677301154 +2024-08-13 03:00:00,3,45.7491224989282,24.25977583319421,64.50808102148135 +2024-08-13 04:00:00,3,19.20203896397323,22.81369529275109,70.97754912313872 +2024-08-13 05:00:00,3,30.739999432659047,20.52544707229623,77.05648385224607 +2024-08-13 06:00:00,3,23.358082005609564,24.247218024906843,67.69089549832894 +2024-08-13 07:00:00,3,19.7846276712832,21.844843664447616,55.420711653708516 +2024-08-13 08:00:00,3,15.893487747035293,20.336164248404287,52.74500300138631 +2024-08-13 09:00:00,3,27.920868586579832,25.65465765640352,45.883041655532864 +2024-08-13 10:00:00,3,43.75572499213979,28.768807440774847,33.111661413970836 +2024-08-13 11:00:00,3,35.38011738996277,22.48558552812022,59.153273658147505 +2024-08-13 12:00:00,3,34.44583813838842,31.767713101142533,49.916590284482496 +2024-08-13 13:00:00,3,9.78941235737089,26.27758896053112,49.13576117795679 +2024-08-13 14:00:00,3,34.02523820244425,22.32734510508694,63.0809723747065 +2024-08-13 15:00:00,3,29.079366586347163,19.351390818193128,61.87389836927348 +2024-08-13 16:00:00,3,46.06494737465047,23.747063467479755,56.78961908664122 +2024-08-13 17:00:00,3,27.334609223917937,23.31106735821305,63.597204690257385 +2024-08-13 18:00:00,3,26.898317048183028,25.227970436871804,47.64826914939672 +2024-08-13 19:00:00,3,11.737920852425871,24.360911289992913,48.779183519426105 +2024-08-13 20:00:00,3,31.183767082168455,22.952199656751546,68.15904782943647 +2024-08-13 21:00:00,3,33.8915519390026,22.20128632283626,63.228727112159284 +2024-08-13 22:00:00,3,23.331058586543104,16.65612697153717,62.16469199226291 +2024-08-13 23:00:00,3,16.42746664033673,22.209562810993496,54.773891930817726 +2024-08-14 00:00:00,3,32.29888295471161,28.721908936341652,50.96655435022949 +2024-08-14 01:00:00,3,32.02500297736733,28.448962031396203,57.97538796990757 +2024-08-14 02:00:00,3,45.34690329785186,26.96401317160473,62.65871575005301 +2024-08-14 03:00:00,3,47.89950737950882,25.065635677967265,68.11752124218997 +2024-08-14 04:00:00,3,12.417556079970964,19.28751083768025,45.363526679954624 +2024-08-14 05:00:00,3,27.44372391321636,25.722751642521814,50.7389156995356 +2024-08-14 06:00:00,3,39.253585096962794,27.483871895184823,42.09349163269652 +2024-08-14 07:00:00,3,20.75608834347748,20.854454506152972,50.73463122007301 +2024-08-14 08:00:00,3,12.851333911034137,23.506026662246146,37.15933792401659 +2024-08-14 09:00:00,3,14.329403446454906,35.30675326898603,48.15821530819745 +2024-08-14 10:00:00,3,25.30594027145111,24.735028627347564,69.92385735725779 +2024-08-14 11:00:00,3,30.6182223293266,24.669619634788685,50.95197996554235 +2024-08-14 12:00:00,3,20.3541030384175,17.885498182695656,60.228868173431586 +2024-08-14 13:00:00,3,19.691357991396714,25.280586315902763,48.602053786442966 +2024-08-14 14:00:00,3,34.30521265307366,26.74988701469151,52.09103676387434 +2024-08-14 15:00:00,3,27.884840921955803,24.722796209235543,58.816840545226334 +2024-08-14 16:00:00,3,18.002024125496717,22.169969592886815,60.2371232698463 +2024-08-14 17:00:00,3,20.144607620935204,27.031998258387848,50.93705737849906 +2024-08-14 18:00:00,3,32.24825987172814,26.79140213499507,48.46150045230691 +2024-08-14 19:00:00,3,25.984446373910483,21.266217863140263,67.97635770592369 +2024-08-14 20:00:00,3,17.897893938768668,28.991936848107258,65.88471393321859 +2024-08-14 21:00:00,3,24.207837351030662,20.884997348593924,51.13898622681425 +2024-08-14 22:00:00,3,25.213004687104913,21.812983681578412,51.86438209836376 +2024-08-14 23:00:00,3,29.426076721710082,25.632552035997964,47.06297950095738 +2024-08-15 00:00:00,3,32.091502997052565,25.15899748580788,67.80335859243637 +2024-08-15 01:00:00,3,26.928395713996835,22.22580728536512,74.1381630065841 +2024-08-15 02:00:00,3,33.00205791273704,25.444264451829483,68.52188805116748 +2024-08-15 03:00:00,3,40.29628198425186,25.92054461224515,57.20945450141853 +2024-08-15 04:00:00,3,19.65479505383344,24.382516477145447,56.525767424151645 +2024-08-15 05:00:00,3,24.566490043550523,27.16401162804901,48.99221929207313 +2024-08-15 06:00:00,3,34.74349143498289,29.294980288112512,56.9558026593 +2024-08-15 07:00:00,3,16.28152707275992,28.746878981292884,62.104449547667684 +2024-08-15 08:00:00,3,29.803052222573605,24.58940923829319,50.14387269953935 +2024-08-15 09:00:00,3,19.0345212019189,29.501289628799693,57.35268321085707 +2024-08-15 10:00:00,3,28.153013020956532,26.159724123127017,57.58677496420198 +2024-08-15 11:00:00,3,34.285412859120854,25.287816674895705,50.13172926408061 +2024-08-15 12:00:00,3,14.617191660072253,21.82168941802842,44.387557541885236 +2024-08-15 13:00:00,3,24.771696491269786,25.319266805669827,62.780865583190874 +2024-08-15 14:00:00,3,26.22114326864681,26.559651139360323,61.38247675299664 +2024-08-15 15:00:00,3,28.056815233048567,23.041308628141078,50.990706472033075 +2024-08-15 16:00:00,3,21.678210617864018,26.511614509874235,63.618817732546354 +2024-08-15 17:00:00,3,44.48854978929924,22.17911341437086,51.3789181670115 +2024-08-15 18:00:00,3,23.45170553765814,24.326319311701155,67.74596583817983 +2024-08-15 19:00:00,3,21.85146932637462,24.424887350986225,57.547750860994164 +2024-08-15 20:00:00,3,31.706686267932113,21.500519227956527,63.07655689414663 +2024-08-15 21:00:00,3,41.79278783670904,22.636696304208016,57.08671315052914 +2024-08-15 22:00:00,3,20.090293211145916,21.42737132373311,44.02774022193793 +2024-08-15 23:00:00,3,20.974607105367646,27.432574053788336,57.196177897077575 +2024-08-16 00:00:00,3,33.38620534718547,26.118587585687422,61.488683844042946 +2024-08-16 01:00:00,3,26.91340840861362,30.8038319190078,68.42492165211539 +2024-08-16 02:00:00,3,22.4508665077426,29.837719009064347,49.80543889032672 +2024-08-16 03:00:00,3,43.08568244379168,21.916919916932475,47.284178191236755 +2024-08-16 04:00:00,3,21.599947429838203,22.81641177284869,55.03136240273419 +2024-08-16 05:00:00,3,23.856082394981122,23.117290525720676,54.6921786773108 +2024-08-16 06:00:00,3,33.509689758890325,21.493107349409296,52.55178086962535 +2024-08-16 07:00:00,3,42.246499807771926,22.50898521066818,64.48706222677197 +2024-08-16 08:00:00,3,23.316609984961193,20.724692222472754,56.286948729187216 +2024-08-16 09:00:00,3,31.69109519399268,25.411180994457442,46.888586009592444 +2024-08-16 10:00:00,3,36.38734436830768,24.74717397240705,45.90385787087615 +2024-08-16 11:00:00,3,31.84223046809695,23.546077832261002,37.085066166121116 +2024-08-16 12:00:00,3,14.023377080369158,23.090028714005285,54.61352994750159 +2024-08-16 13:00:00,3,7.51485665093357,24.83503674161494,50.36060771560223 +2024-08-16 14:00:00,3,20.691707370791924,22.632203019519125,60.284466125611374 +2024-08-16 15:00:00,3,30.44108232897952,24.558317918160142,46.677001825602204 +2024-08-16 16:00:00,3,30.89027853140511,20.35570451621413,54.39913263454093 +2024-08-16 17:00:00,3,27.829894947543483,25.653031988209786,39.461895806847465 +2024-08-16 18:00:00,3,23.370716227831043,21.51037103541323,60.35327336246105 +2024-08-16 19:00:00,3,26.35740917953814,20.565167999546794,43.8203643279226 +2024-08-16 20:00:00,3,32.260298550123515,25.3277172748146,61.877747955006306 +2024-08-16 21:00:00,3,5.150235472199384,25.87027087342392,66.55346285262144 +2024-08-16 22:00:00,3,10.561633357915637,20.535682442842166,55.91858215425879 +2024-08-16 23:00:00,3,24.667152831664147,27.58853857082102,62.736781661194726 +2024-08-17 00:00:00,3,25.421507282181256,27.456553699563983,74.54102570600703 +2024-08-17 01:00:00,3,32.04589250205695,25.956509421469214,54.98694998009597 +2024-08-17 02:00:00,3,29.830554841204474,23.479498460055932,68.47416875176997 +2024-08-17 03:00:00,3,32.64592605664444,28.43207053196898,57.542134461281364 +2024-08-17 04:00:00,3,32.14625900697911,25.650592376034385,63.69730211382591 +2024-08-17 05:00:00,3,28.697020291961397,20.3778614483585,64.28771188030399 +2024-08-17 06:00:00,3,10.440018302148136,25.432874836550532,59.68309400090242 +2024-08-17 07:00:00,3,35.38664192846426,28.731400640963642,57.46411327292902 +2024-08-17 08:00:00,3,13.165002184805786,24.280156661325428,49.515066336108404 +2024-08-17 09:00:00,3,36.04044453701502,27.582208730346483,54.76541346238993 +2024-08-17 10:00:00,3,9.699120622594336,25.90266139464903,35.64462785879171 +2024-08-17 11:00:00,3,29.64273016500858,28.19479869301027,33.537143139187364 +2024-08-17 12:00:00,3,0.0,26.029582450516756,55.065858002373886 +2024-08-17 13:00:00,3,29.452386678516472,20.70358075569556,48.27236493208134 +2024-08-17 14:00:00,3,26.049085180428104,24.427339725493866,51.07708282054552 +2024-08-17 15:00:00,3,20.351389297606218,25.452960357050365,40.941795282748956 +2024-08-17 16:00:00,3,35.415783064584865,30.80353898593326,56.532963932548 +2024-08-17 17:00:00,3,32.936489850982696,22.353460789587213,53.43978433649869 +2024-08-17 18:00:00,3,36.17906006134042,23.84897847345452,63.0925238504874 +2024-08-17 19:00:00,3,17.820172605276206,18.29008609305934,54.41977316358211 +2024-08-17 20:00:00,3,32.27317420856025,20.01526466971142,42.384782331690175 +2024-08-17 21:00:00,3,29.53502565237654,26.573081587829684,49.53131409337854 +2024-08-17 22:00:00,3,25.898173721094288,24.72417338377883,54.24680925868625 +2024-08-17 23:00:00,3,24.470638641210112,23.399339245288893,50.00050734539798 +2024-08-18 00:00:00,3,29.167201158409007,30.966319193955954,48.94033038652103 +2024-08-18 01:00:00,3,21.515565547348864,20.523986358233934,46.74857905558392 +2024-08-18 02:00:00,3,30.003825091765066,24.05503735902467,58.614003875633365 +2024-08-18 03:00:00,3,33.68828270766899,21.987522320751463,50.320450755196376 +2024-08-18 04:00:00,3,35.00705964676229,27.06137452479198,53.53874701134487 +2024-08-18 05:00:00,3,22.927595226374365,27.725452706090387,68.75707455540498 +2024-08-18 06:00:00,3,17.655077745381654,22.950071275938722,58.38179296859846 +2024-08-18 07:00:00,3,27.138130918220188,21.87077724898098,62.973105473694076 +2024-08-18 08:00:00,3,27.374230644319404,28.310488832379118,70.80857134549692 +2024-08-18 09:00:00,3,19.22828907712914,24.87115743322293,64.15247880061361 +2024-08-18 10:00:00,3,36.292257328807246,22.606088126730775,58.153349195941814 +2024-08-18 11:00:00,3,30.575191246072066,27.12626260793135,31.18667441565593 +2024-08-18 12:00:00,3,29.21149338218032,23.935153331470342,62.49978936616931 +2024-08-18 13:00:00,3,37.540809092804444,25.18025012965171,60.77917454724113 +2024-08-18 14:00:00,3,13.113916971118316,26.9255731606919,57.70520826482023 +2024-08-18 15:00:00,3,24.196835382100787,26.029043541412765,54.85980145462317 +2024-08-18 16:00:00,3,22.734318580566118,26.28505502554256,63.79073489694531 +2024-08-18 17:00:00,3,32.39458289234752,24.380736345780768,57.80848420764186 +2024-08-18 18:00:00,3,33.35626174495319,22.575701808137985,54.289693979003744 +2024-08-18 19:00:00,3,33.90092413669195,25.05098529401521,54.95627849888801 +2024-08-18 20:00:00,3,21.468753423487374,27.528297790985167,37.35559310727081 +2024-08-18 21:00:00,3,23.935631689000644,23.495761511138944,38.752860829713995 +2024-08-18 22:00:00,3,27.386213608738437,23.95722962212738,62.876418372599474 +2024-08-18 23:00:00,3,28.939902678872645,21.145867630794918,66.57382791940545 +2024-08-19 00:00:00,3,32.06806600899087,24.567413241415984,55.324550956435665 +2024-08-19 01:00:00,3,15.26150425888314,29.41862231705536,55.02404112525712 +2024-08-19 02:00:00,3,37.02771602949913,26.025465053729587,66.58753284719656 +2024-08-19 03:00:00,3,28.933671929841907,22.450040526211314,55.125619198093055 +2024-08-19 04:00:00,3,35.005463774440855,22.48390297003889,51.67716248796655 +2024-08-19 05:00:00,3,9.37270696987526,26.801122044553438,49.21108651689277 +2024-08-19 06:00:00,3,20.00295278852626,23.65181916527975,46.450886938892424 +2024-08-19 07:00:00,3,19.006504021098067,21.863773633401053,45.27328780313027 +2024-08-19 08:00:00,3,25.416247010859585,21.877172864955515,49.68115294715611 +2024-08-19 09:00:00,3,20.95853523540365,24.43055631190244,46.344525926491286 +2024-08-19 10:00:00,3,25.650398080016398,25.58938269365479,55.672753993454 +2024-08-19 11:00:00,3,38.43087082208777,30.689684150958435,59.58217448550455 +2024-08-19 12:00:00,3,39.803311166556675,31.041235431085465,54.1721423632507 +2024-08-19 13:00:00,3,32.33714369267067,31.482692011674807,51.93740064386977 +2024-08-19 14:00:00,3,19.553021883984343,25.750874404881603,46.394638347546085 +2024-08-19 15:00:00,3,28.468269219651493,29.175338370259976,66.1731531750765 +2024-08-19 16:00:00,3,28.69951674845445,31.88072952447041,53.170923086949166 +2024-08-19 17:00:00,3,16.00933050647294,29.840196559042614,49.73332521874733 +2024-08-19 18:00:00,3,24.029219734040282,26.1307955756611,53.85193905428508 +2024-08-19 19:00:00,3,33.57381500639081,30.645514467562727,66.46270995040103 +2024-08-19 20:00:00,3,29.255788169291826,22.601771160686642,35.66843236415886 +2024-08-19 21:00:00,3,33.31064755546531,24.322852383712117,54.896710867909505 +2024-08-19 22:00:00,3,30.312029022579196,21.119377567243358,58.46796767739805 +2024-08-19 23:00:00,3,28.1828888018692,22.441500457936478,58.858059745992634 +2024-08-20 00:00:00,3,16.14841074493036,17.866627694573666,39.47853601630072 +2024-08-20 01:00:00,3,23.147433323104543,29.029719608022596,55.104294312709236 +2024-08-20 02:00:00,3,16.128010142554714,18.125602506896143,73.54275246236374 +2024-08-20 03:00:00,3,45.15819982139705,25.347851776230367,56.10727938118566 +2024-08-20 04:00:00,3,21.819831483864665,23.94264674432568,47.08039354213887 +2024-08-20 05:00:00,3,26.736898389284214,24.60676856465422,52.21188372721778 +2024-08-20 06:00:00,3,28.79572716732161,27.98619580330574,54.30076713874005 +2024-08-20 07:00:00,3,28.66342800682138,26.306042635789666,41.66579123701262 +2024-08-20 08:00:00,3,21.294773075202926,24.679909381653943,54.13667186358958 +2024-08-20 09:00:00,3,18.312990379340455,28.020569701226265,53.8345389237615 +2024-08-20 10:00:00,3,17.636046995077827,25.071297770347925,45.62527583345002 +2024-08-20 11:00:00,3,30.06992785261633,25.700116339675105,45.55591329398898 +2024-08-20 12:00:00,3,14.095616479918277,21.2105315226518,55.48507564492699 +2024-08-20 13:00:00,3,23.83708328737486,21.709144421817754,49.58341815640584 +2024-08-20 14:00:00,3,24.887433684204925,21.849320195421463,48.88212516593727 +2024-08-20 15:00:00,3,25.58963982309833,25.433153451856686,51.01101791783564 +2024-08-20 16:00:00,3,15.95985892388817,28.228628704930934,51.45071559083311 +2024-08-20 17:00:00,3,11.6998089313411,22.785282110868394,64.65946465711332 +2024-08-20 18:00:00,3,26.86692204142529,30.46577110328574,50.39142813100629 +2024-08-20 19:00:00,3,23.547465417027283,14.306890388953324,48.959518691762625 +2024-08-20 20:00:00,3,20.145220597888958,24.943087701815898,45.24338555409115 +2024-08-20 21:00:00,3,20.541368791583615,23.449984936988315,73.90285498763028 +2024-08-20 22:00:00,3,29.674627911902864,18.43484973857431,52.130328465814195 +2024-08-20 23:00:00,3,21.4603897623963,22.194655192988144,67.65607258218952 +2024-08-21 00:00:00,3,27.70583173203495,23.36448112189584,57.50627664803935 +2024-08-21 01:00:00,3,16.66354506956008,22.88155747217288,70.16698478562509 +2024-08-21 02:00:00,3,42.96442933095378,28.002668414967445,72.86132065519783 +2024-08-21 03:00:00,3,17.721920150972423,26.312819043465467,56.41316181630273 +2024-08-21 04:00:00,3,30.223537058154236,25.75021578121932,49.694193921279116 +2024-08-21 05:00:00,3,21.95579383428779,24.626343665889276,61.968124788994814 +2024-08-21 06:00:00,3,21.04768958066432,21.876550035619744,54.19033656300251 +2024-08-21 07:00:00,3,26.53371677173012,30.172701188521753,42.45068961266983 +2024-08-21 08:00:00,3,19.072458596458624,21.43305809241704,38.66835261570209 +2024-08-21 09:00:00,3,31.426811123556618,26.442338573263157,57.956276712187325 +2024-08-21 10:00:00,3,33.17604323405073,29.328287949807873,52.568497298875286 +2024-08-21 11:00:00,3,20.39781111367958,27.179457156630885,39.744433195354944 +2024-08-21 12:00:00,3,4.3465993027057195,26.907628080712826,43.38933713531465 +2024-08-21 13:00:00,3,16.027766875465375,33.14192322610097,58.21407345796973 +2024-08-21 14:00:00,3,17.185065480650543,21.464244027972075,49.41631993167292 +2024-08-21 15:00:00,3,33.21114690735831,32.27852738658572,63.07781918824128 +2024-08-21 16:00:00,3,21.1099124844725,32.343741266338185,67.39287172119184 +2024-08-21 17:00:00,3,19.96772771567346,29.726404616645503,62.31493695126409 +2024-08-21 18:00:00,3,16.102339692943353,34.68361893078776,63.09255913177732 +2024-08-21 19:00:00,3,20.75529503407531,28.175751196746567,62.65748657145135 +2024-08-21 20:00:00,3,29.556687698405447,18.924053305079646,50.83196821187075 +2024-08-21 21:00:00,3,29.14870720909973,19.233454422908306,63.399487357384366 +2024-08-21 22:00:00,3,27.266092267861332,23.384928175705248,63.60391231647331 +2024-08-21 23:00:00,3,25.408039712454443,28.11678400329207,60.219805209405536 +2024-08-22 00:00:00,3,52.341973768802525,31.542836594234885,43.25551804544929 +2024-08-22 01:00:00,3,11.478444913629037,25.834681383805172,46.12175311034265 +2024-08-22 02:00:00,3,16.650631024266993,25.984933975535995,62.69822762314469 +2024-08-22 03:00:00,3,17.240283988994037,29.348596579607403,66.67376932777161 +2024-08-22 04:00:00,3,32.98638007449329,25.17230203935693,47.661676225243966 +2024-08-22 05:00:00,3,21.604263931727015,27.584409697302565,57.41696979229082 +2024-08-22 06:00:00,3,19.457284996804596,21.493133350045994,47.41032511706095 +2024-08-22 07:00:00,3,15.610109804901265,18.406434000640743,56.64299537700349 +2024-08-22 08:00:00,3,21.884724052432578,24.026122398661375,58.970637913229716 +2024-08-22 09:00:00,3,29.394616818776225,24.578851618149972,61.37111507511697 +2024-08-22 10:00:00,3,18.388807094354277,23.986146101636997,68.22644052728643 +2024-08-22 11:00:00,3,16.467470168761817,27.05532025059228,50.68153444667205 +2024-08-22 12:00:00,3,27.618722584124736,21.196590023833693,63.25460407597278 +2024-08-22 13:00:00,3,21.48169000965414,20.19603244716084,43.31090870304359 +2024-08-22 14:00:00,3,26.400179028289728,19.78515891380765,45.12459164451966 +2024-08-22 15:00:00,3,6.4538446269699286,23.851090188742806,55.868894583062485 +2024-08-22 16:00:00,3,31.216750183309735,26.880161888809678,57.33647553009105 +2024-08-22 17:00:00,3,14.283862768712876,29.009432686041343,75.51047512763114 +2024-08-22 18:00:00,3,17.589577341929036,25.124823909079172,67.30045900935154 +2024-08-22 19:00:00,3,13.664081642154414,27.167918239988914,50.54843718392223 +2024-08-22 20:00:00,3,18.183287300405443,20.91048848013795,67.68031809097874 +2024-08-22 21:00:00,3,14.016481414604913,21.65076138725591,59.49120131360775 +2024-08-22 22:00:00,3,28.506393898514666,28.957359709975382,57.917555989891476 +2024-08-22 23:00:00,3,21.5254995576188,26.044011138966162,38.627038002309746 +2024-08-23 00:00:00,3,5.166495980833918,27.224701660337168,57.017064366430446 +2024-08-23 01:00:00,3,33.6626087871759,26.194826104561113,59.57172625058606 +2024-08-23 02:00:00,3,25.67273118929404,19.361862864760553,68.51981672946307 +2024-08-23 03:00:00,3,19.392102671385068,22.224552743889646,60.42403822799958 +2024-08-23 04:00:00,3,25.685971868940367,17.371845298348184,67.58948540950497 +2024-08-23 05:00:00,3,25.87645939501058,24.87509062050477,67.39687718890981 +2024-08-23 06:00:00,3,34.477721942545216,26.838433568191142,66.31178159677836 +2024-08-23 07:00:00,3,27.82496521835606,28.47504533997266,46.077484293558896 +2024-08-23 08:00:00,3,23.93752812235163,28.09444772948032,45.260845770413454 +2024-08-23 09:00:00,3,31.817241132747455,25.29148310314245,44.28203807065535 +2024-08-23 10:00:00,3,32.86347590501109,25.26063775111516,42.27107468489176 +2024-08-23 11:00:00,3,23.182532864409186,25.774397662835522,40.775258870985255 +2024-08-23 12:00:00,3,15.271302818833805,28.434624877211974,45.67913233418473 +2024-08-23 13:00:00,3,29.259552975100636,28.864151212650462,61.7659633748018 +2024-08-23 14:00:00,3,31.1017562225248,29.31429775839999,31.562515130557806 +2024-08-23 15:00:00,3,28.96775035869002,31.47784798225677,61.70901738916594 +2024-08-23 16:00:00,3,29.940610341944037,29.71913108483559,58.0126702060927 +2024-08-23 17:00:00,3,24.112496245050554,26.505965997675073,37.47377200630068 +2024-08-23 18:00:00,3,24.005080443391762,22.617343528421095,66.94208294909507 +2024-08-23 19:00:00,3,32.89211664357906,25.0458467461655,61.88492478677563 +2024-08-23 20:00:00,3,12.475153661650646,19.284203713092193,58.13214872628988 +2024-08-23 21:00:00,3,21.220043374729958,15.72834895586714,54.92958276070075 +2024-08-23 22:00:00,3,21.90404075267167,20.914282734670355,42.892211586840816 +2024-08-23 23:00:00,3,16.52558901238076,18.14721107866417,52.182828510075154 +2024-08-24 00:00:00,3,23.157541194953478,26.443464667476444,64.7467027797603 +2024-08-24 01:00:00,3,17.268423245344394,19.346497908676238,57.574896678992594 +2024-08-24 02:00:00,3,32.94323312489201,18.81747225639866,58.072937299904524 +2024-08-24 03:00:00,3,30.741705334061656,24.018405883536786,46.54636908067078 +2024-08-24 04:00:00,3,29.813636047535955,28.87793999934572,55.787246231078555 +2024-08-24 05:00:00,3,30.816925893876697,24.147801416367226,60.81103723446703 +2024-08-24 06:00:00,3,42.24700706370436,23.379115102337586,59.27917511988411 +2024-08-24 07:00:00,3,34.30481830465944,25.718480258167464,64.77914058953424 +2024-08-24 08:00:00,3,32.86963505380878,23.991488453864488,47.4721544148347 +2024-08-24 09:00:00,3,34.33098967109997,22.503035050755376,54.68090147941689 +2024-08-24 10:00:00,3,21.24883889170254,22.704471003477103,45.1336202201055 +2024-08-24 11:00:00,3,45.585129486947324,29.98416298385348,36.109661005177415 +2024-08-24 12:00:00,3,27.91733635697014,24.29843675418205,51.61323800213621 +2024-08-24 13:00:00,3,6.136181915842776,28.510322326313286,49.3863564727122 +2024-08-24 14:00:00,3,9.928259827704856,28.525469067397644,50.99380000081138 +2024-08-24 15:00:00,3,32.56328465150982,22.803970515059568,58.49824600456301 +2024-08-24 16:00:00,3,20.8543192678302,23.108476475838952,63.95953340163611 +2024-08-24 17:00:00,3,24.10271004525649,31.06087342000747,54.32672425182545 +2024-08-24 18:00:00,3,27.37125936651584,29.851047797211525,30.434943416557896 +2024-08-24 19:00:00,3,16.89104072687244,22.770716913382287,68.54174465850969 +2024-08-24 20:00:00,3,33.979475022949714,27.048861922836597,41.615974610607225 +2024-08-24 21:00:00,3,21.08861762847303,17.32555445057369,57.01976382020134 +2024-08-24 22:00:00,3,29.261084749706868,21.623319607836464,54.9335315285758 +2024-08-24 23:00:00,3,24.40068805001022,18.409389737369416,50.61091194495549 +2024-08-25 00:00:00,3,40.23231592285809,24.306377426756868,56.647521485139634 +2024-08-25 01:00:00,3,23.936896953895037,22.758893959261684,79.7429259477129 +2024-08-25 02:00:00,3,21.23597463257044,26.13962166733459,53.01383146183252 +2024-08-25 03:00:00,3,33.318293037045876,25.959032488778494,56.26765785499631 +2024-08-25 04:00:00,3,7.455921906248008,23.430342585132475,51.49752614575363 +2024-08-25 05:00:00,3,35.460387410345035,23.48101540241622,61.595817572952576 +2024-08-25 06:00:00,3,19.13904039215061,28.743884358184985,53.87054416696643 +2024-08-25 07:00:00,3,34.37892355452927,26.46260116493057,54.10265798601236 +2024-08-25 08:00:00,3,31.668949211730897,33.825606659523416,48.328025301022365 +2024-08-25 09:00:00,3,15.275218987303038,22.658022688289286,48.16620667122216 +2024-08-25 10:00:00,3,25.98499533559058,26.84727221845135,40.10090880208885 +2024-08-25 11:00:00,3,34.19246281989615,22.713250964192035,47.91119700579753 +2024-08-25 12:00:00,3,22.547617190605138,21.961622569066108,48.89863296683257 +2024-08-25 13:00:00,3,35.811874410685455,26.68916719539441,55.51169115335443 +2024-08-25 14:00:00,3,23.815460843744294,29.833076281261583,47.473396857914906 +2024-08-25 15:00:00,3,12.322857629573793,30.35375132519509,61.72213418261931 +2024-08-25 16:00:00,3,21.969394312083637,23.08814515800783,57.6677971415658 +2024-08-25 17:00:00,3,23.133478483981726,21.299963239772463,53.00209375140946 +2024-08-25 18:00:00,3,31.419228847458506,25.154382891807423,57.835935888891576 +2024-08-25 19:00:00,3,27.71777646485394,20.265036447129294,57.454770258404224 +2024-08-25 20:00:00,3,28.02463917925447,29.341101165420717,61.49450426006853 +2024-08-25 21:00:00,3,24.989500127461994,22.97965247323928,59.18168727568955 +2024-08-25 22:00:00,3,36.77427710867524,24.538146858238463,72.07872545476268 +2024-08-25 23:00:00,3,28.400140289009528,26.565023189985716,54.94625008757944 +2024-08-26 00:00:00,3,31.487521720717556,22.71520714036211,53.293759430444695 +2024-08-26 01:00:00,3,25.829000486973904,25.594602513929708,56.11757973765876 +2024-08-26 02:00:00,3,27.981283852894457,24.555643116017094,50.54575977106389 +2024-08-26 03:00:00,3,35.26994108296757,19.093599167744472,67.11590737197025 +2024-08-26 04:00:00,3,38.192110113468054,22.869799190538934,49.5573874398294 +2024-08-26 05:00:00,3,38.83628997171624,27.663088285945683,59.394103476311386 +2024-08-26 06:00:00,3,14.88336996918048,24.847144668479483,56.261040451557136 +2024-08-26 07:00:00,3,26.017246627820985,21.92218732099751,35.79673808542343 +2024-08-26 08:00:00,3,22.095925239605805,26.686622488100433,50.97554923667782 +2024-08-26 09:00:00,3,17.056580036052438,22.335431237954165,62.92270862816706 +2024-08-26 10:00:00,3,32.182097721333335,30.829976692608682,56.247044651520284 +2024-08-26 11:00:00,3,35.95679270795831,21.985262087477494,61.16483225570765 +2024-08-26 12:00:00,3,16.798185062264963,25.469439401968188,58.23631749184441 +2024-08-26 13:00:00,3,24.96502353688778,24.40249601888688,45.642404458338255 +2024-08-26 14:00:00,3,26.13863519504107,26.836931178557343,52.01279707608328 +2024-08-26 15:00:00,3,5.052035372785014,27.596115750566245,56.88548743655262 +2024-08-26 16:00:00,3,27.131697436636188,30.948060235586627,61.162208486488765 +2024-08-26 17:00:00,3,16.535395340890062,23.750788092389982,60.83954904621491 +2024-08-26 18:00:00,3,27.443669748042815,21.14415371375064,67.87887008735461 +2024-08-26 19:00:00,3,13.61295383419959,26.50995714480102,58.369883947983006 +2024-08-26 20:00:00,3,26.104705402028547,22.50021070229416,55.52467292789414 +2024-08-26 21:00:00,3,10.867884224273721,25.34659069266115,62.515633219787134 +2024-08-26 22:00:00,3,24.919924444332022,19.137778969745753,68.52291109976801 +2024-08-26 23:00:00,3,19.590972494559473,23.198235067917192,52.38019627287527 +2024-08-27 00:00:00,3,20.753710841783043,23.419403311310553,63.12991926441063 +2024-08-27 01:00:00,3,27.98316208359885,20.060501647100843,77.97587172303895 +2024-08-27 02:00:00,3,28.142402519880953,21.37277323215644,47.927358351561615 +2024-08-27 03:00:00,3,21.691598793160644,26.1108526315949,67.04185369283543 +2024-08-27 04:00:00,3,31.560481326310114,23.298162695431284,44.087289719796594 +2024-08-27 05:00:00,3,18.62329973613417,26.73580627381403,64.72560168345734 +2024-08-27 06:00:00,3,15.388669377250238,23.201093862747292,75.81444574614645 +2024-08-27 07:00:00,3,22.838337313526978,25.872260926853606,34.36260835588564 +2024-08-27 08:00:00,3,16.705945730812356,23.857008212546752,50.05931220121241 +2024-08-27 09:00:00,3,19.89438518095036,24.530207688020344,46.90340559009544 +2024-08-27 10:00:00,3,33.94840063778925,30.362360852709536,48.303066082510696 +2024-08-27 11:00:00,3,37.278287839466564,21.395415555642316,63.664068777760974 +2024-08-27 12:00:00,3,18.85449674904607,27.354919355148525,60.20007238901175 +2024-08-27 13:00:00,3,27.25400281871277,20.191778990250732,63.76309297793677 +2024-08-27 14:00:00,3,29.34721331499435,22.173331407041232,61.29293909925727 +2024-08-27 15:00:00,3,22.43042564558138,24.89504709126221,56.608134070336334 +2024-08-27 16:00:00,3,18.304856930549636,31.221437197441187,55.40193216652357 +2024-08-27 17:00:00,3,28.465884189121965,32.070718227912145,61.34476549779538 +2024-08-27 18:00:00,3,23.937023414432907,22.48601411672853,43.681137792173324 +2024-08-27 19:00:00,3,8.088605799013731,24.800945881067914,46.79886414054014 +2024-08-27 20:00:00,3,19.678177817786704,22.562188574198764,56.8751639888149 +2024-08-27 21:00:00,3,23.09036757355353,17.87266237146399,48.725281187819405 +2024-08-27 22:00:00,3,4.330753994112076,29.40786581175839,47.22085083568819 +2024-08-27 23:00:00,3,21.743901137231127,19.719942901750606,53.815212503763206 +2024-08-28 00:00:00,3,25.189691540867372,27.762720148022986,45.66647535737347 +2024-08-28 01:00:00,3,37.078595380549494,18.171141427113987,64.54948446044139 +2024-08-28 02:00:00,3,23.123735163291148,26.935564553569865,56.46056834989656 +2024-08-28 03:00:00,3,22.00757377159766,31.26990233175154,60.33779320405293 +2024-08-28 04:00:00,3,10.845590652656156,25.29540946194186,52.53854440456685 +2024-08-28 05:00:00,3,36.11468983105974,23.746807540367385,57.76896347151248 +2024-08-28 06:00:00,3,31.586198421783646,30.57320795899714,62.09019962438215 +2024-08-28 07:00:00,3,37.938041512035696,25.10103653527642,52.581482620630766 +2024-08-28 08:00:00,3,32.48297152384923,17.743912218086095,61.14240047783671 +2024-08-28 09:00:00,3,30.69100062518136,22.88022766410501,47.129493780207454 +2024-08-28 10:00:00,3,27.12455019457324,23.919421410130767,45.06226392102928 +2024-08-28 11:00:00,3,46.850130804241246,21.14462773335622,37.720043104611165 +2024-08-28 12:00:00,3,12.180987963833324,19.40185779137464,50.990517419850725 +2024-08-28 13:00:00,3,12.276508039083083,26.087062920208485,65.73614269455591 +2024-08-28 14:00:00,3,22.951643057418508,33.629439675631744,38.04230669077271 +2024-08-28 15:00:00,3,33.03130424296214,23.4631504209978,64.24785260261784 +2024-08-28 16:00:00,3,16.30526451878773,27.969473612020565,50.78531484737953 +2024-08-28 17:00:00,3,32.25460049218446,25.004039048634073,69.99485665105371 +2024-08-28 18:00:00,3,22.95936783295949,22.961882819517722,63.59407339787059 +2024-08-28 19:00:00,3,30.784230575677284,25.92892873366614,54.85037779132594 +2024-08-28 20:00:00,3,30.761885048607166,22.14008096981557,55.79073524363065 +2024-08-28 21:00:00,3,15.594300966358176,22.78671276319181,52.91496236271147 +2024-08-28 22:00:00,3,23.81078414269348,22.38059197069213,52.422070801952394 +2024-08-28 23:00:00,3,19.422221341176673,18.61761990266812,44.81550997197186 +2024-08-29 00:00:00,3,21.894871868566145,30.283822394539538,60.52920425482336 +2024-08-29 01:00:00,3,38.68776085764821,22.768199820172498,46.938969337143504 +2024-08-29 02:00:00,3,35.96317955052909,25.774649786499328,47.35227961099005 +2024-08-29 03:00:00,3,14.691096178956492,23.35783969374249,63.14909251367193 +2024-08-29 04:00:00,3,15.153189221246908,25.4693386766466,52.789097890681774 +2024-08-29 05:00:00,3,31.468565982804783,26.11009275148573,54.57458545083708 +2024-08-29 06:00:00,3,49.241755116944276,23.536946139532056,59.98527616829532 +2024-08-29 07:00:00,3,25.698816147403104,23.117843219508895,52.97141503482286 +2024-08-29 08:00:00,3,41.01445166552216,27.565853682825388,68.11348166501068 +2024-08-29 09:00:00,3,17.343966752985885,25.339140167420258,37.73009995395844 +2024-08-29 10:00:00,3,42.597478483362295,29.754471413111496,49.82997290180019 +2024-08-29 11:00:00,3,36.343124264774154,22.32789693626723,52.542769996147534 +2024-08-29 12:00:00,3,28.1379864736951,25.563361708799942,45.60484308986315 +2024-08-29 13:00:00,3,24.95978266658418,25.94911099443621,49.59546421768325 +2024-08-29 14:00:00,3,29.78706958992607,25.542080219057627,56.440734168856736 +2024-08-29 15:00:00,3,17.178145803157168,22.005111092124245,43.79644396424614 +2024-08-29 16:00:00,3,24.58567248539058,23.939205947462543,52.175983236920885 +2024-08-29 17:00:00,3,28.93900257819943,23.313217045380586,47.56918270922714 +2024-08-29 18:00:00,3,27.912150125258837,21.132239691823088,49.355455411675315 +2024-08-29 19:00:00,3,27.056991480011785,30.134870471775436,53.83898617361202 +2024-08-29 20:00:00,3,32.43298212534385,24.956936644552528,43.411482438808186 +2024-08-29 21:00:00,3,7.908825861904077,24.587799084806054,50.436578212567504 +2024-08-29 22:00:00,3,32.5547097224888,22.960968488895908,59.73621064030565 +2024-08-29 23:00:00,3,34.610017637159835,21.08012710221362,55.604177431210026 +2024-08-30 00:00:00,3,19.714767620202323,23.89347786827262,54.7769614370272 +2024-08-30 01:00:00,3,40.161114703982896,25.718489241796934,71.44354315963069 +2024-08-30 02:00:00,3,36.89429412921216,20.576578400018484,47.47684151762696 +2024-08-30 03:00:00,3,35.502412261230695,20.46806981403955,32.63381077677981 +2024-08-30 04:00:00,3,30.91177398723893,31.88531319261252,61.346564125638835 +2024-08-30 05:00:00,3,31.405410612415203,25.211664358053433,64.4027065239306 +2024-08-30 06:00:00,3,29.317739063408467,24.710540440311856,55.79895648034716 +2024-08-30 07:00:00,3,15.38103722907191,22.914311825347152,63.91674708734811 +2024-08-30 08:00:00,3,30.134291749968355,26.213835662330297,46.78740232208499 +2024-08-30 09:00:00,3,22.67421097865336,24.48068212324811,33.0363233865467 +2024-08-30 10:00:00,3,26.330530453309876,31.925652492738212,64.24912933305573 +2024-08-30 11:00:00,3,23.147449207561454,21.299189252732898,47.05373373883589 +2024-08-30 12:00:00,3,35.616494153346395,27.311176114427504,50.69320669750502 +2024-08-30 13:00:00,3,22.386521288130467,24.34370198145745,46.26962837082604 +2024-08-30 14:00:00,3,20.797083914886187,29.02432584783964,49.1181768728512 +2024-08-30 15:00:00,3,43.68829911075274,32.17453056892852,51.13620322801616 +2024-08-30 16:00:00,3,22.990042744688076,27.313813393822617,48.46695476492093 +2024-08-30 17:00:00,3,29.874701701207428,20.13167754562719,60.363409204707196 +2024-08-30 18:00:00,3,26.368122322636008,23.855933438208666,43.847718394767845 +2024-08-30 19:00:00,3,12.658783692736364,29.209842455992302,56.923390602363185 +2024-08-30 20:00:00,3,20.130513970438823,18.142770079750992,46.674033689956524 +2024-08-30 21:00:00,3,15.442288900467931,22.76942285961745,39.99202825226017 +2024-08-30 22:00:00,3,11.167688800363278,26.40019009976924,54.67838537626322 +2024-08-30 23:00:00,3,11.767819174366197,25.26255540727853,54.00145437925937 +2024-08-31 00:00:00,3,45.369021697224625,21.31964273895062,61.34200338272752 +2024-08-31 01:00:00,3,15.581257785231376,30.920327690656872,50.55975447348628 +2024-08-31 02:00:00,3,35.93283316901517,22.41224863382388,61.88050194185827 +2024-08-31 03:00:00,3,21.74614313383321,21.5178053118075,70.48017764698282 +2024-08-31 04:00:00,3,30.82857887611632,21.901241785519012,61.1426356013203 +2024-08-31 05:00:00,3,48.03966814407892,27.826415039606736,57.41626921782779 +2024-08-31 06:00:00,3,30.4474492379893,25.43430723234925,55.45995081423303 +2024-08-31 07:00:00,3,16.888716421282087,23.874875229001795,58.26691163434705 +2024-08-31 08:00:00,3,33.470762344628,24.25949530230416,45.70972878311453 +2024-08-31 09:00:00,3,49.36254197420649,19.90743874709173,48.452459869460334 +2024-08-31 10:00:00,3,24.13018551330557,26.026855703086774,47.66347289829402 +2024-08-31 11:00:00,3,22.74499752090662,28.82577152803453,46.769915896174375 +2024-08-31 12:00:00,3,28.07443171157723,29.348870049672716,49.85754926365865 +2024-08-31 13:00:00,3,19.960057543599966,16.371977973920167,53.12687293107678 +2024-08-31 14:00:00,3,31.229909506616572,26.538158457642897,49.54743319561986 +2024-08-31 15:00:00,3,40.38928585733213,26.068260293069656,65.6347411737197 +2024-08-31 16:00:00,3,23.134989845013337,24.253945165845657,68.00527817888046 +2024-08-31 17:00:00,3,24.5968985175026,23.84301758712328,54.6115823960354 +2024-08-31 18:00:00,3,28.097294408526412,25.09738723496012,63.321499645051716 +2024-08-31 19:00:00,3,31.87364723672807,25.075595037193143,51.18418698637425 +2024-08-31 20:00:00,3,28.696931911982706,20.773371727446857,58.90870789105991 +2024-08-31 21:00:00,3,13.905545533074186,25.899541935133374,64.31794763403052 +2024-08-31 22:00:00,3,30.913652348045293,29.919568123817772,71.52696085680589 +2024-08-31 23:00:00,3,23.115944362036643,23.11108704784654,52.82675019244673 +2024-09-01 00:00:00,3,24.05268759406582,25.30570568110703,55.941230767022994 +2024-09-01 01:00:00,3,41.72838072284354,23.43759512103762,66.34004692584823 +2024-09-01 02:00:00,3,31.26297900737607,18.934032713347566,55.731472903432135 +2024-09-01 03:00:00,3,36.644631090621644,23.183833953329078,53.559680251362636 +2024-09-01 04:00:00,3,21.453832365517613,19.83934212553075,63.257817956413426 +2024-09-01 05:00:00,3,14.027417083738746,26.307704196884274,47.05197864230564 +2024-09-01 06:00:00,3,41.88989805230139,27.47998160345186,64.29527073183606 +2024-09-01 07:00:00,3,30.350537838813036,18.0244985977484,61.199260167549774 +2024-09-01 08:00:00,3,28.218743507641967,24.419805628297603,46.070295255387244 +2024-09-01 09:00:00,3,14.389593798730534,24.259472988573318,62.74224135706586 +2024-09-01 10:00:00,3,40.451413633066316,18.443340878522893,47.2790158475375 +2024-09-01 11:00:00,3,37.203320265630545,18.93105417553187,54.24938060676175 +2024-09-01 12:00:00,3,19.56355681302304,26.652366907794317,69.27654279799711 +2024-09-01 13:00:00,3,37.802869932017515,26.60803654417806,52.76468327391031 +2024-09-01 14:00:00,3,29.93219056708436,27.37070807958894,47.032086271588575 +2024-09-01 15:00:00,3,21.069054691435653,24.956268695868207,67.13290483164056 +2024-09-01 16:00:00,3,28.49237906711837,29.85687290773108,47.21630421396256 +2024-09-01 17:00:00,3,30.6883322085789,19.73920011801222,59.075263669119536 +2024-09-01 18:00:00,3,29.38873588219253,24.59217239215489,55.74826168930637 +2024-09-01 19:00:00,3,16.22434752270007,22.490319302389885,59.430787197732 +2024-09-01 20:00:00,3,15.31916697314345,19.774966283673656,61.068349891437485 +2024-09-01 21:00:00,3,33.439820502357385,23.182590705934423,56.4372300872474 +2024-09-01 22:00:00,3,19.210475375000296,26.70180161150096,57.247098569506726 +2024-09-01 23:00:00,3,23.1031502156717,22.0880494345711,43.58505885068015 +2024-09-02 00:00:00,3,36.3350443064266,27.72713974035586,58.80902064397991 +2024-09-02 01:00:00,3,36.41131054958704,27.018085683499585,50.045289137560715 +2024-09-02 02:00:00,3,24.6770219658603,28.72155850564282,59.304182332226205 +2024-09-02 03:00:00,3,31.29684220455853,25.443179825826178,59.26171246683421 +2024-09-02 04:00:00,3,20.506872828449247,24.891670509256343,70.6467750569046 +2024-09-02 05:00:00,3,41.96254232964303,25.79346289377781,63.32622801499283 +2024-09-02 06:00:00,3,21.126880872867773,22.810405819272212,61.18406688169042 +2024-09-02 07:00:00,3,41.69421190797819,27.496461970363146,66.86964362551363 +2024-09-02 08:00:00,3,32.15644260777513,23.093784427825536,54.94293693040725 +2024-09-02 09:00:00,3,34.557404393290696,22.023164030508717,66.49079527676483 +2024-09-02 10:00:00,3,23.233207530728013,23.517264787332863,53.49613251343406 +2024-09-02 11:00:00,3,34.707389802349034,23.754539626207375,51.69257752633145 +2024-09-02 12:00:00,3,24.362662892072855,22.008092743514357,52.93901137994064 +2024-09-02 13:00:00,3,26.7895940310176,23.99062688742665,53.41914064584346 +2024-09-02 14:00:00,3,28.57823990715038,24.018014642975814,58.6664155759923 +2024-09-02 15:00:00,3,30.33914582094943,28.168036152424023,54.30852632274462 +2024-09-02 16:00:00,3,36.04105855552292,24.500386416252375,57.499702607813916 +2024-09-02 17:00:00,3,8.14362904708949,25.83369182941123,53.3413453719958 +2024-09-02 18:00:00,3,41.25298549724292,23.086766417674603,58.29876414002515 +2024-09-02 19:00:00,3,10.809746164499785,20.76086281572044,61.261125846606234 +2024-09-02 20:00:00,3,30.01739658555965,25.38874572949965,49.729390971844325 +2024-09-02 21:00:00,3,14.04268703202333,31.54502526596136,50.93185752474032 +2024-09-02 22:00:00,3,17.20105859028188,24.98029591258331,56.922212957506375 +2024-09-02 23:00:00,3,21.23877407905013,15.377478833684176,54.09627833286311 +2024-09-03 00:00:00,3,28.343680450965138,28.297993746843854,66.59973175862231 +2024-09-03 01:00:00,3,24.206891120888365,19.539929742244908,52.641244608769895 +2024-09-03 02:00:00,3,15.769180691003434,26.58107563819564,60.26686799247456 +2024-09-03 03:00:00,3,41.464089335400146,22.40694989135679,55.40442059024637 +2024-09-03 04:00:00,3,32.51442697288788,22.415478226490382,45.781366950332526 +2024-09-03 05:00:00,3,22.338132290090016,23.056125012704747,68.61833602892786 +2024-09-03 06:00:00,3,13.000200170826739,25.00178354296675,72.00213366874974 +2024-09-03 07:00:00,3,31.25417533534344,23.596639118771904,62.85409233291063 +2024-09-03 08:00:00,3,30.719967963632072,23.67391391266055,45.54348409739099 +2024-09-03 09:00:00,3,23.81227354586152,21.854447460251933,58.98959434579821 +2024-09-03 10:00:00,3,27.992379875250514,25.32314016720006,44.044744059411244 +2024-09-03 11:00:00,3,33.88584345332413,28.842287767048386,72.89096456562683 +2024-09-03 12:00:00,3,22.726267671120358,24.87980817248274,63.820028407972494 +2024-09-03 13:00:00,3,35.526034089084106,23.756501351931995,51.68068974080446 +2024-09-03 14:00:00,3,15.569699635281781,26.458803450639557,53.458322903375795 +2024-09-03 15:00:00,3,28.34632524812773,29.245483980387224,52.84116691711054 +2024-09-03 16:00:00,3,31.257069266418526,24.789280104011855,54.159296044668 +2024-09-03 17:00:00,3,21.227576353940826,22.19263240946341,59.830726634534315 +2024-09-03 18:00:00,3,22.127501457895576,26.064888910217206,52.45928773984161 +2024-09-03 19:00:00,3,17.184179599213063,21.827970440599543,61.400855944567276 +2024-09-03 20:00:00,3,27.860263498580977,22.452019317543197,59.148795386653866 +2024-09-03 21:00:00,3,35.63654921886077,24.822807070655877,49.7836241253914 +2024-09-03 22:00:00,3,16.791498585507348,29.926467374289825,52.99107372254548 +2024-09-03 23:00:00,3,31.80832985555737,25.062129616107757,59.96949126562014 +2024-09-04 00:00:00,3,33.88075693309692,28.851017070083405,57.23890654939666 +2024-09-04 01:00:00,3,26.758854199127814,21.1173162648426,49.853535517634576 +2024-09-04 02:00:00,3,35.411815246382275,25.354373548896774,61.40988729451155 +2024-09-04 03:00:00,3,37.00878901238417,23.128867894846174,64.36274065154527 +2024-09-04 04:00:00,3,19.942860685217912,23.50977034828217,48.1705255434793 +2024-09-04 05:00:00,3,28.579183668741777,26.884525852908023,69.1415693385882 +2024-09-04 06:00:00,3,18.580779855586485,25.805404810177908,48.46600206544842 +2024-09-04 07:00:00,3,37.97215661692309,21.30112841509836,60.81679762254421 +2024-09-04 08:00:00,3,11.410644381247254,29.469967568440577,45.025717903301256 +2024-09-04 09:00:00,3,6.127094212617784,22.90098375342906,54.67545423932439 +2024-09-04 10:00:00,3,16.750383503804215,26.900363522865884,42.80003641940352 +2024-09-04 11:00:00,3,32.69843778039826,22.29875406844706,41.41383491164739 +2024-09-04 12:00:00,3,38.22873692630047,26.391625803718043,50.671940162794186 +2024-09-04 13:00:00,3,35.22798599248039,22.14917701200894,61.76048228160202 +2024-09-04 14:00:00,3,34.763527568305825,27.843276413827404,48.25215975932572 +2024-09-04 15:00:00,3,20.90221905185152,27.755296214795983,62.69461712154941 +2024-09-04 16:00:00,3,33.13218034403635,28.59230936124305,58.83918548449906 +2024-09-04 17:00:00,3,13.360477515511626,26.245331537911596,63.29219505510653 +2024-09-04 18:00:00,3,17.527736229541386,21.230237115993674,63.39677715809113 +2024-09-04 19:00:00,3,30.98506530234297,28.998025838727592,76.20149279547559 +2024-09-04 20:00:00,3,21.88359297866757,20.188367949781917,57.53141284989006 +2024-09-04 21:00:00,3,27.733771759808363,21.775837279430327,57.43417335024504 +2024-09-04 22:00:00,3,28.378860354510476,17.433430332527358,55.924464380911445 +2024-09-04 23:00:00,3,16.06058417328898,27.438924769995204,66.92234873309202 +2024-09-05 00:00:00,3,32.294682077808474,28.92582086022524,61.54092564035312 +2024-09-05 01:00:00,3,35.502043118448476,28.358294271339222,50.78753229915326 +2024-09-05 02:00:00,3,33.84305304309572,29.770864850952783,56.841802284115865 +2024-09-05 03:00:00,3,34.273663764098146,24.737652619287974,67.19535647763763 +2024-09-05 04:00:00,3,17.81393670586554,27.017094694602466,76.7154611435615 +2024-09-05 05:00:00,3,31.07206088948916,20.23414738836467,66.98959620279788 +2024-09-05 06:00:00,3,33.60228884570289,28.38123089289182,56.513080929234405 +2024-09-05 07:00:00,3,42.76651923903654,30.051628280776978,50.108226259131015 +2024-09-05 08:00:00,3,19.26554284926477,32.253136239184656,39.00262248471396 +2024-09-05 09:00:00,3,33.851648665057006,23.997196545784735,59.25750232731511 +2024-09-05 10:00:00,3,26.420582549048852,21.743241822515735,69.21397240797262 +2024-09-05 11:00:00,3,24.914213380443847,27.432159737642806,25.975222646167378 +2024-09-05 12:00:00,3,36.53482191726691,27.571852254939902,32.88081129292804 +2024-09-05 13:00:00,3,24.14256515819077,23.99129757451679,59.30108772704301 +2024-09-05 14:00:00,3,37.31929888720956,25.346368927278334,55.69477045451012 +2024-09-05 15:00:00,3,23.82784858677805,23.336807621348278,52.49483654095212 +2024-09-05 16:00:00,3,23.6032391649472,24.59381212927564,50.488950754211096 +2024-09-05 17:00:00,3,9.351986520993247,29.33437572738987,52.79636092131655 +2024-09-05 18:00:00,3,30.967448334374083,22.76911830488458,52.93359229008334 +2024-09-05 19:00:00,3,31.61375260278687,21.30791070724944,58.379300123681254 +2024-09-05 20:00:00,3,15.538333216076362,23.833016615734117,45.44432539575607 +2024-09-05 21:00:00,3,29.70208519064785,27.000550206465725,65.01041756987836 +2024-09-05 22:00:00,3,9.938238305548998,21.080642768167497,62.509211578532 +2024-09-05 23:00:00,3,22.128177949057036,21.21848278355857,59.19111266861221 +2024-09-06 00:00:00,3,20.914618284298783,17.865638135154175,54.32786766875524 +2024-09-06 01:00:00,3,42.07535770337473,23.13966194912496,53.837557647204775 +2024-09-06 02:00:00,3,42.85669740791517,23.91439058021524,47.83811982371947 +2024-09-06 03:00:00,3,24.924677501296348,27.72104067198471,50.488261379639106 +2024-09-06 04:00:00,3,36.258497087776895,25.85158596328417,53.62719222824309 +2024-09-06 05:00:00,3,28.905628342945473,21.993203219871013,60.55270005805056 +2024-09-06 06:00:00,3,39.310428375811675,25.014469518628875,52.30245643341734 +2024-09-06 07:00:00,3,18.645182497207255,25.42796204029054,44.64459506752938 +2024-09-06 08:00:00,3,26.199595384204073,26.780059035828867,37.432540417683924 +2024-09-06 09:00:00,3,14.731424158173787,27.008816807548502,42.36956622009818 +2024-09-06 10:00:00,3,23.70935888648488,29.522235684997504,48.46049466119487 +2024-09-06 11:00:00,3,30.616393189829033,23.595493173511606,49.66714597271147 +2024-09-06 12:00:00,3,39.015860166770004,27.914581825170096,49.25284421995533 +2024-09-06 13:00:00,3,14.226394810367145,27.65605360215064,46.140172105573264 +2024-09-06 14:00:00,3,23.159609625308853,20.372577689029697,48.97353425095757 +2024-09-06 15:00:00,3,30.422706855348174,27.79551811294806,41.70945094439296 +2024-09-06 16:00:00,3,37.29275754492886,28.17427184524184,48.530395349065394 +2024-09-06 17:00:00,3,11.662233715347215,25.746878033766603,50.81207629671842 +2024-09-06 18:00:00,3,5.891807317356413,23.723981793402697,35.2201052588985 +2024-09-06 19:00:00,3,32.4038647480853,32.845197721197984,50.162134102787135 +2024-09-06 20:00:00,3,21.00775184636714,28.17469142388509,50.815226655428596 +2024-09-06 21:00:00,3,18.92372837063099,27.043774520697674,45.02171141948273 +2024-09-06 22:00:00,3,7.767000783729998,22.492508717015422,61.80534913388718 +2024-09-06 23:00:00,3,16.927095859906036,26.099636054732656,52.205309644330946 +2024-09-07 00:00:00,3,30.043760731768376,18.90403954998426,60.77143748637122 +2024-09-07 01:00:00,3,36.871115031631504,26.00317940549058,60.70035402818631 +2024-09-07 02:00:00,3,37.51233611344915,21.23409858635789,67.12746629334791 +2024-09-07 03:00:00,3,33.031894604240236,24.886416296623104,65.77889881404408 +2024-09-07 04:00:00,3,27.341390563467787,24.59345541768171,63.576827995993725 +2024-09-07 05:00:00,3,16.636379636517724,24.63082107599067,53.80960484541821 +2024-09-07 06:00:00,3,22.007354798557657,21.497603126178785,47.099138736386756 +2024-09-07 07:00:00,3,20.970539624367298,24.83922472437253,52.12572016095784 +2024-09-07 08:00:00,3,19.836679722493027,24.427166780567795,39.996984925330665 +2024-09-07 09:00:00,3,15.777201119260086,25.608325782106757,46.23654991037222 +2024-09-07 10:00:00,3,32.01808309420056,26.90669296879199,63.03460272672887 +2024-09-07 11:00:00,3,17.62997699492681,21.573950741181108,55.55860073818766 +2024-09-07 12:00:00,3,24.433820966634926,27.85079264589064,48.440442688914715 +2024-09-07 13:00:00,3,25.887317580055768,29.198783783206792,53.013518006081476 +2024-09-07 14:00:00,3,10.742649916899614,19.826596694560326,45.37080004895301 +2024-09-07 15:00:00,3,30.068901973412352,24.56906249420995,46.80474638603237 +2024-09-07 16:00:00,3,30.104822266286497,26.471493781964508,60.58854968398852 +2024-09-07 17:00:00,3,23.888855805438208,27.168967225958767,53.48547720984064 +2024-09-07 18:00:00,3,28.060776419978133,27.24895234532803,59.78423998182077 +2024-09-07 19:00:00,3,31.2970650279587,18.718678645627794,35.00285074373385 +2024-09-07 20:00:00,3,19.81620220828232,17.963853209717655,62.79381112522207 +2024-09-07 21:00:00,3,25.819462497490008,24.936199202499132,43.80344073820523 +2024-09-07 22:00:00,3,13.08895307002377,23.111740378137,52.69555820942178 +2024-09-07 23:00:00,3,34.13568222177577,26.253917261561565,63.98040934588177 +2024-09-08 00:00:00,3,26.330937451342816,26.298021383602933,48.910696466730315 +2024-09-08 01:00:00,3,44.17400580650296,26.80529563600166,68.25352987580955 +2024-09-08 02:00:00,3,28.672872770849413,29.88716363639274,63.02745054531816 +2024-09-08 03:00:00,3,34.835613490405635,26.386763781412895,59.393927353484145 +2024-09-08 04:00:00,3,33.01557135230597,26.462329634146208,40.64008688998733 +2024-09-08 05:00:00,3,35.34411158055027,23.771559164756443,59.59141708260932 +2024-09-08 06:00:00,3,31.954675779891303,30.91677179969103,70.02963282517581 +2024-09-08 07:00:00,3,17.17358235036562,25.13155208869386,62.50551914518286 +2024-09-08 08:00:00,3,26.61986961460925,20.649903194863167,66.2132523572548 +2024-09-08 09:00:00,3,28.87419752100095,24.05366688459253,50.488314534046935 +2024-09-08 10:00:00,3,39.869470651608296,33.054845494179304,61.886972805316546 +2024-09-08 11:00:00,3,20.91917148759915,26.957693171513426,52.100393567885526 +2024-09-08 12:00:00,3,23.504979284286048,23.56157324256695,49.734054805350254 +2024-09-08 13:00:00,3,6.128894982261798,25.11402963024804,49.595804360272325 +2024-09-08 14:00:00,3,27.93942221766486,24.2797236640105,54.25017859538948 +2024-09-08 15:00:00,3,7.475711848310112,24.582832874216702,54.045498074665915 +2024-09-08 16:00:00,3,20.746547526152828,24.835889789777887,49.1027951636526 +2024-09-08 17:00:00,3,26.720423536896227,22.71737515037185,54.965923932468115 +2024-09-08 18:00:00,3,33.284359589594615,20.22954130233263,46.98921236710489 +2024-09-08 19:00:00,3,25.358740551753716,31.028043909562648,50.144326628100046 +2024-09-08 20:00:00,3,26.4290660939209,23.022275717839253,49.426903149996974 +2024-09-08 21:00:00,3,36.05997408482106,27.31321052057434,58.33134173378312 +2024-09-08 22:00:00,3,19.520081049249754,20.790739842601706,66.4811182432619 +2024-09-08 23:00:00,3,20.310014330389436,26.414130168374633,80.98369525123832 +2024-09-09 00:00:00,3,20.14341101116537,26.19719541975211,67.03925366801224 +2024-09-09 01:00:00,3,29.708496216540247,29.385018427641928,66.5506030047819 +2024-09-09 02:00:00,3,29.031771618090087,27.52950028813504,55.42212975324816 +2024-09-09 03:00:00,3,31.25246881818647,25.236000784420874,58.31200428329148 +2024-09-09 04:00:00,3,31.76204185820373,26.49251846672601,55.6032190455821 +2024-09-09 05:00:00,3,25.14016164661235,25.82296747100894,56.34758340907305 +2024-09-09 06:00:00,3,33.80643154199243,28.452894959553156,66.73197968542853 +2024-09-09 07:00:00,3,33.0917449847076,28.485883971744634,49.772199693593485 +2024-09-09 08:00:00,3,35.54713181539647,27.069709468101358,45.86772318789117 +2024-09-09 09:00:00,3,26.013587702775663,19.3821798284371,51.72407503678566 +2024-09-09 10:00:00,3,29.811953272749292,25.265874793754204,41.77195753028812 +2024-09-09 11:00:00,3,32.24219670910204,19.187451316177384,52.27443999807566 +2024-09-09 12:00:00,3,25.987073654318117,27.253372035335218,60.557986251599345 +2024-09-09 13:00:00,3,25.234180101371273,30.561597608149583,50.593463543560695 +2024-09-09 14:00:00,3,18.66989424171196,31.475343863860154,54.673762463181255 +2024-09-09 15:00:00,3,37.87981205839837,20.984711604396143,49.28434601424001 +2024-09-09 16:00:00,3,22.70996273492083,25.99729266900043,53.1623630827779 +2024-09-09 17:00:00,3,23.585002396515275,19.12268706390292,64.11759995034008 +2024-09-09 18:00:00,3,28.005647945754077,25.843464963383035,64.06499573683043 +2024-09-09 19:00:00,3,4.722770433221637,22.578793939423022,56.867727867890494 +2024-09-09 20:00:00,3,23.23598006248741,23.05628303094139,57.732784279410936 +2024-09-09 21:00:00,3,35.90902895305495,26.963389876897303,79.66942916312348 +2024-09-09 22:00:00,3,5.485586867404891,17.125434879032095,45.93662963481721 +2024-09-09 23:00:00,3,20.832540677981914,24.931530175872595,48.58653135091014 +2024-09-10 00:00:00,3,30.856028565791497,32.727313563567506,61.26308341479499 +2024-09-10 01:00:00,3,40.23228337110113,28.937205373607085,61.552326485000364 +2024-09-10 02:00:00,3,44.72024611024567,26.50096950109153,49.355913493161296 +2024-09-10 03:00:00,3,19.290523068158578,26.579019490737476,60.866094457029334 +2024-09-10 04:00:00,3,25.055496997925708,23.368640122951355,77.00774165646402 +2024-09-10 05:00:00,3,33.38486425665491,27.489251505764425,60.22591531886168 +2024-09-10 06:00:00,3,36.71933117599347,20.786695737346527,53.408734891431614 +2024-09-10 07:00:00,3,24.914786474999598,20.26636856129216,58.097769916784365 +2024-09-10 08:00:00,3,28.365606870554288,24.335704182969252,35.35950751703142 +2024-09-10 09:00:00,3,23.15537541354388,35.74380127836737,49.83180352808308 +2024-09-10 10:00:00,3,33.52491224698283,25.590559510935805,41.77615829713931 +2024-09-10 11:00:00,3,32.33309742690875,27.832852032732045,50.602497599212136 +2024-09-10 12:00:00,3,26.824541076373077,23.87677132547241,48.17128222937295 +2024-09-10 13:00:00,3,25.008654047431943,21.770530886992283,49.505273885476434 +2024-09-10 14:00:00,3,30.360370452082904,26.52890177684066,65.83964629305655 +2024-09-10 15:00:00,3,31.666128692830952,28.92645040246353,57.840064659122284 +2024-09-10 16:00:00,3,35.36913391185299,31.868298323494763,47.783063204766655 +2024-09-10 17:00:00,3,20.35479826049948,16.561517488300794,49.32508846100823 +2024-09-10 18:00:00,3,15.647299115912679,28.447656112113375,49.89470219030874 +2024-09-10 19:00:00,3,25.926716963702127,29.33123373171427,48.848777461927334 +2024-09-10 20:00:00,3,43.46438570581461,26.128129932384432,66.26628369156239 +2024-09-10 21:00:00,3,22.684213094401763,19.71341645608288,64.42609029261314 +2024-09-10 22:00:00,3,38.51461590902737,20.00201989133825,56.84034572703961 +2024-09-10 23:00:00,3,22.342379155432862,22.818141771031545,32.87780809252543 +2024-09-11 00:00:00,3,14.146333441829913,25.457411650525703,51.94489719881785 +2024-09-11 01:00:00,3,34.68124010855637,24.678541591300334,66.68798609960425 +2024-09-11 02:00:00,3,23.012388281531415,26.710369409799178,59.83313179975981 +2024-09-11 03:00:00,3,20.732033196567905,25.28546130701884,58.36754820614677 +2024-09-11 04:00:00,3,26.424826914270184,27.017606793395597,50.001632313105674 +2024-09-11 05:00:00,3,24.939470074224317,23.57357554777725,56.86674511099264 +2024-09-11 06:00:00,3,29.003161061886576,27.098295820020475,51.30321821494027 +2024-09-11 07:00:00,3,17.075727612497307,25.306368150681394,34.35395760124729 +2024-09-11 08:00:00,3,40.02427216363399,26.24889056875777,54.06577180395848 +2024-09-11 09:00:00,3,14.906945317484297,26.836079471384824,42.62689674895985 +2024-09-11 10:00:00,3,20.772578443077556,15.316292157057912,30.779510312677868 +2024-09-11 11:00:00,3,23.782477960321167,23.50458635606132,51.44864252333809 +2024-09-11 12:00:00,3,27.02356388449222,31.31607767425753,48.09107247745425 +2024-09-11 13:00:00,3,17.937463993403107,24.87570228636367,49.8430243313272 +2024-09-11 14:00:00,3,8.571635502645705,21.91856713353266,53.80774389133438 +2024-09-11 15:00:00,3,29.094965394236795,19.368657066178272,58.46054619196609 +2024-09-11 16:00:00,3,31.916525423622893,22.76834358224287,56.760966947939785 +2024-09-11 17:00:00,3,17.28997177783137,29.740743196263498,50.772487046293605 +2024-09-11 18:00:00,3,23.007419236434547,26.323079552841754,49.51174710682187 +2024-09-11 19:00:00,3,15.041174131983112,26.910218220856986,51.809711756890344 +2024-09-11 20:00:00,3,14.922380101979291,21.073383452716996,56.51701258909529 +2024-09-11 21:00:00,3,21.89289284688987,15.970715908871075,42.06238833228668 +2024-09-11 22:00:00,3,27.978594980747705,23.770147011650774,38.47704944238939 +2024-09-11 23:00:00,3,23.84501286720214,21.029292442269387,46.5824563531717 +2024-09-12 00:00:00,3,19.733923295313794,21.304071874004478,51.878495700137094 +2024-09-12 01:00:00,3,33.38419663922761,25.455090516565832,66.33517092959711 +2024-09-12 02:00:00,3,22.28221596985011,26.56586185214158,61.287784824766355 +2024-09-12 03:00:00,3,27.732488372171463,25.896155925623063,51.69624259779876 +2024-09-12 04:00:00,3,31.722195882686925,23.353181630864217,54.77758665745136 +2024-09-12 05:00:00,3,29.459677005566064,23.433174885938975,55.06754076536782 +2024-09-12 06:00:00,3,38.38349015296918,28.238536670532234,64.06159569849973 +2024-09-12 07:00:00,3,29.655623780348204,32.44484318954206,46.902036816867664 +2024-09-12 08:00:00,3,28.455326795477635,26.825174692407785,55.54441526776166 +2024-09-12 09:00:00,3,24.11734184642632,20.925579125104605,61.19574435361791 +2024-09-12 10:00:00,3,31.055432084870752,20.750296029052087,66.42273984537077 +2024-09-12 11:00:00,3,36.20345812686361,26.08289711998355,59.5598537727478 +2024-09-12 12:00:00,3,1.5546804525523115,24.98661491395203,44.84183424302106 +2024-09-12 13:00:00,3,17.27250004861286,30.739070533236262,40.49866403528889 +2024-09-12 14:00:00,3,32.528330116297674,29.23138108722336,52.38380090833409 +2024-09-12 15:00:00,3,28.591753550702414,24.303738688593022,47.03397469126488 +2024-09-12 16:00:00,3,20.888890490003792,24.987823849924002,50.66966069099351 +2024-09-12 17:00:00,3,16.306501213405078,34.1615042399963,67.05692682362216 +2024-09-12 18:00:00,3,42.238886363973016,24.144485339231476,43.28224390143474 +2024-09-12 19:00:00,3,30.68274239331873,27.106438024057923,50.05229290801326 +2024-09-12 20:00:00,3,17.652817047521847,20.815680379627644,63.68074863739852 +2024-09-12 21:00:00,3,18.778587639859673,25.427971953218975,48.62654139618794 +2024-09-12 22:00:00,3,13.353893786633979,22.9766920481952,44.20998313276367 +2024-09-12 23:00:00,3,28.968682892236586,25.78384829288383,73.08624603857523 +2024-09-13 00:00:00,3,38.06123290146676,25.836301316545878,74.60834532486214 +2024-09-13 01:00:00,3,30.0579434325515,24.61123448444638,58.022845598520206 +2024-09-13 02:00:00,3,28.809377187147874,25.761452927070817,64.02336202797957 +2024-09-13 03:00:00,3,36.688128775248764,22.790900403224356,60.600139573301426 +2024-09-13 04:00:00,3,19.794956676313255,20.29538605824133,59.85479624810915 +2024-09-13 05:00:00,3,36.86947821935457,28.230378949775194,53.00277547376687 +2024-09-13 06:00:00,3,15.975379060916172,26.931033110997607,56.58289593361301 +2024-09-13 07:00:00,3,17.950631509908202,25.027179637396884,52.40096201033153 +2024-09-13 08:00:00,3,34.01363792738865,24.863504150268398,50.988017275905676 +2024-09-13 09:00:00,3,24.110277568409558,24.200224823594326,49.17385604201581 +2024-09-13 10:00:00,3,28.918813244220296,28.203853561566604,55.99074811287441 +2024-09-13 11:00:00,3,6.514485659632232,14.229533328160935,37.145245758780476 +2024-09-13 12:00:00,3,33.65743516867985,21.770166103860394,48.44974493382724 +2024-09-13 13:00:00,3,33.2725358165864,27.419074877071836,55.55912084297817 +2024-09-13 14:00:00,3,28.276536383650438,22.379530018167248,68.15369183323713 +2024-09-13 15:00:00,3,27.72833497632937,27.445583142627626,63.86164395948118 +2024-09-13 16:00:00,3,37.30658583201972,26.08044908797278,48.246828541348904 +2024-09-13 17:00:00,3,22.223560175197605,27.276028993030206,65.78622871005335 +2024-09-13 18:00:00,3,26.319872749694785,27.252061339245646,66.93570854234966 +2024-09-13 19:00:00,3,36.16218785047573,28.123661311961733,59.90772108925694 +2024-09-13 20:00:00,3,33.52376946265887,26.665687662332598,46.05148249345877 +2024-09-13 21:00:00,3,23.00790024056508,25.084818523167783,59.25982281762232 +2024-09-13 22:00:00,3,13.742891292547618,21.391816549563078,58.0326963388466 +2024-09-13 23:00:00,3,29.232029867394868,19.19405840672476,50.49190895578865 +2024-09-14 00:00:00,3,26.653077562265686,22.536985769192206,48.38032480394216 +2024-09-14 01:00:00,3,21.405835936421717,19.361560328940115,55.929778113144685 +2024-09-14 02:00:00,3,31.77891172492883,29.27118308829606,57.56589739219156 +2024-09-14 03:00:00,3,34.871156070119994,30.009101529779425,57.87700638479103 +2024-09-14 04:00:00,3,33.66179402986245,29.423709050055727,60.337553385854186 +2024-09-14 05:00:00,3,33.75837836529075,20.613223378376244,49.26912862034531 +2024-09-14 06:00:00,3,18.118453721530926,26.109023662423493,64.2825679197448 +2024-09-14 07:00:00,3,35.85067961332316,28.93674244607316,58.37269516421987 +2024-09-14 08:00:00,3,24.47123675674205,25.012773099798356,56.545923413796835 +2024-09-14 09:00:00,3,32.287352339485864,22.81098678935555,59.849184537914134 +2024-09-14 10:00:00,3,27.39747978377446,22.34226590471599,45.618338402579035 +2024-09-14 11:00:00,3,27.462352105821644,25.410819281294174,40.27187267559671 +2024-09-14 12:00:00,3,28.31572018615143,24.599764626118645,46.91147760894481 +2024-09-14 13:00:00,3,32.06246057197987,25.5181043049152,53.303295175776725 +2024-09-14 14:00:00,3,35.35594644698826,19.69870924426282,47.83189032940811 +2024-09-14 15:00:00,3,18.39317898710759,26.060634637857927,51.83768670079922 +2024-09-14 16:00:00,3,20.493226490185776,26.365567736764458,64.13905652513476 +2024-09-14 17:00:00,3,32.0452519105251,20.548410335333696,67.85933503158856 +2024-09-14 18:00:00,3,39.75669907611275,19.51417616501593,46.53639048379033 +2024-09-14 19:00:00,3,20.254518459355154,24.650413062232918,51.313186805311446 +2024-09-14 20:00:00,3,25.459299693347887,18.458842903609444,55.241180852764714 +2024-09-14 21:00:00,3,31.88756919638898,28.44543496056638,43.66419774735144 +2024-09-14 22:00:00,3,28.07602641874439,14.6392899569789,62.289097804799255 +2024-09-14 23:00:00,3,20.862324368398184,28.26254222153606,51.34567933632682 +2024-09-15 00:00:00,3,24.49406302902535,27.52352347248547,68.1900771638021 +2024-09-15 01:00:00,3,30.497235457157238,28.8359942339327,52.68245334238097 +2024-09-15 02:00:00,3,32.56701247721678,22.632863454234943,54.94239918557407 +2024-09-15 03:00:00,3,34.46361415098913,14.87572638007065,57.33444029021694 +2024-09-15 04:00:00,3,30.433986955028875,18.398913052622323,56.18413536032382 +2024-09-15 05:00:00,3,26.903003076667723,22.107577105048904,63.8344054829444 +2024-09-15 06:00:00,3,40.28671751936246,22.511726121095847,52.138044422125844 +2024-09-15 07:00:00,3,38.39442821817629,25.92506943062921,62.59612064583324 +2024-09-15 08:00:00,3,24.449945308594724,22.221290947351655,47.968253364131 +2024-09-15 09:00:00,3,30.085488384940355,23.923091567260467,60.11269368649686 +2024-09-15 10:00:00,3,11.068322258659856,21.7619270585855,46.65001549605177 +2024-09-15 11:00:00,3,15.278842212860521,20.5298792763073,49.52631128576895 +2024-09-15 12:00:00,3,29.40162203924797,27.607874153351823,55.3439680902391 +2024-09-15 13:00:00,3,28.687262956439728,22.8204146759322,47.57853555684384 +2024-09-15 14:00:00,3,15.083379249686583,19.3586029773756,51.41300951166082 +2024-09-15 15:00:00,3,21.87794643467894,29.244031422813627,71.02703465027442 +2024-09-15 16:00:00,3,17.107146009128765,30.408051218613572,54.07719108956478 +2024-09-15 17:00:00,3,24.539029869797265,24.646408267524304,53.101407603888035 +2024-09-15 18:00:00,3,26.778502598745906,28.843759225067885,64.3405315386782 +2024-09-15 19:00:00,3,18.36563654766325,23.110010846961767,64.17312054688156 +2024-09-15 20:00:00,3,18.766942169741043,21.4742067757339,51.69777372581315 +2024-09-15 21:00:00,3,23.988831393826583,22.23334698649682,46.89253666333693 +2024-09-15 22:00:00,3,22.904216185657518,22.683624425944483,54.01190395855667 +2024-09-15 23:00:00,3,36.365216265208076,22.16888135424242,55.23100905436677 +2024-09-16 00:00:00,3,33.097595518784814,25.326324727805474,54.01576561950567 +2024-09-16 01:00:00,3,23.49300216921589,25.45189075436427,66.25773999402561 +2024-09-16 02:00:00,3,42.338269026483864,21.861120637141077,72.12879036304611 +2024-09-16 03:00:00,3,29.104033262907965,17.650427186381002,48.804893122180694 +2024-09-16 04:00:00,3,25.965031518090825,26.232392331027597,55.359775911846974 +2024-09-16 05:00:00,3,19.761617562428043,26.080601267524116,59.99558869455463 +2024-09-16 06:00:00,3,27.750971836996207,25.84376816279372,45.14483851757166 +2024-09-16 07:00:00,3,35.92540382487282,25.454794178854428,42.25701573365197 +2024-09-16 08:00:00,3,21.87979319535865,22.89787538018365,33.192665100642415 +2024-09-16 09:00:00,3,37.01261485603917,23.716340953216275,52.528113316045996 +2024-09-16 10:00:00,3,14.229896808744591,23.879429646088475,55.698671419651 +2024-09-16 11:00:00,3,38.126835307944376,25.900250262938,51.47215415428921 +2024-09-16 12:00:00,3,38.83803245467531,28.943956622431823,39.75846892092362 +2024-09-16 13:00:00,3,22.64897565490305,27.941783666528345,68.25806472605856 +2024-09-16 14:00:00,3,34.12243092323391,24.42680633447691,62.98358016883543 +2024-09-16 15:00:00,3,22.834773160502035,26.822645200507562,56.88279055844281 +2024-09-16 16:00:00,3,3.886092844055309,22.092680809034892,49.64055015286163 +2024-09-16 17:00:00,3,36.7763569506759,15.742431702084685,54.144502692030215 +2024-09-16 18:00:00,3,10.490849199724929,32.542195401140006,45.32461722061821 +2024-09-16 19:00:00,3,22.67270481071003,23.940293805597513,60.95691483477881 +2024-09-16 20:00:00,3,32.26995072436873,24.760647712328552,55.94432925603704 +2024-09-16 21:00:00,3,31.725697669356755,21.36855605766282,47.708484914983494 +2024-09-16 22:00:00,3,31.98627195237207,18.393913102036105,59.299183848057744 +2024-09-16 23:00:00,3,24.993983226429023,28.305533610506465,43.87312934627239 +2024-09-17 00:00:00,3,26.607057227512247,19.22096269109553,54.63157038686916 +2024-09-17 01:00:00,3,27.84567488464014,26.98647331913049,57.31781517634774 +2024-09-17 02:00:00,3,37.45864348524476,21.864162305627744,64.65309286006652 +2024-09-17 03:00:00,3,35.18830306236197,18.839674164212322,48.90239729607037 +2024-09-17 04:00:00,3,31.518291985646513,28.84889503757419,53.70338530660408 +2024-09-17 05:00:00,3,31.023329759368366,26.134112871807606,68.22503511634365 +2024-09-17 06:00:00,3,35.29754390529773,22.250580938363907,64.064355284483 +2024-09-17 07:00:00,3,28.51015257764368,23.797323001231824,50.321127335768765 +2024-09-17 08:00:00,3,35.61357345626217,29.729800501113303,53.328817590357595 +2024-09-17 09:00:00,3,20.655253327233385,24.19191164174981,60.080077490302365 +2024-09-17 10:00:00,3,33.65241852583366,24.05058396565837,39.041130486199364 +2024-09-17 11:00:00,3,27.426929345626732,29.096828820849534,50.65821092930349 +2024-09-17 12:00:00,3,18.286792638668743,24.91338458643297,53.44369098537334 +2024-09-17 13:00:00,3,19.967347089001212,23.666350441348534,49.668607555598754 +2024-09-17 14:00:00,3,36.72676983602678,22.6506623282372,50.69731865231884 +2024-09-17 15:00:00,3,22.53672080069926,23.981565933941027,52.40932000914551 +2024-09-17 16:00:00,3,7.334090668441878,29.389587909426492,47.34471792348063 +2024-09-17 17:00:00,3,20.76860273392423,23.523837906895068,55.24421090917062 +2024-09-17 18:00:00,3,19.121815801238746,26.518360224546086,42.438756674875535 +2024-09-17 19:00:00,3,18.86391419562962,27.78313039231105,49.979927101038875 +2024-09-17 20:00:00,3,34.14354230954288,30.91415343714731,49.48777382286976 +2024-09-17 21:00:00,3,28.511159967354924,23.425361960867576,61.874774345132444 +2024-09-17 22:00:00,3,19.395130362974538,28.603764607257684,39.85191734557951 +2024-09-17 23:00:00,3,28.519138098082884,24.542307432645135,63.7675791106227 +2024-09-18 00:00:00,3,23.60899299249887,30.458814165175017,64.66980793973745 +2024-09-18 01:00:00,3,29.25726143086379,28.0098420550611,60.22323945984289 +2024-09-18 02:00:00,3,21.0042758755838,25.134750245865206,52.79636640756261 +2024-09-18 03:00:00,3,22.142519113522052,27.27900548351463,43.33470339358799 +2024-09-18 04:00:00,3,24.07989780066388,19.64526943060834,51.464839623490015 +2024-09-18 05:00:00,3,18.65256099754562,23.64493512924683,55.41295657893623 +2024-09-18 06:00:00,3,35.80096554400446,28.40034185546952,48.69682242826898 +2024-09-18 07:00:00,3,15.078380773010313,17.83330752524929,44.40652075542412 +2024-09-18 08:00:00,3,25.91937416537432,24.12713851131921,63.04188164212704 +2024-09-18 09:00:00,3,40.28236503516898,24.880988609945565,46.64927552709387 +2024-09-18 10:00:00,3,22.071977190044326,27.289028199512966,59.09217283529182 +2024-09-18 11:00:00,3,13.776260721161515,21.015472601371076,53.68657410375773 +2024-09-18 12:00:00,3,20.482537498875878,21.482492228498433,52.54302973243634 +2024-09-18 13:00:00,3,19.937975267659972,26.205923379826075,56.912579711131386 +2024-09-18 14:00:00,3,20.096930160975283,27.035783721394306,40.69000093075536 +2024-09-18 15:00:00,3,27.838029199632153,24.34349868274591,46.338551515961335 +2024-09-18 16:00:00,3,36.5100778767093,27.774069045814723,53.99611405363277 +2024-09-18 17:00:00,3,28.74611070836692,24.543915785299728,59.34021566789144 +2024-09-18 18:00:00,3,16.03795704616421,24.756567139252365,57.26813068746827 +2024-09-18 19:00:00,3,26.654033091853293,24.415245549168958,54.41036891490972 +2024-09-18 20:00:00,3,15.524939617658598,18.30463009814781,47.6915136942147 +2024-09-18 21:00:00,3,33.89319597628459,28.990800015403202,57.829090306570805 +2024-09-18 22:00:00,3,30.957825849753732,20.155900585222884,41.56426631647618 +2024-09-18 23:00:00,3,17.446603240825752,21.048775118619528,64.67315262498403 +2024-09-19 00:00:00,3,21.64518050777114,27.199779552637864,77.86186997152392 +2024-09-19 01:00:00,3,23.916930701114406,27.159139965937086,70.08813639214316 +2024-09-19 02:00:00,3,26.896401911410177,27.962370822046665,51.93108139857777 +2024-09-19 03:00:00,3,18.748757306303318,33.255911303074626,62.95623514831589 +2024-09-19 04:00:00,3,35.00471851624924,27.038064468132543,54.05352564253336 +2024-09-19 05:00:00,3,26.23945176278551,21.522923244972105,59.086486612645416 +2024-09-19 06:00:00,3,19.025231056900235,24.23340518687631,69.25368879230041 +2024-09-19 07:00:00,3,38.05590982488172,26.102441421797803,43.41869808439949 +2024-09-19 08:00:00,3,22.307954054987604,25.696687965468332,54.87596166547513 +2024-09-19 09:00:00,3,31.550540578150994,24.4481824678729,64.23312691296611 +2024-09-19 10:00:00,3,24.689148207042248,26.502856195293766,47.406754968423186 +2024-09-19 11:00:00,3,12.60313502425272,22.795817808914464,49.56775185786015 +2024-09-19 12:00:00,3,19.69655877689569,26.973361085036306,65.52372070846754 +2024-09-19 13:00:00,3,29.344693412693093,18.49125708319871,47.077969146062486 +2024-09-19 14:00:00,3,25.87924288205287,19.540454420880707,60.7980895585655 +2024-09-19 15:00:00,3,20.153158452372818,28.27341321081565,63.55944745528151 +2024-09-19 16:00:00,3,32.316559385719586,22.604374561229957,56.32335568299654 +2024-09-19 17:00:00,3,21.92717919377455,24.922995558135938,67.87200699322027 +2024-09-19 18:00:00,3,27.25608790927957,25.452089488772955,40.44886203472157 +2024-09-19 19:00:00,3,26.659560805363775,29.3230558316177,52.16472870242235 +2024-09-19 20:00:00,3,32.996084875063474,27.88904951204271,51.43707798459285 +2024-09-19 21:00:00,3,16.621109294387608,17.11527521995791,61.19915212330421 +2024-09-19 22:00:00,3,25.609921547051158,18.898450160307203,53.84647179956929 +2024-09-19 23:00:00,3,21.56205095315423,17.73574462779782,67.29864926992542 +2024-09-20 00:00:00,3,26.880918571312378,20.613169161847466,68.87995567836981 +2024-09-20 01:00:00,3,9.923991337085539,26.13782330911238,50.13849195553274 +2024-09-20 02:00:00,3,24.41785354946231,26.121365948222824,48.29056361330997 +2024-09-20 03:00:00,3,42.210484153247045,25.109840891559152,62.33084033104643 +2024-09-20 04:00:00,3,22.966095315112902,31.913133190566462,62.97539970415703 +2024-09-20 05:00:00,3,31.100264862548496,26.03386510105072,40.130558782299985 +2024-09-20 06:00:00,3,30.970248266446298,29.125636671204163,46.11260548277957 +2024-09-20 07:00:00,3,31.2489019441933,26.405085747515958,54.92568957467956 +2024-09-20 08:00:00,3,16.493207441697514,23.984933328413806,59.360878367069766 +2024-09-20 09:00:00,3,45.90308277054561,23.158830817721245,69.04937678182353 +2024-09-20 10:00:00,3,42.537435712470185,25.997327132053297,45.92817299947901 +2024-09-20 11:00:00,3,19.39179013849075,24.02437296299327,48.05190543595852 +2024-09-20 12:00:00,3,31.21558993980698,26.51845423464958,50.779700746635896 +2024-09-20 13:00:00,3,20.62746652337232,29.747456819378378,48.28010226486079 +2024-09-20 14:00:00,3,28.530723487601954,26.12072471799709,47.779801124756624 +2024-09-20 15:00:00,3,18.807433110387898,26.45941054183723,57.99287100368654 +2024-09-20 16:00:00,3,28.23276510755215,30.750923791823826,56.15117210319684 +2024-09-20 17:00:00,3,32.53088219697337,23.45337822190433,59.28069182225236 +2024-09-20 18:00:00,3,23.0765762797878,24.082887374597014,67.12718655687979 +2024-09-20 19:00:00,3,42.869424676166304,20.240417865837326,40.95651741849763 +2024-09-20 20:00:00,3,23.04316583656805,22.080927980144004,54.657001508297604 +2024-09-20 21:00:00,3,15.903711122262587,13.47166939141798,47.62887040444499 +2024-09-20 22:00:00,3,12.235846139409528,21.50241844214386,46.73343544346869 +2024-09-20 23:00:00,3,32.64628651698293,28.409665085389108,64.08900766117895 +2024-09-21 00:00:00,3,29.651167780347524,25.404615871762985,68.12220788435557 +2024-09-21 01:00:00,3,29.259369800752403,29.839337989104482,71.13525523807353 +2024-09-21 02:00:00,3,34.59785142466605,26.60487154383087,61.89167246342623 +2024-09-21 03:00:00,3,39.348378597812655,22.513094649844213,41.10165144044301 +2024-09-21 04:00:00,3,35.1027217975094,23.69209329530425,70.68091892752373 +2024-09-21 05:00:00,3,14.248202676664864,24.509159735838555,61.21889398067484 +2024-09-21 06:00:00,3,26.411996409733863,24.888971627570555,63.91791268420385 +2024-09-21 07:00:00,3,37.16627418683511,26.268720395924692,49.05060800952109 +2024-09-21 08:00:00,3,20.84045222665772,27.87846399492293,49.21108786457628 +2024-09-21 09:00:00,3,20.81873738145462,17.689419438319423,44.836959470428134 +2024-09-21 10:00:00,3,20.40494458827817,22.516403863891195,67.12729203631214 +2024-09-21 11:00:00,3,14.053184762620532,23.56032700104119,54.60040109150111 +2024-09-21 12:00:00,3,24.742629784768305,23.02847266032707,52.577933223044546 +2024-09-21 13:00:00,3,30.464463936978085,22.09375537085004,53.8011519130499 +2024-09-21 14:00:00,3,29.56040191967618,23.68444127700452,59.00091081275752 +2024-09-21 15:00:00,3,42.30859065177467,23.21050469848784,67.46137980143095 +2024-09-21 16:00:00,3,37.18607516740518,26.747526724664777,65.95711535226096 +2024-09-21 17:00:00,3,18.625426696751575,24.076441476322035,47.29318694564005 +2024-09-21 18:00:00,3,17.52337865229416,21.19570537815928,52.20050502829042 +2024-09-21 19:00:00,3,28.000411239802677,23.067498759306353,57.87897162805551 +2024-09-21 20:00:00,3,23.91038248772264,22.47091660708893,43.41997374453568 +2024-09-21 21:00:00,3,18.436275636188874,14.421765287398319,56.18033475449801 +2024-09-21 22:00:00,3,20.5687900266795,22.561063860801607,54.736098352848956 +2024-09-21 23:00:00,3,14.249027839448653,16.881136227182335,55.70297943548456 +2024-09-22 00:00:00,3,31.856200369555175,30.286613955187555,58.4397160319538 +2024-09-22 01:00:00,3,34.23660141640768,25.626617563966082,45.99827328620804 +2024-09-22 02:00:00,3,27.788591256202093,30.157852009966284,50.86413976001806 +2024-09-22 03:00:00,3,33.81225891288687,23.42473171359016,64.26214718658828 +2024-09-22 04:00:00,3,16.33832302648241,21.67841550542834,60.82226066380219 +2024-09-22 05:00:00,3,17.4843560481319,25.711335493839094,60.6277601817409 +2024-09-22 06:00:00,3,5.08668291514001,26.156624144378977,48.41556705877456 +2024-09-22 07:00:00,3,27.40170549688183,27.317173887078766,63.16980236302066 +2024-09-22 08:00:00,3,20.698744230357015,27.8734443668805,60.76129316395143 +2024-09-22 09:00:00,3,26.173333931664992,16.31814380057964,58.12393493921177 +2024-09-22 10:00:00,3,32.05189507025278,25.642705356205568,61.475802201232966 +2024-09-22 11:00:00,3,17.238779628249812,22.251042410364928,42.75981182728871 +2024-09-22 12:00:00,3,34.49057457610315,22.21035107254706,51.24177960942227 +2024-09-22 13:00:00,3,23.12021489812475,27.276652598273984,47.780042476910765 +2024-09-22 14:00:00,3,19.183591455530106,27.36161757499113,52.32931233113911 +2024-09-22 15:00:00,3,19.493163822836596,20.961060680088423,52.97841719776384 +2024-09-22 16:00:00,3,41.04426137188128,24.514742576921137,55.275951444737984 +2024-09-22 17:00:00,3,24.32464055371435,30.181327900589707,48.221737028456104 +2024-09-22 18:00:00,3,19.272111914337863,19.90329625219165,42.909392399200335 +2024-09-22 19:00:00,3,20.34732646050448,20.73273410188368,40.809347456542554 +2024-09-22 20:00:00,3,32.214286502649884,21.734914807597416,52.164572800063915 +2024-09-22 21:00:00,3,32.051930750520356,22.80657310853664,57.45394015968465 +2024-09-22 22:00:00,3,12.712349471414987,21.632292945955935,58.84470053482681 +2024-09-22 23:00:00,3,16.7763568047125,21.073331844513287,61.9969023497237 +2024-09-23 00:00:00,3,35.86966511336854,21.450757076186726,48.741080836576565 +2024-09-23 01:00:00,3,19.806506466366297,31.131821588541285,53.98686943722934 +2024-09-23 02:00:00,3,34.15197600426471,24.56732667597965,59.20050226241077 +2024-09-23 03:00:00,3,26.709334640698337,20.233206718202407,51.31008583204786 +2024-09-23 04:00:00,3,30.00522589018023,25.390127211796464,49.843670370534426 +2024-09-23 05:00:00,3,27.476697510681152,28.201600418237547,69.6502114335855 +2024-09-23 06:00:00,3,25.453293766621965,22.961944657352106,53.317088852129075 +2024-09-23 07:00:00,3,17.10968351453592,25.460361292363547,50.223210622288796 +2024-09-23 08:00:00,3,41.108788693137086,24.462924310665453,56.22003342284515 +2024-09-23 09:00:00,3,16.197924204769528,26.65256315749959,46.39629193121764 +2024-09-23 10:00:00,3,20.279446966517977,27.97948459851767,39.153533523212616 +2024-09-23 11:00:00,3,7.124834173567468,20.53888415518069,49.534623063275916 +2024-09-23 12:00:00,3,9.821410863648987,25.004987928367463,47.40656754082028 +2024-09-23 13:00:00,3,20.100738823614982,27.50047577128432,43.40791195595744 +2024-09-23 14:00:00,3,39.75019484674703,27.050565103402473,51.91047014442466 +2024-09-23 15:00:00,3,16.440280930947758,30.404615649741164,41.23365682830266 +2024-09-23 16:00:00,3,29.783499605712898,23.343530907251843,53.257137363707294 +2024-09-23 17:00:00,3,36.48468629935721,27.910912215153232,60.78300483351844 +2024-09-23 18:00:00,3,28.701546946395894,21.362780000515876,50.74745259302854 +2024-09-23 19:00:00,3,16.942563895053844,25.307308769389167,57.582831910949174 +2024-09-23 20:00:00,3,36.531142461809964,19.946971932935536,52.712061815531236 +2024-09-23 21:00:00,3,13.148965041229973,24.695318551638817,51.65321787491088 +2024-09-23 22:00:00,3,28.870184397304392,24.886341305159657,74.47477499728633 +2024-09-23 23:00:00,3,19.42470520993743,19.429308596426317,55.48344918775614 +2024-09-24 00:00:00,3,13.241960743937833,20.26057841987959,56.3149026911498 +2024-09-24 01:00:00,3,18.54111214551645,27.68629869572906,51.058034136960686 +2024-09-24 02:00:00,3,35.13664448704888,25.13662412614167,64.2544062307663 +2024-09-24 03:00:00,3,22.935894455586286,21.74165456226188,60.751516682033596 +2024-09-24 04:00:00,3,29.71497754541721,26.783752597252374,69.4108261655703 +2024-09-24 05:00:00,3,32.07564789534457,24.94171048234385,56.23738083418929 +2024-09-24 06:00:00,3,34.172608109368475,24.313327689471112,52.2489448669525 +2024-09-24 07:00:00,3,21.184861206021207,20.475742521319926,53.89316601781657 +2024-09-24 08:00:00,3,31.043710870535786,18.37486484972193,36.919902099256646 +2024-09-24 09:00:00,3,31.11961669303878,27.133210250849892,50.30549179419119 +2024-09-24 10:00:00,3,24.876236270993143,21.08298769587294,42.66079688417736 +2024-09-24 11:00:00,3,36.10018974541434,24.41249934283461,52.946281943391526 +2024-09-24 12:00:00,3,15.083029331195041,30.98818894652796,49.63742147646189 +2024-09-24 13:00:00,3,30.254146654215432,20.008293094795093,70.37452021610915 +2024-09-24 14:00:00,3,26.87198267930708,28.97713642499493,69.51109130725614 +2024-09-24 15:00:00,3,48.0404318394787,26.96304914992533,56.22601925661111 +2024-09-24 16:00:00,3,24.77381383372228,19.23133449112406,52.89149262860871 +2024-09-24 17:00:00,3,8.987747995573272,24.90177923219806,50.10523425742725 +2024-09-24 18:00:00,3,37.35811520581897,27.147582524372034,52.044237966420575 +2024-09-24 19:00:00,3,7.4913407741112295,22.410325875834953,42.612152143057024 +2024-09-24 20:00:00,3,34.572400859768464,28.17970371951003,54.11316753057715 +2024-09-24 21:00:00,3,30.181267205380784,17.82907877794137,72.61022016891414 +2024-09-24 22:00:00,3,25.832618780681155,23.71101575009723,55.449329027029655 +2024-09-24 23:00:00,3,25.708918532972184,25.287310357174235,52.231831558558234 +2024-09-25 00:00:00,3,25.52326469201251,26.13648265176862,51.31410799940995 +2024-09-25 01:00:00,3,21.92137910047459,27.7940617214326,62.529199687510044 +2024-09-25 02:00:00,3,25.56356125641934,26.439323304132063,56.750121700710515 +2024-09-25 03:00:00,3,35.726531218752655,24.08667642489651,56.594436768215004 +2024-09-25 04:00:00,3,23.065207546546787,28.197225398906355,66.49266463624876 +2024-09-25 05:00:00,3,31.259554651871465,24.910490628655705,61.010378135188546 +2024-09-25 06:00:00,3,9.09714064830499,29.021151600330377,54.49540750091305 +2024-09-25 07:00:00,3,25.817455127351785,23.728396523354103,52.21511530027765 +2024-09-25 08:00:00,3,36.980857105639316,26.40689163947252,57.17499884597792 +2024-09-25 09:00:00,3,19.475576241133897,24.618754887656312,56.700830393153765 +2024-09-25 10:00:00,3,22.056172520719485,22.581002028822546,42.725774137282954 +2024-09-25 11:00:00,3,16.48828205986043,25.766577593614763,61.28400681950961 +2024-09-25 12:00:00,3,21.908757678819452,27.68524097175083,57.918568102847416 +2024-09-25 13:00:00,3,17.369835226783053,22.881294714913345,43.65596755329162 +2024-09-25 14:00:00,3,10.516278365432228,22.967553186751402,53.811907661022126 +2024-09-25 15:00:00,3,29.138946946234448,28.801338901887277,64.42486000128069 +2024-09-25 16:00:00,3,19.31687637947086,29.29759295997608,63.476546231850286 +2024-09-25 17:00:00,3,28.085248384012807,27.078870414595986,56.60604778047219 +2024-09-25 18:00:00,3,36.970592433281,21.13088872085588,59.3015701073903 +2024-09-25 19:00:00,3,27.48057899774811,21.20844992597315,44.96046943685641 +2024-09-25 20:00:00,3,7.554893012549542,24.53970959659745,57.54068820377696 +2024-09-25 21:00:00,3,17.267035694040676,21.9227935515209,46.965282567983905 +2024-09-25 22:00:00,3,19.827625053984566,23.52020215590356,48.907861761853084 +2024-09-25 23:00:00,3,31.08529684495939,25.18005256191112,64.14410397824544 +2024-09-26 00:00:00,3,14.377386918942133,27.224514872882093,56.150152896367366 +2024-09-26 01:00:00,3,30.063994990976795,35.65783271488723,68.22660322255832 +2024-09-26 02:00:00,3,24.26097838767358,21.11638255946437,64.74686183611384 +2024-09-26 03:00:00,3,9.323781100198257,28.639553838924208,45.43529088793146 +2024-09-26 04:00:00,3,20.599747754102236,27.799432324528546,62.31359005767992 +2024-09-26 05:00:00,3,28.049064225972437,21.757571282277524,54.48322106302769 +2024-09-26 06:00:00,3,44.6119442706982,21.020971240561177,53.62017729905814 +2024-09-26 07:00:00,3,40.27339828698081,27.75004545067307,52.79767936113683 +2024-09-26 08:00:00,3,16.31084380817608,23.6375705443867,47.17382700580655 +2024-09-26 09:00:00,3,16.12999022558433,25.280944363596703,48.25621822682747 +2024-09-26 10:00:00,3,14.873486517854454,24.322074275865564,64.10357501303434 +2024-09-26 11:00:00,3,26.22770303338133,22.040509127837076,51.5252222956853 +2024-09-26 12:00:00,3,22.777716327293092,26.793542042413875,47.506708413182764 +2024-09-26 13:00:00,3,17.93279879970636,22.312290949709965,60.3336912514613 +2024-09-26 14:00:00,3,26.31919699647282,24.782926907311033,53.588120235651225 +2024-09-26 15:00:00,3,21.81750731920613,31.956809219547758,42.04639891468267 +2024-09-26 16:00:00,3,10.724168543096853,27.177809274376266,58.153496396317124 +2024-09-26 17:00:00,3,29.123500769607197,27.958870242531248,69.98721248182684 +2024-09-26 18:00:00,3,24.46795100838052,26.985149169011322,54.080911430650836 +2024-09-26 19:00:00,3,12.737607146501956,21.082479255012487,57.05256575642057 +2024-09-26 20:00:00,3,24.517944434817547,18.268542424439982,58.73536432657007 +2024-09-26 21:00:00,3,22.186109239336197,19.738248127916005,56.41423747816164 +2024-09-26 22:00:00,3,30.99338641718412,18.635193686109183,51.328357599687045 +2024-09-26 23:00:00,3,22.507766323190626,18.479363898294395,70.96844359293708 +2024-09-27 00:00:00,3,16.012385746325187,23.16445725538664,60.0983792953517 +2024-09-27 01:00:00,3,27.239799273787092,25.902826756609905,40.86449648222157 +2024-09-27 02:00:00,3,29.588343114733554,24.73087534721906,66.82032705170914 +2024-09-27 03:00:00,3,28.087570485350625,27.146077411116327,58.23074018167008 +2024-09-27 04:00:00,3,32.108927240784844,19.591742323619652,72.54547942230066 +2024-09-27 05:00:00,3,14.103065872741068,24.30810943895342,64.37099872175699 +2024-09-27 06:00:00,3,30.952762252654075,25.201419840980073,66.23956116769602 +2024-09-27 07:00:00,3,26.90734183109034,29.524868912634474,57.45498242251734 +2024-09-27 08:00:00,3,12.74289529290709,29.07740600153474,52.015107225820515 +2024-09-27 09:00:00,3,35.142725949582754,23.204770227702785,49.46437789949056 +2024-09-27 10:00:00,3,32.357691932713045,24.98742674847922,47.002287657877474 +2024-09-27 11:00:00,3,16.562917951774143,27.124187191917272,56.38510633943083 +2024-09-27 12:00:00,3,28.81710740818802,26.464662988866415,48.966092324971385 +2024-09-27 13:00:00,3,16.33653243287531,23.92763403780659,38.767473454268526 +2024-09-27 14:00:00,3,14.935416860970303,20.493076939051985,44.012269714481356 +2024-09-27 15:00:00,3,26.774235099120276,27.797993349095186,64.10849029966217 +2024-09-27 16:00:00,3,30.345006010710094,17.9846847909399,45.0160578692382 +2024-09-27 17:00:00,3,16.241012062198088,28.547389177084124,67.29136514312849 +2024-09-27 18:00:00,3,8.654634705117672,20.312647768934557,34.49799290293872 +2024-09-27 19:00:00,3,29.221997949429294,22.039374226261863,42.91464691389734 +2024-09-27 20:00:00,3,28.263779572534464,26.832053724322336,60.00619648190683 +2024-09-27 21:00:00,3,14.373319101454163,23.642450524065712,46.09291610167393 +2024-09-27 22:00:00,3,25.215973879462805,25.363698928803853,49.71168496641981 +2024-09-27 23:00:00,3,16.941007899905035,22.991776897093022,44.3894390562372 +2024-09-28 00:00:00,3,30.625629416585628,26.106563608302668,54.46662500484246 +2024-09-28 01:00:00,3,39.13359758885545,26.10814396351959,59.83416360886058 +2024-09-28 02:00:00,3,20.000180339763453,17.509874482299224,64.52905108171979 +2024-09-28 03:00:00,3,25.329477221774095,31.915504395692118,62.70346725621819 +2024-09-28 04:00:00,3,16.77996588905369,27.796830644732054,63.20056323726587 +2024-09-28 05:00:00,3,40.45643401254739,22.77740999855917,51.13829592566025 +2024-09-28 06:00:00,3,10.783639495987671,27.58089838193564,64.21035055468705 +2024-09-28 07:00:00,3,36.210685563196,29.77926056737543,59.90839694830376 +2024-09-28 08:00:00,3,37.98750847048391,27.41702152729698,58.14323960414535 +2024-09-28 09:00:00,3,19.900051745182356,20.589768529755098,73.07397385133343 +2024-09-28 10:00:00,3,25.927565181180746,24.64519557122794,46.37861667972615 +2024-09-28 11:00:00,3,21.49997059652786,24.99803757464315,61.60759771558683 +2024-09-28 12:00:00,3,23.737956338341117,24.798200635745705,51.21687000074034 +2024-09-28 13:00:00,3,28.774100603414528,26.183192865603868,51.77888693018338 +2024-09-28 14:00:00,3,32.17915628277095,22.59312321410249,48.30955427910281 +2024-09-28 15:00:00,3,40.47518695768486,25.60794142316654,55.16474328490169 +2024-09-28 16:00:00,3,31.552991900703905,23.95606107163936,48.108228446500384 +2024-09-28 17:00:00,3,41.84594948545749,26.893499072206744,48.86940327971071 +2024-09-28 18:00:00,3,13.477888665647844,22.30319053949666,60.02117669017119 +2024-09-28 19:00:00,3,11.214024104224109,24.307226481627566,49.24522717952484 +2024-09-28 20:00:00,3,16.18038051369688,28.01306928050903,55.29550260317641 +2024-09-28 21:00:00,3,32.6378736344647,22.17393245669325,51.95900012213418 +2024-09-28 22:00:00,3,27.708072474654582,20.843925911118504,63.7126812493867 +2024-09-28 23:00:00,3,26.010463860384174,23.300864745216845,59.53602468199119 +2024-09-29 00:00:00,3,32.44484646744136,24.07175842091127,47.08959717466734 +2024-09-29 01:00:00,3,20.133439548492216,31.24266815278348,59.13218845415556 +2024-09-29 02:00:00,3,29.369229960187035,31.836702940111394,79.29300012879139 +2024-09-29 03:00:00,3,31.994110494157532,27.16483506309417,66.20525292467113 +2024-09-29 04:00:00,3,18.81128400231509,19.551450054321062,46.69298556201704 +2024-09-29 05:00:00,3,39.19653280751645,21.75177170800868,58.40367645109235 +2024-09-29 06:00:00,3,21.50216503700951,28.475763221504252,52.50327014572379 +2024-09-29 07:00:00,3,27.070332832549795,24.279575634006243,59.94412120632027 +2024-09-29 08:00:00,3,21.243662005065012,24.592088840313536,47.33967284090012 +2024-09-29 09:00:00,3,34.49851715114823,20.93140535586842,42.78263210578069 +2024-09-29 10:00:00,3,28.462375019905437,25.2284187862871,46.56017661293102 +2024-09-29 11:00:00,3,46.24065609101196,23.47316858351567,54.456528835781285 +2024-09-29 12:00:00,3,39.263612106766026,24.35623455653921,49.300288412089365 +2024-09-29 13:00:00,3,20.947167654088986,25.615666104957697,50.09983405900368 +2024-09-29 14:00:00,3,22.832959938436357,28.72604014967512,47.391482541817425 +2024-09-29 15:00:00,3,33.414613358710795,24.240172390711606,56.13314992089574 +2024-09-29 16:00:00,3,15.995605704707001,31.03900342181428,52.58452243060324 +2024-09-29 17:00:00,3,30.01067161635006,32.573860898897905,51.1991475618569 +2024-09-29 18:00:00,3,26.053307487105805,24.73546517896054,66.21211893464383 +2024-09-29 19:00:00,3,25.34445065784385,24.42999207783581,36.44073082217655 +2024-09-29 20:00:00,3,27.406908833785316,25.6013861366995,70.0217927597722 +2024-09-29 21:00:00,3,18.065050208291897,24.043756477909486,57.924624089568525 +2024-09-29 22:00:00,3,10.463610216003067,23.91676077592745,41.770250421934726 +2024-09-29 23:00:00,3,14.31517079618835,25.393189427515924,52.391116915811885 +2024-09-30 00:00:00,3,33.699148855753634,25.000658116236615,60.977925144390284 +2024-09-30 01:00:00,3,28.532538570629917,26.959147063238536,58.84941908772147 +2024-09-30 02:00:00,3,38.61669872064624,22.03623524890429,69.51610918403073 +2024-09-30 03:00:00,3,21.860503202656233,27.47725804476516,55.8716179420921 +2024-09-30 04:00:00,3,29.353859095186895,26.70966716042494,63.55920557770649 +2024-09-30 05:00:00,3,36.07281084519041,29.363650151682805,52.80604402826514 +2024-09-30 06:00:00,3,35.11010900874504,23.74425075176178,56.228480839662026 +2024-09-30 07:00:00,3,22.644717709606766,19.574901817301193,48.236184530826584 +2024-09-30 08:00:00,3,14.593319793314757,20.843094683180233,59.21714908323612 +2024-09-30 09:00:00,3,27.530797992252076,25.742277246379597,57.26984645467785 +2024-09-30 10:00:00,3,27.542140751175946,20.329857457071583,48.18660871570382 +2024-09-30 11:00:00,3,36.2108686644524,28.196574609613386,50.386508175451674 +2024-09-30 12:00:00,3,35.67367217407989,25.24803461486496,45.2273069931628 +2024-09-30 13:00:00,3,14.194919117983536,29.416524993217738,65.3070664837575 +2024-09-30 14:00:00,3,31.61305661491287,25.824422479515018,67.27818387026397 +2024-09-30 15:00:00,3,13.952568740975476,28.772328808400882,67.01197888998013 +2024-09-30 16:00:00,3,22.97973850259191,22.966049891095572,52.78513773056372 +2024-09-30 17:00:00,3,14.45158670393691,29.716780038365812,52.653600665090345 +2024-09-30 18:00:00,3,20.41067645941632,27.786126512132036,57.58280175895073 +2024-09-30 19:00:00,3,28.530810561007588,29.7071078410805,50.0520017652579 +2024-09-30 20:00:00,3,25.34510546045734,30.087689990227936,53.58098785345322 +2024-09-30 21:00:00,3,23.41175422690656,22.066770131785283,52.091926283406444 +2024-09-30 22:00:00,3,13.012665439781784,23.561953505050983,48.754035710438444 +2024-09-30 23:00:00,3,33.76206539075079,18.679714138601675,54.606351057620934 +2024-10-01 00:00:00,3,28.027299651515175,29.18545931415036,52.527042191624616 +2024-10-01 01:00:00,3,22.422390105996033,26.814972736750065,52.59927892813288 +2024-10-01 02:00:00,3,17.240401240177686,24.041377700512335,59.781775490046556 +2024-10-01 03:00:00,3,34.76663207497389,24.312958855276175,52.846939311803396 +2024-10-01 04:00:00,3,18.66216138198714,27.12041017800179,62.85287299924622 +2024-10-01 05:00:00,3,21.637034241258714,23.46235044550603,62.48710870229267 +2024-10-01 06:00:00,3,23.36620253723468,21.86832887949203,56.471991245739076 +2024-10-01 07:00:00,3,35.21494437103524,25.53320915262909,44.0203624821166 +2024-10-01 08:00:00,3,21.67430233663815,24.179856938673847,59.69509112270054 +2024-10-01 09:00:00,3,44.2781829950418,20.989582579068276,57.52168692260548 +2024-10-01 10:00:00,3,8.049019992013069,22.542870871365484,28.61941951957657 +2024-10-01 11:00:00,3,29.29041522898243,27.072677866272564,55.035135850679225 +2024-10-01 12:00:00,3,30.515814826596078,28.0780605673232,55.389609581859126 +2024-10-01 13:00:00,3,20.137992919192826,23.731180811063272,63.88004422747632 +2024-10-01 14:00:00,3,26.972935544623905,23.758966415494797,52.733492074436484 +2024-10-01 15:00:00,3,26.65289962962397,23.79661595847437,42.29887221166047 +2024-10-01 16:00:00,3,30.591394690169228,24.746573470972006,56.81107626153883 +2024-10-01 17:00:00,3,26.776206976996566,23.492903679962474,44.51415672963881 +2024-10-01 18:00:00,3,33.69772827569994,14.933610977794201,52.39878020817467 +2024-10-01 19:00:00,3,26.185846516501417,26.051475594444074,51.238765335067484 +2024-10-01 20:00:00,3,21.86784801146538,24.04231646453484,59.08514038484078 +2024-10-01 21:00:00,3,19.577493314791397,25.199942798945813,52.60793270449966 +2024-10-01 22:00:00,3,30.85094445462364,25.921982654036256,46.10428888844134 +2024-10-01 23:00:00,3,33.235202736817484,25.50685636621314,58.546498489666384 +2024-10-02 00:00:00,3,32.744639729658935,26.726749674726218,44.979047147167 +2024-10-02 01:00:00,3,49.017526661542306,26.843987405350706,62.21657422839025 +2024-10-02 02:00:00,3,26.896839039422073,23.035415794050532,67.21642329218344 +2024-10-02 03:00:00,3,49.83334529229575,23.449089498615233,62.945375668573675 +2024-10-02 04:00:00,3,32.24035677656141,25.673831818605496,52.52879065801894 +2024-10-02 05:00:00,3,21.155259121053994,22.505111440781228,54.0870862580574 +2024-10-02 06:00:00,3,22.8148180970876,24.091020917009487,59.27389262388144 +2024-10-02 07:00:00,3,20.611430437492658,25.342786305165824,63.19756424611185 +2024-10-02 08:00:00,3,11.67833026774651,20.50835174740004,63.23985893016466 +2024-10-02 09:00:00,3,43.79661595667351,22.850460011720415,43.89706895408742 +2024-10-02 10:00:00,3,35.71038818436947,19.134043296715003,46.70901896717713 +2024-10-02 11:00:00,3,43.02863579827601,22.797650875879214,38.47468368680894 +2024-10-02 12:00:00,3,25.02403063696326,25.84041429055488,61.94443336991982 +2024-10-02 13:00:00,3,22.54768201998012,30.345932979414805,62.648823793820334 +2024-10-02 14:00:00,3,40.82239954300424,27.317829638704325,51.8738714039851 +2024-10-02 15:00:00,3,30.67617328908702,23.82276701985829,54.56202084039561 +2024-10-02 16:00:00,3,13.613500230409718,26.628466542805686,54.48468368031099 +2024-10-02 17:00:00,3,25.54203633351666,25.66586256278255,61.56228964158149 +2024-10-02 18:00:00,3,20.244997002607555,17.932418449676177,68.10943018624647 +2024-10-02 19:00:00,3,41.81466991289892,22.191385426556863,56.64341088155519 +2024-10-02 20:00:00,3,16.347259030936506,28.220287843681792,49.41126541372748 +2024-10-02 21:00:00,3,47.43427995941809,17.282663388645943,65.1133801498481 +2024-10-02 22:00:00,3,26.346726882938896,22.25842478291822,55.050989540727684 +2024-10-02 23:00:00,3,23.241319035294318,19.799516444711244,53.91270762178318 +2024-10-03 00:00:00,3,38.67396112696326,21.30992445995867,70.80290702489643 +2024-10-03 01:00:00,3,22.450230625461842,25.635833460212055,63.595211925420806 +2024-10-03 02:00:00,3,17.23585579334972,30.44487795729913,51.731069810728584 +2024-10-03 03:00:00,3,24.94576897756453,24.74849270220621,54.62835806748615 +2024-10-03 04:00:00,3,30.302145916175242,24.522982559778207,58.02851823714014 +2024-10-03 05:00:00,3,28.211884732903417,24.089450217783643,50.82382269288344 +2024-10-03 06:00:00,3,33.133951688387086,23.070030029072953,63.26980875788036 +2024-10-03 07:00:00,3,38.260207268964436,24.658510696631723,51.89140076865166 +2024-10-03 08:00:00,3,20.520434112521897,20.038740333430162,62.50604269692932 +2024-10-03 09:00:00,3,34.80515997055108,25.60430110027728,59.938314523703795 +2024-10-03 10:00:00,3,22.507379216490328,24.24656359677902,42.35718993444809 +2024-10-03 11:00:00,3,11.995178901800191,25.6842950864135,54.559640083953276 +2024-10-03 12:00:00,3,34.35959591107489,22.71488624196434,55.90629054515074 +2024-10-03 13:00:00,3,24.81925465355138,22.328926914943594,51.86408202760848 +2024-10-03 14:00:00,3,21.60589899659986,24.700409351331206,37.85340063262788 +2024-10-03 15:00:00,3,19.258648406342317,26.893336008130834,55.97257255425338 +2024-10-03 16:00:00,3,26.112344114118113,26.033133699723244,67.58270328981511 +2024-10-03 17:00:00,3,25.201865983795354,21.11911645479817,71.34497979240726 +2024-10-03 18:00:00,3,26.743083660428738,26.100081534269634,62.70819362608171 +2024-10-03 19:00:00,3,26.29616252288215,29.415216780335037,42.52520601367533 +2024-10-03 20:00:00,3,34.68817775670642,24.8953950083576,56.83026666711745 +2024-10-03 21:00:00,3,20.5689977463721,24.119388631377657,59.67805542779935 +2024-10-03 22:00:00,3,34.79203825330502,21.75993880429158,57.70412354208179 +2024-10-03 23:00:00,3,11.55721625949737,20.670933677088296,58.43905373825941 +2024-10-04 00:00:00,3,12.74482168967248,25.57312022563437,63.23898748436266 +2024-10-04 01:00:00,3,31.321120291565936,26.461496407648468,59.96103518100564 +2024-10-04 02:00:00,3,35.79554463099365,24.70549022294641,73.9412916128644 +2024-10-04 03:00:00,3,33.250162718357096,18.175222678715215,61.816234430504004 +2024-10-04 04:00:00,3,29.781750229496815,25.5350138853249,61.56217599847871 +2024-10-04 05:00:00,3,36.852223642138426,18.551967873960375,54.39617506480654 +2024-10-04 06:00:00,3,45.74057184827545,26.131719402738458,60.779277485195514 +2024-10-04 07:00:00,3,28.989719347314093,25.321722164194576,75.30739074139476 +2024-10-04 08:00:00,3,14.900463554979387,23.092408797846147,64.1970676407106 +2024-10-04 09:00:00,3,37.667407863698045,25.66258839761552,44.27617541792861 +2024-10-04 10:00:00,3,33.97993360003482,19.228037676902254,39.39056293796827 +2024-10-04 11:00:00,3,21.56488372114113,24.2929740964236,48.36914421198386 +2024-10-04 12:00:00,3,25.213978167936286,27.055003827917208,52.21744403977146 +2024-10-04 13:00:00,3,21.128759102275527,31.75428498111669,71.88249350170803 +2024-10-04 14:00:00,3,18.725525076307903,21.72671292548104,49.7818111119408 +2024-10-04 15:00:00,3,22.828007079653133,26.351970933822837,55.41388172600205 +2024-10-04 16:00:00,3,18.657102951459365,25.860354920908616,45.37413700335676 +2024-10-04 17:00:00,3,25.74882624216235,23.177757468802316,46.22912993866244 +2024-10-04 18:00:00,3,32.74417537658581,21.75160603250943,48.26637986909125 +2024-10-04 19:00:00,3,15.342443179858039,23.992914753492602,54.07406407303283 +2024-10-04 20:00:00,3,32.805246071023895,24.810216115626563,54.48974964471506 +2024-10-04 21:00:00,3,44.183396143010384,21.073811889495172,63.33438726289022 +2024-10-04 22:00:00,3,26.455866171993204,22.800625176621427,58.38784871088774 +2024-10-04 23:00:00,3,22.92580618744707,16.810485542257744,79.98334005359685 +2024-10-05 00:00:00,3,42.84054498049748,27.532551238591402,52.13559728531363 +2024-10-05 01:00:00,3,24.27851101142922,24.26245858386468,66.59401598615479 +2024-10-05 02:00:00,3,38.76600992312497,28.66405848131277,58.95220497282341 +2024-10-05 03:00:00,3,29.023933993848676,26.04385601110495,61.40054707287787 +2024-10-05 04:00:00,3,26.39967018063544,26.37852295315007,57.287470200287956 +2024-10-05 05:00:00,3,17.21703376113362,21.890556743974482,56.65508023162369 +2024-10-05 06:00:00,3,33.52022041768491,24.937257539344124,54.55724470701828 +2024-10-05 07:00:00,3,35.21738688692471,26.712011818512757,57.54062186678673 +2024-10-05 08:00:00,3,33.6136406319672,32.293003515660004,44.31154289656179 +2024-10-05 09:00:00,3,54.95732228367335,25.585251727742943,38.30326210407986 +2024-10-05 10:00:00,3,32.342473525307604,25.2596529860904,45.29260305447738 +2024-10-05 11:00:00,3,32.94359801872629,22.56753977309066,54.95432270215998 +2024-10-05 12:00:00,3,21.438246921994725,34.15252024820531,44.625636162418566 +2024-10-05 13:00:00,3,26.132865751905022,24.174242398468788,41.87795472173396 +2024-10-05 14:00:00,3,9.13145429766126,29.76482343346263,55.54550095347502 +2024-10-05 15:00:00,3,39.359954807163135,22.260431348064465,53.032897273799946 +2024-10-05 16:00:00,3,31.628532614596214,29.124934847357967,46.00773890858227 +2024-10-05 17:00:00,3,20.46958140776117,28.248330428545415,39.042057669344814 +2024-10-05 18:00:00,3,36.22975996642345,31.057160323990324,51.37202277841302 +2024-10-05 19:00:00,3,19.099985073120813,23.30138815979236,56.250160483367864 +2024-10-05 20:00:00,3,13.055896102317138,29.49695092059553,40.76585255747585 +2024-10-05 21:00:00,3,19.311505942989463,21.522269748831643,51.53790439831959 +2024-10-05 22:00:00,3,12.485990371336094,23.553193452968497,67.07893162388055 +2024-10-05 23:00:00,3,13.24445863009783,23.48184194161744,62.09536226911972 +2024-10-06 00:00:00,3,33.69311763435894,25.38793253509398,68.34869514318436 +2024-10-06 01:00:00,3,32.322576143828385,17.32128083543525,51.393354659924064 +2024-10-06 02:00:00,3,23.243904568304504,18.90841780512858,67.38057922814009 +2024-10-06 03:00:00,3,26.549007866008484,27.801497006977623,57.79398536131327 +2024-10-06 04:00:00,3,17.91625035611016,22.028342129240873,58.82777481686273 +2024-10-06 05:00:00,3,29.31650236019883,27.004550061717854,59.11943637885934 +2024-10-06 06:00:00,3,12.97094899779625,26.68950537760732,52.35903292231746 +2024-10-06 07:00:00,3,27.415097209351774,25.096323466706394,55.05240553344326 +2024-10-06 08:00:00,3,31.6682191905643,23.665971784165645,55.47360527871825 +2024-10-06 09:00:00,3,48.91454409110967,25.438647385081214,58.515708351094396 +2024-10-06 10:00:00,3,20.660189404891163,21.501491675357027,48.65519992334808 +2024-10-06 11:00:00,3,21.91820741080773,24.12557654570505,31.60500914774759 +2024-10-06 12:00:00,3,29.68166613506238,25.10062870455078,57.122534385332784 +2024-10-06 13:00:00,3,19.51889379839989,21.420120254478636,54.81354813988192 +2024-10-06 14:00:00,3,5.264626625204777,22.57455640639851,42.67681262489231 +2024-10-06 15:00:00,3,30.459389277955793,19.370046969372936,67.17781700059234 +2024-10-06 16:00:00,3,32.875374044120214,24.072135264786088,56.47088091464228 +2024-10-06 17:00:00,3,22.85640148498645,23.685497236796117,46.09379314091053 +2024-10-06 18:00:00,3,23.86624526884858,26.96091201362523,67.97058326391317 +2024-10-06 19:00:00,3,18.869956942653115,26.37082675152527,59.59837377777117 +2024-10-06 20:00:00,3,36.466488276650765,22.511539563393256,59.191582247768665 +2024-10-06 21:00:00,3,21.87948280381901,24.6873407486784,55.50723371374425 +2024-10-06 22:00:00,3,38.54162614790488,24.079617716977573,58.84307644415604 +2024-10-06 23:00:00,3,38.87945855104151,24.95909308826974,61.21000129390668 +2024-10-07 00:00:00,3,36.57198143179365,23.263856909006215,58.172420179685254 +2024-10-07 01:00:00,3,25.67260155421134,27.766723623476498,62.37042552041087 +2024-10-07 02:00:00,3,39.47659450691123,19.325212401987628,45.04136086641352 +2024-10-07 03:00:00,3,34.24072196057167,22.29249317289173,68.20225112743508 +2024-10-07 04:00:00,3,30.04450117744576,21.341389498945293,57.974879059498235 +2024-10-07 05:00:00,3,21.252876696305886,26.761195695749464,47.73227486930614 +2024-10-07 06:00:00,3,29.09502000683439,21.0368038311917,59.69709495739276 +2024-10-07 07:00:00,3,24.66485431021454,26.880487109738596,54.25534494548846 +2024-10-07 08:00:00,3,27.268516944249516,23.957170786373887,54.707534712978514 +2024-10-07 09:00:00,3,15.64805311463556,23.682068008832314,50.25821332988384 +2024-10-07 10:00:00,3,17.811895696748778,20.402559678672613,50.79456902065145 +2024-10-07 11:00:00,3,28.719099562088093,25.70924414324473,66.8281853469518 +2024-10-07 12:00:00,3,31.07884414801642,25.662654774603823,49.40143836376792 +2024-10-07 13:00:00,3,20.723116563013726,27.409812489593712,58.4073449747512 +2024-10-07 14:00:00,3,27.07315725333847,26.372343805494076,50.707762764759444 +2024-10-07 15:00:00,3,37.360076586906494,30.778064194972533,53.048275314168684 +2024-10-07 16:00:00,3,30.04371120756529,23.159751900363293,46.000120720286326 +2024-10-07 17:00:00,3,36.0028445918518,29.126477126908213,56.326112347838695 +2024-10-07 18:00:00,3,24.985219417919883,25.92979726869307,66.1653300777402 +2024-10-07 19:00:00,3,34.58664801272138,33.448281930589914,61.12164822819661 +2024-10-07 20:00:00,3,22.17875182696124,21.733257450303153,66.51996325968935 +2024-10-07 21:00:00,3,17.39171542140562,25.449771336405064,53.98733149183853 +2024-10-07 22:00:00,3,22.32985418005104,20.94458428051218,52.59424429064906 +2024-10-07 23:00:00,3,20.004525467557407,23.463182154201462,69.22195131593588 +2024-10-08 00:00:00,3,21.282422719760632,23.1663265470785,50.320058041585874 +2024-10-08 01:00:00,3,17.793638446409055,17.617320242406603,60.51214582528548 +2024-10-08 02:00:00,3,20.352050897779755,27.162185614955845,60.157219866137865 +2024-10-08 03:00:00,3,16.401692493291012,29.161939480611878,54.84332068130607 +2024-10-08 04:00:00,3,33.73889285512047,22.60760545782236,51.93082719908748 +2024-10-08 05:00:00,3,14.80585975498723,27.47831891376659,37.21350544906033 +2024-10-08 06:00:00,3,19.334364765689234,25.593616757424712,59.81594040516788 +2024-10-08 07:00:00,3,22.313048459896034,27.392214346884582,62.78665856115077 +2024-10-08 08:00:00,3,28.531024264208767,32.60296214569512,51.88000167361311 +2024-10-08 09:00:00,3,31.83012100832956,27.01677975814287,43.87116659099034 +2024-10-08 10:00:00,3,26.780167731392453,22.014961235827084,55.593988569546994 +2024-10-08 11:00:00,3,20.008469395655254,26.676567675060614,63.71967771208368 +2024-10-08 12:00:00,3,25.454021566706057,27.027630995940683,34.38642957949196 +2024-10-08 13:00:00,3,37.41604710549099,25.918916544147365,48.09191549403897 +2024-10-08 14:00:00,3,13.603276609848528,30.52442339523037,53.98952904526711 +2024-10-08 15:00:00,3,30.044583850904914,22.669375752352934,59.60329430659516 +2024-10-08 16:00:00,3,36.31620621846368,14.127215336823452,63.33870390808281 +2024-10-08 17:00:00,3,20.237387322145135,27.40125891594844,63.58283507066915 +2024-10-08 18:00:00,3,17.004345882933613,26.14235279935778,54.59093789588053 +2024-10-08 19:00:00,3,25.02892137230652,23.433049005919756,66.68903012506424 +2024-10-08 20:00:00,3,25.46009327570651,23.438182725776993,48.94401893427929 +2024-10-08 21:00:00,3,11.733511522211007,22.17825252468186,49.084716174829175 +2024-10-08 22:00:00,3,27.86518227322717,24.057116557014034,49.49970425897558 +2024-10-08 23:00:00,3,27.87519702700425,23.175585480079945,45.15180096716345 +2024-10-09 00:00:00,3,24.250381111876138,21.551685012826578,60.79618679559342 +2024-10-09 01:00:00,3,25.685402731534943,27.55951421557855,65.10628786694149 +2024-10-09 02:00:00,3,21.182362693685576,27.84946170764289,74.20439146647092 +2024-10-09 03:00:00,3,26.75089372689007,26.59964379444797,66.08464366686114 +2024-10-09 04:00:00,3,41.71721068936958,30.46108426688613,56.192085320980446 +2024-10-09 05:00:00,3,33.17540893098378,20.79384118755594,53.56366916996557 +2024-10-09 06:00:00,3,29.104606787470235,28.220295266589332,69.62602621093022 +2024-10-09 07:00:00,3,17.10024436972367,25.03513192660439,56.62318482664797 +2024-10-09 08:00:00,3,27.44850955020073,28.81356321135461,46.89922143241945 +2024-10-09 09:00:00,3,19.82926913725168,27.022534174002097,41.24061540141164 +2024-10-09 10:00:00,3,14.200219353554495,20.26113096711733,59.37340071413852 +2024-10-09 11:00:00,3,29.006088252884865,23.95652064421725,62.790810213346646 +2024-10-09 12:00:00,3,21.340985924553024,17.48242948548787,46.5377125189189 +2024-10-09 13:00:00,3,26.98750956112844,22.513185034084643,45.02887435368682 +2024-10-09 14:00:00,3,32.243175984432874,20.30023655667437,47.963638675592314 +2024-10-09 15:00:00,3,25.971329301593435,28.349682984356818,52.41277085282847 +2024-10-09 16:00:00,3,14.545079357647415,27.859613666986885,66.5387464939118 +2024-10-09 17:00:00,3,22.66024859358959,17.700199667505768,60.0585770841954 +2024-10-09 18:00:00,3,34.744445796511556,29.959268780856966,58.52375620549002 +2024-10-09 19:00:00,3,16.847526218634925,27.67039134445617,52.298786814723286 +2024-10-09 20:00:00,3,20.60029518835453,22.0127881554558,56.805812324030015 +2024-10-09 21:00:00,3,19.548801842305927,22.665098919485136,46.563152428423116 +2024-10-09 22:00:00,3,37.2656553183928,16.835979245039972,61.86691114742953 +2024-10-09 23:00:00,3,21.893604361765398,15.777449802401703,54.48764510323354 +2024-10-10 00:00:00,3,35.88635689008048,21.598672445766205,63.70766262998106 +2024-10-10 01:00:00,3,33.6178204044902,24.516587727837194,69.66739695753647 +2024-10-10 02:00:00,3,25.55453916464952,22.61258851974246,50.66101119198517 +2024-10-10 03:00:00,3,21.39726428925628,25.732835944374692,48.190191576008885 +2024-10-10 04:00:00,3,26.06164954263597,23.874865343372413,64.42506174252131 +2024-10-10 05:00:00,3,33.72843464649967,25.667063069204836,70.91314013329558 +2024-10-10 06:00:00,3,37.82138800456117,20.88278088066101,63.15458444120793 +2024-10-10 07:00:00,3,25.498614283280265,25.74987320457189,50.530202208554016 +2024-10-10 08:00:00,3,29.620735409046578,24.035313130925292,54.141584205126996 +2024-10-10 09:00:00,3,42.35857635699578,28.64699384016917,56.15957896545253 +2024-10-10 10:00:00,3,25.260120577684937,29.35088587685794,37.88912050370441 +2024-10-10 11:00:00,3,30.22642578715052,26.42114613682558,61.88353217770164 +2024-10-10 12:00:00,3,35.17061798216615,24.162392045733597,49.77418017398936 +2024-10-10 13:00:00,3,29.016078147038094,18.945730272792808,53.0104755924916 +2024-10-10 14:00:00,3,22.09061735174987,21.667434780486943,52.98420583535354 +2024-10-10 15:00:00,3,22.960856596042277,22.339721416003567,43.337475838132946 +2024-10-10 16:00:00,3,13.448665349008277,26.106921330148896,51.00320958085456 +2024-10-10 17:00:00,3,0.0,26.415316167823377,72.27081955425224 +2024-10-10 18:00:00,3,22.157900196670965,20.87324825739041,51.16313143127641 +2024-10-10 19:00:00,3,30.083096144239825,24.206254629598828,63.21311800094282 +2024-10-10 20:00:00,3,21.119930098427524,29.417051070420847,60.793116343417964 +2024-10-10 21:00:00,3,18.233571671526747,23.47503471476524,56.28505620614159 +2024-10-10 22:00:00,3,15.340450198823401,21.994992384516692,56.11086891825519 +2024-10-10 23:00:00,3,20.127293565136103,22.92401359316889,52.21179212143148 +2024-10-11 00:00:00,3,22.499603457467558,28.313517253342347,67.14021207127163 +2024-10-11 01:00:00,3,32.16264975164406,19.884577428737924,58.31513144435016 +2024-10-11 02:00:00,3,28.547887303084078,26.08988198122208,63.981550503101246 +2024-10-11 03:00:00,3,27.3846948820601,26.19100696652781,53.02336490613432 +2024-10-11 04:00:00,3,26.057803583616725,30.330396786990015,47.87260677321576 +2024-10-11 05:00:00,3,41.86779277903142,25.29186157484725,74.84556141404907 +2024-10-11 06:00:00,3,34.96727693103394,31.457514005271847,53.77416042831065 +2024-10-11 07:00:00,3,28.022591368193094,25.531138490688193,66.50033110250914 +2024-10-11 08:00:00,3,25.222983682204415,23.692052402302128,33.05386066050525 +2024-10-11 09:00:00,3,33.11715523319919,22.15862637150572,39.348440785290705 +2024-10-11 10:00:00,3,37.838905149278986,23.36254243459243,47.03632139833294 +2024-10-11 11:00:00,3,33.636558530675444,21.0380028090755,45.75321579010974 +2024-10-11 12:00:00,3,38.83200022269941,15.943353852452733,55.44067523536997 +2024-10-11 13:00:00,3,29.755641709515732,25.627704082612446,52.23096930451884 +2024-10-11 14:00:00,3,20.551729192454516,21.18147908458833,57.11024860659538 +2024-10-11 15:00:00,3,25.208568044143778,26.220644251824886,52.409747271197766 +2024-10-11 16:00:00,3,32.140348430966036,26.67194706142049,58.381764965661446 +2024-10-11 17:00:00,3,17.359304887160114,26.891215770302264,56.253070885802146 +2024-10-11 18:00:00,3,29.259654630654445,28.9905079076569,56.63479774512447 +2024-10-11 19:00:00,3,43.87023328039149,22.86534817136298,47.757124812844644 +2024-10-11 20:00:00,3,34.482691386989714,26.34047214549834,62.688896081128014 +2024-10-11 21:00:00,3,30.41463524539723,30.671573223236603,53.61618332434959 +2024-10-11 22:00:00,3,32.20837711469984,23.29065286672358,52.77012336522861 +2024-10-11 23:00:00,3,30.75554352814106,18.72102239917326,49.68083605693728 +2024-10-12 00:00:00,3,43.712211034076375,24.693280356681527,55.943193909867404 +2024-10-12 01:00:00,3,40.91140749524538,26.98649564815266,69.25234581268975 +2024-10-12 02:00:00,3,20.61936755939098,23.582678722966232,60.82473257582135 +2024-10-12 03:00:00,3,35.78527808256205,25.735759412118437,55.98755964339947 +2024-10-12 04:00:00,3,32.148049328366284,26.730629862387012,62.78018544524583 +2024-10-12 05:00:00,3,27.59677579039241,20.69929810343836,46.14692542371432 +2024-10-12 06:00:00,3,6.859039112278726,18.997209777868182,61.32313446475657 +2024-10-12 07:00:00,3,52.975968984558946,26.721874798561505,59.15782129545167 +2024-10-12 08:00:00,3,22.94492078412319,26.975033329606923,56.34213725243252 +2024-10-12 09:00:00,3,25.701589455284573,24.500990153262542,43.983529820616866 +2024-10-12 10:00:00,3,22.652413717336387,20.270382627588404,39.45858648460032 +2024-10-12 11:00:00,3,25.43866635398415,22.35167494798532,69.06318964442642 +2024-10-12 12:00:00,3,28.9449392197668,26.253469086620647,53.80726056279812 +2024-10-12 13:00:00,3,16.126696691962227,27.963428592067576,44.451334167074336 +2024-10-12 14:00:00,3,14.756646141038521,27.354208732073285,63.15321377206724 +2024-10-12 15:00:00,3,34.58735130476402,24.61639667850195,44.27869887841605 +2024-10-12 16:00:00,3,33.55161254863684,19.816940124768497,51.87655538551962 +2024-10-12 17:00:00,3,19.48732731114942,24.567805048765845,57.42892043677408 +2024-10-12 18:00:00,3,24.898482759908337,22.744588488276662,62.08436524954942 +2024-10-12 19:00:00,3,21.368158470754377,24.087462247382923,67.78920527451233 +2024-10-12 20:00:00,3,17.500019557622196,26.62791319881121,56.70889126717182 +2024-10-12 21:00:00,3,17.22136964293839,20.676481594260494,51.41765311781724 +2024-10-12 22:00:00,3,23.311609137097694,25.19829817008045,67.62468511173873 +2024-10-12 23:00:00,3,30.410812578920883,25.023845766581047,40.37297961236375 +2024-10-13 00:00:00,3,23.494880725888265,25.628673128886433,60.66675795126229 +2024-10-13 01:00:00,3,20.739816392246226,28.99976181606303,64.3134057119623 +2024-10-13 02:00:00,3,30.0984201391986,30.374511026445603,50.28553531483435 +2024-10-13 03:00:00,3,25.065809361978847,19.022684254253956,48.38530504405328 +2024-10-13 04:00:00,3,26.276059799449868,25.535272205705652,70.84866075183614 +2024-10-13 05:00:00,3,24.715049952878015,23.07609383500111,58.79070233949724 +2024-10-13 06:00:00,3,27.589383634404292,27.15993228167563,49.67488600292655 +2024-10-13 07:00:00,3,24.787642111148227,21.90627796990045,45.648706031375255 +2024-10-13 08:00:00,3,22.69536820418121,23.543582180444364,59.13425351715978 +2024-10-13 09:00:00,3,27.76445709996473,22.27838020655521,45.56510118411178 +2024-10-13 10:00:00,3,39.70012358996063,17.756993046270324,66.34811394709284 +2024-10-13 11:00:00,3,24.954628197078467,24.424583548448933,47.53168287719997 +2024-10-13 12:00:00,3,39.184642212867715,23.561077577777606,43.45035869959096 +2024-10-13 13:00:00,3,20.282090794887864,27.28841397471898,60.874177874567906 +2024-10-13 14:00:00,3,31.731338914400126,22.461444982675946,57.541637220720425 +2024-10-13 15:00:00,3,42.739753232047946,25.56249244815715,39.65865639335034 +2024-10-13 16:00:00,3,25.145921162343846,26.97905557823596,49.402228351429635 +2024-10-13 17:00:00,3,19.73874660709867,23.729052536633173,48.10228320101706 +2024-10-13 18:00:00,3,34.39835742559285,26.55685452822021,59.17350937524508 +2024-10-13 19:00:00,3,29.458273130697464,17.986733250847934,59.74102099189846 +2024-10-13 20:00:00,3,19.44218327169552,23.59922545971689,48.11482916493185 +2024-10-13 21:00:00,3,34.07213724747017,27.393021940826227,41.89803090381636 +2024-10-13 22:00:00,3,19.02542897427905,23.524050925136322,43.92811056037929 +2024-10-13 23:00:00,3,21.91842647539389,21.723209201766768,52.41521779486185 +2024-10-14 00:00:00,3,26.642365179749582,25.28858160774294,65.80741280532136 +2024-10-14 01:00:00,3,24.461657791455952,23.857274708564432,66.2876960938653 +2024-10-14 02:00:00,3,18.379146016398252,21.4352557852545,68.42345837338382 +2024-10-14 03:00:00,3,30.036320902902162,24.16766806862382,53.09867437689963 +2024-10-14 04:00:00,3,28.481622540200615,25.858643095777282,44.80290453344422 +2024-10-14 05:00:00,3,22.542291671085735,20.16816079012958,66.46757330652423 +2024-10-14 06:00:00,3,33.42356624373919,26.429164286667962,49.4681191693396 +2024-10-14 07:00:00,3,14.427208325591273,26.62693732586239,65.91473327783032 +2024-10-14 08:00:00,3,28.864104852999468,23.07897045461232,59.3475643213197 +2024-10-14 09:00:00,3,26.592858332061212,21.27517014763511,55.91317504497069 +2024-10-14 10:00:00,3,11.818801069894045,17.831573565441825,52.91174891814227 +2024-10-14 11:00:00,3,27.616076355436768,28.490246689905135,56.21169562556453 +2024-10-14 12:00:00,3,16.897814861423868,24.824991033830536,56.990523527942166 +2024-10-14 13:00:00,3,20.900076261864374,29.442698638156322,39.633733752687135 +2024-10-14 14:00:00,3,46.0926861716525,30.798630017747826,50.03711135427311 +2024-10-14 15:00:00,3,26.191272740629177,23.762114958533488,56.58031303004171 +2024-10-14 16:00:00,3,39.76477306682267,24.76858014448195,56.333750898257115 +2024-10-14 17:00:00,3,31.717483896980365,23.44766031909941,55.927482210153 +2024-10-14 18:00:00,3,24.399646684055345,20.40706346331497,59.39327509612321 +2024-10-14 19:00:00,3,38.3598192307643,23.34261769086771,43.84791944765407 +2024-10-14 20:00:00,3,26.46303574151254,25.970400853054052,72.20905359744962 +2024-10-14 21:00:00,3,23.108124701316243,18.54263351533234,46.56364706216032 +2024-10-14 22:00:00,3,19.84252213967662,27.096340986371395,31.60927850239087 +2024-10-14 23:00:00,3,14.557709448669955,30.970280136904407,51.670879508768664 +2024-10-15 00:00:00,3,35.06375084853004,27.585569068854873,52.12674062575441 +2024-10-15 01:00:00,3,28.128189027196722,28.256194550339863,65.38372374815393 +2024-10-15 02:00:00,3,20.17827926841484,28.733844347788246,60.86260671975058 +2024-10-15 03:00:00,3,20.410567370119548,25.460297763179742,59.36140150790402 +2024-10-15 04:00:00,3,30.879726016689045,26.460985146788964,76.56862230913373 +2024-10-15 05:00:00,3,23.940714209308858,23.968959460743648,56.70305109730064 +2024-10-15 06:00:00,3,14.809653721079352,23.31797243581284,50.470655885597424 +2024-10-15 07:00:00,3,23.125451947598417,22.277142165657803,49.20261463976429 +2024-10-15 08:00:00,3,24.775073010066794,27.74736901068973,40.00411337395369 +2024-10-15 09:00:00,3,5.7931776787440725,21.944925278199985,44.72085390412742 +2024-10-15 10:00:00,3,38.47137940931511,23.74893165261792,51.471954039363986 +2024-10-15 11:00:00,3,30.104937510120823,24.166158604535504,43.12120058816954 +2024-10-15 12:00:00,3,38.15463347456564,24.339442888668167,51.76896846359012 +2024-10-15 13:00:00,3,28.65324276387363,32.84896880820341,36.37822600826756 +2024-10-15 14:00:00,3,8.612522891314232,26.577193640550632,55.426486168891536 +2024-10-15 15:00:00,3,19.206990882095024,31.49014855717978,49.3529954740357 +2024-10-15 16:00:00,3,23.48329083131565,27.76498051031891,60.01000365461495 +2024-10-15 17:00:00,3,23.96484008569011,26.3427080354989,50.59038624453335 +2024-10-15 18:00:00,3,24.966768079120307,21.36658145382415,56.08418456123124 +2024-10-15 19:00:00,3,26.274212876923716,23.34249294156631,64.6065914179279 +2024-10-15 20:00:00,3,34.41148792409609,23.67225955229861,53.80372320755311 +2024-10-15 21:00:00,3,31.230281667883546,15.939083031096162,49.76653235241595 +2024-10-15 22:00:00,3,20.63332182424803,20.97091039161522,64.80292685259312 +2024-10-15 23:00:00,3,29.892211648917566,22.719682285740962,49.39490363504135 +2024-10-16 00:00:00,3,32.1965271260377,32.8035517147101,58.09819526373731 +2024-10-16 01:00:00,3,19.200467182565795,20.773812012091643,70.04422192742129 +2024-10-16 02:00:00,3,31.43591234823775,23.393014988865847,53.90334443282874 +2024-10-16 03:00:00,3,18.456428187944702,28.53662094084674,56.793674845390456 +2024-10-16 04:00:00,3,25.848059810592744,27.67292730919828,49.034752733492795 +2024-10-16 05:00:00,3,12.833718608090136,28.68576495634187,51.29718688290596 +2024-10-16 06:00:00,3,43.86029245592162,22.178105661506017,50.42026124795569 +2024-10-16 07:00:00,3,25.312100058462097,22.036717212224644,44.03714226422481 +2024-10-16 08:00:00,3,17.04292628724608,20.890981532937467,54.51163455339017 +2024-10-16 09:00:00,3,29.824428400290728,21.162090010447177,64.47679347025749 +2024-10-16 10:00:00,3,30.103274018848825,24.388011368591147,56.01095987328083 +2024-10-16 11:00:00,3,28.408323916790422,23.934379075150662,39.14905027929834 +2024-10-16 12:00:00,3,30.700143822838132,25.395084836915967,64.32554950410255 +2024-10-16 13:00:00,3,13.800992086511354,27.79355511135864,48.88880654192184 +2024-10-16 14:00:00,3,20.028526130815237,22.144208656075175,52.44132846703291 +2024-10-16 15:00:00,3,10.871744131817408,24.745482865674866,48.710483714956375 +2024-10-16 16:00:00,3,23.179661332098142,29.302507768165828,49.026328612691835 +2024-10-16 17:00:00,3,31.734225003662,24.875119136980203,68.11228942274974 +2024-10-16 18:00:00,3,25.599819137927962,20.180495759686508,61.46759226424417 +2024-10-16 19:00:00,3,20.200145256650565,22.847753754207442,56.445574048134084 +2024-10-16 20:00:00,3,28.73816642793167,19.944769452301625,60.46729079900285 +2024-10-16 21:00:00,3,28.555317278705683,28.123487148571694,64.53242860019338 +2024-10-16 22:00:00,3,12.260171707731836,14.155078784144225,57.86844402861133 +2024-10-16 23:00:00,3,40.82436429885838,16.67276133148731,62.25244213078109 +2024-10-17 00:00:00,3,37.36955664983567,27.282919890857073,59.41622013855135 +2024-10-17 01:00:00,3,38.511154476143645,29.24880741763686,70.39031988307568 +2024-10-17 02:00:00,3,41.64856160693189,24.200054321730835,76.29307790194363 +2024-10-17 03:00:00,3,50.820710147723496,26.616285395320414,66.60304049029506 +2024-10-17 04:00:00,3,31.306296188763973,17.43971302860291,54.724583633591536 +2024-10-17 05:00:00,3,27.151476060139906,24.17280972585747,52.03494981871723 +2024-10-17 06:00:00,3,31.596203274001503,19.630739159076896,49.88475674832009 +2024-10-17 07:00:00,3,37.85620138915201,28.00481859223463,60.415435527196195 +2024-10-17 08:00:00,3,43.01190621122805,29.997600548365696,49.27024419864646 +2024-10-17 09:00:00,3,26.95471281445531,17.41097838547759,54.12697691501859 +2024-10-17 10:00:00,3,28.681666529693796,26.625896774422984,56.930878947412985 +2024-10-17 11:00:00,3,32.51333605437363,24.158025108222127,46.08691982663896 +2024-10-17 12:00:00,3,26.531291788081745,22.16448936122105,41.777253997330504 +2024-10-17 13:00:00,3,32.0784384408581,22.06477507521803,46.288270969156635 +2024-10-17 14:00:00,3,29.299119711650622,26.015258174728682,63.37812162576569 +2024-10-17 15:00:00,3,28.30402812322002,27.85825726658512,51.74820245136467 +2024-10-17 16:00:00,3,35.20235764883647,23.91374798502648,52.89592594785515 +2024-10-17 17:00:00,3,34.09178894813705,24.492090562224956,41.63479026222974 +2024-10-17 18:00:00,3,23.754243021483678,23.08353204405475,49.09840790869191 +2024-10-17 19:00:00,3,24.55923703973684,26.13661598986903,52.627023804977696 +2024-10-17 20:00:00,3,31.834783518932127,23.876228808350003,64.17027680736305 +2024-10-17 21:00:00,3,22.36160007435466,22.53397286634999,58.049518807368464 +2024-10-17 22:00:00,3,9.992446511295393,17.017568157413006,54.55649971748767 +2024-10-17 23:00:00,3,20.291214021355096,27.313809029176152,43.52581769978791 +2024-10-18 00:00:00,3,25.349890317376463,27.39455088281504,74.8962263886701 +2024-10-18 01:00:00,3,29.088801157770973,20.69480703558072,68.28910301034954 +2024-10-18 02:00:00,3,19.612028544711563,19.269407866309727,74.02946670887162 +2024-10-18 03:00:00,3,24.771400861129365,27.748110817273595,65.8676787726319 +2024-10-18 04:00:00,3,16.210193322483757,26.67493448171742,48.39713559909819 +2024-10-18 05:00:00,3,23.014464422210985,25.41624952010547,44.55044767293113 +2024-10-18 06:00:00,3,36.855830716400106,28.573311486892013,53.55574063731304 +2024-10-18 07:00:00,3,32.28784522450691,27.439893854173466,54.28180137380727 +2024-10-18 08:00:00,3,21.977146498666514,23.111757220966794,56.7712021834557 +2024-10-18 09:00:00,3,24.084701618170783,27.50491743580006,54.69579942969986 +2024-10-18 10:00:00,3,19.403522345281942,29.28710081637613,38.93810200704306 +2024-10-18 11:00:00,3,22.104518657884814,24.543182923410143,48.385736939136244 +2024-10-18 12:00:00,3,26.657175830516692,23.967761116926283,44.400638873599455 +2024-10-18 13:00:00,3,22.73822683969525,25.24682336815079,57.28729305484945 +2024-10-18 14:00:00,3,7.182328827481541,30.49452108037503,61.59502585887641 +2024-10-18 15:00:00,3,34.37339666495174,34.53446798080747,47.0187208721783 +2024-10-18 16:00:00,3,30.89398977365343,21.38796478355263,50.33428867919452 +2024-10-18 17:00:00,3,17.524550882418428,24.661978271166983,51.68652353727394 +2024-10-18 18:00:00,3,34.638345902543236,26.31615175714082,57.48294636613838 +2024-10-18 19:00:00,3,31.515309921468077,28.20080105820727,56.25804619127712 +2024-10-18 20:00:00,3,34.16904503641753,28.55694441285699,60.62259304447315 +2024-10-18 21:00:00,3,20.219699122292248,22.13465168805038,58.631078551614 +2024-10-18 22:00:00,3,28.12002172684327,17.444461005088748,61.45827016766772 +2024-10-18 23:00:00,3,30.073585754812385,17.28517285424902,57.561010576554516 +2024-10-19 00:00:00,3,23.96305992948093,27.07076805287893,58.15667348392317 +2024-10-19 01:00:00,3,11.104189346595088,26.121064900194874,66.84142708196038 +2024-10-19 02:00:00,3,30.623693931400574,23.337280902343682,57.96750071323358 +2024-10-19 03:00:00,3,30.653155686877593,28.535655980135868,64.66012031797688 +2024-10-19 04:00:00,3,29.07371017280866,22.64594883272766,52.06368013881603 +2024-10-19 05:00:00,3,26.705388355908457,28.32253014354641,72.64131798091587 +2024-10-19 06:00:00,3,31.90028205233179,23.500324989601566,60.56071692510231 +2024-10-19 07:00:00,3,13.637329953074476,25.13324545820726,58.65621357103818 +2024-10-19 08:00:00,3,26.757391725861925,20.204824358933468,62.38002850857059 +2024-10-19 09:00:00,3,46.20176825104958,24.886791513290284,58.027658296503134 +2024-10-19 10:00:00,3,28.54782264435334,25.519831989858442,58.70025524437867 +2024-10-19 11:00:00,3,30.368237306459203,22.036454157499367,52.15377418102971 +2024-10-19 12:00:00,3,32.78247413083614,23.22814616112091,54.856736342853345 +2024-10-19 13:00:00,3,55.845729356913765,24.107172069365397,43.368550803653335 +2024-10-19 14:00:00,3,20.694242559755544,17.49639533320758,51.408598418389374 +2024-10-19 15:00:00,3,11.071472427669729,24.829720252761184,42.69995479601132 +2024-10-19 16:00:00,3,35.39246536377716,25.642799120421056,54.8538295281123 +2024-10-19 17:00:00,3,19.682435512217204,29.265995563074277,57.38057717171243 +2024-10-19 18:00:00,3,20.851511710159528,25.116127485182883,43.486575025292524 +2024-10-19 19:00:00,3,15.016145537138872,26.325877889891117,65.24755655037633 +2024-10-19 20:00:00,3,11.23466526528868,14.42583277740584,65.56607455477346 +2024-10-19 21:00:00,3,28.604266955497373,19.370894778551826,60.75736516136077 +2024-10-19 22:00:00,3,15.97646028277805,14.59977201249409,53.131144348587526 +2024-10-19 23:00:00,3,19.246799259379593,21.851819877998025,56.377809618406005 +2024-10-20 00:00:00,3,19.270822652756056,31.487451169882178,58.295895613839505 +2024-10-20 01:00:00,3,39.50633054832528,21.92490260239924,61.998855344077846 +2024-10-20 02:00:00,3,34.22747230507318,23.340003160469927,57.543817859728726 +2024-10-20 03:00:00,3,28.330200332644125,26.59919172196263,60.70260814255503 +2024-10-20 04:00:00,3,18.734562889771624,27.644736267691155,63.97378367169072 +2024-10-20 05:00:00,3,38.623132960600934,17.85363147784432,70.59158963186486 +2024-10-20 06:00:00,3,37.44828223807288,21.174201757760237,51.764126069223565 +2024-10-20 07:00:00,3,33.55663602425282,23.650721942148262,62.44104308666687 +2024-10-20 08:00:00,3,14.328714141794197,19.26617984094223,59.235536576365966 +2024-10-20 09:00:00,3,21.850715408885982,25.26567014275457,46.969658612706816 +2024-10-20 10:00:00,3,13.510050382115805,22.674550209541017,55.332988846473896 +2024-10-20 11:00:00,3,22.295923061592184,25.028222822757773,49.28255450617725 +2024-10-20 12:00:00,3,16.25798691207281,24.57203485002862,47.36262953836587 +2024-10-20 13:00:00,3,16.02647540592603,25.124491430895922,66.10944195572185 +2024-10-20 14:00:00,3,25.673744755966965,27.41181041195512,54.34672153168286 +2024-10-20 15:00:00,3,22.019417320355778,22.833461095133835,75.11124105336998 +2024-10-20 16:00:00,3,30.50117280466219,23.477584599549054,59.75989305953317 +2024-10-20 17:00:00,3,25.129131227964077,25.252959618501535,58.206611603181116 +2024-10-20 18:00:00,3,31.482034920113925,26.008617515888986,52.01477298916276 +2024-10-20 19:00:00,3,34.537495059073365,21.34518209329509,62.574946379421014 +2024-10-20 20:00:00,3,28.14160862745325,22.70487690424772,56.3907249227694 +2024-10-20 21:00:00,3,30.422946068524627,23.25880742538451,67.06574461195572 +2024-10-20 22:00:00,3,17.143584174960605,23.742403723384765,49.55645893134215 +2024-10-20 23:00:00,3,17.330375691280125,24.333632382212947,56.39238284667216 +2024-10-21 00:00:00,3,30.925216191295547,28.144810082732207,58.0863953648821 +2024-10-21 01:00:00,3,33.98445675756612,15.770998741097696,38.02388142404111 +2024-10-21 02:00:00,3,26.722273603798946,22.149650281918856,51.64208816854849 +2024-10-21 03:00:00,3,36.70040405624113,27.590048866924718,54.27628936279278 +2024-10-21 04:00:00,3,16.34921805911749,28.031646835660133,58.16578138772749 +2024-10-21 05:00:00,3,39.45519018970605,25.212830966343247,52.6316730299432 +2024-10-21 06:00:00,3,20.885941331194477,27.892791828569937,46.91946689686888 +2024-10-21 07:00:00,3,29.28399763251851,25.618722694430385,61.41116419593925 +2024-10-21 08:00:00,3,41.794968641818464,18.161161723280042,48.9288532429213 +2024-10-21 09:00:00,3,28.839505731524966,26.45391375206912,46.56501016286898 +2024-10-21 10:00:00,3,21.72693994282062,23.752766438438453,73.56757719278629 +2024-10-21 11:00:00,3,16.314348888262945,23.593617243541978,41.53320234227948 +2024-10-21 12:00:00,3,37.81138820521231,25.17058862830785,67.60555884323124 +2024-10-21 13:00:00,3,21.75306539392034,25.848291581365814,51.568059179264345 +2024-10-21 14:00:00,3,34.83934271145969,23.318675838460805,51.96126682792796 +2024-10-21 15:00:00,3,31.210219558470158,21.770425077240148,43.46441416009562 +2024-10-21 16:00:00,3,38.82300178428915,26.979762370305597,50.836254371503564 +2024-10-21 17:00:00,3,40.84161053047395,29.36621268419599,62.98897748547004 +2024-10-21 18:00:00,3,30.393361131267906,22.521315297184504,48.830263405387925 +2024-10-21 19:00:00,3,9.670937908858624,23.189120639814963,56.725768872126224 +2024-10-21 20:00:00,3,12.055927286933542,27.120594315398584,44.219050092460904 +2024-10-21 21:00:00,3,10.841875207039237,23.409761034680464,69.67288659642068 +2024-10-21 22:00:00,3,14.952003657759542,24.992083089810393,70.66492269748827 +2024-10-21 23:00:00,3,33.525521034055714,17.762757719296204,62.49125941060124 +2024-10-22 00:00:00,3,35.89723548496538,24.3408236073075,55.440746333647894 +2024-10-22 01:00:00,3,36.585378733938555,20.199413245243534,38.188616510098385 +2024-10-22 02:00:00,3,27.1525375355061,29.79840247457691,58.72507318569247 +2024-10-22 03:00:00,3,24.25397736858177,29.029640006503666,63.05068544710147 +2024-10-22 04:00:00,3,39.09168305763882,21.491576573608405,66.67635641062857 +2024-10-22 05:00:00,3,22.81661427381189,28.577391055038284,60.50783009213414 +2024-10-22 06:00:00,3,24.006151627381897,20.485712878383577,39.328466792288594 +2024-10-22 07:00:00,3,29.536096672657205,22.422084227162912,56.97595237600759 +2024-10-22 08:00:00,3,30.693490681022304,27.30845147030461,41.327801435528855 +2024-10-22 09:00:00,3,23.70933865243878,25.357794297419083,51.71077551726223 +2024-10-22 10:00:00,3,24.84801616977197,30.898008860197464,43.05831734117857 +2024-10-22 11:00:00,3,29.77270110737988,34.79830132127585,54.84924454146608 +2024-10-22 12:00:00,3,23.029841387443522,21.94089349726339,58.4153196169991 +2024-10-22 13:00:00,3,19.543151479938388,28.796348041366194,65.6605218343909 +2024-10-22 14:00:00,3,19.67359887299039,28.97303330052985,71.26358814513829 +2024-10-22 15:00:00,3,28.58526245281516,26.601992668424685,54.82826709635262 +2024-10-22 16:00:00,3,27.71034228667452,23.923370473965782,60.791992190254916 +2024-10-22 17:00:00,3,35.461978984805185,31.207300993274714,38.46604882014737 +2024-10-22 18:00:00,3,30.299097727449755,19.784011537163494,41.934329914208654 +2024-10-22 19:00:00,3,28.16380751301135,28.586978678555198,56.09373421120808 +2024-10-22 20:00:00,3,37.433764941505586,21.39828364455562,36.505555449708865 +2024-10-22 21:00:00,3,11.386733694399254,24.07838333928424,71.60032856423464 +2024-10-22 22:00:00,3,13.95896135469499,22.034025187020013,55.69459305363017 +2024-10-22 23:00:00,3,27.89334009749051,27.957797534729274,63.145741028103814 +2024-10-23 00:00:00,3,25.42698274757363,24.61665736383775,65.9785083111248 +2024-10-23 01:00:00,3,24.800646013762154,29.54282428385074,60.38727689709213 +2024-10-23 02:00:00,3,13.545061697944071,24.052272768228875,44.22264402603743 +2024-10-23 03:00:00,3,32.38712426436512,20.40437985785492,43.03259113613649 +2024-10-23 04:00:00,3,28.11292212233274,27.208728954623485,59.73289276649883 +2024-10-23 05:00:00,3,27.8932195541149,19.13963670612329,67.17133051843481 +2024-10-23 06:00:00,3,26.885168069017723,20.15500191589083,63.22993023583697 +2024-10-23 07:00:00,3,25.86537038249697,29.086696740709332,49.933082834617046 +2024-10-23 08:00:00,3,27.597354723598222,24.250888354138667,54.000430292578564 +2024-10-23 09:00:00,3,16.45856770733695,24.95536983989184,53.74174646782389 +2024-10-23 10:00:00,3,22.615647404141285,20.16745508684884,44.62221847651534 +2024-10-23 11:00:00,3,11.320341758864705,27.72946743191648,61.452132283300344 +2024-10-23 12:00:00,3,15.78357428751653,24.06748403690875,54.15592090921585 +2024-10-23 13:00:00,3,21.40102924317833,24.429463211089697,54.4281093671874 +2024-10-23 14:00:00,3,25.107476506879937,26.29837159673496,57.92643684867815 +2024-10-23 15:00:00,3,43.06918925535189,20.848392471214453,55.11359867528004 +2024-10-23 16:00:00,3,25.569964483934104,22.692950980488803,56.47684828938168 +2024-10-23 17:00:00,3,12.798531039490614,25.134811448790373,54.42277369324853 +2024-10-23 18:00:00,3,25.26366973015517,33.25566273404439,48.40139831652023 +2024-10-23 19:00:00,3,7.396722469067594,23.134380628564273,38.78127044755733 +2024-10-23 20:00:00,3,13.453729304453933,22.082083882593395,68.88809238250215 +2024-10-23 21:00:00,3,25.336155607190843,26.190141749401175,58.994696926057756 +2024-10-23 22:00:00,3,31.0192586266297,23.01114991696603,53.02156083039952 +2024-10-23 23:00:00,3,12.604007162483454,25.534867648295222,51.95512993252637 +2024-10-24 00:00:00,3,24.83645653964359,25.964947553006326,47.82299934868571 +2024-10-24 01:00:00,3,17.463105462355834,27.997387953047166,49.87566636343803 +2024-10-24 02:00:00,3,25.68684209621859,22.193297816936226,56.63063883865722 +2024-10-24 03:00:00,3,40.461307180376366,22.077632843960412,63.68505105480335 +2024-10-24 04:00:00,3,33.10913748297904,21.29629022071044,65.34420745571384 +2024-10-24 05:00:00,3,44.467034509499776,26.661427757244613,58.322271555460475 +2024-10-24 06:00:00,3,35.13242314110611,30.239171907781817,67.56772228269233 +2024-10-24 07:00:00,3,17.16580608794812,20.14910958549246,59.814552958993985 +2024-10-24 08:00:00,3,33.92701231201968,28.098493573600056,56.83652361959225 +2024-10-24 09:00:00,3,22.54544126872528,24.365552850081222,61.6438904299831 +2024-10-24 10:00:00,3,20.009412130102355,26.939794402801773,37.18508718980214 +2024-10-24 11:00:00,3,20.179008887956396,21.552554789304132,46.55384230580021 +2024-10-24 12:00:00,3,35.2699257923545,21.7375509193455,40.86303347494415 +2024-10-24 13:00:00,3,10.35069682135949,27.42660307581507,63.52000911041722 +2024-10-24 14:00:00,3,10.55261618081089,27.218518119530263,58.28632252381152 +2024-10-24 15:00:00,3,15.703554681375874,21.891579498243743,59.8737306515475 +2024-10-24 16:00:00,3,34.323728967679145,22.7145682985112,73.81515257704635 +2024-10-24 17:00:00,3,28.88600218065225,22.573974664493257,54.62524883315748 +2024-10-24 18:00:00,3,19.287879729006963,21.420442625848505,51.48084182692584 +2024-10-24 19:00:00,3,32.060266044649396,20.911033138133146,54.399229784255 +2024-10-24 20:00:00,3,26.80568617900152,21.448924986914594,52.807515496235254 +2024-10-24 21:00:00,3,22.31381410249549,26.620516746662666,46.278280562709725 +2024-10-24 22:00:00,3,21.898100115577005,26.948689777929978,69.83453762733318 +2024-10-24 23:00:00,3,20.543674039666286,24.88872891234794,59.33290650327322 +2024-10-25 00:00:00,3,21.11368491719154,24.68332022226722,57.659357489144895 +2024-10-25 01:00:00,3,32.8535700775318,25.193551818905046,46.71158523282574 +2024-10-25 02:00:00,3,18.63627507613602,27.329904945562305,63.27206931388503 +2024-10-25 03:00:00,3,22.388760718541437,24.32966333761917,77.28442341521128 +2024-10-25 04:00:00,3,16.139895302812853,24.083200596839713,67.8821922175674 +2024-10-25 05:00:00,3,43.482889206463305,16.48074677061261,54.10232810126563 +2024-10-25 06:00:00,3,23.541804675195998,22.195820581412775,57.23739706568804 +2024-10-25 07:00:00,3,26.906646592831468,21.575838866606148,73.29488440184187 +2024-10-25 08:00:00,3,31.39148217267838,28.136910192088077,47.59146680828158 +2024-10-25 09:00:00,3,24.862330732656044,23.44403840892284,59.8507696954452 +2024-10-25 10:00:00,3,21.864597466193814,21.00388390090729,60.20631492846352 +2024-10-25 11:00:00,3,16.260288540003547,25.885229118897417,55.95536019754608 +2024-10-25 12:00:00,3,26.804668148207615,25.80517013284863,50.40832241541046 +2024-10-25 13:00:00,3,38.598562526106136,21.4912993121241,43.1541305400041 +2024-10-25 14:00:00,3,49.5019481616591,23.922474078548802,55.76941802397244 +2024-10-25 15:00:00,3,13.184770747005654,22.390142391528848,55.24996772511341 +2024-10-25 16:00:00,3,33.84354755882855,26.054052665016403,57.19482616029734 +2024-10-25 17:00:00,3,17.14546017639453,21.915705913515154,50.7304637796084 +2024-10-25 18:00:00,3,29.48762929196965,26.890121057563896,43.39772359558958 +2024-10-25 19:00:00,3,27.035082018463967,25.782128209376836,49.36005707557954 +2024-10-25 20:00:00,3,34.48023048891788,22.897274996789662,54.7499966755311 +2024-10-25 21:00:00,3,19.032030279034206,21.912096527355487,52.52703983599305 +2024-10-25 22:00:00,3,16.80427492085873,26.808638481955846,57.49451744706674 +2024-10-25 23:00:00,3,17.959665999313962,19.828992832232366,47.76793236657455 +2024-10-26 00:00:00,3,28.22181973041032,27.825740806951192,56.6320228071885 +2024-10-26 01:00:00,3,31.163623086697026,30.474163348002122,45.59405805210852 +2024-10-26 02:00:00,3,33.696193525191546,24.04806504628924,65.64662170830627 +2024-10-26 03:00:00,3,31.65591818081904,22.745933808545008,54.943061395743 +2024-10-26 04:00:00,3,18.682564497967,20.33240354327693,56.525468114034936 +2024-10-26 05:00:00,3,37.61786077093808,23.469896161247913,64.14025857223268 +2024-10-26 06:00:00,3,35.62077862376119,22.605553937344705,43.47795833562032 +2024-10-26 07:00:00,3,41.47341837517848,28.261498794295647,49.40533783621737 +2024-10-26 08:00:00,3,28.1049203214652,20.819023867249854,51.25194488209115 +2024-10-26 09:00:00,3,27.22650409187975,28.37313189236427,49.670194196506195 +2024-10-26 10:00:00,3,31.028716735768857,19.75686691542302,41.89745025819017 +2024-10-26 11:00:00,3,22.74228713976102,27.473605939593746,50.63463996979089 +2024-10-26 12:00:00,3,40.13420051027468,27.49170652917016,55.30232270342916 +2024-10-26 13:00:00,3,30.75145239890475,27.265688060101233,57.81538222494696 +2024-10-26 14:00:00,3,31.639307409438743,22.21002732600084,54.39368741364056 +2024-10-26 15:00:00,3,14.599481561526487,18.860831476031176,57.045439918365254 +2024-10-26 16:00:00,3,28.102685417926324,29.27010677880255,57.7574269919394 +2024-10-26 17:00:00,3,7.812999872760251,20.09052670988722,52.52008352250786 +2024-10-26 18:00:00,3,40.164076547915215,20.220750836286324,58.975789470507486 +2024-10-26 19:00:00,3,16.065651634635007,22.609805878855838,56.96192360832021 +2024-10-26 20:00:00,3,21.66432357341586,24.699045403795875,48.81958811310037 +2024-10-26 21:00:00,3,26.343375556327324,21.055358032242207,46.09985944222537 +2024-10-26 22:00:00,3,14.773044323891614,21.429631121983114,53.0739438012948 +2024-10-26 23:00:00,3,27.84844913589886,20.315899380725163,64.75339728863007 +2024-10-27 00:00:00,3,36.60529474804662,22.484211417710057,68.26742525689295 +2024-10-27 01:00:00,3,29.598011148848176,21.1270705928858,59.68939864752001 +2024-10-27 02:00:00,3,32.124464913243955,26.067316364579476,62.43465337334488 +2024-10-27 03:00:00,3,19.240327454045918,24.597095152772567,65.00911628257984 +2024-10-27 04:00:00,3,27.915176919128324,32.05473452195394,60.87709374802689 +2024-10-27 05:00:00,3,27.047224749513997,27.805598927769804,57.13599712151081 +2024-10-27 06:00:00,3,21.081571416483374,22.74676513698054,51.443975553740756 +2024-10-27 07:00:00,3,27.556301136167296,28.155243343993124,41.88708800161102 +2024-10-27 08:00:00,3,16.190097093270168,21.293909156380202,48.204031380552706 +2024-10-27 09:00:00,3,38.93547329927742,28.977876504454343,51.272360084583404 +2024-10-27 10:00:00,3,27.566353902419735,34.55277649764698,59.74656359241281 +2024-10-27 11:00:00,3,29.798389265659463,27.930262193104074,31.571924668339587 +2024-10-27 12:00:00,3,21.939347714023782,23.6506948679031,65.36513490268172 +2024-10-27 13:00:00,3,17.8958556377633,30.07101639367524,63.69739626608558 +2024-10-27 14:00:00,3,17.78515697141498,26.18847846892524,60.0279625564747 +2024-10-27 15:00:00,3,21.438059036362006,27.808436460924444,54.87263001944391 +2024-10-27 16:00:00,3,39.4827913451869,22.04587903571211,66.6317451687288 +2024-10-27 17:00:00,3,10.894775245222917,25.884446960286215,51.789441868666096 +2024-10-27 18:00:00,3,11.843472986021888,20.73793396218189,70.39622914026124 +2024-10-27 19:00:00,3,10.723087101666678,19.27864324186185,52.29241853457439 +2024-10-27 20:00:00,3,33.69299860337257,26.516477756528886,57.13231988565093 +2024-10-27 21:00:00,3,7.540035790154693,17.413198597257626,57.98473861683373 +2024-10-27 22:00:00,3,21.186348535076668,24.17888275441779,54.27358775918847 +2024-10-27 23:00:00,3,23.799471621360794,20.520913734326864,49.894733367390245 +2024-10-28 00:00:00,3,53.50291170968281,24.14908248219992,58.207047689250544 +2024-10-28 01:00:00,3,32.948839595911785,24.43914690872148,42.22572333766786 +2024-10-28 02:00:00,3,29.40819356511098,23.555703128215587,60.81024616841942 +2024-10-28 03:00:00,3,26.090876326895657,27.022544647466866,57.477139716338264 +2024-10-28 04:00:00,3,24.963043955433644,22.359111852530916,61.560641052846464 +2024-10-28 05:00:00,3,29.667399554543074,21.39585926611568,53.72681980567442 +2024-10-28 06:00:00,3,29.388331099920933,23.69615886439214,49.089618611161626 +2024-10-28 07:00:00,3,17.30998672555689,29.596618139545498,59.09198958794429 +2024-10-28 08:00:00,3,46.59686573010616,33.139905489902624,53.11739603328358 +2024-10-28 09:00:00,3,11.559318128976109,25.363784221127144,55.16150414474385 +2024-10-28 10:00:00,3,37.618189923999,16.053750809932197,52.87956575155394 +2024-10-28 11:00:00,3,24.245930879691862,17.971698753265997,46.682510896029676 +2024-10-28 12:00:00,3,28.893932012441084,28.26675962806705,48.99046024148837 +2024-10-28 13:00:00,3,19.238239575141275,26.77901436790079,64.77327950866088 +2024-10-28 14:00:00,3,31.204812911322005,22.784001391003095,53.26795911389031 +2024-10-28 15:00:00,3,24.335497533664782,29.28261248566127,52.65348621889973 +2024-10-28 16:00:00,3,23.56962236588437,25.916454151629583,49.56005970691416 +2024-10-28 17:00:00,3,29.338621589571655,23.694383620175152,60.99526095602617 +2024-10-28 18:00:00,3,24.202815038919884,19.68533584192492,55.2724732935293 +2024-10-28 19:00:00,3,29.8089625159382,24.40864897846682,61.65901645664697 +2024-10-28 20:00:00,3,21.463618702203497,28.253876193553054,53.064726658878115 +2024-10-28 21:00:00,3,22.370318415543746,20.214713338059493,58.40455119936696 +2024-10-28 22:00:00,3,25.727344984828218,24.07188047197866,39.371609604457255 +2024-10-28 23:00:00,3,22.96001568605069,21.09103682218287,65.70288650301546 +2024-10-29 00:00:00,3,38.49842594546939,19.37220990010064,68.69520623870886 +2024-10-29 01:00:00,3,34.28498231896283,22.259688869690645,64.51470561294151 +2024-10-29 02:00:00,3,31.777493930778153,23.868296568559515,62.878873047034624 +2024-10-29 03:00:00,3,31.604931575141265,20.52669699081709,48.24380731001341 +2024-10-29 04:00:00,3,40.86224871536527,30.136437837453144,80.52495950888982 +2024-10-29 05:00:00,3,12.054114002174543,27.429063440520846,48.67208335680783 +2024-10-29 06:00:00,3,36.56857291212916,25.11611256542712,50.136640579733715 +2024-10-29 07:00:00,3,33.8695756089666,28.601376427141133,54.17530265618533 +2024-10-29 08:00:00,3,12.812448725884373,26.83041770458202,51.47441374880216 +2024-10-29 09:00:00,3,22.66771361944488,25.215300065393404,63.56412822185786 +2024-10-29 10:00:00,3,32.72384834094585,24.39063664875211,61.57951714121873 +2024-10-29 11:00:00,3,30.712970114489742,25.73536276345466,72.62296382802289 +2024-10-29 12:00:00,3,31.33239289756785,27.867041790686816,40.39759880357272 +2024-10-29 13:00:00,3,24.189114875645018,30.61913009040422,39.77804074485372 +2024-10-29 14:00:00,3,20.66350430870998,25.641558175996817,65.93987544762791 +2024-10-29 15:00:00,3,22.584100008585985,27.828216449703756,48.81460521731953 +2024-10-29 16:00:00,3,34.70068739693986,24.562587086821,42.542077311039804 +2024-10-29 17:00:00,3,17.33699107730944,19.91684919110677,55.857738425558836 +2024-10-29 18:00:00,3,29.98343927506454,22.830335834429647,51.7305373799028 +2024-10-29 19:00:00,3,8.145447620843164,28.508836453595094,57.05351841891213 +2024-10-29 20:00:00,3,32.80310294338861,24.63982398667653,43.00839812833775 +2024-10-29 21:00:00,3,22.385520464133137,24.87746639598045,33.724958975663135 +2024-10-29 22:00:00,3,24.008751558872397,19.79263121934823,71.16776501919438 +2024-10-29 23:00:00,3,34.10986287490481,23.319455550789357,65.53769885912558 +2024-10-30 00:00:00,3,26.602226526305916,26.932630880990335,61.935439074549656 +2024-10-30 01:00:00,3,36.45443024841135,26.274219431830154,64.0697943229669 +2024-10-30 02:00:00,3,27.95938814532945,26.92503657826587,58.63884933435196 +2024-10-30 03:00:00,3,22.475980065517803,26.944238702567738,60.792533631391315 +2024-10-30 04:00:00,3,19.4426803211869,22.96911075361005,54.45766215895558 +2024-10-30 05:00:00,3,36.93105144348984,22.322451028158373,64.04797024954398 +2024-10-30 06:00:00,3,36.157805424145316,22.168257653507737,52.72331533908552 +2024-10-30 07:00:00,3,25.375946398548447,22.554749419058762,60.8955360491902 +2024-10-30 08:00:00,3,19.050020050367387,23.39475182947482,72.13809403014753 +2024-10-30 09:00:00,3,17.97813129781114,16.7187030477059,35.64503536557633 +2024-10-30 10:00:00,3,17.744376860804117,21.074424408625738,65.79172309864855 +2024-10-30 11:00:00,3,31.452064104577214,25.411336709204612,64.79560367876381 +2024-10-30 12:00:00,3,30.86925970138538,29.240010214041565,52.22902090515045 +2024-10-30 13:00:00,3,26.394821071699507,25.64924717897271,41.99029873212248 +2024-10-30 14:00:00,3,19.081726854207936,25.89884251377716,61.9147436168692 +2024-10-30 15:00:00,3,22.62763268605638,28.749601452964267,65.9617241414755 +2024-10-30 16:00:00,3,35.10028340254001,19.800640734308537,47.332619225820906 +2024-10-30 17:00:00,3,31.316485483234345,24.80217432285224,51.336951405621456 +2024-10-30 18:00:00,3,27.046147156935092,26.296269050879097,69.07168845775031 +2024-10-30 19:00:00,3,33.46235442708179,29.85036053140643,53.26387750528702 +2024-10-30 20:00:00,3,14.998533108015826,28.25570252325145,54.15704579726609 +2024-10-30 21:00:00,3,27.14096811882694,20.865679628630723,47.94255172149273 +2024-10-30 22:00:00,3,21.15657871990037,16.29160911007098,64.73644448058585 +2024-10-30 23:00:00,3,17.46443018898175,21.27325903176731,45.736505781497435 +2024-10-31 00:00:00,3,29.729302533116783,22.889444550541665,65.08769325036744 +2024-10-31 01:00:00,3,13.961048102422783,27.380940531911254,46.06421315570369 +2024-10-31 02:00:00,3,31.550956904613578,19.72519830556055,46.08482204433035 +2024-10-31 03:00:00,3,34.82262896886173,25.917680787496543,61.07787967824449 +2024-10-31 04:00:00,3,31.16982952026956,20.185154917375897,70.54999980677326 +2024-10-31 05:00:00,3,23.93734611749806,20.7492932010653,49.96586070246668 +2024-10-31 06:00:00,3,31.541649824297256,24.53212694170386,58.88429470730977 +2024-10-31 07:00:00,3,40.105337817944374,25.913561894800445,51.19491991746726 +2024-10-31 08:00:00,3,32.33759372177661,24.491881211008945,55.4406177585779 +2024-10-31 09:00:00,3,28.26167244231111,21.489863222547946,64.01073415375555 +2024-10-31 10:00:00,3,20.45037703018991,27.162975979437945,46.950294393722736 +2024-10-31 11:00:00,3,27.77553187964994,21.038967715368287,51.45457942217145 +2024-10-31 12:00:00,3,33.805485660638666,25.93391406972356,33.873846371026616 +2024-10-31 13:00:00,3,34.21648534343037,26.127345837839727,43.018229908360084 +2024-10-31 14:00:00,3,22.89029040829693,30.143529314464352,60.95416379822121 +2024-10-31 15:00:00,3,20.038550162672806,24.224068030682716,65.14303946270427 +2024-10-31 16:00:00,3,29.162023693298174,26.924481698174496,59.070444343720155 +2024-10-31 17:00:00,3,42.57424069397216,29.185914612764524,59.8081590574736 +2024-10-31 18:00:00,3,21.87455893882012,20.9030638803426,47.44369340674665 +2024-10-31 19:00:00,3,21.609195255911004,25.489904101266312,58.15175416262964 +2024-10-31 20:00:00,3,20.337359029371292,25.21556115876052,48.81075676608975 +2024-10-31 21:00:00,3,20.910703133579773,21.74627957769634,25.36949182809507 +2024-10-31 22:00:00,3,25.918364182073276,22.384029123620014,55.03723278047509 +2024-10-31 23:00:00,3,21.943048766182326,20.73496449693829,51.11793115375958 +2024-11-01 00:00:00,3,18.610581870547378,20.48547919460519,55.118052246663574 +2024-11-01 01:00:00,3,21.336932918257077,23.031438149735,59.575416037260545 +2024-11-01 02:00:00,3,36.65378893250309,16.19210839346483,52.106574921588766 +2024-11-01 03:00:00,3,40.63070422193666,26.225125171831788,72.54060987637237 +2024-11-01 04:00:00,3,20.29968364988926,27.134583877822386,67.73386652946341 +2024-11-01 05:00:00,3,34.09857569377415,23.495629514551574,56.480400534838374 +2024-11-01 06:00:00,3,33.88473377046885,30.031738537484745,61.180444096709294 +2024-11-01 07:00:00,3,32.511422477650704,22.661386081772605,47.8044813000879 +2024-11-01 08:00:00,3,17.11363274623736,14.402851319511182,55.743150132339196 +2024-11-01 09:00:00,3,31.89573187986406,24.645982698988696,57.84252454165218 +2024-11-01 10:00:00,3,27.72830856195062,26.466887573811572,54.694545845276934 +2024-11-01 11:00:00,3,26.051962744066333,27.489234486793233,53.359878164004364 +2024-11-01 12:00:00,3,23.426846778507663,27.26321064544051,51.612414263785155 +2024-11-01 13:00:00,3,19.14587254358788,27.007656451511195,48.6674456712502 +2024-11-01 14:00:00,3,25.88526159402677,22.421437629283144,47.053879919431424 +2024-11-01 15:00:00,3,33.42281596721019,19.011158373753975,51.28111741821743 +2024-11-01 16:00:00,3,13.443995772340054,22.932920492288492,50.44771089761395 +2024-11-01 17:00:00,3,27.947429420497446,28.32555251430501,54.642168336454866 +2024-11-01 18:00:00,3,17.793584487618762,28.2681178423572,46.42924261348186 +2024-11-01 19:00:00,3,25.79961049904452,20.80269300602318,60.85466905396673 +2024-11-01 20:00:00,3,30.325731035157947,22.034302039097746,60.90195133335007 +2024-11-01 21:00:00,3,25.817816315161927,21.13290874226368,60.63016744697061 +2024-11-01 22:00:00,3,24.438452786817436,28.98112027522371,45.51169377169574 +2024-11-01 23:00:00,3,17.49774530846816,21.673361697257253,59.84986219531888 +2024-11-02 00:00:00,3,24.105512553780734,21.293177144501783,50.87177333539991 +2024-11-02 01:00:00,3,32.309343850279895,22.847855553695762,47.3062398786139 +2024-11-02 02:00:00,3,32.3262044834927,21.973634144848326,61.80283745818656 +2024-11-02 03:00:00,3,19.716472108335786,20.651473837460316,54.90251828717206 +2024-11-02 04:00:00,3,15.03454431210006,22.846254083068384,57.868817209524686 +2024-08-04 05:00:00,4,17.710753498383724,13.63791049953212,49.237001327176195 +2024-08-04 06:00:00,4,38.64872500327047,27.019704673224403,52.979997388214734 +2024-08-04 07:00:00,4,30.254159553294215,28.918733010850275,64.17213722851982 +2024-08-04 08:00:00,4,23.182876653210585,24.421136673306968,53.9342269984455 +2024-08-04 09:00:00,4,28.999246850166674,24.571948018862695,72.42552212703177 +2024-08-04 10:00:00,4,31.167127058837128,26.3044195203643,47.76215607051096 +2024-08-04 11:00:00,4,28.40417322002652,23.582047452861318,60.88979393851753 +2024-08-04 12:00:00,4,25.448066244351185,26.30509153736205,68.62453318866768 +2024-08-04 13:00:00,4,22.288722278400424,18.859313506070617,58.7231972924759 +2024-08-04 14:00:00,4,36.03262639235493,22.39649027046791,50.90615488424602 +2024-08-04 15:00:00,4,27.078356774627906,21.609397349255314,37.21062207351545 +2024-08-04 16:00:00,4,16.09346615071493,30.43217612034068,45.484129423092554 +2024-08-04 17:00:00,4,35.726222913643554,28.525228078934646,58.4310898816811 +2024-08-04 18:00:00,4,27.42036482240416,26.661647671671766,54.24187918547298 +2024-08-04 19:00:00,4,23.281048540933288,16.370098355516653,67.37593132983099 +2024-08-04 20:00:00,4,25.254644349133425,28.26846229046024,65.44972760626769 +2024-08-04 21:00:00,4,28.444364974272997,24.452481796098255,61.77842206608751 +2024-08-04 22:00:00,4,29.170327728733852,24.87453062328845,49.88018630264249 +2024-08-04 23:00:00,4,21.83331434057201,22.23005242271627,47.96266873479465 +2024-08-05 00:00:00,4,25.61170362675817,22.152634306563957,69.18265301726579 +2024-08-05 01:00:00,4,23.002619911279,33.754830006919505,50.53647419577277 +2024-08-05 02:00:00,4,29.645328588569992,30.941019563692926,59.691492258267964 +2024-08-05 03:00:00,4,17.404479874561016,31.767326660899606,47.27620866407661 +2024-08-05 04:00:00,4,22.107334186676198,21.365197171415897,65.39269131934934 +2024-08-05 05:00:00,4,22.546484594796915,25.049058413658923,51.902423944243296 +2024-08-05 06:00:00,4,37.470358436870825,28.5106967716509,55.43185669702495 +2024-08-05 07:00:00,4,19.982783867393728,21.769121412582866,49.19120295762893 +2024-08-05 08:00:00,4,33.32026165414605,21.0919430601619,67.68993702490191 +2024-08-05 09:00:00,4,20.408573562461253,24.760412294819368,50.23354624772149 +2024-08-05 10:00:00,4,29.586353963221015,21.662363880527757,42.86400471716664 +2024-08-05 11:00:00,4,38.85115679445995,18.92758654539616,49.15367526952253 +2024-08-05 12:00:00,4,44.68851852861383,23.511654933049744,70.5476114436325 +2024-08-05 13:00:00,4,27.83919567412094,22.54753975881406,50.735548782359636 +2024-08-05 14:00:00,4,29.398182744866205,22.03451363878552,57.20880798202484 +2024-08-05 15:00:00,4,21.378311995172613,25.76880629882878,54.933687467177066 +2024-08-05 16:00:00,4,7.496469519924485,25.593864862856474,65.08514477084768 +2024-08-05 17:00:00,4,18.827983189242808,25.338952760672946,62.48726596460388 +2024-08-05 18:00:00,4,24.293108961426594,21.43850985014986,58.69924529747935 +2024-08-05 19:00:00,4,23.08959210527632,21.759826016086915,60.08808008700877 +2024-08-05 20:00:00,4,34.394097767961995,24.49934366923638,45.03552292734169 +2024-08-05 21:00:00,4,42.1006496790331,28.689292329206033,52.68487796517276 +2024-08-05 22:00:00,4,25.946041238283957,28.65865262454431,71.51245138131556 +2024-08-05 23:00:00,4,38.21416708492775,18.35698641259571,64.15963064842046 +2024-08-06 00:00:00,4,37.79385782512276,21.993187209106665,55.96835029976522 +2024-08-06 01:00:00,4,25.96380926765951,28.89742859323874,52.40841010624083 +2024-08-06 02:00:00,4,17.65874528228853,19.03246863113654,49.273231419209004 +2024-08-06 03:00:00,4,21.680698989696747,29.090086742900294,76.36727180479419 +2024-08-06 04:00:00,4,21.39888192338681,16.379971926296708,55.5403596167964 +2024-08-06 05:00:00,4,31.21152792229561,28.05638930478311,55.19220868335298 +2024-08-06 06:00:00,4,21.598693111809034,25.75129997005288,58.401008097846145 +2024-08-06 07:00:00,4,33.750367991605906,27.683793136676222,42.312159274571016 +2024-08-06 08:00:00,4,17.645975219353147,28.592143185326705,66.72614374217284 +2024-08-06 09:00:00,4,20.16189430776593,24.453781954961634,65.68806281523057 +2024-08-06 10:00:00,4,31.125196817675022,23.7485122380077,47.20722141603562 +2024-08-06 11:00:00,4,19.23741923321274,16.409430793040897,69.01666518481838 +2024-08-06 12:00:00,4,24.15869677352437,23.170485652558064,53.83571766564635 +2024-08-06 13:00:00,4,26.09139768431453,27.364144783731284,68.3906068318427 +2024-08-06 14:00:00,4,37.67754816802645,22.600232087422057,58.35491097847467 +2024-08-06 15:00:00,4,30.02816985884061,24.506462538721564,53.03620141888831 +2024-08-06 16:00:00,4,10.04905224252699,22.80704960681814,60.5941605216625 +2024-08-06 17:00:00,4,33.26293626936164,23.02654286407575,41.26766643364069 +2024-08-06 18:00:00,4,18.559687760140434,30.583488087556844,58.2199505565623 +2024-08-06 19:00:00,4,20.167937208700558,23.27549410751153,52.539005704881966 +2024-08-06 20:00:00,4,17.55047877992411,27.472497144836925,61.44650130252998 +2024-08-06 21:00:00,4,41.76515063377593,28.661319784786617,50.616431008222996 +2024-08-06 22:00:00,4,23.697614024234927,30.85967317253469,44.40883608579472 +2024-08-06 23:00:00,4,28.21304895185566,23.213268610602476,47.20384427861756 +2024-08-07 00:00:00,4,28.062578920089752,28.96626489724691,44.4655941588505 +2024-08-07 01:00:00,4,40.20972526849065,25.518037269276903,68.27385256917 +2024-08-07 02:00:00,4,26.335547842367983,25.502239779391523,54.723344139345635 +2024-08-07 03:00:00,4,26.513800466554684,24.824780119966633,38.839671873597574 +2024-08-07 04:00:00,4,18.97206759891052,26.226536678876457,67.27779457760523 +2024-08-07 05:00:00,4,24.27878917701583,21.00317351868553,53.666880301497365 +2024-08-07 06:00:00,4,17.02686994656885,23.998755894210856,60.616668652360374 +2024-08-07 07:00:00,4,15.582882454582187,25.709255129814125,71.85778039748256 +2024-08-07 08:00:00,4,19.402565128719452,22.75403522844475,47.94833147935784 +2024-08-07 09:00:00,4,23.815344390716827,24.4289839716592,66.02447178212127 +2024-08-07 10:00:00,4,32.6201098220629,23.450619447919216,74.72985993470903 +2024-08-07 11:00:00,4,32.30749263483768,27.320725487587293,54.949791963890355 +2024-08-07 12:00:00,4,17.138057326023464,26.502548029506684,57.88557002006869 +2024-08-07 13:00:00,4,35.35224788866739,23.50234147008633,68.71755277523422 +2024-08-07 14:00:00,4,17.38676330838895,28.563955365681977,57.731747250503005 +2024-08-07 15:00:00,4,23.671132181206687,16.425658332220305,52.60439781371437 +2024-08-07 16:00:00,4,15.668623487890667,23.342561293579465,63.85212874424156 +2024-08-07 17:00:00,4,17.258726789239535,24.05442300645253,56.30827997741989 +2024-08-07 18:00:00,4,25.72287578210738,22.606913758889245,57.77013860724318 +2024-08-07 19:00:00,4,26.73355694895338,25.143793370061537,40.78836414828001 +2024-08-07 20:00:00,4,17.65339642643127,23.993858698459146,42.278951868057995 +2024-08-07 21:00:00,4,22.106263998990006,29.308713844825952,58.56372343782857 +2024-08-07 22:00:00,4,24.38852646840643,25.270997692781417,38.58711563552856 +2024-08-07 23:00:00,4,1.239685960159374,28.138165799170846,58.90241719437355 +2024-08-08 00:00:00,4,29.609045880519226,26.38494982224603,53.32256085793473 +2024-08-08 01:00:00,4,18.181021798477055,27.644395183315098,51.8392237927731 +2024-08-08 02:00:00,4,17.85854223450763,23.137245244124905,61.61690458739383 +2024-08-08 03:00:00,4,27.896538513485353,24.069696242703287,51.32784121068809 +2024-08-08 04:00:00,4,30.638588789946564,18.822145018027467,47.18977572266871 +2024-08-08 05:00:00,4,35.56906357791236,22.741007009345456,62.29868569998057 +2024-08-08 06:00:00,4,23.400142643942726,17.49161144801661,64.09265964871523 +2024-08-08 07:00:00,4,34.69901023378646,20.556379736001368,44.94830214305887 +2024-08-08 08:00:00,4,15.583143945768047,20.244914204599738,46.26128645650866 +2024-08-08 09:00:00,4,33.39263377644309,22.598261499119328,62.941241218578 +2024-08-08 10:00:00,4,24.194299964102044,18.635993694012836,78.89917691737668 +2024-08-08 11:00:00,4,10.56202356402397,21.448326570867085,51.43519826146368 +2024-08-08 12:00:00,4,23.02691830985765,17.322114881072505,73.49784101216278 +2024-08-08 13:00:00,4,26.35459154604367,24.23973256237075,56.084892079963765 +2024-08-08 14:00:00,4,27.709491928771914,21.663089254377397,44.42111424355498 +2024-08-08 15:00:00,4,12.241883771845105,22.820758484036634,61.23035522407705 +2024-08-08 16:00:00,4,22.06532768473294,23.19450194360486,53.495040889175606 +2024-08-08 17:00:00,4,20.94790637724402,28.478980331677505,64.16339218928809 +2024-08-08 18:00:00,4,37.684564420343015,23.81624725218518,53.473013660656925 +2024-08-08 19:00:00,4,34.31834554062042,27.266980980094754,49.77859890440251 +2024-08-08 20:00:00,4,25.246604384213228,23.70393981370427,56.02905760188741 +2024-08-08 21:00:00,4,19.85447686187566,28.75967026262667,52.22981883670499 +2024-08-08 22:00:00,4,23.49289017492262,29.184282546108303,62.9939043472922 +2024-08-08 23:00:00,4,27.56736189381739,24.870868193448146,61.311830874578554 +2024-08-09 00:00:00,4,16.860321293934746,20.35196193901936,31.441108346956277 +2024-08-09 01:00:00,4,40.194218438069754,25.510754025919013,50.32551015690176 +2024-08-09 02:00:00,4,26.926198548930945,27.189047364334247,46.883766738918176 +2024-08-09 03:00:00,4,29.889145057048996,30.350307458843858,50.84880026618315 +2024-08-09 04:00:00,4,15.391455490075286,23.686704677180803,50.32112862103476 +2024-08-09 05:00:00,4,31.875519899197183,26.047588049289775,58.552131784131284 +2024-08-09 06:00:00,4,28.942073247950407,27.565878373571664,48.87449175590061 +2024-08-09 07:00:00,4,29.333473734902046,26.05652543356132,60.30758238354096 +2024-08-09 08:00:00,4,31.57089216836232,24.150702961743672,57.0182791714968 +2024-08-09 09:00:00,4,24.66744646658617,29.82942394115265,57.24136073228636 +2024-08-09 10:00:00,4,23.6265221341587,24.762253565829266,56.02363875632644 +2024-08-09 11:00:00,4,16.375763779955946,26.169675836336758,42.79554554923477 +2024-08-09 12:00:00,4,28.735909463550005,21.517685823729376,56.70215084165158 +2024-08-09 13:00:00,4,13.314787820288531,25.594212644154446,55.87487327457455 +2024-08-09 14:00:00,4,10.269312609275365,26.138117468525166,52.12117254886631 +2024-08-09 15:00:00,4,22.724617307940196,21.886364176066444,64.60130055426019 +2024-08-09 16:00:00,4,13.742925735436717,25.645531783120024,61.10940680552214 +2024-08-09 17:00:00,4,11.48482325908872,25.593145444459356,65.0590900388914 +2024-08-09 18:00:00,4,24.86862354526036,21.99929886294641,64.38736948323252 +2024-08-09 19:00:00,4,5.96084812568844,26.742671712687283,47.6243013310092 +2024-08-09 20:00:00,4,39.230580023102036,24.413839313255917,53.61194319655781 +2024-08-09 21:00:00,4,36.56872223082949,25.66986931660651,53.17047812864602 +2024-08-09 22:00:00,4,23.42134326709762,24.350566735508117,58.452464222606984 +2024-08-09 23:00:00,4,19.139075065915875,26.745565283424778,57.09325506892181 +2024-08-10 00:00:00,4,31.54890287848493,29.772920780362426,59.79641953032977 +2024-08-10 01:00:00,4,27.79845409744368,24.07905216012975,51.31317319320744 +2024-08-10 02:00:00,4,31.5891798883697,18.139315106250102,57.53471558273048 +2024-08-10 03:00:00,4,30.215539589744857,26.2966123690481,61.90546448615351 +2024-08-10 04:00:00,4,17.81230603270793,25.10426311801584,53.42470682801074 +2024-08-10 05:00:00,4,23.90145723932629,28.48983387036409,74.69086955763301 +2024-08-10 06:00:00,4,21.499066902829437,20.19103887795999,54.1212340775103 +2024-08-10 07:00:00,4,22.3428891853091,19.1812582040331,54.34591662967739 +2024-08-10 08:00:00,4,29.610847812105128,26.482600808124605,55.28419297784818 +2024-08-10 09:00:00,4,28.254288711151816,19.870446689366428,77.26207583879233 +2024-08-10 10:00:00,4,26.27649977718184,22.481235327242242,54.16524800041734 +2024-08-10 11:00:00,4,27.25325390237124,22.89067150407982,58.218791171120756 +2024-08-10 12:00:00,4,23.5240398758085,24.191094960895224,47.59484653190569 +2024-08-10 13:00:00,4,30.908248669424374,24.083874469238474,56.81697462828373 +2024-08-10 14:00:00,4,12.578835955532625,16.30970619656603,88.89259860781297 +2024-08-10 15:00:00,4,8.795097351204298,20.640219950114826,56.3136953652364 +2024-08-10 16:00:00,4,28.074989174536675,24.19643360328309,50.296152524448175 +2024-08-10 17:00:00,4,20.44842784735935,19.406433378348815,51.50925583304285 +2024-08-10 18:00:00,4,16.25021182617705,22.6082889338697,41.49142281514745 +2024-08-10 19:00:00,4,15.139034640290857,31.155983547116442,62.669153104517186 +2024-08-10 20:00:00,4,12.007026971164189,20.044334788702404,48.460587379970946 +2024-08-10 21:00:00,4,25.42382664308504,24.837127044461404,61.3790073010395 +2024-08-10 22:00:00,4,24.900255056346875,27.919341643485133,55.670756140401686 +2024-08-10 23:00:00,4,12.835581856132404,29.611353283091304,57.965735580735306 +2024-08-11 00:00:00,4,34.67464066543141,24.916431420005594,53.91400050430547 +2024-08-11 01:00:00,4,31.105856220761734,26.34266521892343,42.73147588628495 +2024-08-11 02:00:00,4,25.363822925021786,24.5943139066475,46.566273133296214 +2024-08-11 03:00:00,4,17.204349872855985,27.62263643431826,55.07826334704519 +2024-08-11 04:00:00,4,36.40623706785385,30.460407095183783,45.74037483247338 +2024-08-11 05:00:00,4,33.739110496388925,22.26406169478898,64.29555154630833 +2024-08-11 06:00:00,4,17.695394763409084,22.070889832465593,59.14923612159412 +2024-08-11 07:00:00,4,9.889166254901745,19.660318399131658,59.15192415571266 +2024-08-11 08:00:00,4,21.125781253744837,26.45383147464189,59.896686461077394 +2024-08-11 09:00:00,4,33.94259642769986,25.52574426037456,51.678355446954484 +2024-08-11 10:00:00,4,29.317769118108693,22.93581727995648,81.60695697820654 +2024-08-11 11:00:00,4,18.52338434589862,23.349251142280192,59.8134239707876 +2024-08-11 12:00:00,4,32.66931491149846,22.887365952388944,42.75784267056862 +2024-08-11 13:00:00,4,26.42981764138917,24.771268095974182,47.76715145769559 +2024-08-11 14:00:00,4,26.923603158977116,23.232770993591775,66.3491821240303 +2024-08-11 15:00:00,4,26.932891492715825,24.430968369291417,38.202229147511964 +2024-08-11 16:00:00,4,17.444437366744417,24.000741943173804,58.636423929901824 +2024-08-11 17:00:00,4,35.23291167255948,27.338737121125316,54.141446142650054 +2024-08-11 18:00:00,4,24.496899929778543,19.44310741987494,43.15438190972378 +2024-08-11 19:00:00,4,31.258170275470352,33.9142103037537,60.13030335167714 +2024-08-11 20:00:00,4,18.565002943877786,27.674717690732823,68.85706922185445 +2024-08-11 21:00:00,4,19.632354097328037,29.95876067322463,66.22394125525672 +2024-08-11 22:00:00,4,35.590477937444554,22.637160172636435,53.074392604419195 +2024-08-11 23:00:00,4,14.720386522885208,26.60934833754596,78.94913753322203 +2024-08-12 00:00:00,4,25.447673375189776,23.143904651374864,43.21839708549082 +2024-08-12 01:00:00,4,11.871990987557759,24.250853554189113,61.07995768171188 +2024-08-12 02:00:00,4,21.788871801283978,24.601934561146216,73.74966173188746 +2024-08-12 03:00:00,4,24.82847769476719,24.518383733984436,65.14520252818144 +2024-08-12 04:00:00,4,35.287883109230094,25.387991955453636,55.036893504167956 +2024-08-12 05:00:00,4,22.363193944844447,22.75526397090999,47.754604805822424 +2024-08-12 06:00:00,4,36.521545207253084,22.947078285146425,73.99270006521044 +2024-08-12 07:00:00,4,10.300607894160486,23.887024990489973,71.50948421922558 +2024-08-12 08:00:00,4,35.42090021311289,27.682784429663755,52.79792495457031 +2024-08-12 09:00:00,4,29.477959361476852,20.95147607988837,68.31576555062738 +2024-08-12 10:00:00,4,31.819735489824875,25.17934419663039,77.27669473286814 +2024-08-12 11:00:00,4,45.684742234514836,22.317925575493238,64.98104132021408 +2024-08-12 12:00:00,4,16.878379826605578,24.28814942690757,50.19148081224925 +2024-08-12 13:00:00,4,19.502526795810248,28.780097755357783,50.788617374713546 +2024-08-12 14:00:00,4,20.08770779290897,21.53482044668091,66.95470947793739 +2024-08-12 15:00:00,4,22.854225573065133,25.47079088233123,52.94668505760709 +2024-08-12 16:00:00,4,35.768500662335384,21.9246657276715,61.640429852247706 +2024-08-12 17:00:00,4,30.100966207970842,30.358387307622078,41.611884726203975 +2024-08-12 18:00:00,4,20.395417655433537,25.753167763616258,62.15829663704318 +2024-08-12 19:00:00,4,19.076651158117986,24.63488245171938,51.787562122669705 +2024-08-12 20:00:00,4,15.173379674229528,31.67442269936114,47.39884653290668 +2024-08-12 21:00:00,4,21.402330577125507,21.62507648176996,63.14235961829972 +2024-08-12 22:00:00,4,20.850568658209543,27.173891342084033,41.6705920580336 +2024-08-12 23:00:00,4,28.708276984037546,21.412594425335147,64.92381188261245 +2024-08-13 00:00:00,4,46.27002026645016,27.121022621232388,54.202275775698084 +2024-08-13 01:00:00,4,34.90250706114665,23.462199428929793,57.15559431925203 +2024-08-13 02:00:00,4,30.388838166673537,16.758899207716834,65.38129458442546 +2024-08-13 03:00:00,4,42.635923241709015,21.935016017198862,52.43985861107619 +2024-08-13 04:00:00,4,20.334829256646405,20.016914603994586,41.82355280050142 +2024-08-13 05:00:00,4,20.303838282851686,25.906273506808894,46.06846901235462 +2024-08-13 06:00:00,4,16.32916426017376,17.7776870470509,53.364330237831815 +2024-08-13 07:00:00,4,36.376012180328885,22.79033930923167,59.33381934017909 +2024-08-13 08:00:00,4,10.67383880045513,21.366049393495167,60.32115527913584 +2024-08-13 09:00:00,4,30.368159413071545,22.19742659811148,59.44216624182048 +2024-08-13 10:00:00,4,32.508522501048525,16.134552341504303,42.063683731966215 +2024-08-13 11:00:00,4,6.1391614108006465,29.516502913597638,53.04476797888674 +2024-08-13 12:00:00,4,18.952214170795994,18.03495371465518,51.7705091954648 +2024-08-13 13:00:00,4,29.100965889478978,24.611009524852342,47.39063107159056 +2024-08-13 14:00:00,4,27.464675595861603,20.75436559568176,68.51545806027177 +2024-08-13 15:00:00,4,15.58925012020677,24.452178190338202,49.61774166697604 +2024-08-13 16:00:00,4,17.489549344452787,24.346817005809736,57.218674034704854 +2024-08-13 17:00:00,4,17.359044892892875,23.184357032595926,52.6247585128491 +2024-08-13 18:00:00,4,21.695009230329337,21.998784887411308,33.74106845614201 +2024-08-13 19:00:00,4,25.803128053690074,30.564693217753458,67.88741081536276 +2024-08-13 20:00:00,4,36.59069298456657,32.47136423126597,46.39870075345662 +2024-08-13 21:00:00,4,18.115958862512027,23.961612221030443,67.94691712216103 +2024-08-13 22:00:00,4,12.843443504128842,29.746693062105862,67.2493939522054 +2024-08-13 23:00:00,4,16.46436516669424,31.897406214938425,36.50397439871361 +2024-08-14 00:00:00,4,26.65831184318603,19.871644243400954,64.71226540438886 +2024-08-14 01:00:00,4,21.780113235672065,26.766160308096794,35.799167045898024 +2024-08-14 02:00:00,4,21.012238513729894,26.554477090257098,68.9753377078426 +2024-08-14 03:00:00,4,30.64894347977968,30.391195196155113,53.31571926020448 +2024-08-14 04:00:00,4,28.3417087518658,30.719748726432826,54.52373342659096 +2024-08-14 05:00:00,4,11.773145810200612,21.934337014763734,71.51575564927508 +2024-08-14 06:00:00,4,30.406281116117423,19.986850549674628,41.604058815511465 +2024-08-14 07:00:00,4,38.6213429955651,25.853626387724816,71.65314592864598 +2024-08-14 08:00:00,4,31.863962831781986,27.134415012846453,53.11522250820005 +2024-08-14 09:00:00,4,15.926789518758921,21.074554686358944,67.73806999681585 +2024-08-14 10:00:00,4,2.3578507721398374,23.103558961297626,56.54979864621674 +2024-08-14 11:00:00,4,41.13689060111221,20.071732976010694,66.8448731565597 +2024-08-14 12:00:00,4,25.75024828098072,18.300219180544662,49.355109537113016 +2024-08-14 13:00:00,4,32.10891594491636,26.66572999261653,66.20465092263582 +2024-08-14 14:00:00,4,17.446379955420866,23.58431774591324,55.782677337043914 +2024-08-14 15:00:00,4,8.094968206417436,28.481748845090483,53.58870414524826 +2024-08-14 16:00:00,4,25.29284681726954,21.357354306402286,50.618129119317516 +2024-08-14 17:00:00,4,19.265976973653956,26.054401002837686,53.349773137702726 +2024-08-14 18:00:00,4,27.97783099505282,18.840985931242837,49.25194175923838 +2024-08-14 19:00:00,4,32.755155841781544,25.652879776829995,61.20970148094479 +2024-08-14 20:00:00,4,29.473363219024833,24.98014804373278,58.8241445464259 +2024-08-14 21:00:00,4,21.521107566806943,22.69751719338977,69.7296210674892 +2024-08-14 22:00:00,4,27.887189839957596,23.90207156317266,53.715968805411215 +2024-08-14 23:00:00,4,26.25972690937865,25.675023570772634,63.540419060494344 +2024-08-15 00:00:00,4,30.007895767614116,29.41628574645192,52.333933044053545 +2024-08-15 01:00:00,4,28.959585040138546,22.89210558102283,58.84511259586331 +2024-08-15 02:00:00,4,35.608860516560796,26.42231547766751,51.964627030764724 +2024-08-15 03:00:00,4,8.99463600679535,27.222439573412792,66.42647397454998 +2024-08-15 04:00:00,4,17.387233763401476,31.920835971875157,46.50212473167113 +2024-08-15 05:00:00,4,12.565359733381229,19.98598045673015,52.33249543241789 +2024-08-15 06:00:00,4,19.047169417052046,25.950487405424546,66.50450363833531 +2024-08-15 07:00:00,4,25.798512002755075,31.10640193824016,37.979235147287156 +2024-08-15 08:00:00,4,25.94833883495966,22.44394957274678,53.748524407118765 +2024-08-15 09:00:00,4,12.198002458622398,14.14659390622376,71.35360606268917 +2024-08-15 10:00:00,4,31.96960116531799,23.052860450608403,45.75916251857543 +2024-08-15 11:00:00,4,37.78073992589165,17.101388628151536,74.53948437529293 +2024-08-15 12:00:00,4,13.873592458581884,22.08376533480453,63.426272371873594 +2024-08-15 13:00:00,4,20.783030895327997,22.036356828934508,51.189983121637155 +2024-08-15 14:00:00,4,14.648542392133374,19.160613206307815,64.21778006819814 +2024-08-15 15:00:00,4,10.86991342430788,27.206804703465384,55.05752325080353 +2024-08-15 16:00:00,4,6.841313258826121,26.76139147732025,58.40091348107545 +2024-08-15 17:00:00,4,12.074246286672091,20.59343015774082,41.939712674282134 +2024-08-15 18:00:00,4,19.814474115326195,24.833211620676035,44.780044997411416 +2024-08-15 19:00:00,4,35.511490610791704,18.53863076711423,55.77659206452419 +2024-08-15 20:00:00,4,27.337329846838067,27.36707232998826,57.64640894567185 +2024-08-15 21:00:00,4,30.132819839224915,25.932785850541997,60.71741779984836 +2024-08-15 22:00:00,4,35.856629395823774,22.050246345353596,54.99919383959273 +2024-08-15 23:00:00,4,16.767858021078894,24.944772544421102,53.70445618780479 +2024-08-16 00:00:00,4,27.415848652106224,26.116303638213402,52.840979882193196 +2024-08-16 01:00:00,4,13.20789028308191,18.701366897436188,54.82144382322987 +2024-08-16 02:00:00,4,40.39326300715304,21.923973487169214,65.25975064471831 +2024-08-16 03:00:00,4,13.80099201690297,24.8609523545192,51.807396288928906 +2024-08-16 04:00:00,4,36.19383714050052,27.459086214856725,44.469096941771795 +2024-08-16 05:00:00,4,28.94151302426864,26.77485247258039,58.282812499038634 +2024-08-16 06:00:00,4,28.80044821347131,22.894173541062358,64.95546980373445 +2024-08-16 07:00:00,4,28.530669504741212,22.219383174147517,66.86316093627374 +2024-08-16 08:00:00,4,28.868549722969384,21.35981315860933,44.58129265071025 +2024-08-16 09:00:00,4,25.887115045008557,23.015869647421706,52.37937836142868 +2024-08-16 10:00:00,4,23.061977969181243,21.351453818229214,67.78772242843895 +2024-08-16 11:00:00,4,15.572621623402636,24.98376296673636,68.00783756581765 +2024-08-16 12:00:00,4,13.272565839843663,21.68753020602151,58.36556276933715 +2024-08-16 13:00:00,4,9.734120013279554,28.518529709977244,56.36120680333974 +2024-08-16 14:00:00,4,5.346874890993721,23.616226248662848,54.80992760872144 +2024-08-16 15:00:00,4,31.287061032420265,25.77559822454792,59.74517976748456 +2024-08-16 16:00:00,4,27.41464502200124,15.9626302321422,63.88752434160072 +2024-08-16 17:00:00,4,25.428932356712632,20.94518487651323,55.95299189188238 +2024-08-16 18:00:00,4,6.297649529054951,28.28655654747881,47.32490864604284 +2024-08-16 19:00:00,4,13.153830973156772,24.764867708435634,64.99743557074312 +2024-08-16 20:00:00,4,15.282868375629079,23.040029703869113,58.590077789733975 +2024-08-16 21:00:00,4,34.24509749572148,23.630895016272042,43.593373037161896 +2024-08-16 22:00:00,4,16.55013404526263,21.942582125599,66.55246668718408 +2024-08-16 23:00:00,4,33.43813973504487,15.340686059535745,45.64643781688319 +2024-08-17 00:00:00,4,46.256561941599635,23.776415256112557,46.99922976908052 +2024-08-17 01:00:00,4,31.248052960618256,19.158067330534934,63.92279017648362 +2024-08-17 02:00:00,4,21.77505950089529,24.19554894492053,72.37980010217018 +2024-08-17 03:00:00,4,31.809562052877965,19.510521848908684,60.35460391664201 +2024-08-17 04:00:00,4,15.592941783800777,23.977155900972313,55.035850962093456 +2024-08-17 05:00:00,4,34.85663560916171,20.470235732738097,59.197805382144686 +2024-08-17 06:00:00,4,42.634277073230756,26.015268770328607,49.01102617619027 +2024-08-17 07:00:00,4,34.607995447813245,24.51886820840615,52.856604350459165 +2024-08-17 08:00:00,4,17.66666544249405,26.709703426399074,63.12683095717175 +2024-08-17 09:00:00,4,27.609027909924563,24.750358070979594,64.60931894295291 +2024-08-17 10:00:00,4,19.749099828770376,23.716698146838546,51.69754806387233 +2024-08-17 11:00:00,4,43.73690570613476,27.24707863182805,60.50406812663503 +2024-08-17 12:00:00,4,23.142532634089285,19.46875774649693,67.82767161169454 +2024-08-17 13:00:00,4,17.916989578387852,25.723455314543116,55.25781207605605 +2024-08-17 14:00:00,4,20.950442565342566,26.960612788623212,75.78119771434386 +2024-08-17 15:00:00,4,13.316571128206377,23.67210029602404,49.46550642850945 +2024-08-17 16:00:00,4,13.39334107866436,16.811906820467623,59.455134119348926 +2024-08-17 17:00:00,4,15.150336649828997,20.49505797737636,51.20305299956251 +2024-08-17 18:00:00,4,29.735928880599094,25.354322629730188,57.487984907931335 +2024-08-17 19:00:00,4,24.52616190279783,22.66033485091294,46.3090223135207 +2024-08-17 20:00:00,4,13.580827114930452,27.537533827066184,47.957155335984766 +2024-08-17 21:00:00,4,25.44462184386962,26.990655951791464,55.521855206347794 +2024-08-17 22:00:00,4,24.67061065819138,27.489912219490883,52.90033453846446 +2024-08-17 23:00:00,4,30.699142285622425,29.076849932862192,67.07860486105989 +2024-08-18 00:00:00,4,19.101611594748768,26.01946192707476,64.18715188676512 +2024-08-18 01:00:00,4,30.161645130950827,18.635323276821936,57.10448643858514 +2024-08-18 02:00:00,4,27.29919310671179,24.453546040161555,57.312864838862446 +2024-08-18 03:00:00,4,26.413143434908733,18.919757062075245,68.91725383085122 +2024-08-18 04:00:00,4,25.924576332658667,23.219362921444507,49.237658368604926 +2024-08-18 05:00:00,4,15.726110157432931,21.700952833721157,45.803145846782144 +2024-08-18 06:00:00,4,30.14871119958804,25.518678720598384,71.11860143509524 +2024-08-18 07:00:00,4,28.565884792049907,24.12568357185572,63.12135108385109 +2024-08-18 08:00:00,4,13.964899671204597,19.13551490346704,51.473862467768434 +2024-08-18 09:00:00,4,25.04117105657563,19.93053064242952,69.11808749657385 +2024-08-18 10:00:00,4,26.13055889093621,24.539211335218003,59.57245724993271 +2024-08-18 11:00:00,4,23.01967623996575,23.666309768931864,48.05236908744081 +2024-08-18 12:00:00,4,30.74421623742046,20.99162451050339,48.42735143221552 +2024-08-18 13:00:00,4,10.846999591820111,24.123692073903293,74.1514632258721 +2024-08-18 14:00:00,4,21.094966801031905,26.028639088925715,50.54132849781166 +2024-08-18 15:00:00,4,14.957561156580688,21.788331129138786,53.78839740041291 +2024-08-18 16:00:00,4,7.710608014425764,26.143412537561886,77.84860358271705 +2024-08-18 17:00:00,4,8.867728628959611,23.022409303145928,51.532314804887775 +2024-08-18 18:00:00,4,42.389787088210376,24.985774366270547,44.244334382477575 +2024-08-18 19:00:00,4,27.555508705388345,21.154581285268694,63.20474190261969 +2024-08-18 20:00:00,4,15.188867976341594,22.535709367578193,54.96914340732127 +2024-08-18 21:00:00,4,32.61416296252787,26.098951222013724,53.5056224897965 +2024-08-18 22:00:00,4,16.685379399102136,25.468323332033144,62.4190651229574 +2024-08-18 23:00:00,4,26.011869607334404,24.217312609620485,58.048494579386244 +2024-08-19 00:00:00,4,52.58711915432619,27.052195236567794,43.21908422229297 +2024-08-19 01:00:00,4,15.793083101256803,30.577656329977916,50.83173649639677 +2024-08-19 02:00:00,4,37.83747205299998,26.5680414403176,68.29863013300893 +2024-08-19 03:00:00,4,32.79692535159189,24.67525006994994,57.02460956368082 +2024-08-19 04:00:00,4,28.855987058277943,18.49212401261488,52.84506697279302 +2024-08-19 05:00:00,4,33.02905595621087,23.929571621454425,60.829473798287154 +2024-08-19 06:00:00,4,17.177575468842637,27.499577004022893,68.43425276397173 +2024-08-19 07:00:00,4,25.533427171853848,22.785842991978882,62.4186693619738 +2024-08-19 08:00:00,4,19.34893657198968,30.164530159851203,43.183702538274545 +2024-08-19 09:00:00,4,30.260924686058384,14.614451829521737,58.47599629490295 +2024-08-19 10:00:00,4,26.652317355198708,22.89886863398778,40.18906755900328 +2024-08-19 11:00:00,4,36.49063330128679,24.016600887972984,58.5157346618067 +2024-08-19 12:00:00,4,31.94360899484485,17.909793415810277,42.81496620139643 +2024-08-19 13:00:00,4,26.22201020692708,25.038077435774436,51.12185271805741 +2024-08-19 14:00:00,4,21.240244470323162,19.96104999171128,54.767971835149396 +2024-08-19 15:00:00,4,31.246115102724005,25.508845917249417,69.05043433150779 +2024-08-19 16:00:00,4,25.759906283126814,25.77894251888294,51.85763020426861 +2024-08-19 17:00:00,4,28.693842012077127,18.40559329546446,70.468736760503 +2024-08-19 18:00:00,4,29.506092481433203,25.885588422512296,56.89567946099677 +2024-08-19 19:00:00,4,33.66266962854375,25.161394941140724,62.514004601061096 +2024-08-19 20:00:00,4,9.75805684097875,28.28902237461626,58.3869953091383 +2024-08-19 21:00:00,4,17.76467461731184,28.022712785253596,66.87276020346911 +2024-08-19 22:00:00,4,17.6931952849165,27.147753770466185,59.87293620126273 +2024-08-19 23:00:00,4,18.32928052355086,28.257337921930485,63.5651096800971 +2024-08-20 00:00:00,4,29.712201403759533,21.953037816494785,53.850536463013285 +2024-08-20 01:00:00,4,23.20447084610847,26.846178238625487,44.889995855866744 +2024-08-20 02:00:00,4,21.768644035677493,26.040862349838083,62.8314231967722 +2024-08-20 03:00:00,4,35.46756302349358,21.82857128614288,56.53006522860808 +2024-08-20 04:00:00,4,17.43410936517876,23.406015554857564,56.24181351420591 +2024-08-20 05:00:00,4,20.75636542079289,25.351090567631132,38.077362178187954 +2024-08-20 06:00:00,4,16.236295291274267,23.998966621724175,72.13094312674096 +2024-08-20 07:00:00,4,14.975310256988017,26.61218156976752,52.96824857769609 +2024-08-20 08:00:00,4,33.58389941556655,18.943970366893893,67.6059678083945 +2024-08-20 09:00:00,4,22.4147822153839,22.232084383654573,65.79863643271557 +2024-08-20 10:00:00,4,35.24867705868617,16.397050764323506,53.7109595683477 +2024-08-20 11:00:00,4,37.37268236111606,17.2521585468263,70.64651590138129 +2024-08-20 12:00:00,4,29.19902581244293,24.146734374345435,64.3195002213978 +2024-08-20 13:00:00,4,39.38143568992293,20.511709644547686,45.78601817751935 +2024-08-20 14:00:00,4,21.48168492525904,24.24748423615691,52.28872009890616 +2024-08-20 15:00:00,4,33.22741629503771,28.029388992455743,64.5255950089146 +2024-08-20 16:00:00,4,27.100468178075918,27.41911348561175,47.839874341737676 +2024-08-20 17:00:00,4,19.843856290689157,28.82825352415741,49.222147524243006 +2024-08-20 18:00:00,4,40.4742891963344,24.823785451533638,53.91928837088986 +2024-08-20 19:00:00,4,26.88570951189056,28.996557920826966,58.68486474962929 +2024-08-20 20:00:00,4,28.364061006766892,20.52351221153632,59.162152096094125 +2024-08-20 21:00:00,4,27.10851935012966,19.934807057592522,51.373642582289435 +2024-08-20 22:00:00,4,21.07905076226782,23.46415718332116,59.545144591016424 +2024-08-20 23:00:00,4,17.9751765005486,23.55158591834719,62.138600355965785 +2024-08-21 00:00:00,4,26.780186045107516,22.35458704592783,64.1370288231062 +2024-08-21 01:00:00,4,24.944923707733246,24.87346863414556,53.27630450184988 +2024-08-21 02:00:00,4,23.2690814925198,26.5172776138435,56.38678985830584 +2024-08-21 03:00:00,4,33.11714422870721,25.976332884712168,61.414434899442384 +2024-08-21 04:00:00,4,28.101864094437435,22.5014922362449,50.86041130782414 +2024-08-21 05:00:00,4,39.16386968802161,24.345011012331945,45.629863536802866 +2024-08-21 06:00:00,4,19.643344546994104,15.947277258414719,38.02955126181776 +2024-08-21 07:00:00,4,41.740410288274155,24.800304449215595,70.83379364848807 +2024-08-21 08:00:00,4,19.94200196385053,22.10735332357966,57.15303196999462 +2024-08-21 09:00:00,4,18.278568406196978,17.39892998359931,56.47544138683767 +2024-08-21 10:00:00,4,37.79261271114577,24.229460378988808,59.9408420526744 +2024-08-21 11:00:00,4,32.94802049803438,22.514190985739845,56.516655050339544 +2024-08-21 12:00:00,4,23.02373945853063,25.242878407347483,48.63861136183563 +2024-08-21 13:00:00,4,14.846287552510285,29.639170801035316,60.49437659210838 +2024-08-21 14:00:00,4,12.417921007141732,26.796769828227376,65.80993968548049 +2024-08-21 15:00:00,4,15.20085527026134,21.18704945184062,60.560803050190785 +2024-08-21 16:00:00,4,21.66105050524245,19.693561432114706,72.72822750872417 +2024-08-21 17:00:00,4,15.329268446730483,27.565409184681847,48.4057519691989 +2024-08-21 18:00:00,4,33.531749098042745,22.01452810023759,47.83448873907558 +2024-08-21 19:00:00,4,31.347774615549106,23.138366289057444,55.401658328718376 +2024-08-21 20:00:00,4,13.027697363909589,25.24414362518424,64.01104191118526 +2024-08-21 21:00:00,4,21.436420434099293,28.43508581650774,59.6018152131902 +2024-08-21 22:00:00,4,29.117931223906343,26.00453836287969,45.09289761396647 +2024-08-21 23:00:00,4,22.94672966271277,24.88040745482547,38.43422079451656 +2024-08-22 00:00:00,4,21.417723662863573,26.257812945839365,57.28841294875976 +2024-08-22 01:00:00,4,24.17070642545597,26.444770459423797,58.408822706498306 +2024-08-22 02:00:00,4,39.93356954591188,21.16303006870516,61.8591740018392 +2024-08-22 03:00:00,4,25.632892583227186,27.683167308194424,60.67267378260996 +2024-08-22 04:00:00,4,23.219939577589116,22.500692592986503,46.805973324856915 +2024-08-22 05:00:00,4,5.5756767452306235,15.846635770294393,60.63626474515269 +2024-08-22 06:00:00,4,22.26881774685112,29.139135339670823,36.38547434685007 +2024-08-22 07:00:00,4,49.1193437022589,25.83074609868472,52.44790651808587 +2024-08-22 08:00:00,4,37.823554905691175,18.158509113303023,53.23054432616803 +2024-08-22 09:00:00,4,24.967370997361176,26.838563054261087,41.98493305782674 +2024-08-22 10:00:00,4,21.665243230622934,27.11952330320584,47.91616843996439 +2024-08-22 11:00:00,4,32.854919304101415,26.210106001262496,73.04041120427976 +2024-08-22 12:00:00,4,5.972781659249872,24.90885209891475,46.49094976115747 +2024-08-22 13:00:00,4,19.903262660776726,22.340278161008328,62.775787237688625 +2024-08-22 14:00:00,4,14.094214636342397,20.167863112243225,60.778027092158915 +2024-08-22 15:00:00,4,32.16242928551911,28.198039393747436,68.2648583844175 +2024-08-22 16:00:00,4,11.508687010116077,28.242438114039174,38.85376016848993 +2024-08-22 17:00:00,4,19.680544107916376,19.985142009395695,44.77427924287799 +2024-08-22 18:00:00,4,20.589965498248244,26.664631174020222,67.63420261159567 +2024-08-22 19:00:00,4,35.45116395956569,22.984059099324842,57.198312040615185 +2024-08-22 20:00:00,4,14.328048233378547,21.139836175694935,57.65534546058305 +2024-08-22 21:00:00,4,29.06567093416786,17.535487352657,57.33874911772147 +2024-08-22 22:00:00,4,24.42379094026612,22.41939172399184,61.899676010991094 +2024-08-22 23:00:00,4,16.772866425901277,26.740039693352152,52.863582465935316 +2024-08-23 00:00:00,4,26.782372578900226,29.72222760732702,43.59393113692337 +2024-08-23 01:00:00,4,18.805588727387473,25.715119798589928,54.02688859873948 +2024-08-23 02:00:00,4,37.27552305935555,26.97633721241036,56.5677941943794 +2024-08-23 03:00:00,4,36.57575793618991,25.25285153619014,45.57940238224148 +2024-08-23 04:00:00,4,40.24759717692763,26.346882001835098,54.70855020011124 +2024-08-23 05:00:00,4,28.66258325002277,21.882362224187617,62.89855633560547 +2024-08-23 06:00:00,4,16.427730267839756,23.613293475651194,55.05561775487321 +2024-08-23 07:00:00,4,35.1519543743848,27.553544311571468,78.58717900025042 +2024-08-23 08:00:00,4,18.75382557239135,24.80000686201221,57.16937380968072 +2024-08-23 09:00:00,4,23.46975609839371,21.199382414344875,50.48026619497526 +2024-08-23 10:00:00,4,27.98862712031115,24.098826072568535,49.93945295074065 +2024-08-23 11:00:00,4,36.04609965217281,21.236155715213286,46.88873687786474 +2024-08-23 12:00:00,4,23.444682576885974,16.26082258355278,57.08607735409592 +2024-08-23 13:00:00,4,25.320537413437936,24.225147750964105,64.57646478609092 +2024-08-23 14:00:00,4,24.953072784814033,24.43820011357093,50.44093600177127 +2024-08-23 15:00:00,4,19.64767884200773,28.023871462358386,58.685855372938214 +2024-08-23 16:00:00,4,16.53599713940674,23.88912171580704,43.92884236478962 +2024-08-23 17:00:00,4,20.194942460288605,20.2968116808115,35.96174147355707 +2024-08-23 18:00:00,4,15.686596212528706,20.977168595920052,48.03804960027452 +2024-08-23 19:00:00,4,16.714506799113558,23.84194254653132,40.40839632977411 +2024-08-23 20:00:00,4,8.428394328634107,29.536379514940364,47.28452974867706 +2024-08-23 21:00:00,4,32.94395250265817,31.111709813790092,64.83868448325298 +2024-08-23 22:00:00,4,37.420464567377635,21.642431262180224,52.394326549395174 +2024-08-23 23:00:00,4,16.1193158838706,19.261600050618483,47.49011066378318 +2024-08-24 00:00:00,4,15.316372403079288,20.431509868504104,58.8317420469325 +2024-08-24 01:00:00,4,28.51483107218998,19.065679162010973,48.6623777282394 +2024-08-24 02:00:00,4,31.538977310069402,21.81489779414456,57.30077111850618 +2024-08-24 03:00:00,4,30.565061433510643,22.8314559810247,44.687611468970225 +2024-08-24 04:00:00,4,26.800386324021346,22.469001415275134,55.44925082213491 +2024-08-24 05:00:00,4,28.506138068780423,17.550541493027865,55.83208224491575 +2024-08-24 06:00:00,4,18.763666879202447,30.604702219163748,54.36322274078328 +2024-08-24 07:00:00,4,20.888293809198846,22.171520543472127,64.95427565172938 +2024-08-24 08:00:00,4,36.562009626645875,21.03941945472437,51.27059750214813 +2024-08-24 09:00:00,4,29.62073471523179,23.57506960476823,61.28778151476663 +2024-08-24 10:00:00,4,51.07095775351762,26.82511498649457,56.0118490799005 +2024-08-24 11:00:00,4,22.263533278830636,18.6858130588895,51.87052317105581 +2024-08-24 12:00:00,4,39.62605164525406,19.73808792360416,62.91996729127452 +2024-08-24 13:00:00,4,12.032096027133239,25.75587869334048,50.784433527741356 +2024-08-24 14:00:00,4,5.146345888881413,21.190483325985245,53.26933540784589 +2024-08-24 15:00:00,4,27.20686897301111,21.420222815271302,55.72875524001517 +2024-08-24 16:00:00,4,5.622292264776103,25.14662950509938,49.4142503948713 +2024-08-24 17:00:00,4,21.224425817362512,23.956187748462348,58.31472498963009 +2024-08-24 18:00:00,4,0.0,24.065006882628893,49.1225238983556 +2024-08-24 19:00:00,4,25.643534622157738,31.195311101712015,56.568147765836756 +2024-08-24 20:00:00,4,21.084251708269136,31.72886243127048,51.271982107223586 +2024-08-24 21:00:00,4,26.1884460039557,25.54726104571616,52.49014150506686 +2024-08-24 22:00:00,4,40.26444477955098,28.62103355213479,47.172124977326376 +2024-08-24 23:00:00,4,13.956238448835265,29.16496822065308,77.92563170253797 +2024-08-25 00:00:00,4,41.01142425006576,25.037670325652673,73.41669153252434 +2024-08-25 01:00:00,4,17.127864616797176,14.17626109732102,56.633964048494015 +2024-08-25 02:00:00,4,47.95204915582325,16.9805997984257,53.55545583463477 +2024-08-25 03:00:00,4,33.7345804489822,26.852191928727784,56.84502131428513 +2024-08-25 04:00:00,4,24.895355585725948,24.501434827174318,59.453903828089906 +2024-08-25 05:00:00,4,27.40329549634881,17.707854748803772,40.42890330896014 +2024-08-25 06:00:00,4,27.227531045178342,25.880643226329745,66.40284765277194 +2024-08-25 07:00:00,4,26.191265611610806,17.63480726828205,42.07648105585713 +2024-08-25 08:00:00,4,42.51821969115764,22.662465923211375,64.89877815502236 +2024-08-25 09:00:00,4,21.43759388505388,18.624286135403324,75.209171030286 +2024-08-25 10:00:00,4,18.892572418371095,21.79670459906605,53.432160790073276 +2024-08-25 11:00:00,4,38.96417810433844,33.4212040878542,60.62035396993587 +2024-08-25 12:00:00,4,22.178134209504613,21.23521738615971,47.38213279752051 +2024-08-25 13:00:00,4,16.80462351294132,26.109025130525353,67.40427413598808 +2024-08-25 14:00:00,4,19.59575848286381,22.840737434705453,57.80459107353315 +2024-08-25 15:00:00,4,27.951297562756547,25.703927335766885,60.016735905118466 +2024-08-25 16:00:00,4,8.288889322832564,23.85262091915849,43.89519428200151 +2024-08-25 17:00:00,4,25.051976844115213,25.30053087306903,55.49674529941048 +2024-08-25 18:00:00,4,28.86065593186402,28.522594156669037,39.551857201581946 +2024-08-25 19:00:00,4,20.186499461368093,25.722204923209972,37.08748784183635 +2024-08-25 20:00:00,4,21.302032821152768,16.7535396396912,44.855554890709975 +2024-08-25 21:00:00,4,19.25816973578828,20.37208629760611,41.81065263918332 +2024-08-25 22:00:00,4,26.215744944425474,28.853812375415966,50.64687320576227 +2024-08-25 23:00:00,4,28.842358690562953,19.60896410287953,42.737873133232746 +2024-08-26 00:00:00,4,38.348488093079474,23.603563749713487,47.7596939879679 +2024-08-26 01:00:00,4,24.45577566421523,23.75215132098923,56.44604224462261 +2024-08-26 02:00:00,4,17.71766706430034,22.488982981373056,56.19496567217681 +2024-08-26 03:00:00,4,16.815324217410527,22.861236185771187,43.56631410122761 +2024-08-26 04:00:00,4,10.097766251374715,29.070102184750972,59.572940681205374 +2024-08-26 05:00:00,4,16.575655957702587,17.06196080295374,50.450970998151845 +2024-08-26 06:00:00,4,21.56376145002989,24.82285251715008,50.600107570508726 +2024-08-26 07:00:00,4,21.04292164499706,17.44467504498072,49.83625113687071 +2024-08-26 08:00:00,4,25.33271094596879,21.304676111646305,62.56380745839682 +2024-08-26 09:00:00,4,29.19855766831185,20.532206353120788,72.5285144875289 +2024-08-26 10:00:00,4,27.812481125976085,16.27187776734824,62.4772014040936 +2024-08-26 11:00:00,4,21.565972762371338,20.4586627025435,61.10363240031564 +2024-08-26 12:00:00,4,19.17607322024151,20.808344337952448,70.90665312895247 +2024-08-26 13:00:00,4,17.99319412470522,22.546338262615212,47.35481732221242 +2024-08-26 14:00:00,4,20.22131289302948,22.919419620733862,63.07480109189238 +2024-08-26 15:00:00,4,26.99081394950348,22.17446488981282,50.53536692606285 +2024-08-26 16:00:00,4,10.671974717716456,28.009605987959365,49.856869247993316 +2024-08-26 17:00:00,4,36.35172535259939,27.117480977088896,48.70551950175211 +2024-08-26 18:00:00,4,20.815970241342104,26.037532314229672,61.21569318183309 +2024-08-26 19:00:00,4,15.487458867857995,16.683702200039534,47.352052876066836 +2024-08-26 20:00:00,4,25.741884792201052,28.394828331233782,59.03876907471658 +2024-08-26 21:00:00,4,13.854993487824492,23.387030410133207,55.79085813041909 +2024-08-26 22:00:00,4,30.24062840737808,26.09958623448914,37.185089295249334 +2024-08-26 23:00:00,4,16.967140440365974,26.76117132768704,62.167254527601486 +2024-08-27 00:00:00,4,8.488238322767572,21.711990937097625,51.629673269016095 +2024-08-27 01:00:00,4,14.975049036836756,19.809009481625186,48.25163405029947 +2024-08-27 02:00:00,4,29.501864881621703,27.03008994035414,58.626872230790134 +2024-08-27 03:00:00,4,27.724294429156277,22.215807967876287,65.69110745726421 +2024-08-27 04:00:00,4,42.00153715162903,33.06749218289319,41.22078137676128 +2024-08-27 05:00:00,4,18.247457112492768,22.844048827503073,55.437893960639094 +2024-08-27 06:00:00,4,40.23949555261156,24.151947529686346,57.28280830212094 +2024-08-27 07:00:00,4,39.233806277007105,15.991970809961296,76.87657291158426 +2024-08-27 08:00:00,4,14.894478329857423,20.7935904325028,62.19568899158908 +2024-08-27 09:00:00,4,25.90374161126014,20.978256777094963,65.3614632567571 +2024-08-27 10:00:00,4,25.574983302503423,17.380162258120492,56.01780377862827 +2024-08-27 11:00:00,4,0.0,19.106524580880436,59.02978851477426 +2024-08-27 12:00:00,4,31.956204382693535,22.39947233032763,77.5631842286347 +2024-08-27 13:00:00,4,15.64228412249631,27.03983944165398,59.389844982216204 +2024-08-27 14:00:00,4,23.264195516888194,24.569060233592563,56.41505709867252 +2024-08-27 15:00:00,4,27.26860484336326,25.776625091270525,59.47830641969065 +2024-08-27 16:00:00,4,23.660899687599617,19.35910097886847,40.8751408542865 +2024-08-27 17:00:00,4,16.41202694874387,27.033683752394463,50.04970438846157 +2024-08-27 18:00:00,4,19.64887610633019,18.92513982276597,50.457685797705224 +2024-08-27 19:00:00,4,28.109719841773973,28.641306290277996,46.14815276859636 +2024-08-27 20:00:00,4,28.08666435075433,20.964630318391812,54.65973711510742 +2024-08-27 21:00:00,4,12.995376052084309,17.416431009194497,38.03576135392752 +2024-08-27 22:00:00,4,42.24653580112023,23.536515287947747,44.770607671742724 +2024-08-27 23:00:00,4,34.48830113551932,26.247498179250563,60.85823887362895 +2024-08-28 00:00:00,4,13.456501714439604,23.41749311265287,79.62759153208047 +2024-08-28 01:00:00,4,21.096202766653292,25.943266794969134,64.60581269523081 +2024-08-28 02:00:00,4,11.138199987506407,23.707103681323233,72.22492714078645 +2024-08-28 03:00:00,4,19.35919369746177,28.96020478723252,51.82694737120445 +2024-08-28 04:00:00,4,29.9120778429251,21.710976188316916,59.99541089892375 +2024-08-28 05:00:00,4,34.98747873376895,18.372629302451283,59.6148264978701 +2024-08-28 06:00:00,4,28.50134977673737,23.88256247930676,54.76315837357876 +2024-08-28 07:00:00,4,34.433580230727365,26.714705129389635,59.016025191389154 +2024-08-28 08:00:00,4,33.15657848436794,26.560880188429092,66.6180934227184 +2024-08-28 09:00:00,4,46.593736960612716,20.49057384308961,55.72611268826631 +2024-08-28 10:00:00,4,24.3913557699081,28.017796913923323,60.7975270031484 +2024-08-28 11:00:00,4,26.05019008590407,25.027784479056745,61.85354530935023 +2024-08-28 12:00:00,4,8.755622897076044,20.757438153898775,55.1423371659343 +2024-08-28 13:00:00,4,1.2198050596919892,23.572388156071924,61.716938795850524 +2024-08-28 14:00:00,4,19.022826871403584,24.872915356546585,62.06403304450018 +2024-08-28 15:00:00,4,22.91164156144754,28.86529461325799,56.37022323859665 +2024-08-28 16:00:00,4,32.74335716362329,23.403379095413197,46.88900470975514 +2024-08-28 17:00:00,4,30.572100568574868,31.007172494954823,55.52794810079329 +2024-08-28 18:00:00,4,17.664369686339683,25.967905553025044,57.75272195412646 +2024-08-28 19:00:00,4,28.05835778439861,25.283017289136623,64.73747791531144 +2024-08-28 20:00:00,4,9.25268958741846,23.53541498140925,56.4190127579791 +2024-08-28 21:00:00,4,17.93887932303369,26.887121176922896,53.808753414942906 +2024-08-28 22:00:00,4,17.688822685231173,20.831229876301578,58.82963263120601 +2024-08-28 23:00:00,4,7.867149961513938,26.049986049681458,62.50838638917381 +2024-08-29 00:00:00,4,31.385272601231474,27.58766101765443,62.94799775055773 +2024-08-29 01:00:00,4,30.78232681977787,22.958346511375023,42.72108569390986 +2024-08-29 02:00:00,4,20.586866402505013,25.3827236650394,46.03205317897692 +2024-08-29 03:00:00,4,19.791902518736556,23.36862456449836,51.32029960955745 +2024-08-29 04:00:00,4,28.63704899799395,20.2782958736852,51.691862293748756 +2024-08-29 05:00:00,4,19.431770345001933,23.722714327734174,39.380407723485234 +2024-08-29 06:00:00,4,36.56631955248005,30.77561868329645,55.59318499630638 +2024-08-29 07:00:00,4,18.810692406380625,29.647541204983323,61.557099894055014 +2024-08-29 08:00:00,4,29.96048750447381,23.88922981760371,60.074076952132586 +2024-08-29 09:00:00,4,29.396579824064766,26.071778999830823,84.42860865303254 +2024-08-29 10:00:00,4,21.97766346424337,29.533368161468687,76.17508720681828 +2024-08-29 11:00:00,4,19.569776180023517,17.81380148077125,77.1161630432188 +2024-08-29 12:00:00,4,15.996171457476159,20.910631771457115,52.43751686218608 +2024-08-29 13:00:00,4,19.376733566125775,17.585492447319123,66.06488546296016 +2024-08-29 14:00:00,4,6.131658460701402,24.756638063557272,44.236759510006856 +2024-08-29 15:00:00,4,32.92789158485079,22.968466762274897,54.59755239918701 +2024-08-29 16:00:00,4,19.767545051125964,24.261724281953768,78.47695307032505 +2024-08-29 17:00:00,4,25.523639246653644,21.239956118629916,49.11562132943712 +2024-08-29 18:00:00,4,35.233825101900685,20.784218278699516,67.57893972505823 +2024-08-29 19:00:00,4,5.59124075658702,23.583690971383433,64.76681060121712 +2024-08-29 20:00:00,4,28.109539633497576,25.669672544943403,68.40654433030001 +2024-08-29 21:00:00,4,33.39712737445122,25.95155084051375,61.282766160997966 +2024-08-29 22:00:00,4,36.15180923738317,23.143939529648733,47.304346279137036 +2024-08-29 23:00:00,4,25.87485328395937,30.364594679182083,66.07032335912456 +2024-08-30 00:00:00,4,13.127476644965101,21.796494136905494,65.92132178863581 +2024-08-30 01:00:00,4,34.60887645694413,18.54926289110494,42.72452040727887 +2024-08-30 02:00:00,4,30.286336041425532,22.18984630103769,58.297928668845124 +2024-08-30 03:00:00,4,32.985651656214955,24.099436061096444,61.47870490984983 +2024-08-30 04:00:00,4,28.755886642866162,30.445560737810478,42.12776130241154 +2024-08-30 05:00:00,4,17.67806947187654,22.737652658072584,50.06344500686104 +2024-08-30 06:00:00,4,22.871557544644833,25.162386304034893,66.00619530346447 +2024-08-30 07:00:00,4,20.999660480397942,23.51422344133061,70.94757916661408 +2024-08-30 08:00:00,4,35.4961619107022,19.230094154690722,64.75750494024425 +2024-08-30 09:00:00,4,26.44279721021837,26.61039789911484,46.75348044219494 +2024-08-30 10:00:00,4,18.969155888383398,17.42902409317726,64.64016713898918 +2024-08-30 11:00:00,4,20.166225216387243,18.865618433040414,51.23304850933668 +2024-08-30 12:00:00,4,39.246593517435265,23.238156352106916,53.49637198210384 +2024-08-30 13:00:00,4,5.152422506488737,16.321587963402468,43.614608904199244 +2024-08-30 14:00:00,4,16.530616571331745,24.590585503128235,48.60441737195028 +2024-08-30 15:00:00,4,22.212722417441277,20.089332093130245,55.81192692000638 +2024-08-30 16:00:00,4,32.63542320022222,22.8825371915997,50.933088604376174 +2024-08-30 17:00:00,4,25.455787793742573,28.69763296269569,46.92873010414628 +2024-08-30 18:00:00,4,28.891157061243355,32.675070568886156,59.75018444787003 +2024-08-30 19:00:00,4,24.05503935195819,22.163589660492242,29.142392338491845 +2024-08-30 20:00:00,4,16.78794758848613,22.069448395399984,56.969693859643044 +2024-08-30 21:00:00,4,30.083936753316173,22.572714070791683,65.51442926845066 +2024-08-30 22:00:00,4,28.204306501895147,22.82281599842133,45.870623741542175 +2024-08-30 23:00:00,4,22.31321390417166,24.441034716828618,44.618784256436534 +2024-08-31 00:00:00,4,30.874399677717275,23.721087038538446,46.40231680529146 +2024-08-31 01:00:00,4,27.114171588667606,19.35669321419019,58.24168736104647 +2024-08-31 02:00:00,4,21.784962977187657,23.585536740062583,57.202573025698534 +2024-08-31 03:00:00,4,30.617490067102942,28.85375107342168,56.922882360798326 +2024-08-31 04:00:00,4,33.56304627215731,23.599611376823837,66.89081164370279 +2024-08-31 05:00:00,4,29.800523493262226,19.19464188365998,54.44091389931044 +2024-08-31 06:00:00,4,14.634754084905438,22.479828127607203,48.666676957454946 +2024-08-31 07:00:00,4,34.12201747237593,26.931061078321562,46.76135729660322 +2024-08-31 08:00:00,4,25.6143059968003,22.641434311293267,60.2344925246833 +2024-08-31 09:00:00,4,16.782112772693523,22.479243485220344,64.2143664630023 +2024-08-31 10:00:00,4,26.380914531291538,23.07459466042729,59.84506596544126 +2024-08-31 11:00:00,4,24.311242274950732,21.269899800642364,48.88891157222433 +2024-08-31 12:00:00,4,7.335529715438181,26.308857099251163,55.47425720477121 +2024-08-31 13:00:00,4,23.909930293144516,21.403828272122226,51.73732777461518 +2024-08-31 14:00:00,4,29.32671202904173,20.50655578417595,58.45131624938282 +2024-08-31 15:00:00,4,36.05250315001486,21.84549322299913,60.59312143994485 +2024-08-31 16:00:00,4,21.19882781574215,25.902231547293116,62.54346808719424 +2024-08-31 17:00:00,4,31.111983436628847,19.341501478934415,51.13576531593267 +2024-08-31 18:00:00,4,28.17848981239686,19.75843612845329,38.91449701556515 +2024-08-31 19:00:00,4,13.58199740997332,26.11657906063183,46.13035131903169 +2024-08-31 20:00:00,4,18.413319115523752,29.64144743738657,53.5159957325111 +2024-08-31 21:00:00,4,35.49835463273811,21.437746119067565,58.22925670593234 +2024-08-31 22:00:00,4,27.501903778967602,24.009285458870174,60.618139471279704 +2024-08-31 23:00:00,4,16.203645515829052,24.18234497815332,70.69354635924438 +2024-09-01 00:00:00,4,26.02348102790095,20.598594275929326,39.37627699641561 +2024-09-01 01:00:00,4,35.077585275144486,20.398817607068114,69.65467925120137 +2024-09-01 02:00:00,4,27.07350568450699,26.140014259953197,56.89108343177078 +2024-09-01 03:00:00,4,33.521059047082666,22.010571501877383,54.704761261978724 +2024-09-01 04:00:00,4,32.11878922193035,24.34450597040265,54.03492893453335 +2024-09-01 05:00:00,4,33.11681634684473,18.06205071004049,58.73429302928898 +2024-09-01 06:00:00,4,11.954167262929898,25.721565251772194,49.15803292724625 +2024-09-01 07:00:00,4,36.50145430875981,24.521227895290437,56.83313172420921 +2024-09-01 08:00:00,4,34.56192482389814,19.286164891862953,54.255108619578216 +2024-09-01 09:00:00,4,15.990672580131424,25.61694872926023,73.16972492875588 +2024-09-01 10:00:00,4,13.321521163599552,19.04036820968768,54.80840707109445 +2024-09-01 11:00:00,4,25.19722083939572,28.018385373214848,61.878071353513484 +2024-09-01 12:00:00,4,24.73616264337516,19.915508057913748,53.53456431209637 +2024-09-01 13:00:00,4,26.818604045557752,21.75911846318211,54.05718276522697 +2024-09-01 14:00:00,4,32.7476516887194,26.20460194537266,54.338380119317144 +2024-09-01 15:00:00,4,13.847391044178233,22.19422380036121,36.353652457822506 +2024-09-01 16:00:00,4,23.35119796411635,21.064648134011044,54.2074185998769 +2024-09-01 17:00:00,4,23.05974838815278,22.919379739469296,50.95837448214294 +2024-09-01 18:00:00,4,29.318586920653555,28.162762518941637,44.33953631339034 +2024-09-01 19:00:00,4,48.864207226277166,20.389575902047852,45.311621004564685 +2024-09-01 20:00:00,4,38.83159715453004,25.345989985695272,51.22317213387679 +2024-09-01 21:00:00,4,26.622392161118018,22.694513994698973,40.19201899915157 +2024-09-01 22:00:00,4,23.99428439640411,20.601535739392453,60.72876987347798 +2024-09-01 23:00:00,4,27.689802708939503,29.99627596105894,54.36532764921403 +2024-09-02 00:00:00,4,24.36994457404021,20.879178697165166,45.01068641263089 +2024-09-02 01:00:00,4,28.239518057444702,24.698927981590927,51.818612237711775 +2024-09-02 02:00:00,4,22.78898174023187,27.58256935499076,46.97961949455716 +2024-09-02 03:00:00,4,25.684360971598217,21.97130771587779,45.732216328100165 +2024-09-02 04:00:00,4,47.83733594359691,24.96675537791116,50.459705243197966 +2024-09-02 05:00:00,4,34.7539084063685,22.30383969327713,63.09721545871308 +2024-09-02 06:00:00,4,30.903217230779227,23.284621241140446,44.114809740451705 +2024-09-02 07:00:00,4,22.928231314756694,23.93340734399148,75.48642983613138 +2024-09-02 08:00:00,4,37.362450671719145,18.618132433768963,60.84722527078357 +2024-09-02 09:00:00,4,49.82692145564247,24.90554347148934,65.4468010611027 +2024-09-02 10:00:00,4,40.69919351111139,23.16809541864893,51.26855887893026 +2024-09-02 11:00:00,4,27.313860443656974,24.782598844513597,59.592175368228645 +2024-09-02 12:00:00,4,27.004551710329523,24.100964969035147,51.83273953931514 +2024-09-02 13:00:00,4,28.376249959426694,19.663716532402642,30.798889700483524 +2024-09-02 14:00:00,4,33.068938523246196,20.749905326152792,46.31037633862151 +2024-09-02 15:00:00,4,23.42864147164284,29.77833955700789,47.72627967264698 +2024-09-02 16:00:00,4,29.730930837409527,26.006005740535002,60.514732788076316 +2024-09-02 17:00:00,4,28.81196320556052,26.407966720817086,43.211169426949034 +2024-09-02 18:00:00,4,22.148360038553072,15.935592269236276,52.47531697887183 +2024-09-02 19:00:00,4,16.43634491654851,23.41836320733399,41.6378947607145 +2024-09-02 20:00:00,4,22.16666048774185,21.8949053595629,55.00681685381122 +2024-09-02 21:00:00,4,28.8154254733425,29.76714459476498,64.72458590044721 +2024-09-02 22:00:00,4,27.490541368204447,29.758376309659113,44.74408326110357 +2024-09-02 23:00:00,4,27.31623847274035,22.697977754699295,55.68640907373759 +2024-09-03 00:00:00,4,37.25835100886512,22.059534487440068,51.51315700606844 +2024-09-03 01:00:00,4,37.20550515520713,24.422114805682813,52.37712332769058 +2024-09-03 02:00:00,4,30.377510701136075,29.348081861485696,42.318218372779654 +2024-09-03 03:00:00,4,24.687306040334217,21.15501376297466,63.13124589385127 +2024-09-03 04:00:00,4,27.264957978963555,24.87819745542446,56.088831927080484 +2024-09-03 05:00:00,4,14.459980635747854,23.156123368321087,61.69127994322699 +2024-09-03 06:00:00,4,35.85822619210052,27.9180836417759,43.358940297732076 +2024-09-03 07:00:00,4,30.795460660040966,20.283219822056378,37.471016186256236 +2024-09-03 08:00:00,4,28.118071999245565,26.945940296406086,48.9544818127132 +2024-09-03 09:00:00,4,22.97043906052244,23.192183104774667,39.44790801249689 +2024-09-03 10:00:00,4,27.05153201276864,17.679467946124273,64.98412080671362 +2024-09-03 11:00:00,4,30.51784788635838,31.624955537268917,64.56949154982274 +2024-09-03 12:00:00,4,20.236523535793115,23.278957355024072,62.758043547760956 +2024-09-03 13:00:00,4,17.93561094373826,28.15615443287556,50.34362562444588 +2024-09-03 14:00:00,4,12.956285798769734,23.94621439086655,36.68368748897677 +2024-09-03 15:00:00,4,25.45305104475122,31.862689620696237,66.45000456089763 +2024-09-03 16:00:00,4,14.19795137754184,24.474420055827135,34.51607741614158 +2024-09-03 17:00:00,4,25.617220331783273,19.695407761465162,60.04222794695234 +2024-09-03 18:00:00,4,25.467469275298605,21.927942347246727,55.04645008103896 +2024-09-03 19:00:00,4,22.20026219652345,25.168834885270805,41.68768684760876 +2024-09-03 20:00:00,4,30.404019369226397,20.229725241720292,34.41056829261495 +2024-09-03 21:00:00,4,27.946158672658168,19.266941045682216,57.176173339257375 +2024-09-03 22:00:00,4,40.448455509056004,24.12449289042474,48.5731383893533 +2024-09-03 23:00:00,4,21.424704374528364,26.268170235776736,39.83006278950463 +2024-09-04 00:00:00,4,30.877150815429555,26.204482670039965,46.566445222439974 +2024-09-04 01:00:00,4,29.753102919338527,23.58982963964324,47.91717575704381 +2024-09-04 02:00:00,4,13.499184769442001,25.05127761493839,63.18901403375902 +2024-09-04 03:00:00,4,28.018228951620486,27.504172321562955,47.58842536695352 +2024-09-04 04:00:00,4,17.83405543691094,21.988385054286688,45.02128658029421 +2024-09-04 05:00:00,4,33.87504618324637,22.736149882645705,46.70709156847704 +2024-09-04 06:00:00,4,19.773993053779026,18.479010193307865,40.22911922297313 +2024-09-04 07:00:00,4,33.614042709787846,28.407920581147557,56.556814847352214 +2024-09-04 08:00:00,4,26.307922804539494,27.938540513156312,58.12598902442022 +2024-09-04 09:00:00,4,28.238678680417006,19.784273801587254,48.81832957298799 +2024-09-04 10:00:00,4,26.40718393871473,21.61343477129472,56.95051520090559 +2024-09-04 11:00:00,4,31.520254521040794,22.569188734573363,54.80462575203578 +2024-09-04 12:00:00,4,31.70630205091096,27.78004671948549,46.02223717575913 +2024-09-04 13:00:00,4,26.647919407528853,21.36406866030152,69.33812023310483 +2024-09-04 14:00:00,4,12.42573689901038,24.231669045287102,58.77623342721843 +2024-09-04 15:00:00,4,14.724145516034081,19.045823266153867,52.901868134402676 +2024-09-04 16:00:00,4,14.243038097182957,23.930005016896505,55.572632364942464 +2024-09-04 17:00:00,4,30.989825914289458,22.29034200244792,59.873523646038024 +2024-09-04 18:00:00,4,19.354026219091324,25.5900883945697,40.40773074442897 +2024-09-04 19:00:00,4,37.79320666522747,19.79850460086456,45.16772423038849 +2024-09-04 20:00:00,4,27.69980492018565,25.99263987328889,55.720720651702685 +2024-09-04 21:00:00,4,23.164905061110286,23.62399853681064,28.80676738563082 +2024-09-04 22:00:00,4,22.012458918660617,31.571833746540804,62.88067205076839 +2024-09-04 23:00:00,4,29.139421860923456,26.860738978676856,58.85678308711373 +2024-09-05 00:00:00,4,19.586730227895735,27.960470089294688,61.73036936996199 +2024-09-05 01:00:00,4,24.982318739197215,30.152520306904147,68.47907115139152 +2024-09-05 02:00:00,4,22.546704537238664,26.408154183150394,58.40753708703351 +2024-09-05 03:00:00,4,29.353322862961807,29.028161457442284,56.09096531253971 +2024-09-05 04:00:00,4,16.562076440048664,21.478963499126472,51.53798739130721 +2024-09-05 05:00:00,4,33.684408343349055,23.74530340212776,56.96749311237082 +2024-09-05 06:00:00,4,30.055368487891098,23.539852056884193,55.556704783885785 +2024-09-05 07:00:00,4,24.50368788489546,23.631619510871513,51.906612592516915 +2024-09-05 08:00:00,4,17.331767587017843,22.557307168243987,47.112514050825055 +2024-09-05 09:00:00,4,18.107980208734432,23.741280376391522,49.77396359846482 +2024-09-05 10:00:00,4,8.474916625275533,25.97070593811976,42.421785780468134 +2024-09-05 11:00:00,4,22.723990218255143,27.679579047132062,44.06340057345598 +2024-09-05 12:00:00,4,29.924050913218725,23.04996601371207,61.356922314130806 +2024-09-05 13:00:00,4,26.464295007824944,28.70736890070681,73.21111770734652 +2024-09-05 14:00:00,4,25.821523815565378,24.84440035613373,64.76835708728407 +2024-09-05 15:00:00,4,18.812333990335926,18.377738327611404,69.82822133323468 +2024-09-05 16:00:00,4,10.070539758587252,23.31404120103587,44.79833488813112 +2024-09-05 17:00:00,4,27.605882960534174,22.847681087017506,73.81779164187536 +2024-09-05 18:00:00,4,34.50622431033742,23.630664158067066,40.822960268568075 +2024-09-05 19:00:00,4,32.15691313341799,23.094219579882733,43.77434669404569 +2024-09-05 20:00:00,4,25.925847240434365,19.23072080798795,63.03067126358281 +2024-09-05 21:00:00,4,24.279683848443813,23.40557944175308,60.96522679895591 +2024-09-05 22:00:00,4,22.93053063387897,27.053563937694665,52.84403013123173 +2024-09-05 23:00:00,4,38.58287641278852,20.818085569172773,63.790501747308426 +2024-09-06 00:00:00,4,21.62353532772238,22.49665314153851,56.63195602284396 +2024-09-06 01:00:00,4,21.300765646204027,17.970013770186657,49.1599288459704 +2024-09-06 02:00:00,4,23.719939114857755,19.182378392705694,55.260911873398186 +2024-09-06 03:00:00,4,37.055889509768754,18.589259198207248,57.23615998044068 +2024-09-06 04:00:00,4,27.418253576898987,18.706337644443142,48.90427641136662 +2024-09-06 05:00:00,4,26.824936110088636,25.31111951271659,57.79293942518015 +2024-09-06 06:00:00,4,22.344289309107722,23.568825252515595,45.02303079631821 +2024-09-06 07:00:00,4,41.23939434898893,25.243174942217593,66.9729930285142 +2024-09-06 08:00:00,4,29.28624423337025,19.71637844855032,66.45404230161854 +2024-09-06 09:00:00,4,14.47450270379955,24.168128906064887,71.79871619869509 +2024-09-06 10:00:00,4,28.674538969215774,21.671440500298736,69.25600879911691 +2024-09-06 11:00:00,4,19.87079412904392,23.684929514586248,46.37190513134739 +2024-09-06 12:00:00,4,27.705181835372034,26.233254433925257,64.78281829574745 +2024-09-06 13:00:00,4,14.723393294376372,18.99037884279172,56.295365772170605 +2024-09-06 14:00:00,4,38.71207874027424,27.03190929775344,54.283741564169674 +2024-09-06 15:00:00,4,8.728722592584889,24.93528530586553,55.47834753506986 +2024-09-06 16:00:00,4,39.953310721760694,33.313639230745196,48.651255405149094 +2024-09-06 17:00:00,4,38.52831742388656,27.149737114578272,45.21185016994896 +2024-09-06 18:00:00,4,16.21766588754578,21.35341782510958,52.01877232906205 +2024-09-06 19:00:00,4,31.49513242592628,24.581638023322352,45.89077756735711 +2024-09-06 20:00:00,4,20.810552108130587,27.889133794474272,63.654452602585394 +2024-09-06 21:00:00,4,31.303930884140055,25.548208653380367,40.22007507678932 +2024-09-06 22:00:00,4,31.85352870379709,28.323005895841217,57.730805818507996 +2024-09-06 23:00:00,4,23.773450857159986,28.116719385542456,55.60217275332389 +2024-09-07 00:00:00,4,21.46774263154026,22.909734841921725,48.50641786343705 +2024-09-07 01:00:00,4,9.229766363285798,27.479313782087438,44.78897746206343 +2024-09-07 02:00:00,4,37.34307906669363,22.153331449393285,61.321494431873575 +2024-09-07 03:00:00,4,19.36999178457677,24.663245159903358,45.137047104067435 +2024-09-07 04:00:00,4,32.15107872119642,24.45066512610459,49.45270338286498 +2024-09-07 05:00:00,4,34.707186498659425,25.150004288488926,70.42020483178958 +2024-09-07 06:00:00,4,21.637093986496104,19.371109911603323,46.23874875262509 +2024-09-07 07:00:00,4,29.63024249397429,29.233413812688973,61.552930107483824 +2024-09-07 08:00:00,4,36.12804499568813,24.30303492493859,56.676569677494946 +2024-09-07 09:00:00,4,19.171691627184188,22.114884346675677,53.645851118306446 +2024-09-07 10:00:00,4,24.810565586785643,25.40012612160367,46.41779325071893 +2024-09-07 11:00:00,4,40.27154072418382,20.877755309753255,54.26348260896022 +2024-09-07 12:00:00,4,34.96643975407373,22.097396099426955,56.38770559008339 +2024-09-07 13:00:00,4,20.59925933014816,26.24004282093646,67.10017759131716 +2024-09-07 14:00:00,4,14.791110553040664,25.46407606849904,38.28454854372775 +2024-09-07 15:00:00,4,36.31658872295357,24.32439254805613,60.734106629558305 +2024-09-07 16:00:00,4,22.60636703085137,30.57331233550721,46.55857086493044 +2024-09-07 17:00:00,4,15.994785409774511,21.87784345672583,61.99894644922859 +2024-09-07 18:00:00,4,15.28985859550444,25.873736358543265,51.56425254341678 +2024-09-07 19:00:00,4,35.06372574265666,27.542270647820835,59.57271994377617 +2024-09-07 20:00:00,4,22.478194474362336,29.08265698962344,44.528814606425286 +2024-09-07 21:00:00,4,34.80359066817245,24.020995786512884,52.02170237148636 +2024-09-07 22:00:00,4,15.641481663195812,23.31308971769949,60.845825866238705 +2024-09-07 23:00:00,4,17.739143955783966,25.517914566812266,57.77127355939057 +2024-09-08 00:00:00,4,46.613775112397825,16.60980412453643,60.90448642645239 +2024-09-08 01:00:00,4,21.722408659228698,20.811739062621438,48.27471816687368 +2024-09-08 02:00:00,4,11.384271837923793,19.925541152317265,65.74457096342451 +2024-09-08 03:00:00,4,17.231442931733024,23.110456837218987,61.049829816325385 +2024-09-08 04:00:00,4,37.881496083857755,31.747396686427184,47.10657837452265 +2024-09-08 05:00:00,4,36.445564681254375,25.08914602245722,60.058101802222 +2024-09-08 06:00:00,4,34.535430246286204,22.34025164024484,59.12162335874308 +2024-09-08 07:00:00,4,37.3580981439247,20.895246418570053,71.89563632334426 +2024-09-08 08:00:00,4,13.959535972671004,24.47121153249747,67.08365994807266 +2024-09-08 09:00:00,4,25.089939669396692,26.568702993843196,51.62808774873166 +2024-09-08 10:00:00,4,28.955240168993765,22.171851637241584,39.80698041542251 +2024-09-08 11:00:00,4,7.857687709082519,23.9494152913257,58.55575268335453 +2024-09-08 12:00:00,4,34.21328089749646,20.141996183614626,51.14050161118913 +2024-09-08 13:00:00,4,18.966483210058282,22.49829943793425,57.268417471425344 +2024-09-08 14:00:00,4,26.200701300227664,27.81629256447408,55.60969329644519 +2024-09-08 15:00:00,4,8.801615947745095,19.30478305372094,45.414138212014116 +2024-09-08 16:00:00,4,10.445805037269709,19.00983360372819,50.55996667582691 +2024-09-08 17:00:00,4,25.547774130522907,25.59475788438611,61.66732533973183 +2024-09-08 18:00:00,4,21.741081013086806,23.342147685673133,63.55546935001905 +2024-09-08 19:00:00,4,28.94435084500158,29.72148199984019,52.61529275498679 +2024-09-08 20:00:00,4,32.982676764604406,22.507286113182914,50.2451619801317 +2024-09-08 21:00:00,4,0.0,26.66213605715366,42.77072227984017 +2024-09-08 22:00:00,4,21.87120329297405,23.95489301780143,64.45433450796298 +2024-09-08 23:00:00,4,32.05723881499456,28.19694869351436,54.662851369202926 +2024-09-09 00:00:00,4,30.29207435177667,22.06294079418476,53.87260286227464 +2024-09-09 01:00:00,4,33.50031351903302,22.565015833559382,53.993337203924106 +2024-09-09 02:00:00,4,23.76687317351036,28.96240400422775,54.32515203392781 +2024-09-09 03:00:00,4,37.59175624218386,22.813840261772636,75.6407080906624 +2024-09-09 04:00:00,4,32.078745316027174,28.226162938571115,55.40834309388241 +2024-09-09 05:00:00,4,18.795778181794873,25.214392608525774,55.88200132216159 +2024-09-09 06:00:00,4,32.339212680933585,23.90899007373882,70.50487424750568 +2024-09-09 07:00:00,4,28.691314089030982,24.9063743656237,37.60418275870348 +2024-09-09 08:00:00,4,28.388214539859163,24.634597111774024,53.255217617492605 +2024-09-09 09:00:00,4,35.01959411904396,23.717182515648233,64.75492402606538 +2024-09-09 10:00:00,4,30.510731507891812,20.700294953663096,64.1894081581747 +2024-09-09 11:00:00,4,23.28670112502529,23.375432704062586,67.10818650462596 +2024-09-09 12:00:00,4,13.69806374394681,20.932216699057754,55.266856792655624 +2024-09-09 13:00:00,4,32.72966760017309,26.25255065770318,63.63719944778811 +2024-09-09 14:00:00,4,18.243489899698087,16.81005058685236,62.327081496816646 +2024-09-09 15:00:00,4,31.731123087534378,17.8720133481596,68.77291444673016 +2024-09-09 16:00:00,4,26.903104095461707,24.630415822415234,38.2978935868984 +2024-09-09 17:00:00,4,24.10923171728672,25.35325281428803,57.914703030718734 +2024-09-09 18:00:00,4,3.5773149290528963,28.732370906011173,45.339173385616704 +2024-09-09 19:00:00,4,38.509100852145856,27.326757620944456,49.16765851449652 +2024-09-09 20:00:00,4,43.18800349979131,21.555968493700206,61.177658901538436 +2024-09-09 21:00:00,4,19.76302674157835,24.308826768432034,51.301999150561485 +2024-09-09 22:00:00,4,23.46873434615955,26.238793948027602,51.170305798056305 +2024-09-09 23:00:00,4,20.85193015762916,24.754551542608688,47.55010558209723 +2024-09-10 00:00:00,4,39.953758064043775,18.93951785403054,58.35874504593913 +2024-09-10 01:00:00,4,40.04102125811529,20.592487899165967,61.78557409326302 +2024-09-10 02:00:00,4,38.53761606907825,23.51277384836737,65.1784215159427 +2024-09-10 03:00:00,4,40.93878620081024,28.57833597630596,39.549838179834374 +2024-09-10 04:00:00,4,30.24096245635383,24.341304766331827,58.082248186072874 +2024-09-10 05:00:00,4,6.433045049323624,16.566763731475152,66.6034004851067 +2024-09-10 06:00:00,4,26.888157254098363,17.047743658577296,60.64214002224201 +2024-09-10 07:00:00,4,38.76411820360559,18.273944894652256,51.842640948215845 +2024-09-10 08:00:00,4,28.05155029988722,18.924076331575165,45.446390708047716 +2024-09-10 09:00:00,4,33.771446715580396,17.58388165766348,42.857762376929195 +2024-09-10 10:00:00,4,25.218733911425335,23.058584529503918,58.26728478672793 +2024-09-10 11:00:00,4,27.689425307021832,25.160234845660867,56.40574090207184 +2024-09-10 12:00:00,4,35.82002853023351,26.704999726156306,54.33689568082084 +2024-09-10 13:00:00,4,14.152884896374772,18.739438782928566,54.19891202453888 +2024-09-10 14:00:00,4,20.02377925527351,23.200742381053754,50.05875936310868 +2024-09-10 15:00:00,4,6.60593476783432,23.243589785846762,49.6466998457864 +2024-09-10 16:00:00,4,34.98924680773932,19.969890627716122,70.74792684258861 +2024-09-10 17:00:00,4,0.8940631914299573,16.100388052763726,42.78595258286008 +2024-09-10 18:00:00,4,28.072267959760794,21.205102966961856,46.68632932441359 +2024-09-10 19:00:00,4,8.285133505804493,22.917683182394395,55.71831005365141 +2024-09-10 20:00:00,4,3.2132369514754124,24.100616670707186,59.103514565952665 +2024-09-10 21:00:00,4,27.831454677884132,25.111577518185126,61.62929644539031 +2024-09-10 22:00:00,4,43.9408577500901,24.294508539270506,67.89818064076655 +2024-09-10 23:00:00,4,21.3056915244593,29.55744814645921,57.459430341892904 +2024-09-11 00:00:00,4,17.596112664961833,22.771458048216015,44.368784293099424 +2024-09-11 01:00:00,4,38.304258275289314,25.662452275521346,56.1523434571141 +2024-09-11 02:00:00,4,21.84870462641341,27.959381656025478,48.47691568188782 +2024-09-11 03:00:00,4,29.958421324925105,27.495923752840156,59.72015529391255 +2024-09-11 04:00:00,4,30.296743194496155,30.13436895552053,51.832707641964326 +2024-09-11 05:00:00,4,18.12631175947942,22.324813526114404,68.58079859693177 +2024-09-11 06:00:00,4,34.49732188154845,18.638118336055214,50.98747554213373 +2024-09-11 07:00:00,4,26.534706144648588,20.551708499418734,58.57594197468481 +2024-09-11 08:00:00,4,30.05140476471908,19.499466426967434,77.25962077328643 +2024-09-11 09:00:00,4,25.55912636638597,26.259371523715274,66.92907757057581 +2024-09-11 10:00:00,4,34.0248080692651,24.485404052150617,51.62805214246386 +2024-09-11 11:00:00,4,20.64464264409875,27.271853466283545,52.91551729544375 +2024-09-11 12:00:00,4,23.360292879386364,24.335610841810382,58.78961724877147 +2024-09-11 13:00:00,4,10.7689431818046,22.082623631977743,50.39627565465215 +2024-09-11 14:00:00,4,30.291610667891327,21.76944065404509,46.246460471803736 +2024-09-11 15:00:00,4,30.17414415190381,24.33931875971798,65.4898061860817 +2024-09-11 16:00:00,4,23.015736644631232,19.877274076477768,47.36740266282636 +2024-09-11 17:00:00,4,31.86457855632936,26.184902483349397,47.33377740052992 +2024-09-11 18:00:00,4,23.10553455200776,26.86104482729168,57.74976250796301 +2024-09-11 19:00:00,4,33.73677861105957,20.10742302562677,48.407778811591065 +2024-09-11 20:00:00,4,22.79673862341008,32.562693422322305,45.09423637404673 +2024-09-11 21:00:00,4,38.01037018915461,25.044355670410393,63.267986119982695 +2024-09-11 22:00:00,4,19.69226289506196,22.783940115968306,54.168692628118194 +2024-09-11 23:00:00,4,8.373823494153534,24.4953894748873,41.40726227039791 +2024-09-12 00:00:00,4,29.7499776125222,27.081092104433058,67.32722315513628 +2024-09-12 01:00:00,4,42.68738000839265,30.357616477142052,50.69196232520505 +2024-09-12 02:00:00,4,17.820301841648146,18.055102838442718,68.74196764191262 +2024-09-12 03:00:00,4,30.93922158530326,22.334284545727076,69.01767603644821 +2024-09-12 04:00:00,4,37.15706518380051,30.3099874852039,51.96142583627843 +2024-09-12 05:00:00,4,32.8534035972962,24.633850448999283,59.33136560586837 +2024-09-12 06:00:00,4,26.624338100911196,22.33290890072053,72.65404368149842 +2024-09-12 07:00:00,4,22.389908511255893,27.32894142576708,68.19248918459925 +2024-09-12 08:00:00,4,15.952125963503057,22.944058235702826,42.9462497355265 +2024-09-12 09:00:00,4,36.52772124872061,21.752693512231964,52.38953621806962 +2024-09-12 10:00:00,4,23.699722781596385,24.813751354915077,62.07474580772706 +2024-09-12 11:00:00,4,28.55900771493199,23.827320958273617,54.709121739382596 +2024-09-12 12:00:00,4,19.951795287584375,27.36938765243704,56.58172749787437 +2024-09-12 13:00:00,4,16.848999338998265,25.106963302879066,59.075442036802286 +2024-09-12 14:00:00,4,38.54530273266536,21.705440030211328,53.00695859858288 +2024-09-12 15:00:00,4,29.62148651591425,27.547966084977546,55.42442512585812 +2024-09-12 16:00:00,4,6.098981809015267,25.669210218781856,38.66487728480373 +2024-09-12 17:00:00,4,13.267568932214273,25.16977375132921,62.68182054253433 +2024-09-12 18:00:00,4,16.238790011747156,19.666897191733188,47.36027868029602 +2024-09-12 19:00:00,4,0.9924594808530571,24.65945904507583,70.77845059405064 +2024-09-12 20:00:00,4,9.374253231336814,21.2019437520143,54.8886532298135 +2024-09-12 21:00:00,4,35.81864977197278,25.498029632962837,59.01422304916702 +2024-09-12 22:00:00,4,34.60914806881428,26.143900047416818,64.00107999379519 +2024-09-12 23:00:00,4,12.733581875165838,28.90088801100758,40.62276434813184 +2024-09-13 00:00:00,4,28.44337673069444,21.45262008376691,71.15172841091258 +2024-09-13 01:00:00,4,30.23128724150756,24.158767909201853,52.63701297407481 +2024-09-13 02:00:00,4,42.65441783533564,25.305202569598347,44.70530862238729 +2024-09-13 03:00:00,4,38.16146935908854,26.62567412377068,56.20305308541973 +2024-09-13 04:00:00,4,28.401667087869335,24.525504118585662,50.15391884441817 +2024-09-13 05:00:00,4,34.09214524256025,22.213600539421325,63.963439241288846 +2024-09-13 06:00:00,4,28.5059407247149,23.574439911848934,46.42260531894972 +2024-09-13 07:00:00,4,24.059841648749888,23.537312658746263,62.19644160758373 +2024-09-13 08:00:00,4,15.961844410590238,22.87694885691766,59.79154182918015 +2024-09-13 09:00:00,4,15.999063849377185,17.093845900617765,55.70392113714407 +2024-09-13 10:00:00,4,24.62247021064195,25.980728365367387,34.41863150162423 +2024-09-13 11:00:00,4,38.274550556781335,19.046474649189506,46.09280068910639 +2024-09-13 12:00:00,4,30.652733967195005,25.266497538190155,52.11325068630416 +2024-09-13 13:00:00,4,23.593659393509032,21.3532213266598,69.03202786879599 +2024-09-13 14:00:00,4,23.142681286348044,21.66325652254486,62.49205614981339 +2024-09-13 15:00:00,4,16.014694545111727,20.275542151900172,55.49074895940999 +2024-09-13 16:00:00,4,21.042724807105373,22.366594338179596,44.04840852175369 +2024-09-13 17:00:00,4,28.56296500265209,28.145398637569336,65.0341038062705 +2024-09-13 18:00:00,4,22.738284148544846,22.742091924218208,60.117885142617084 +2024-09-13 19:00:00,4,21.44910074736323,22.886368578506936,60.21037410727715 +2024-09-13 20:00:00,4,17.719171677792776,20.928105453845383,52.96923969152497 +2024-09-13 21:00:00,4,14.91764930332335,22.52426941924552,62.2849453043596 +2024-09-13 22:00:00,4,17.032498613403945,28.242084361302766,53.79214605563715 +2024-09-13 23:00:00,4,31.119429756372114,28.497192145768246,63.22380952163182 +2024-09-14 00:00:00,4,35.3875376912039,20.82811523424929,56.70960612238114 +2024-09-14 01:00:00,4,19.580834298285765,28.357133826236943,38.64095695454773 +2024-09-14 02:00:00,4,18.129237782234668,28.180112190129943,58.291065451264075 +2024-09-14 03:00:00,4,12.010547080982027,23.9090014893619,50.602368302721445 +2024-09-14 04:00:00,4,36.46782137324685,16.908668870816122,56.11041647546425 +2024-09-14 05:00:00,4,43.55560253608692,22.55559628122483,46.8835512035043 +2024-09-14 06:00:00,4,13.436427265205689,23.67472535617697,46.248003974607265 +2024-09-14 07:00:00,4,29.24906980136697,21.82015189727762,49.26604816592129 +2024-09-14 08:00:00,4,27.325225535173114,22.329715410107486,54.329959795656876 +2024-09-14 09:00:00,4,26.091787747183314,23.052738174870807,61.73618283329519 +2024-09-14 10:00:00,4,32.74313545377715,22.23485978169288,55.49601942127111 +2024-09-14 11:00:00,4,31.63128974705414,22.9405485690444,60.976842989672264 +2024-09-14 12:00:00,4,18.93602608279047,20.810507099412714,62.28345500929758 +2024-09-14 13:00:00,4,34.178051888612714,27.186608999993286,64.93751884444623 +2024-09-14 14:00:00,4,21.255669667508915,26.63471911249235,40.548026403413324 +2024-09-14 15:00:00,4,23.152495932825623,17.337046917442052,57.397567666680544 +2024-09-14 16:00:00,4,21.252868760383294,19.81856355389334,59.74886495022129 +2024-09-14 17:00:00,4,36.52210480806564,31.945618300893212,51.904464743494515 +2024-09-14 18:00:00,4,11.40485267020332,29.46689073469532,37.49302667137184 +2024-09-14 19:00:00,4,17.14930688247564,24.59807927450722,50.48306065705731 +2024-09-14 20:00:00,4,18.002483403255656,26.207663781911553,65.01412399004 +2024-09-14 21:00:00,4,13.249178765778534,23.275009168224713,42.61117096184893 +2024-09-14 22:00:00,4,24.29896798369126,25.801649369128615,70.61329368354833 +2024-09-14 23:00:00,4,22.081244897682144,33.64104224798058,49.93093579683967 +2024-09-15 00:00:00,4,30.877480359896207,23.632249386105414,57.80572347841554 +2024-09-15 01:00:00,4,20.96256452924264,28.880545426156807,57.72567578701458 +2024-09-15 02:00:00,4,35.03866079183685,26.993542776269273,69.62325766379314 +2024-09-15 03:00:00,4,26.766824626430022,25.8957086408248,70.4764803050359 +2024-09-15 04:00:00,4,7.240826476641487,22.664289537129815,56.25063375572177 +2024-09-15 05:00:00,4,21.904761492882198,24.18859960617264,51.00332321731109 +2024-09-15 06:00:00,4,39.23720305067388,23.244996785588285,55.356957360520376 +2024-09-15 07:00:00,4,27.386158211694614,26.86717658154959,62.919500599463916 +2024-09-15 08:00:00,4,32.52849636410795,22.56173418064675,54.77303066625463 +2024-09-15 09:00:00,4,14.447927067463636,23.304055563219276,72.17688197547696 +2024-09-15 10:00:00,4,17.695887824735262,22.479386498049838,64.49119879024441 +2024-09-15 11:00:00,4,26.820118930603464,18.6031058041645,58.132247618995166 +2024-09-15 12:00:00,4,13.46871661125682,27.961946351838538,49.72296639440016 +2024-09-15 13:00:00,4,29.930658777076147,26.505495433845308,69.04474925992298 +2024-09-15 14:00:00,4,14.328383368879825,22.45519716546138,52.205830762899325 +2024-09-15 15:00:00,4,24.893878566199522,16.81554882815734,54.85966002060018 +2024-09-15 16:00:00,4,15.987870375377629,22.50949119629472,47.23532854567802 +2024-09-15 17:00:00,4,18.73042398402281,22.810296042483134,58.306524704834054 +2024-09-15 18:00:00,4,11.27732458406154,24.357273291136018,60.983836830358335 +2024-09-15 19:00:00,4,20.635669050835524,21.594800318372382,58.91705761851807 +2024-09-15 20:00:00,4,30.964196008094884,27.929705924798192,49.30030293645067 +2024-09-15 21:00:00,4,20.065196082460453,27.558660733399975,54.444850871037055 +2024-09-15 22:00:00,4,32.458582946624105,17.669247887707836,70.71713912184174 +2024-09-15 23:00:00,4,20.726353191379538,24.800759816257912,49.16741248950123 +2024-09-16 00:00:00,4,9.194748461529038,17.965526027579205,42.40249614052496 +2024-09-16 01:00:00,4,28.555896844872503,23.606659111247637,52.19908789019554 +2024-09-16 02:00:00,4,23.158355296795797,22.22047662408884,44.11270946210379 +2024-09-16 03:00:00,4,20.601465099781766,24.176654133267277,57.32385524347394 +2024-09-16 04:00:00,4,8.455586166127585,20.186112322519,51.09706798297965 +2024-09-16 05:00:00,4,33.083351989333764,27.012586876619956,62.10017000913384 +2024-09-16 06:00:00,4,28.92391790338595,25.96907146665489,61.69589961819514 +2024-09-16 07:00:00,4,32.00930576815519,15.601406573942068,41.59702016136037 +2024-09-16 08:00:00,4,4.98815411542353,25.12042448036649,55.773201132377345 +2024-09-16 09:00:00,4,25.42988913076971,19.122085545201465,53.38929968000089 +2024-09-16 10:00:00,4,23.039399513009826,26.36577181573475,63.7708682558857 +2024-09-16 11:00:00,4,29.072410202917432,22.343671458738008,74.84434425893124 +2024-09-16 12:00:00,4,22.520684467143244,22.15629245822812,60.86830551486662 +2024-09-16 13:00:00,4,15.589813546502572,22.449579501643345,56.42259198774474 +2024-09-16 14:00:00,4,19.082142768914853,25.33554515116249,57.75205390685558 +2024-09-16 15:00:00,4,25.853106176138613,20.668164622685246,65.72218771028872 +2024-09-16 16:00:00,4,25.45805746853625,20.145698327137715,43.40669750853918 +2024-09-16 17:00:00,4,24.480937369569094,19.35090930092357,47.80729224331312 +2024-09-16 18:00:00,4,33.93487558465111,24.140859712480133,37.39864383208467 +2024-09-16 19:00:00,4,20.430915649624673,22.990383045627418,41.50723561389274 +2024-09-16 20:00:00,4,8.509909262684996,22.620946188666554,78.93514255564895 +2024-09-16 21:00:00,4,24.127134302383073,25.846644349971513,58.99880220964192 +2024-09-16 22:00:00,4,11.271972247468565,18.978830311434486,49.880438924953836 +2024-09-16 23:00:00,4,29.959183012804758,28.531137280148293,62.32649489358909 +2024-09-17 00:00:00,4,21.845151124387776,25.549669541419043,52.37173509797448 +2024-09-17 01:00:00,4,29.529687863564916,23.96945920571491,60.03080381669077 +2024-09-17 02:00:00,4,29.08517343486614,23.2937882082412,62.65941307289124 +2024-09-17 03:00:00,4,27.13164426966395,27.462907312767072,46.71727850492877 +2024-09-17 04:00:00,4,30.814697879699114,19.699876763118226,62.475796595065525 +2024-09-17 05:00:00,4,29.419295247879646,23.473196517156936,71.79049300577609 +2024-09-17 06:00:00,4,40.063065648847434,24.405905512790444,62.20511307964125 +2024-09-17 07:00:00,4,20.47230107411814,28.82684073341104,72.24088541267015 +2024-09-17 08:00:00,4,26.90004279961506,26.676449638667226,66.62802191556838 +2024-09-17 09:00:00,4,15.530502147449651,22.94519645682447,74.31221514522748 +2024-09-17 10:00:00,4,31.29232797570595,26.041885453226268,65.89736454213794 +2024-09-17 11:00:00,4,16.60213982148581,16.3464106451435,65.24425818453672 +2024-09-17 12:00:00,4,27.432995723402186,17.442096678314435,48.53333167320055 +2024-09-17 13:00:00,4,28.7443502739467,24.07592616610537,44.97445742927573 +2024-09-17 14:00:00,4,25.21151154418797,19.028723402298418,55.97619708060965 +2024-09-17 15:00:00,4,5.185772000298073,23.048558908341096,56.10317679625643 +2024-09-17 16:00:00,4,31.41121304661706,24.84094849353476,46.67135591045975 +2024-09-17 17:00:00,4,25.134495981314437,26.72079104893569,48.677600450619465 +2024-09-17 18:00:00,4,22.803423307664776,23.95780491626761,67.65441621831116 +2024-09-17 19:00:00,4,23.146045220349063,25.5887349490072,65.90797912795514 +2024-09-17 20:00:00,4,20.376978537720674,21.216014621066815,50.18313203601479 +2024-09-17 21:00:00,4,45.131694557834344,22.608468521956063,51.32589378397393 +2024-09-17 22:00:00,4,41.90181847045241,28.14586558253121,49.27348820100493 +2024-09-17 23:00:00,4,24.677615183356256,22.785794369899524,54.63370263201823 +2024-09-18 00:00:00,4,19.436310439500932,24.067005464674352,42.478287645108104 +2024-09-18 01:00:00,4,25.249559372529326,24.532392832151075,50.36570639093018 +2024-09-18 02:00:00,4,24.02526861566269,20.813365042920044,51.20489085313136 +2024-09-18 03:00:00,4,26.49803443399507,25.75556776341526,76.21803783019877 +2024-09-18 04:00:00,4,39.77108202726718,24.575416660629593,58.20596575753116 +2024-09-18 05:00:00,4,23.20989118875435,19.465161550323625,45.0332565208285 +2024-09-18 06:00:00,4,24.450510174132464,22.0682604176341,56.67442065693455 +2024-09-18 07:00:00,4,41.32804005021397,21.22501132362091,53.774150617881176 +2024-09-18 08:00:00,4,31.9894124693474,23.830168812664283,62.96661702047691 +2024-09-18 09:00:00,4,28.968014622423755,16.527919022584374,61.64848687568849 +2024-09-18 10:00:00,4,20.138836937424657,20.634769760202353,69.01432213806062 +2024-09-18 11:00:00,4,21.772610475930883,20.794846078971684,56.97480715535078 +2024-09-18 12:00:00,4,38.663593526918,22.554381259553484,53.179728211556046 +2024-09-18 13:00:00,4,7.956390681591527,20.224327872821053,47.52301735350655 +2024-09-18 14:00:00,4,23.91152708981504,23.653004742224756,44.988307528069214 +2024-09-18 15:00:00,4,21.692165795043472,26.194372970596937,59.97061704741222 +2024-09-18 16:00:00,4,9.112813085357612,24.37567586829463,34.0547281216589 +2024-09-18 17:00:00,4,36.72183926347048,28.05582309947836,74.52137440068117 +2024-09-18 18:00:00,4,8.998295184973239,21.05939633892299,61.54457234708609 +2024-09-18 19:00:00,4,30.163217507769943,23.94917741331868,52.843513928104855 +2024-09-18 20:00:00,4,27.359022708592228,23.177537851424823,68.98706700377662 +2024-09-18 21:00:00,4,30.551554635542214,19.265875965510382,34.20745804188335 +2024-09-18 22:00:00,4,24.14536590544356,26.2498124220481,58.92063786439267 +2024-09-18 23:00:00,4,25.427181134252212,24.572108909786586,63.429058830898 +2024-09-19 00:00:00,4,23.01019505678118,25.75928996232932,53.78673992106911 +2024-09-19 01:00:00,4,22.59383962243046,22.78995416513173,52.40387189094045 +2024-09-19 02:00:00,4,21.420186591285024,21.613514411457817,56.94871315674373 +2024-09-19 03:00:00,4,49.69562128496824,25.43782531447817,46.61668625516691 +2024-09-19 04:00:00,4,35.38780882550691,21.043294294944957,46.716427642159246 +2024-09-19 05:00:00,4,13.437006891641847,20.392678566635663,66.87994670445924 +2024-09-19 06:00:00,4,26.048287363988713,23.160199965575153,63.05907339229375 +2024-09-19 07:00:00,4,42.309620297145585,24.532648577015067,47.51785128574652 +2024-09-19 08:00:00,4,32.55309688413249,25.743780522629258,39.83287227643242 +2024-09-19 09:00:00,4,24.19495567166475,23.40545945381221,71.25183939104213 +2024-09-19 10:00:00,4,33.63494690469222,20.911208185770636,57.82759434859047 +2024-09-19 11:00:00,4,7.75499392792651,18.733317844259535,58.42933325035206 +2024-09-19 12:00:00,4,40.55495201388385,28.909382045777043,76.07628841301519 +2024-09-19 13:00:00,4,24.542143679818444,21.14969636509015,35.59149321417699 +2024-09-19 14:00:00,4,29.58566818029016,19.189554313100498,46.63408462286159 +2024-09-19 15:00:00,4,25.176502468903724,26.292163895269784,42.84332380640676 +2024-09-19 16:00:00,4,16.820043112167102,27.04918988592304,44.393952720114704 +2024-09-19 17:00:00,4,33.95654504599611,20.869217197864902,51.58914097415302 +2024-09-19 18:00:00,4,22.201160173763927,21.45130784404417,56.05377220269717 +2024-09-19 19:00:00,4,26.926281651149683,19.968166326629625,42.141190437688884 +2024-09-19 20:00:00,4,23.21071245172595,24.094144545050415,72.49402829830665 +2024-09-19 21:00:00,4,26.239951406411834,20.43260119709757,53.946357319445426 +2024-09-19 22:00:00,4,19.296206996204383,27.02757393288673,54.66774777511203 +2024-09-19 23:00:00,4,27.414210353877692,22.905462592407265,49.1101657910701 +2024-09-20 00:00:00,4,41.6673521180133,20.15758177411341,50.44389298235912 +2024-09-20 01:00:00,4,29.76914962508231,24.77736338863466,50.12524706202142 +2024-09-20 02:00:00,4,13.037577762391743,23.463333413603007,50.78038593993868 +2024-09-20 03:00:00,4,20.83729809667779,24.71680621020428,66.180823717581 +2024-09-20 04:00:00,4,28.962619108255385,23.104824828342608,52.13867177102904 +2024-09-20 05:00:00,4,30.0361130393603,23.81538412479276,68.60472207299165 +2024-09-20 06:00:00,4,21.33211882177472,26.085780477064823,57.06112277584066 +2024-09-20 07:00:00,4,20.694450962492965,32.3575197594789,42.66441881980745 +2024-09-20 08:00:00,4,20.019893400747367,20.176581572081613,57.13410757329709 +2024-09-20 09:00:00,4,26.59957893629054,25.07084569070779,52.85544777263104 +2024-09-20 10:00:00,4,22.988343888847677,20.912027613845144,56.25601201930852 +2024-09-20 11:00:00,4,18.75328768088788,26.938021245571594,50.30770904312482 +2024-09-20 12:00:00,4,15.739447724158577,20.369475324845613,68.99752535429718 +2024-09-20 13:00:00,4,29.786622042157216,27.42121635094809,58.784399926930625 +2024-09-20 14:00:00,4,29.25684570466827,21.365436572586226,61.904355274643166 +2024-09-20 15:00:00,4,21.608422809604058,21.705946697357167,52.20896354544773 +2024-09-20 16:00:00,4,30.131962682849565,20.593642847042208,57.000210294041885 +2024-09-20 17:00:00,4,15.59747478623395,21.9916087400684,58.66995288465362 +2024-09-20 18:00:00,4,0.0,18.644640464314964,53.84507148866168 +2024-09-20 19:00:00,4,27.53428170695107,23.097078257365194,37.93587983718798 +2024-09-20 20:00:00,4,16.562022757937406,27.09468000304806,58.72565344633983 +2024-09-20 21:00:00,4,26.53839349711985,24.684718245324685,61.6931625223605 +2024-09-20 22:00:00,4,12.93918597115434,23.375005150751793,54.7950771618275 +2024-09-20 23:00:00,4,32.90741939303191,31.235044166843366,67.81335184678596 +2024-09-21 00:00:00,4,36.15992063084888,24.702480861827848,57.9516476186197 +2024-09-21 01:00:00,4,33.08467736193885,20.67393301873753,51.14363923696776 +2024-09-21 02:00:00,4,26.303689824109508,29.448603634644506,55.14321184679064 +2024-09-21 03:00:00,4,28.21443440110884,25.55399918249391,62.20957964175626 +2024-09-21 04:00:00,4,32.98116136686389,22.243342280760356,67.24696834492093 +2024-09-21 05:00:00,4,39.13525868120311,22.130519123248924,51.81366756497774 +2024-09-21 06:00:00,4,29.883477144548074,22.05504739424748,45.219575515725566 +2024-09-21 07:00:00,4,25.899155385043663,24.654577287850266,60.51987852045261 +2024-09-21 08:00:00,4,30.095343318848748,24.5561657201581,42.04477625719372 +2024-09-21 09:00:00,4,27.46463790915336,28.494295190282294,55.993878908366945 +2024-09-21 10:00:00,4,17.79731241328912,26.136485191375854,55.548890185091416 +2024-09-21 11:00:00,4,28.445401817298105,21.833015688365624,51.45602766093359 +2024-09-21 12:00:00,4,35.57033675933115,20.382064922304576,48.323386630540284 +2024-09-21 13:00:00,4,15.66505329879371,25.391072365729897,62.445884049325336 +2024-09-21 14:00:00,4,19.247217867229494,20.503866363544176,70.58306269010623 +2024-09-21 15:00:00,4,26.508493488442422,22.249504246807366,51.17716229180613 +2024-09-21 16:00:00,4,22.322049822042416,24.47253003347688,59.21316079970808 +2024-09-21 17:00:00,4,19.064742911956387,16.552689070813784,44.98996230046956 +2024-09-21 18:00:00,4,23.572041366843322,26.078061421359642,43.54215276934713 +2024-09-21 19:00:00,4,32.712340534121054,23.278884272366852,51.39727741978885 +2024-09-21 20:00:00,4,22.0355214383484,24.761191012109492,58.929636304783806 +2024-09-21 21:00:00,4,37.203290639659116,24.97211537482146,53.85189102322474 +2024-09-21 22:00:00,4,37.21100159290256,28.066956958676382,60.93646847618144 +2024-09-21 23:00:00,4,17.669185437516468,23.676787429689714,62.9640636230832 +2024-09-22 00:00:00,4,30.55533122117985,21.80810632822399,33.40674774085936 +2024-09-22 01:00:00,4,25.09595787836034,20.570809707124354,43.37756758984086 +2024-09-22 02:00:00,4,22.92765052464123,21.471682519755703,42.802424232601844 +2024-09-22 03:00:00,4,29.522483106731343,21.406663307461066,59.86467754430179 +2024-09-22 04:00:00,4,32.17432218165156,14.412763290605511,52.94137993940511 +2024-09-22 05:00:00,4,26.14316339512597,18.214793946395243,65.29034935808119 +2024-09-22 06:00:00,4,41.94432137295699,23.14646875996644,51.10076921903515 +2024-09-22 07:00:00,4,20.897035464682748,19.535576299601257,40.51850994493994 +2024-09-22 08:00:00,4,34.00986496854348,21.526296199748845,45.31782269549562 +2024-09-22 09:00:00,4,20.124254900309715,19.525625782815645,58.13616925412508 +2024-09-22 10:00:00,4,21.794726472437183,21.687091572199453,46.845222350034206 +2024-09-22 11:00:00,4,20.34212309377016,27.81998222028115,53.47450790999403 +2024-09-22 12:00:00,4,27.600579878822824,29.684445987112227,67.94208221703678 +2024-09-22 13:00:00,4,39.932800194047665,27.415718088133453,60.414290203002594 +2024-09-22 14:00:00,4,28.834505105183524,21.540706122989054,52.07469525870929 +2024-09-22 15:00:00,4,21.683181715657316,21.387640989636537,40.2323021072132 +2024-09-22 16:00:00,4,18.006696307910882,24.317632436426145,57.60646296725548 +2024-09-22 17:00:00,4,25.719760544191256,24.82944338757968,73.36249688984975 +2024-09-22 18:00:00,4,21.670745676004678,24.503043482469337,63.78060550967621 +2024-09-22 19:00:00,4,18.66481040766245,29.74707790083449,46.7240033523469 +2024-09-22 20:00:00,4,13.323351989971554,26.588416263163612,67.17529465824786 +2024-09-22 21:00:00,4,27.630945022424754,25.746803267203866,44.15722652144279 +2024-09-22 22:00:00,4,26.775498962882708,29.623752557061326,60.19132661198637 +2024-09-22 23:00:00,4,8.308506474765789,23.521537933473454,49.30650736961654 +2024-09-23 00:00:00,4,30.353163704339668,23.47577469928771,68.62031243403311 +2024-09-23 01:00:00,4,14.967047348600774,24.391029480099572,48.758099929159336 +2024-09-23 02:00:00,4,31.764823883811804,28.901726923272264,52.694404395112365 +2024-09-23 03:00:00,4,40.66003898182268,27.490704533791774,48.396603162607114 +2024-09-23 04:00:00,4,27.160198021395097,29.445189188053632,54.25677556879307 +2024-09-23 05:00:00,4,24.177375653541933,28.270163401697143,59.36136026331072 +2024-09-23 06:00:00,4,31.850278552215546,24.582379750230793,64.52597038325806 +2024-09-23 07:00:00,4,16.186095201974947,27.678285067590306,61.30250079197136 +2024-09-23 08:00:00,4,26.557591141682646,21.675247452990334,48.11981255616292 +2024-09-23 09:00:00,4,25.721310311972037,19.279121933711384,65.76349305925211 +2024-09-23 10:00:00,4,44.8355940015085,23.32481253390497,40.3646587919112 +2024-09-23 11:00:00,4,19.166228932397104,18.584772487704694,72.57214770775335 +2024-09-23 12:00:00,4,39.57678580549188,28.743744021839433,48.46702656756267 +2024-09-23 13:00:00,4,33.338159346999966,25.62923480643938,44.13301108332367 +2024-09-23 14:00:00,4,31.635537406426373,25.797949376276588,57.66668430737927 +2024-09-23 15:00:00,4,18.54970156030225,18.879885646111937,56.078144443121175 +2024-09-23 16:00:00,4,24.737765563757883,18.026850043146453,77.33104373771822 +2024-09-23 17:00:00,4,24.976698442398767,29.28861845991886,50.68065994192818 +2024-09-23 18:00:00,4,20.465016096333912,23.041448463514655,60.024788024716926 +2024-09-23 19:00:00,4,31.32266017479993,19.457557060498996,49.40864297075603 +2024-09-23 20:00:00,4,24.05523936579655,21.92216738387319,49.18664532141785 +2024-09-23 21:00:00,4,20.80441199886025,29.05910355647127,47.28152760565678 +2024-09-23 22:00:00,4,24.433996539376647,23.14155858684646,62.0039998577706 +2024-09-23 23:00:00,4,17.76593749964559,19.574007871504843,43.219332653984274 +2024-09-24 00:00:00,4,33.30902613153457,21.894414788533155,50.97328133397679 +2024-09-24 01:00:00,4,36.48166172539783,19.793604683252152,57.15633620302632 +2024-09-24 02:00:00,4,22.701909561781576,21.55866848768169,52.135819592163855 +2024-09-24 03:00:00,4,19.07638725105688,23.77838430587796,59.411339323721165 +2024-09-24 04:00:00,4,39.717270076054234,17.843694974922514,51.23877754702492 +2024-09-24 05:00:00,4,18.712389017234116,24.71063311102959,43.55760091305744 +2024-09-24 06:00:00,4,9.920815282547832,26.055046126789513,68.54902227515923 +2024-09-24 07:00:00,4,27.67776073683667,22.99003062433695,68.33441652259208 +2024-09-24 08:00:00,4,15.87165048050313,29.833474713878203,61.276033974351975 +2024-09-24 09:00:00,4,30.172745795053928,26.05468391955167,58.264930861883535 +2024-09-24 10:00:00,4,29.586494091708648,23.49628694717525,45.28473395902866 +2024-09-24 11:00:00,4,31.33681889538786,31.160021296450093,47.65725891061072 +2024-09-24 12:00:00,4,27.411902702464285,25.172468401918266,55.19941607762079 +2024-09-24 13:00:00,4,21.077070305931528,28.303315108092637,55.54219121269774 +2024-09-24 14:00:00,4,11.684956210814464,26.44661578104304,61.21509719186141 +2024-09-24 15:00:00,4,24.79508449191217,29.277108353831835,63.470867053115214 +2024-09-24 16:00:00,4,12.055392924429352,28.264537695460355,49.0537741307447 +2024-09-24 17:00:00,4,23.108124810228084,20.580208777438166,46.55491056613142 +2024-09-24 18:00:00,4,26.3323305222488,23.528437044668035,53.55208453538621 +2024-09-24 19:00:00,4,22.902505586776456,31.921663028401262,41.71174982679461 +2024-09-24 20:00:00,4,20.37979663867735,23.106893341319424,35.01169747873611 +2024-09-24 21:00:00,4,11.627654609701526,23.920702369834345,52.46671029588124 +2024-09-24 22:00:00,4,37.138967035282626,23.28354484811358,51.525889338717874 +2024-09-24 23:00:00,4,25.74630277170813,25.66340776967187,55.42699023081148 +2024-09-25 00:00:00,4,23.83327151026151,23.39240098590397,41.66094623914181 +2024-09-25 01:00:00,4,20.27793725352018,23.261895463542995,63.87807087675313 +2024-09-25 02:00:00,4,9.14616457743977,25.982873849112913,39.75818803787181 +2024-09-25 03:00:00,4,25.818092863110056,15.654576448335222,60.13711465320477 +2024-09-25 04:00:00,4,42.09946898407191,20.44428246218237,54.61250469446848 +2024-09-25 05:00:00,4,38.36866407876257,22.485229284997583,41.48510998362229 +2024-09-25 06:00:00,4,22.828778849301443,24.80342963946241,52.63469296816582 +2024-09-25 07:00:00,4,16.221334459602417,22.4466163532387,49.03374344284961 +2024-09-25 08:00:00,4,41.48254895990784,24.812937801224987,57.85055934578608 +2024-09-25 09:00:00,4,10.395226208903871,28.802981804409544,71.79290921168521 +2024-09-25 10:00:00,4,4.8135236819748215,22.49578847223978,51.75203439782517 +2024-09-25 11:00:00,4,33.51373434115719,18.146283300770904,56.68082660974241 +2024-09-25 12:00:00,4,33.43493219116397,20.57216374651007,75.51313658826697 +2024-09-25 13:00:00,4,15.521570823032768,29.67885423592406,54.745931711337725 +2024-09-25 14:00:00,4,27.53869553493534,23.12974674935326,49.21770211884303 +2024-09-25 15:00:00,4,22.215257734927796,23.708997842317306,60.00555212939163 +2024-09-25 16:00:00,4,11.114713938873676,17.717986130112024,43.43351817301684 +2024-09-25 17:00:00,4,26.793563914410456,21.71970695020166,57.75875909802659 +2024-09-25 18:00:00,4,24.31486405237852,19.448867065115106,57.092873049048386 +2024-09-25 19:00:00,4,24.804008355649817,17.33175938315248,47.23847175120967 +2024-09-25 20:00:00,4,28.34020400116248,25.94636050013573,74.15988055275906 +2024-09-25 21:00:00,4,38.379465208906026,22.7817949570114,66.50273445460286 +2024-09-25 22:00:00,4,31.394479496409197,30.280607752509685,60.717078334621526 +2024-09-25 23:00:00,4,29.264374574654166,26.1704412912746,48.472031882264325 +2024-09-26 00:00:00,4,13.794191071904537,27.415918433014767,41.61929789583763 +2024-09-26 01:00:00,4,26.167263639324382,19.315794872201796,53.84287474022575 +2024-09-26 02:00:00,4,23.367887512812747,24.81153072547587,60.4713589395572 +2024-09-26 03:00:00,4,26.74417601069712,22.836795706224656,62.968442928153294 +2024-09-26 04:00:00,4,25.850346837567134,29.831374948775842,57.39973863604904 +2024-09-26 05:00:00,4,19.007253030412482,23.55176365904674,76.00994026928039 +2024-09-26 06:00:00,4,27.827780447036247,23.548562909553013,71.88011759361143 +2024-09-26 07:00:00,4,28.81854269844026,25.430694804999675,49.6067868130329 +2024-09-26 08:00:00,4,25.961166997441534,22.578469808110643,66.90680636756915 +2024-09-26 09:00:00,4,18.861890131904655,23.269431703317565,47.744951357863165 +2024-09-26 10:00:00,4,14.145101718263899,23.289476649188376,71.35731898010582 +2024-09-26 11:00:00,4,13.350956347954948,23.36815873245078,41.247235168370025 +2024-09-26 12:00:00,4,18.708118479923414,23.869336515334233,67.10942513962584 +2024-09-26 13:00:00,4,37.184512064868564,24.13856036694438,58.28429219639631 +2024-09-26 14:00:00,4,25.843529519995176,23.27105952995298,49.04039809673984 +2024-09-26 15:00:00,4,20.78476282264579,26.6086697896776,54.642419345724115 +2024-09-26 16:00:00,4,34.595922288344376,23.839211358314067,50.36943038387596 +2024-09-26 17:00:00,4,36.541340826546744,28.617743815584998,58.36582122957154 +2024-09-26 18:00:00,4,20.460483928167232,24.843702845070272,45.94287892527525 +2024-09-26 19:00:00,4,26.32065193440895,24.484028369243003,31.44053000502796 +2024-09-26 20:00:00,4,24.300701797900345,19.373144841397313,62.13804537128821 +2024-09-26 21:00:00,4,32.12330396596512,25.984242451841506,51.03995149213692 +2024-09-26 22:00:00,4,34.09429606786821,24.99601785477346,56.3995710997493 +2024-09-26 23:00:00,4,38.82359797224035,25.737477406257394,64.06199101838462 +2024-09-27 00:00:00,4,42.24504266427775,22.432102317202705,51.0623805602668 +2024-09-27 01:00:00,4,43.325056790997934,26.48827213986627,58.19557629326176 +2024-09-27 02:00:00,4,39.071186186994275,22.07271993689234,43.347100224836886 +2024-09-27 03:00:00,4,16.57787482761565,22.749091627357476,60.80435204907923 +2024-09-27 04:00:00,4,12.47159744001575,21.656132338769133,56.859753949568685 +2024-09-27 05:00:00,4,33.20496600700629,17.93095538768638,57.23335678844054 +2024-09-27 06:00:00,4,27.48648695279255,20.33961241838871,54.52107746991871 +2024-09-27 07:00:00,4,25.464568136136908,25.00240329909193,62.36164102582502 +2024-09-27 08:00:00,4,22.75276026938538,22.04091674491914,45.64435849410194 +2024-09-27 09:00:00,4,20.865354434565127,26.674911209875063,67.83548453088574 +2024-09-27 10:00:00,4,19.01118047718513,20.889215867944063,50.069987621574 +2024-09-27 11:00:00,4,25.413768209994473,29.497450027202028,62.855232617515064 +2024-09-27 12:00:00,4,34.676032088968356,21.59991184389655,64.1645339008233 +2024-09-27 13:00:00,4,23.412482258844772,22.047195966405994,61.77006872511433 +2024-09-27 14:00:00,4,39.04471835854614,21.250558531914688,41.21587481932945 +2024-09-27 15:00:00,4,34.67540839737074,26.513538729315222,60.700837585136284 +2024-09-27 16:00:00,4,10.440852655466246,24.433592002227,53.13060834572146 +2024-09-27 17:00:00,4,29.492180307562826,13.65761558797447,56.96629739150949 +2024-09-27 18:00:00,4,30.175495039150505,12.447055603071115,57.83093888289208 +2024-09-27 19:00:00,4,15.799812799288278,17.08304767600135,73.60643373523328 +2024-09-27 20:00:00,4,32.87143877412621,28.862202032430762,61.45071295616357 +2024-09-27 21:00:00,4,23.24295492425947,28.954552415919146,36.282050756959094 +2024-09-27 22:00:00,4,27.881549164089193,20.739754528736576,54.87988001705942 +2024-09-27 23:00:00,4,26.53249055827246,23.250300370165373,67.7390499825185 +2024-09-28 00:00:00,4,32.46227267739259,25.312928906101977,61.391915105887996 +2024-09-28 01:00:00,4,15.654121361242547,21.184312312279722,60.16719033129018 +2024-09-28 02:00:00,4,25.736525501064786,28.59073801840865,49.424769264799465 +2024-09-28 03:00:00,4,21.257618865212287,20.899283999248258,62.603733317457475 +2024-09-28 04:00:00,4,53.451144419893446,21.527252380366303,65.36145038706019 +2024-09-28 05:00:00,4,31.777158718291325,29.034799119511042,50.24754710872052 +2024-09-28 06:00:00,4,21.244915044703532,22.994010791826593,66.32895009321541 +2024-09-28 07:00:00,4,20.273394397235727,20.416217743033773,53.703175129899755 +2024-09-28 08:00:00,4,12.846047463818271,22.632087518046617,74.27730329667051 +2024-09-28 09:00:00,4,31.20305772879079,23.42138332438019,48.398259298654985 +2024-09-28 10:00:00,4,24.750350291044064,24.134462739505157,51.79180300279912 +2024-09-28 11:00:00,4,16.65988154048051,19.431670786221062,62.84171464625807 +2024-09-28 12:00:00,4,27.687396386699902,24.75504763085142,56.832899849277084 +2024-09-28 13:00:00,4,21.210707121474567,21.432517552438927,61.759523754985466 +2024-09-28 14:00:00,4,14.261181768399545,22.941294944616732,56.27324089981637 +2024-09-28 15:00:00,4,19.690054024391493,23.277351503243587,63.35615727071163 +2024-09-28 16:00:00,4,33.07489561836471,24.00020210293461,63.98682247209636 +2024-09-28 17:00:00,4,2.711599673279842,21.59395024369504,55.7882772332787 +2024-09-28 18:00:00,4,25.875674324847164,25.112163065034693,53.449227545156596 +2024-09-28 19:00:00,4,18.737883627955785,27.731642270610976,38.546469290259324 +2024-09-28 20:00:00,4,27.58486623861802,23.479156734723833,61.71195330896652 +2024-09-28 21:00:00,4,15.162358305586562,24.552085312710453,55.264471903537306 +2024-09-28 22:00:00,4,29.23734409518741,21.842462759791626,57.03575284953483 +2024-09-28 23:00:00,4,13.18936541658427,27.572981812557817,65.92299493500832 +2024-09-29 00:00:00,4,15.061629189425982,24.878181394793625,50.39519365154444 +2024-09-29 01:00:00,4,22.289367383543155,32.635420985143696,64.14433577676905 +2024-09-29 02:00:00,4,42.31473481066807,28.251703077872982,50.316565991016624 +2024-09-29 03:00:00,4,35.75530478836948,26.501271129263284,58.73197827752371 +2024-09-29 04:00:00,4,30.59262452450569,19.69204777845403,48.32476980395951 +2024-09-29 05:00:00,4,32.00087111939826,25.05880679421341,58.416222682325206 +2024-09-29 06:00:00,4,35.252711382073684,20.331869801619206,53.42464240111431 +2024-09-29 07:00:00,4,27.168412646373483,22.828233976936676,56.35353765087846 +2024-09-29 08:00:00,4,3.668698544840577,18.336666259044968,41.986067375140706 +2024-09-29 09:00:00,4,28.258193156775715,29.20684465811685,71.9318412309791 +2024-09-29 10:00:00,4,28.161840415962594,27.79154115536489,73.85240587841476 +2024-09-29 11:00:00,4,40.40843016078436,24.91867182820862,49.11977207285093 +2024-09-29 12:00:00,4,27.263523843943737,25.57766471115366,74.05547994184315 +2024-09-29 13:00:00,4,29.699682985034848,29.168483410386024,57.185811558149375 +2024-09-29 14:00:00,4,22.426468880029258,20.416833664493716,45.28685241416396 +2024-09-29 15:00:00,4,30.592862152932156,22.84040562811086,49.72883213817718 +2024-09-29 16:00:00,4,19.14907907054286,20.184434143890822,40.312198055782176 +2024-09-29 17:00:00,4,25.215543882500658,19.933957725020438,58.9964417047371 +2024-09-29 18:00:00,4,20.198565864642436,27.141292936928146,36.2140773878827 +2024-09-29 19:00:00,4,14.016330077207813,19.760427751778447,58.31553196120234 +2024-09-29 20:00:00,4,28.37767100467097,27.63227725504706,50.24824358379194 +2024-09-29 21:00:00,4,14.892143264095663,26.2489867677035,50.663212460141345 +2024-09-29 22:00:00,4,26.989546507805223,19.258098398514534,42.357199588873215 +2024-09-29 23:00:00,4,27.37086198006926,29.71949691327008,48.80587505071104 +2024-09-30 00:00:00,4,33.62305058454469,21.541674690509954,66.02073685290125 +2024-09-30 01:00:00,4,8.999871694772143,19.326183749386853,66.35014122682331 +2024-09-30 02:00:00,4,32.3041468417182,27.7321617102899,54.40462771088064 +2024-09-30 03:00:00,4,17.750867535007863,20.137011348296305,55.18055343374367 +2024-09-30 04:00:00,4,17.970083580661054,19.502658767429327,56.61527202251318 +2024-09-30 05:00:00,4,37.98922259552608,17.4310199624661,51.8797511933492 +2024-09-30 06:00:00,4,9.039617307549381,25.84728674433108,46.54594434967554 +2024-09-30 07:00:00,4,21.1354735979412,24.032558602193966,58.331103547936266 +2024-09-30 08:00:00,4,29.345986182595666,26.00629306279924,50.463102572050616 +2024-09-30 09:00:00,4,16.776006125467653,23.315197284098314,43.23797737818927 +2024-09-30 10:00:00,4,19.44732697320482,25.799093175124163,59.65349902146493 +2024-09-30 11:00:00,4,16.56730875856107,15.664704182630555,51.93734043832736 +2024-09-30 12:00:00,4,26.065880835839057,25.28711702209635,54.20691350799982 +2024-09-30 13:00:00,4,18.844884325356418,23.672723609289513,54.95880897467149 +2024-09-30 14:00:00,4,22.181865516023926,19.67867249106756,45.3673877676464 +2024-09-30 15:00:00,4,13.271707106317267,28.383106656040475,51.3679269698747 +2024-09-30 16:00:00,4,19.231162369414225,27.139945633835488,38.10194484324615 +2024-09-30 17:00:00,4,10.443100692626576,23.453414864430542,68.30039951032495 +2024-09-30 18:00:00,4,27.134406666628802,22.544700660082025,62.91359072633261 +2024-09-30 19:00:00,4,27.392471644391158,21.074010513580888,54.714667061796376 +2024-09-30 20:00:00,4,15.00783983833668,22.518900399773997,59.19621568047488 +2024-09-30 21:00:00,4,30.345717110027834,23.815769033904694,86.3250380116598 +2024-09-30 22:00:00,4,27.66087971549321,19.74686238675532,44.3967362831392 +2024-09-30 23:00:00,4,21.546532491450485,21.61587026554472,45.04108571374584 +2024-10-01 00:00:00,4,35.93008686813372,28.856500559391147,55.35188466672052 +2024-10-01 01:00:00,4,35.14415242042102,21.013635510220222,45.03529844910143 +2024-10-01 02:00:00,4,22.999675732741455,26.597148908052635,54.8103790639516 +2024-10-01 03:00:00,4,40.75081535255926,24.913243909486667,50.757359273774554 +2024-10-01 04:00:00,4,21.445913356582647,19.063507028123876,53.690427179503814 +2024-10-01 05:00:00,4,32.07529179648472,19.003344176502704,49.5285222669869 +2024-10-01 06:00:00,4,33.93346733206746,20.45927407925872,44.53542126039922 +2024-10-01 07:00:00,4,17.73635359054802,22.116341776530255,62.047866309578396 +2024-10-01 08:00:00,4,13.242009024230512,24.253585159171994,44.63214786732048 +2024-10-01 09:00:00,4,26.282273005240928,27.081134812795643,60.77483816499636 +2024-10-01 10:00:00,4,17.01505720560054,19.43524244488735,47.66147203096227 +2024-10-01 11:00:00,4,20.65097223788263,30.990918204321087,59.61527622180246 +2024-10-01 12:00:00,4,27.05402366110771,19.2663562933686,63.22878566701423 +2024-10-01 13:00:00,4,34.86171037738334,20.36476219323306,60.784261911618174 +2024-10-01 14:00:00,4,13.247291643544251,20.85134575900927,58.82501885817204 +2024-10-01 15:00:00,4,19.29387226196577,17.797628669645746,56.26940212370565 +2024-10-01 16:00:00,4,19.49795811067696,24.563354861958395,52.526791070565565 +2024-10-01 17:00:00,4,21.00838984636154,22.61818937987273,50.9981756134283 +2024-10-01 18:00:00,4,20.48373689164875,18.987221608377855,51.566147162124764 +2024-10-01 19:00:00,4,14.102177314830692,26.9193665828163,56.499546180957715 +2024-10-01 20:00:00,4,37.418956406358596,18.461426336701415,66.8516252060664 +2024-10-01 21:00:00,4,15.63956747219662,24.654663221633598,38.40808261022347 +2024-10-01 22:00:00,4,35.79290946643705,28.202706331596907,57.279001366751515 +2024-10-01 23:00:00,4,14.177044684752829,24.19479257252333,62.63725235904948 +2024-10-02 00:00:00,4,24.209179457903883,22.212795622364354,67.22059333307996 +2024-10-02 01:00:00,4,10.68376950476841,23.756736936200134,44.493802535509325 +2024-10-02 02:00:00,4,42.62138030589548,27.00611022344701,50.90043119351905 +2024-10-02 03:00:00,4,15.51461418509286,27.83617080448978,55.592708791373475 +2024-10-02 04:00:00,4,26.863395265269816,25.99990903934762,52.995874246837104 +2024-10-02 05:00:00,4,22.147395412134703,22.027036592554186,63.75108729973654 +2024-10-02 06:00:00,4,35.3468035953437,25.950779249179096,49.55864997461675 +2024-10-02 07:00:00,4,26.736349819618752,27.098782461451286,48.88003478381315 +2024-10-02 08:00:00,4,37.34360279187415,22.504572382028112,67.35365438499407 +2024-10-02 09:00:00,4,22.290924469252303,26.016671970787637,54.68371825805559 +2024-10-02 10:00:00,4,27.96233849335556,20.172328741778475,67.13757155177706 +2024-10-02 11:00:00,4,19.424490703430656,21.852968765662215,63.544527156825104 +2024-10-02 12:00:00,4,19.16155582938007,28.382117404024736,53.287845353844276 +2024-10-02 13:00:00,4,26.34840093504141,25.33425197660339,65.36514073677462 +2024-10-02 14:00:00,4,16.514900494995633,26.20547815006736,65.75366134520891 +2024-10-02 15:00:00,4,16.54936733654953,24.131976439903426,37.88405521163528 +2024-10-02 16:00:00,4,9.901345374138378,29.10570355900666,63.09160586222596 +2024-10-02 17:00:00,4,58.91054697647574,24.659554648887926,47.921681185032995 +2024-10-02 18:00:00,4,17.70757355055232,23.482326287315402,52.9798802779823 +2024-10-02 19:00:00,4,19.59804615966121,24.13953690055178,50.784333175223296 +2024-10-02 20:00:00,4,19.249226656163536,18.707611092687486,48.189229594937736 +2024-10-02 21:00:00,4,27.991057559992743,15.241782314054658,60.17561276233708 +2024-10-02 22:00:00,4,31.544134177552834,25.524752850171126,66.28323730332944 +2024-10-02 23:00:00,4,16.17575102512762,26.488670739846537,60.888174457248006 +2024-10-03 00:00:00,4,32.66290804999136,24.963298891770584,57.75429690967773 +2024-10-03 01:00:00,4,27.26962467489085,20.612812222198848,54.87868943210968 +2024-10-03 02:00:00,4,18.392410948051037,17.6655911217955,41.59589996873273 +2024-10-03 03:00:00,4,27.688732471577964,24.510537402517958,37.13470210104075 +2024-10-03 04:00:00,4,13.993308606334026,26.237712876158163,66.53822904350918 +2024-10-03 05:00:00,4,21.19141790935513,22.72663965992117,45.56754375686632 +2024-10-03 06:00:00,4,35.893301871068765,19.923565260011667,55.7836925178609 +2024-10-03 07:00:00,4,28.058704734281417,20.176249930924484,70.53639579590137 +2024-10-03 08:00:00,4,38.681930157747004,21.498257077523704,57.549624688300035 +2024-10-03 09:00:00,4,32.471483700252904,22.85892380194658,54.21191285601283 +2024-10-03 10:00:00,4,18.164526603891176,24.469626884221,66.39568579499924 +2024-10-03 11:00:00,4,26.59102710267164,20.562838982833817,45.34455966783286 +2024-10-03 12:00:00,4,27.835739017094774,21.894865388468567,54.976589401110225 +2024-10-03 13:00:00,4,34.743929589744255,19.891055152091578,56.88499901818082 +2024-10-03 14:00:00,4,28.743007297606596,22.626816593062298,53.11725188598119 +2024-10-03 15:00:00,4,36.420081536285636,20.53903164516171,51.642888560041186 +2024-10-03 16:00:00,4,9.056904965475367,24.54161251894853,55.54877373745086 +2024-10-03 17:00:00,4,22.597872486851855,24.863565381163912,44.46511918838871 +2024-10-03 18:00:00,4,20.44255583609567,23.114010370985792,57.357471414096295 +2024-10-03 19:00:00,4,16.1128433852343,21.422746778832497,55.49527167772135 +2024-10-03 20:00:00,4,23.05715193497784,32.83025617488761,36.28978606741217 +2024-10-03 21:00:00,4,31.098658377643698,24.11671409944764,58.83664355115977 +2024-10-03 22:00:00,4,28.550742292571705,24.151918033511592,67.1373372815631 +2024-10-03 23:00:00,4,20.349422300915943,21.973080327618558,54.721965621136185 +2024-10-04 00:00:00,4,25.949445578011787,26.38155998718283,36.60428939263697 +2024-10-04 01:00:00,4,36.6408581516337,30.166534303422793,57.12385497683182 +2024-10-04 02:00:00,4,13.679697267472317,26.645814874596077,52.82465474354085 +2024-10-04 03:00:00,4,35.09444392100961,22.85468260884419,54.068202678701795 +2024-10-04 04:00:00,4,20.035425106492042,23.853983990348773,53.659298504706754 +2024-10-04 05:00:00,4,39.43791677529177,27.10722443939519,42.70495994544572 +2024-10-04 06:00:00,4,9.447781363992494,23.625893261478442,63.84425857053377 +2024-10-04 07:00:00,4,43.83230794561156,22.970977149657465,46.92593797976349 +2024-10-04 08:00:00,4,19.34569490543515,21.9237732337087,52.48184781353895 +2024-10-04 09:00:00,4,24.108937440407264,25.308564928508396,54.16401049277623 +2024-10-04 10:00:00,4,19.82227211898214,26.177478480075937,52.96342946919421 +2024-10-04 11:00:00,4,23.617893223151214,21.666709458037182,60.84908971781763 +2024-10-04 12:00:00,4,19.353910916641286,21.98186103246741,45.66481808555722 +2024-10-04 13:00:00,4,23.363558586653376,21.610602564521955,55.59500898812246 +2024-10-04 14:00:00,4,26.713418528557185,22.26343015305012,48.77271892829075 +2024-10-04 15:00:00,4,37.218705144121074,21.129716767359362,44.304920514510016 +2024-10-04 16:00:00,4,11.302525381494501,25.41602141302463,45.155902238093205 +2024-10-04 17:00:00,4,26.54161735685193,24.429552878156493,55.36324908460136 +2024-10-04 18:00:00,4,5.976827642958526,19.821259545214488,53.680914800498634 +2024-10-04 19:00:00,4,17.285626025555953,19.40992834158093,51.75000829814789 +2024-10-04 20:00:00,4,26.37346748766122,25.44708217008787,48.586813635279285 +2024-10-04 21:00:00,4,22.308978034937574,20.159941209265668,58.15830732608448 +2024-10-04 22:00:00,4,30.275352075557358,30.255971218305426,51.05074376884239 +2024-10-04 23:00:00,4,28.485131034466022,23.803706591172755,57.21665063501851 +2024-10-05 00:00:00,4,0.0,21.761575988302276,57.039535573570085 +2024-10-05 01:00:00,4,30.479529699948106,25.999625656499603,67.1827425501845 +2024-10-05 02:00:00,4,22.29235800435138,19.35435932468102,63.242162689935824 +2024-10-05 03:00:00,4,21.924345516841225,23.063548433256848,58.460439357056025 +2024-10-05 04:00:00,4,36.690942312350096,25.88564969119267,51.02975547894282 +2024-10-05 05:00:00,4,29.62993298757794,22.416878504460612,57.7232604923305 +2024-10-05 06:00:00,4,22.153865665892994,26.93424962663017,58.224906881957104 +2024-10-05 07:00:00,4,20.3829948453037,26.355003775299704,52.732122954462675 +2024-10-05 08:00:00,4,20.826309435171474,24.801696317614606,62.142955651582255 +2024-10-05 09:00:00,4,29.511250613068363,23.100469961779485,66.08540598039558 +2024-10-05 10:00:00,4,33.85199023319727,24.968357434740057,55.07206518728743 +2024-10-05 11:00:00,4,24.48117031765453,24.490503995190924,58.53417304201041 +2024-10-05 12:00:00,4,21.587488290796447,19.206787624216783,39.08553565435679 +2024-10-05 13:00:00,4,37.153498279586294,27.75484870112262,53.92795812554809 +2024-10-05 14:00:00,4,13.061372224360387,25.48449493870494,49.348169276101196 +2024-10-05 15:00:00,4,15.377725901432683,24.66381497007642,64.98588386643354 +2024-10-05 16:00:00,4,0.0,30.113132789823297,46.63291384800304 +2024-10-05 17:00:00,4,23.912313759450395,27.24992941515899,56.735031208612504 +2024-10-05 18:00:00,4,20.22904798059776,20.38324107885012,52.4469346786576 +2024-10-05 19:00:00,4,34.86297064458138,26.553510653824663,53.804520848101326 +2024-10-05 20:00:00,4,32.46157953360195,27.320726741152583,47.63390373834491 +2024-10-05 21:00:00,4,26.856868282782685,21.349799900073158,60.084426568052145 +2024-10-05 22:00:00,4,20.669414644005702,25.313614070989406,58.46852643599251 +2024-10-05 23:00:00,4,37.41129572215391,26.373911291311398,52.67630045634462 +2024-10-06 00:00:00,4,48.10173350255383,23.949946319767115,43.452778008111224 +2024-10-06 01:00:00,4,28.422498050601106,31.85733008505634,54.838268478722426 +2024-10-06 02:00:00,4,24.788571678626404,25.78224579848916,53.18842570951348 +2024-10-06 03:00:00,4,42.814317639951824,29.323190218481002,51.75090592074725 +2024-10-06 04:00:00,4,26.392396009851748,25.50901585533582,39.153286069832774 +2024-10-06 05:00:00,4,35.37973006994244,22.11000667074062,59.76136252243617 +2024-10-06 06:00:00,4,33.55873618676432,21.85252883602895,55.9275611871538 +2024-10-06 07:00:00,4,25.94723553511166,22.203350478593634,40.45418806535491 +2024-10-06 08:00:00,4,32.89208222493366,16.26871227159991,56.24419410284609 +2024-10-06 09:00:00,4,18.78114443219291,24.5943998344262,49.10456745969323 +2024-10-06 10:00:00,4,20.10863079206338,21.229935976555034,53.97247342407938 +2024-10-06 11:00:00,4,31.457901226578798,20.982241283798057,65.99250068724322 +2024-10-06 12:00:00,4,3.645515217466407,21.057842284423756,46.02872595357296 +2024-10-06 13:00:00,4,28.39132974107791,22.542738904179195,54.1305115902883 +2024-10-06 14:00:00,4,19.0275377265075,19.973020834774644,59.56920417354586 +2024-10-06 15:00:00,4,20.95457522811116,29.87996518663171,46.283809817736696 +2024-10-06 16:00:00,4,15.883904332066255,23.382509330942877,37.413420981388356 +2024-10-06 17:00:00,4,21.000541452307775,22.21537840601667,49.60417717737056 +2024-10-06 18:00:00,4,32.3926969588201,19.8477193352903,46.06539937033353 +2024-10-06 19:00:00,4,23.420340549510605,22.84406723751807,51.36049843140921 +2024-10-06 20:00:00,4,25.747302803642658,20.153581485938474,68.44891676113767 +2024-10-06 21:00:00,4,12.600443629608542,17.58890303446082,62.67209334034952 +2024-10-06 22:00:00,4,24.446099927191902,25.40421108626985,32.255519937977446 +2024-10-06 23:00:00,4,19.252882914795972,21.71775512369532,46.53472334520184 +2024-10-07 00:00:00,4,16.864175361906067,22.880937537441987,39.60162191534081 +2024-10-07 01:00:00,4,25.43410890334361,26.982535961440444,55.77288406115344 +2024-10-07 02:00:00,4,35.29786136528524,29.052282101234717,40.45190791139661 +2024-10-07 03:00:00,4,23.31742267481289,28.524876421339815,61.2070419584051 +2024-10-07 04:00:00,4,21.215262065738226,25.54776788385841,66.17200607252379 +2024-10-07 05:00:00,4,33.38863100348637,29.807002482399284,60.27281153928823 +2024-10-07 06:00:00,4,35.29060368733837,27.98960191338992,63.87754083611712 +2024-10-07 07:00:00,4,43.85772567976535,25.572706750651122,58.85443998109538 +2024-10-07 08:00:00,4,29.90687874700742,25.830152834105107,69.4534876717201 +2024-10-07 09:00:00,4,21.78022037223687,21.57200733375512,63.321077494095626 +2024-10-07 10:00:00,4,37.83220423400059,23.32782175563084,36.197983365862086 +2024-10-07 11:00:00,4,21.82010404403972,21.391335009563775,51.956018994161774 +2024-10-07 12:00:00,4,24.226201736100926,27.423480493511384,56.50892142766665 +2024-10-07 13:00:00,4,35.3317626031932,27.59193612379882,50.30989689667028 +2024-10-07 14:00:00,4,24.99362323619606,24.026317697329915,54.27379849575183 +2024-10-07 15:00:00,4,22.337302899194167,19.23134407866027,60.920588753978556 +2024-10-07 16:00:00,4,27.668833100881756,31.081990950266334,50.75420214526584 +2024-10-07 17:00:00,4,20.475992456887948,29.65828882909964,71.71664911379192 +2024-10-07 18:00:00,4,37.080043835438346,26.492840970979834,53.328647919314555 +2024-10-07 19:00:00,4,9.991522304540268,21.041661521389624,43.51459540383062 +2024-10-07 20:00:00,4,16.05721407725106,30.287269227736605,40.6104044138377 +2024-10-07 21:00:00,4,28.653180293426942,26.614260225683317,51.971514790055544 +2024-10-07 22:00:00,4,13.054900006513456,23.48939752098976,34.31784331286887 +2024-10-07 23:00:00,4,36.989773823152426,25.02579009840505,64.07763900800202 +2024-10-08 00:00:00,4,8.801286405391615,26.458037278674006,61.335860679889045 +2024-10-08 01:00:00,4,30.093586315046217,23.4413276533148,67.21338224214173 +2024-10-08 02:00:00,4,36.1966602191877,18.79598856023756,67.85584046885104 +2024-10-08 03:00:00,4,13.90014386580432,29.358191697564642,57.179161142553255 +2024-10-08 04:00:00,4,30.50258089540749,27.979325062118548,55.44782551033813 +2024-10-08 05:00:00,4,37.83644248256961,23.62196089673685,67.30688671911747 +2024-10-08 06:00:00,4,26.184723574183757,28.131093715364088,53.42528940892282 +2024-10-08 07:00:00,4,25.429560824028123,25.662013650759746,40.27305986923856 +2024-10-08 08:00:00,4,28.111717701797463,21.517799099277372,46.590789675633665 +2024-10-08 09:00:00,4,13.559405979231329,15.107158485030588,57.25171587315964 +2024-10-08 10:00:00,4,25.764068077627904,22.408872875684384,50.696938262528775 +2024-10-08 11:00:00,4,19.601116659200226,26.134780338976526,55.56125885240653 +2024-10-08 12:00:00,4,11.881411497579265,24.207432418228468,40.64799768450972 +2024-10-08 13:00:00,4,19.371914185499612,26.8026715766272,59.87218060097497 +2024-10-08 14:00:00,4,40.725707175083755,26.036786886903464,47.04317409219095 +2024-10-08 15:00:00,4,37.90954630434727,28.696498348243175,68.56058354532095 +2024-10-08 16:00:00,4,28.112063558811453,25.03532195585877,42.29730705774671 +2024-10-08 17:00:00,4,21.66377108290769,21.40299215843843,38.1976620169102 +2024-10-08 18:00:00,4,13.776759445703863,30.826574158977515,54.307751906002565 +2024-10-08 19:00:00,4,24.37730550670703,26.74326863449803,59.410266472711285 +2024-10-08 20:00:00,4,20.915052210497194,25.913927480311205,58.652622327015784 +2024-10-08 21:00:00,4,32.225146504675806,27.957261611068958,49.54508677070379 +2024-10-08 22:00:00,4,6.086029542568635,28.597097702113064,50.330583149095034 +2024-10-08 23:00:00,4,21.460715044889966,25.29446673669684,68.10117573464109 +2024-10-09 00:00:00,4,31.038615965981165,27.38009179372297,61.55802734126731 +2024-10-09 01:00:00,4,22.256448552086805,24.859663663236063,54.9538544740161 +2024-10-09 02:00:00,4,39.87179370262673,22.286899733243217,56.7700966279759 +2024-10-09 03:00:00,4,48.95783077805085,16.801546624448996,52.120338693064326 +2024-10-09 04:00:00,4,27.640912591094082,19.86554410704389,68.83301739782168 +2024-10-09 05:00:00,4,20.357302601178887,25.663523631026887,67.21757858200601 +2024-10-09 06:00:00,4,44.23712898178678,20.671850500754143,68.64469673385958 +2024-10-09 07:00:00,4,31.299749534218623,26.00683018883524,44.17304754863754 +2024-10-09 08:00:00,4,27.34866416420036,22.726427604508732,68.42582473582111 +2024-10-09 09:00:00,4,35.42609188025678,27.877468646776506,51.357565093810635 +2024-10-09 10:00:00,4,25.306805422747253,18.785160423449568,57.3981804212332 +2024-10-09 11:00:00,4,25.96672085470702,23.684308012608685,55.993178599122906 +2024-10-09 12:00:00,4,14.35329778338277,18.79603873779557,61.27327780861451 +2024-10-09 13:00:00,4,42.00369733801122,19.321169283503806,51.625216426001764 +2024-10-09 14:00:00,4,20.042978552731647,27.677124142304486,46.15461086578986 +2024-10-09 15:00:00,4,7.999117800991524,21.61428984573524,48.52086991592353 +2024-10-09 16:00:00,4,21.395549092262485,22.335830106194813,51.04328354782836 +2024-10-09 17:00:00,4,23.20500288011521,21.928241334299884,40.239196061222074 +2024-10-09 18:00:00,4,21.328906981693798,18.011062807873255,60.88994120449192 +2024-10-09 19:00:00,4,41.28327329787518,29.086989200494116,43.73643297819551 +2024-10-09 20:00:00,4,20.66099772944282,23.49604740970147,70.76086321799603 +2024-10-09 21:00:00,4,18.382665868027438,25.008274701554164,55.89607782982118 +2024-10-09 22:00:00,4,32.292590989726264,20.945281252959493,40.17662071878381 +2024-10-09 23:00:00,4,29.372574544441,21.9401168863851,55.72847819771537 +2024-10-10 00:00:00,4,25.082305080165057,27.403299293921812,50.16831135005711 +2024-10-10 01:00:00,4,30.330991619283402,23.615533579366364,47.420362453499926 +2024-10-10 02:00:00,4,35.18305495018087,25.616816761248394,63.86009648925316 +2024-10-10 03:00:00,4,19.082067515500043,21.551839210139367,48.6077034681863 +2024-10-10 04:00:00,4,33.25089183057406,27.846998782766747,57.76661880392311 +2024-10-10 05:00:00,4,18.348347240937702,29.684643518048297,46.3492625813163 +2024-10-10 06:00:00,4,19.694797396275636,24.92460290978424,63.16668038889938 +2024-10-10 07:00:00,4,34.209757845180164,21.976109390689206,52.33453537674748 +2024-10-10 08:00:00,4,42.121511326960416,23.289368577229634,45.052570737916966 +2024-10-10 09:00:00,4,28.481727160134735,19.488509288131368,66.25051182268376 +2024-10-10 10:00:00,4,23.23955000011851,22.6683562582985,52.95396039461754 +2024-10-10 11:00:00,4,23.017588761232876,20.3286246949589,67.97979510202046 +2024-10-10 12:00:00,4,28.989866360429474,27.47628065151109,81.27029628084551 +2024-10-10 13:00:00,4,33.306265889266385,17.36130692804174,40.63357942342998 +2024-10-10 14:00:00,4,14.079202835447854,20.39363330786604,32.877799217562135 +2024-10-10 15:00:00,4,14.062323731925733,20.29891585723139,64.78506659924501 +2024-10-10 16:00:00,4,28.404142155356077,25.596820617030374,37.0558891067504 +2024-10-10 17:00:00,4,5.270771786114654,22.215567020756364,73.10937956147455 +2024-10-10 18:00:00,4,31.52444459838793,27.547802859500184,44.362800968443125 +2024-10-10 19:00:00,4,21.61460461971935,25.93203780449848,63.82848231639169 +2024-10-10 20:00:00,4,25.69644538738718,25.26017276706351,45.97312532871176 +2024-10-10 21:00:00,4,11.966551517604842,23.66711071149219,60.46741117950978 +2024-10-10 22:00:00,4,32.89097852468966,24.274660206235545,59.04962292543849 +2024-10-10 23:00:00,4,37.38683483730586,24.212221812519918,47.887779174387084 +2024-10-11 00:00:00,4,19.000442321045995,24.82845162892219,56.189036801977565 +2024-10-11 01:00:00,4,25.19326139390456,34.54587672628817,58.55150138791535 +2024-10-11 02:00:00,4,30.12489374360073,21.360711160481017,47.7818760967895 +2024-10-11 03:00:00,4,32.80161174542366,25.236307760915363,44.82198791055877 +2024-10-11 04:00:00,4,29.677078139631448,24.735309935303594,54.18356122388786 +2024-10-11 05:00:00,4,24.999921725311165,26.702513199344832,74.51920905350833 +2024-10-11 06:00:00,4,43.16618118275019,25.271737691130426,60.05449017812884 +2024-10-11 07:00:00,4,38.6340575732736,31.79522624210953,59.272859226679515 +2024-10-11 08:00:00,4,23.67658690859562,27.08746134171111,45.490154942473666 +2024-10-11 09:00:00,4,33.820147341801075,28.03196726191444,57.82562740344958 +2024-10-11 10:00:00,4,29.909853422452763,26.40971260433526,63.376728948096265 +2024-10-11 11:00:00,4,27.203317723394427,21.42966266038079,55.60130143185567 +2024-10-11 12:00:00,4,29.31294372562661,24.646115348564557,57.8607126254303 +2024-10-11 13:00:00,4,36.58394531151609,25.61881928193135,46.91638134963685 +2024-10-11 14:00:00,4,50.15871584405387,22.750095614758106,58.28035144419281 +2024-10-11 15:00:00,4,22.539697996331405,24.110813346984024,63.47560842845917 +2024-10-11 16:00:00,4,21.057301943614956,20.75840564766181,52.15634937421786 +2024-10-11 17:00:00,4,29.327793256560632,20.818347285505894,53.9233044662663 +2024-10-11 18:00:00,4,16.869254133814316,19.836034253148703,57.42644003359594 +2024-10-11 19:00:00,4,31.613432877880392,22.79112616797994,56.25096132417863 +2024-10-11 20:00:00,4,34.416381221666306,17.33926162302557,64.213620262763 +2024-10-11 21:00:00,4,33.349116636062234,28.535534248318005,46.45916065105168 +2024-10-11 22:00:00,4,18.998857636185445,27.486801770971965,60.12658501716285 +2024-10-11 23:00:00,4,38.94745542390318,28.974338436623537,70.56851566703774 +2024-10-12 00:00:00,4,23.929988334291806,26.09879323666495,59.03882875766203 +2024-10-12 01:00:00,4,13.084025695348066,24.58043467495685,59.55966019482432 +2024-10-12 02:00:00,4,25.496521268816995,25.50359070019331,47.61843040742469 +2024-10-12 03:00:00,4,21.5423816660801,23.7938541181434,55.60079083905923 +2024-10-12 04:00:00,4,37.116514969083724,28.927628554881256,62.96029448553009 +2024-10-12 05:00:00,4,14.920376524843762,17.06073455729495,64.54413430072952 +2024-10-12 06:00:00,4,28.424567471025643,19.33955354281172,63.46962545690677 +2024-10-12 07:00:00,4,13.98689976744214,23.27237497224868,60.879947421715826 +2024-10-12 08:00:00,4,22.12486820474613,28.208719358143938,52.14773842222367 +2024-10-12 09:00:00,4,30.46974431719539,23.764213098923566,64.25953321863092 +2024-10-12 10:00:00,4,21.95899376872179,25.099400832059057,57.78321794892281 +2024-10-12 11:00:00,4,20.319313152694395,21.301266605224384,48.61434099230118 +2024-10-12 12:00:00,4,29.14668463988335,22.044721796598218,66.71882339090475 +2024-10-12 13:00:00,4,12.041338697190831,21.49662591439947,46.74536633846415 +2024-10-12 14:00:00,4,14.06878986659965,28.919691611569633,54.915156722608664 +2024-10-12 15:00:00,4,19.244361671279233,22.811889047986483,59.59154110503793 +2024-10-12 16:00:00,4,23.83923263266624,27.29953524683334,78.55094775919264 +2024-10-12 17:00:00,4,14.608083015402938,30.97746467356811,50.216355878775325 +2024-10-12 18:00:00,4,36.314073297126036,22.963145354558616,52.31376866668543 +2024-10-12 19:00:00,4,23.561994509182096,23.28903248711678,48.97881362674069 +2024-10-12 20:00:00,4,19.244299158730968,15.288622308152354,51.56497447739759 +2024-10-12 21:00:00,4,20.031554620809153,25.77824115939173,45.57777432235855 +2024-10-12 22:00:00,4,35.0590152297066,26.57385556808882,66.65655026332972 +2024-10-12 23:00:00,4,17.19794562896663,24.11173674757702,48.6683937250352 +2024-10-13 00:00:00,4,34.96353669399521,25.368578980828847,60.725921510378136 +2024-10-13 01:00:00,4,25.008009689891715,23.89864031669454,63.935933274393406 +2024-10-13 02:00:00,4,36.966628675461486,21.32888241833482,55.88298111384118 +2024-10-13 03:00:00,4,27.78335601276144,20.473750785029498,55.34726069984838 +2024-10-13 04:00:00,4,32.78589356843596,22.651934818075837,48.22005923756606 +2024-10-13 05:00:00,4,42.06421876460783,21.628791733577522,36.49792014641557 +2024-10-13 06:00:00,4,27.640439538106286,26.565756692183083,51.88588463077603 +2024-10-13 07:00:00,4,30.951560346922356,26.284251588123617,63.12954998857651 +2024-10-13 08:00:00,4,15.97890475743676,27.937732184955536,65.89198815393087 +2024-10-13 09:00:00,4,33.23714195797794,23.697696947878757,59.817297054745396 +2024-10-13 10:00:00,4,23.35505765069466,21.29467304015053,57.81753537354653 +2024-10-13 11:00:00,4,22.279421379241697,23.342975722835636,57.44364470977321 +2024-10-13 12:00:00,4,28.98982241990975,26.838760085476217,66.56302724665571 +2024-10-13 13:00:00,4,28.85844810017442,22.50206565073826,60.183265853903485 +2024-10-13 14:00:00,4,34.308230227045044,24.843227211315146,57.69511547202126 +2024-10-13 15:00:00,4,26.226611796506916,25.274455190736653,63.845716937582154 +2024-10-13 16:00:00,4,49.169198839558426,34.029987274669246,38.89294680919195 +2024-10-13 17:00:00,4,21.75141330990473,21.389137219695964,47.35118014108404 +2024-10-13 18:00:00,4,25.299549579504415,23.788394208150624,55.847485570594294 +2024-10-13 19:00:00,4,25.632662873794395,17.820218004234764,62.124776392368275 +2024-10-13 20:00:00,4,23.05017898510702,28.089920773140655,62.16829949915297 +2024-10-13 21:00:00,4,28.6522266481243,25.750359931909404,66.12287239991133 +2024-10-13 22:00:00,4,23.378714852431266,21.60417118748245,45.44624723582338 +2024-10-13 23:00:00,4,35.808606128397976,21.824725788953934,46.21585056366047 +2024-10-14 00:00:00,4,28.000013876146074,23.339295151404844,56.77873245191982 +2024-10-14 01:00:00,4,20.732583993087797,24.052184867472945,58.71022811831467 +2024-10-14 02:00:00,4,38.92480533889793,21.644199867984995,55.910591434999596 +2024-10-14 03:00:00,4,41.507159949325256,21.55184738044878,66.77175395977123 +2024-10-14 04:00:00,4,23.962698956122722,28.690816713752067,46.772648954385346 +2024-10-14 05:00:00,4,17.400246027533903,25.007522208332237,46.811141654581405 +2024-10-14 06:00:00,4,14.250368321885654,23.419011768507083,55.42849669959061 +2024-10-14 07:00:00,4,38.45310257555299,25.552139919330536,49.534509135780276 +2024-10-14 08:00:00,4,35.59853850064185,25.350719183503475,46.834095432639785 +2024-10-14 09:00:00,4,18.885557398572058,22.59918687315239,41.43405120063322 +2024-10-14 10:00:00,4,12.230040523216818,22.46010385341558,62.916395964652466 +2024-10-14 11:00:00,4,18.677224567648665,22.065687739132088,70.55419622414043 +2024-10-14 12:00:00,4,37.21301670789775,24.072384257427267,52.480017759088476 +2024-10-14 13:00:00,4,17.412627704176003,26.58442703450317,67.19392071171517 +2024-10-14 14:00:00,4,26.32081680665103,26.494020505772284,52.20008637940751 +2024-10-14 15:00:00,4,26.75570327635569,27.58610043467717,52.0143810418855 +2024-10-14 16:00:00,4,26.25874963384561,22.649962800847156,70.22954847012335 +2024-10-14 17:00:00,4,20.882164110450617,26.59497480139352,62.40420885805682 +2024-10-14 18:00:00,4,52.58272041248448,21.20851488357627,46.594858532652914 +2024-10-14 19:00:00,4,18.531652775561042,26.61204293868564,53.599336782127 +2024-10-14 20:00:00,4,28.90716201522983,24.310106344783225,48.59755499393533 +2024-10-14 21:00:00,4,37.789397750852004,21.74674564991706,56.14867754230742 +2024-10-14 22:00:00,4,24.460971146151696,19.14300342173681,56.8439549587868 +2024-10-14 23:00:00,4,31.404125567759486,20.52932173047993,66.87684190079239 +2024-10-15 00:00:00,4,28.826581150522678,23.903957941105855,44.5394469903401 +2024-10-15 01:00:00,4,31.456548940821428,27.804006507383985,46.99705956203715 +2024-10-15 02:00:00,4,20.44530056038226,25.6532252061932,53.02730726954502 +2024-10-15 03:00:00,4,35.459069657520324,24.080446345538462,54.19520240551415 +2024-10-15 04:00:00,4,21.11921178924262,22.88921806963671,56.47104243615809 +2024-10-15 05:00:00,4,16.93194033222823,24.683684997969834,41.07259417577933 +2024-10-15 06:00:00,4,24.785122925270706,21.90902089821567,56.016171633698136 +2024-10-15 07:00:00,4,33.862055715929074,24.815467402251723,47.3407777447321 +2024-10-15 08:00:00,4,35.86113967041213,20.19818384583482,68.65237399469929 +2024-10-15 09:00:00,4,13.972003061660622,23.146905042128804,53.71715269562502 +2024-10-15 10:00:00,4,27.13481116411725,20.734262847236984,44.06043485698613 +2024-10-15 11:00:00,4,34.091574945286716,19.487786910887685,52.526431639240215 +2024-10-15 12:00:00,4,26.27756286149532,25.353701060487307,54.49845179473682 +2024-10-15 13:00:00,4,15.189207351710404,25.762952867252153,56.32916277734966 +2024-10-15 14:00:00,4,38.97683831867187,19.9028893159472,46.62631490076464 +2024-10-15 15:00:00,4,8.74105181239637,25.880966887746883,60.53050863297209 +2024-10-15 16:00:00,4,26.295968949452792,25.285716546288086,38.640500611018155 +2024-10-15 17:00:00,4,38.14305590235813,26.283728649655473,48.02144503553127 +2024-10-15 18:00:00,4,37.25948706299582,26.358188146418136,57.778195134820024 +2024-10-15 19:00:00,4,17.017212089606858,21.918328923259814,47.36521884954199 +2024-10-15 20:00:00,4,18.027612257112544,25.57664911893816,68.17007926255641 +2024-10-15 21:00:00,4,40.16659424859459,26.45490899041768,42.55314413125264 +2024-10-15 22:00:00,4,20.968922402369106,20.12890599680442,41.09232842500988 +2024-10-15 23:00:00,4,18.354819006369926,22.34014785824624,76.28075631012067 +2024-10-16 00:00:00,4,23.575866066376843,26.39436121086035,57.446997785316434 +2024-10-16 01:00:00,4,19.177555961947547,24.23309469497831,60.05195247671151 +2024-10-16 02:00:00,4,37.55619769653828,18.81747308598421,54.94670115790175 +2024-10-16 03:00:00,4,30.60262562553443,22.962132737039806,57.4922038756056 +2024-10-16 04:00:00,4,18.2647753692602,13.986275869547915,65.17614484654406 +2024-10-16 05:00:00,4,25.906172817826807,22.712423102859315,44.67221324768575 +2024-10-16 06:00:00,4,31.896311016400404,25.141601777049797,73.06986054873666 +2024-10-16 07:00:00,4,24.153751924928912,18.44864149754575,51.547028371308485 +2024-10-16 08:00:00,4,40.727023371743655,24.61884780082389,64.69343809255531 +2024-10-16 09:00:00,4,17.478510811944574,21.748376417328224,65.25384516858739 +2024-10-16 10:00:00,4,30.276317483048054,21.539545317738725,60.245306047735724 +2024-10-16 11:00:00,4,23.551583307381417,22.431410152071333,62.16813969396148 +2024-10-16 12:00:00,4,27.327902535467498,21.65086947987132,57.3980818582341 +2024-10-16 13:00:00,4,20.339749990909695,21.32978531178555,54.16159224057472 +2024-10-16 14:00:00,4,10.424715141337384,20.400656345387638,65.2628272438706 +2024-10-16 15:00:00,4,23.10461836529545,19.853868244792146,52.11187183578585 +2024-10-16 16:00:00,4,17.201664731090045,25.819963110999357,46.74052972612566 +2024-10-16 17:00:00,4,25.476173066560605,24.49947575395943,50.344317042839904 +2024-10-16 18:00:00,4,20.014269802791578,28.63809863326987,52.721491359745215 +2024-10-16 19:00:00,4,38.57231554721508,26.521883901312975,61.77416217546716 +2024-10-16 20:00:00,4,26.016618781131115,27.03801880303486,35.38145762734439 +2024-10-16 21:00:00,4,18.257125589543715,29.835623793362327,81.15549150395847 +2024-10-16 22:00:00,4,11.651761829921053,32.42136148684107,64.98824534723875 +2024-10-16 23:00:00,4,14.55623438347565,25.372315167427512,62.15081927209313 +2024-10-17 00:00:00,4,25.32068462053044,19.215982894761428,48.252221141374896 +2024-10-17 01:00:00,4,21.418126622826293,25.61823227148013,52.249411549347194 +2024-10-17 02:00:00,4,25.996023191627884,21.111307385587903,44.2078300874293 +2024-10-17 03:00:00,4,40.19346443184784,20.86556045240596,44.987843026678945 +2024-10-17 04:00:00,4,12.987525667217355,25.643033194004076,65.9699610890191 +2024-10-17 05:00:00,4,23.133248775169317,18.173507989222372,44.96710761743035 +2024-10-17 06:00:00,4,43.88447580607861,22.11564167425634,56.02800594216451 +2024-10-17 07:00:00,4,24.60264930171319,22.580677465833748,45.63720056497766 +2024-10-17 08:00:00,4,33.05066857194099,27.587717803532907,59.51776876327861 +2024-10-17 09:00:00,4,32.16671897349884,21.422527014399897,67.38469563562234 +2024-10-17 10:00:00,4,23.43841701626667,27.075771590390534,64.28398490069048 +2024-10-17 11:00:00,4,25.876076040935704,26.594255232949862,55.19122970191048 +2024-10-17 12:00:00,4,26.908649283018978,24.47400535996475,62.210873498835724 +2024-10-17 13:00:00,4,21.421330020672464,25.273208520748085,63.42419239548308 +2024-10-17 14:00:00,4,52.76778280152429,27.310682773740485,51.09453328761696 +2024-10-17 15:00:00,4,22.682121117347428,19.296297407682918,57.73721887001657 +2024-10-17 16:00:00,4,27.069717487729047,23.54075781407717,47.09498729194023 +2024-10-17 17:00:00,4,0.5419054198970485,26.315250127487516,59.76546150889085 +2024-10-17 18:00:00,4,31.041377903184866,26.694408563727993,54.115934914742446 +2024-10-17 19:00:00,4,18.4956050701844,26.726004167720326,42.7561403842599 +2024-10-17 20:00:00,4,33.05782510866519,26.9154053980211,47.7058012828885 +2024-10-17 21:00:00,4,30.710813078068675,23.19262257475171,52.232699421051734 +2024-10-17 22:00:00,4,37.7398228842361,28.91266254155941,42.1684683078167 +2024-10-17 23:00:00,4,17.5453467665551,18.481932462192134,63.49900738270382 +2024-10-18 00:00:00,4,26.244871721312137,23.677748258827556,52.82768488656599 +2024-10-18 01:00:00,4,29.363016998422356,27.01741790008272,51.19997387608349 +2024-10-18 02:00:00,4,31.294555936830825,20.099623407440625,59.769935122133965 +2024-10-18 03:00:00,4,29.310207810459048,26.00373782083146,48.79248298926647 +2024-10-18 04:00:00,4,29.22995706669445,25.688623590372522,58.01169139741085 +2024-10-18 05:00:00,4,17.27806182712567,26.55156965333257,50.01133362683619 +2024-10-18 06:00:00,4,39.49370426275714,18.92685154753033,49.49101802008734 +2024-10-18 07:00:00,4,29.927491598280515,23.868882815952936,70.28023483811532 +2024-10-18 08:00:00,4,21.874053839142423,24.43210222046083,53.11134142993928 +2024-10-18 09:00:00,4,20.68236336549819,24.39627767104509,72.68739677007218 +2024-10-18 10:00:00,4,16.574455049854095,26.02478851465173,46.851933703631026 +2024-10-18 11:00:00,4,17.761353255313384,21.952436796462766,68.63541312866786 +2024-10-18 12:00:00,4,16.54340744969717,24.1267517338943,59.15422644534467 +2024-10-18 13:00:00,4,12.41489376722312,24.31155764780895,58.9435356862794 +2024-10-18 14:00:00,4,15.629118508221094,23.167241758989974,57.37848542837039 +2024-10-18 15:00:00,4,25.40182569908643,23.6548621454447,56.72666868927544 +2024-10-18 16:00:00,4,19.976223270399245,18.500578322831657,51.56500318064465 +2024-10-18 17:00:00,4,24.768807327527178,29.491686305197668,52.24269901570493 +2024-10-18 18:00:00,4,16.43577869518944,19.976578616148863,58.209939808851495 +2024-10-18 19:00:00,4,21.65204030772978,26.464172772509315,52.86586881330979 +2024-10-18 20:00:00,4,29.39150856666016,27.642845567397067,53.833438468097754 +2024-10-18 21:00:00,4,32.99076534238052,22.139274535605125,58.6384684775219 +2024-10-18 22:00:00,4,27.610124337248497,27.544300455058377,32.50804555549912 +2024-10-18 23:00:00,4,32.276465801681375,24.437273695644024,55.086012929534704 +2024-10-19 00:00:00,4,27.721462861223195,22.444258015290114,38.82384888834379 +2024-10-19 01:00:00,4,35.00909916441002,20.262190782323692,52.307661323080254 +2024-10-19 02:00:00,4,28.152138528740068,23.826382907795352,70.5018027154264 +2024-10-19 03:00:00,4,31.698277955698188,26.012921599431927,63.12489801449915 +2024-10-19 04:00:00,4,63.76772775916023,24.98791977194758,46.52827058102122 +2024-10-19 05:00:00,4,32.19177434017985,24.51340225425762,53.82864450947256 +2024-10-19 06:00:00,4,1.9669492752907658,25.419201022811116,55.563411160043806 +2024-10-19 07:00:00,4,30.760010736205764,21.26272342327306,68.34752392665357 +2024-10-19 08:00:00,4,13.99996760305773,19.624826379634747,57.55299016934657 +2024-10-19 09:00:00,4,36.862120023728345,17.692733095728364,65.24472255599223 +2024-10-19 10:00:00,4,47.79061311020392,21.249160077781532,56.99378500551166 +2024-10-19 11:00:00,4,14.049415775500153,22.64804695121127,71.4009447702687 +2024-10-19 12:00:00,4,15.3393922392707,27.25869012677292,55.502259536397126 +2024-10-19 13:00:00,4,25.92943659653306,22.75422037477177,67.96488564428952 +2024-10-19 14:00:00,4,49.16230054342313,20.48266217224751,55.74077755074759 +2024-10-19 15:00:00,4,6.70136887459298,26.57307920749321,52.30102497345442 +2024-10-19 16:00:00,4,11.79148834087214,16.43114341338216,47.35471757875652 +2024-10-19 17:00:00,4,16.483074517471714,20.05403698455941,60.76851127369342 +2024-10-19 18:00:00,4,29.971758307820732,21.331813085026578,58.67021443578968 +2024-10-19 19:00:00,4,17.963664692391852,25.955551420679107,55.15801479873776 +2024-10-19 20:00:00,4,17.944687780661813,26.350325531383866,55.03246901544954 +2024-10-19 21:00:00,4,30.706183641759043,14.146277626422172,40.7698299840319 +2024-10-19 22:00:00,4,29.014502392123333,27.112151099870903,56.482370969515244 +2024-10-19 23:00:00,4,27.114714181808193,23.115677192713747,73.90425899284674 +2024-10-20 00:00:00,4,23.15324095838658,24.90996988007449,52.828794730051925 +2024-10-20 01:00:00,4,19.418394950404767,28.52188737482371,44.88710453240567 +2024-10-20 02:00:00,4,24.966371133759466,26.43166793331393,65.3040307339684 +2024-10-20 03:00:00,4,16.69343506143188,24.494661969552258,74.64000609225799 +2024-10-20 04:00:00,4,13.69114489290882,26.929523840745173,55.427745887759144 +2024-10-20 05:00:00,4,20.4037336975665,16.952542100142765,55.04767207201111 +2024-10-20 06:00:00,4,24.64800534994353,27.275467560882138,49.72139253197909 +2024-10-20 07:00:00,4,24.441133541769855,21.440575487599872,62.05927437452307 +2024-10-20 08:00:00,4,37.32658086215136,19.84786188662519,62.383174101975975 +2024-10-20 09:00:00,4,5.206567975258341,26.782124737907544,58.30228890550195 +2024-10-20 10:00:00,4,43.116000877505826,23.05330686452721,54.17445420928614 +2024-10-20 11:00:00,4,34.009913440745706,24.038389885201404,60.95873940252026 +2024-10-20 12:00:00,4,11.127514796610482,22.568799020728118,41.383705630823464 +2024-10-20 13:00:00,4,4.093260643090321,25.828231426058416,52.417413059398186 +2024-10-20 14:00:00,4,22.467151462890307,29.718798439293703,54.14571223235689 +2024-10-20 15:00:00,4,17.02711849843475,28.327085298464368,57.25922287438899 +2024-10-20 16:00:00,4,18.571496699959628,20.83925271149826,45.30406970130534 +2024-10-20 17:00:00,4,19.27623463793852,23.927830463547203,61.96352544428368 +2024-10-20 18:00:00,4,13.989175354363073,26.148724769698198,49.44358846665314 +2024-10-20 19:00:00,4,10.965765904924956,28.640553541501717,51.03994280170517 +2024-10-20 20:00:00,4,17.36749380486414,27.64407481685361,41.34172032378541 +2024-10-20 21:00:00,4,16.174352677991102,21.00755926609907,51.21305656262361 +2024-10-20 22:00:00,4,24.632072128583953,34.79285292109514,51.38045468853368 +2024-10-20 23:00:00,4,21.887074633732006,26.1365410663117,55.68456819574253 +2024-10-21 00:00:00,4,27.846321058544785,25.068264001899333,29.037060015175346 +2024-10-21 01:00:00,4,36.87311024755441,27.478947816259232,61.286022621258624 +2024-10-21 02:00:00,4,26.279536180780696,26.188091575955724,49.589673273760596 +2024-10-21 03:00:00,4,23.515172125050473,20.842756762020773,52.60197931856912 +2024-10-21 04:00:00,4,19.139672840613912,27.10880994433768,53.72256059532886 +2024-10-21 05:00:00,4,37.27560682747137,23.956436948332815,50.951559914506554 +2024-10-21 06:00:00,4,30.415225893324614,29.93384661464536,62.01642873640279 +2024-10-21 07:00:00,4,9.325070152205942,23.08169322393098,42.25315375838308 +2024-10-21 08:00:00,4,13.98894072723532,24.584514011726945,61.225269043045174 +2024-10-21 09:00:00,4,33.03275914744369,23.629027510332765,60.72259526807639 +2024-10-21 10:00:00,4,37.76817599081548,22.91418572433057,57.95574750711319 +2024-10-21 11:00:00,4,16.58419561879846,34.112515155722754,71.11729129188873 +2024-10-21 12:00:00,4,8.359466324676745,23.321540463204524,66.6738435313462 +2024-10-21 13:00:00,4,22.64517812946043,24.178107657139122,69.7926038852563 +2024-10-21 14:00:00,4,18.60592033210142,23.129028381576305,69.14195434273567 +2024-10-21 15:00:00,4,27.011098600965347,22.42186529978153,61.37431082265817 +2024-10-21 16:00:00,4,8.534414683099339,30.52278531818057,43.195613219336806 +2024-10-21 17:00:00,4,22.428052587468283,23.298113884009076,50.49945151958352 +2024-10-21 18:00:00,4,22.850707861712795,23.895423600036388,68.06188489243668 +2024-10-21 19:00:00,4,18.98916669478103,17.348458229747422,57.538369491736304 +2024-10-21 20:00:00,4,21.549769817905446,19.19630562992355,52.884630277236724 +2024-10-21 21:00:00,4,25.85462762031862,27.10349555058914,43.71725430903644 +2024-10-21 22:00:00,4,24.590139202520863,23.552012622975848,52.35851431890631 +2024-10-21 23:00:00,4,32.11946842344186,26.708515232502666,65.45395449686947 +2024-10-22 00:00:00,4,22.019532080140912,25.94002873306939,45.994326051207366 +2024-10-22 01:00:00,4,28.994514364175387,24.661272626384967,55.86975409503754 +2024-10-22 02:00:00,4,28.619528027950096,25.036449432714214,63.43757753995884 +2024-10-22 03:00:00,4,28.175062523623804,21.202478699891792,60.58466145749612 +2024-10-22 04:00:00,4,18.947257315410358,19.867650379901498,54.33156096438949 +2024-10-22 05:00:00,4,35.902160277521894,24.51769448750022,46.26062002126127 +2024-10-22 06:00:00,4,30.676017746727734,28.82901367720058,59.552001702225596 +2024-10-22 07:00:00,4,26.404973985970642,20.533598273102925,42.57365840341982 +2024-10-22 08:00:00,4,23.697355496193126,26.632193007420007,53.42715779624654 +2024-10-22 09:00:00,4,39.44661877591166,23.28651841551033,61.4794590027355 +2024-10-22 10:00:00,4,29.104016434308523,24.544845751013547,59.287947882011736 +2024-10-22 11:00:00,4,36.112742017866026,18.69210285011783,55.616000661723156 +2024-10-22 12:00:00,4,37.24260403147453,25.83054681492765,58.027332005172916 +2024-10-22 13:00:00,4,24.438674641753344,22.952495647312553,70.6783563448011 +2024-10-22 14:00:00,4,11.60603124153961,23.90392911405823,37.30465319237122 +2024-10-22 15:00:00,4,26.169195230194887,22.414923196462887,55.59109006074191 +2024-10-22 16:00:00,4,24.677146926769524,28.288568579415234,53.04792842384931 +2024-10-22 17:00:00,4,19.600084834764505,24.674923511764206,62.26330809642879 +2024-10-22 18:00:00,4,19.219621828584867,26.239920575050835,41.129992367131216 +2024-10-22 19:00:00,4,25.096334739579063,29.215688486770773,49.15765287386692 +2024-10-22 20:00:00,4,21.56811055994541,21.363657370561853,49.703028736151616 +2024-10-22 21:00:00,4,20.185846057353167,23.696561139696577,35.531000471995384 +2024-10-22 22:00:00,4,25.843332174353595,24.144466385618944,54.26626972291734 +2024-10-22 23:00:00,4,19.73485974198133,29.12110770040143,52.68671248792037 +2024-10-23 00:00:00,4,38.10118764780292,25.08231887092108,49.59174276267572 +2024-10-23 01:00:00,4,28.221458241140187,23.650793396603355,57.02032049778187 +2024-10-23 02:00:00,4,30.71563558596071,26.59665003794241,44.0911700749335 +2024-10-23 03:00:00,4,33.416064754457835,25.33081872694661,44.20185466448895 +2024-10-23 04:00:00,4,41.71923598646985,26.324898118529028,48.120545873758516 +2024-10-23 05:00:00,4,27.252538647028597,23.329385669139587,54.6052995417674 +2024-10-23 06:00:00,4,10.959568790624184,23.36189168218797,52.17923343381882 +2024-10-23 07:00:00,4,16.794295814235042,26.61371307163855,58.75429864578543 +2024-10-23 08:00:00,4,35.21489854662437,22.85824356195468,64.4406754053343 +2024-10-23 09:00:00,4,19.165221147240597,25.53330524773179,49.20725897627937 +2024-10-23 10:00:00,4,34.31916893610139,23.558835994259606,54.9670067945457 +2024-10-23 11:00:00,4,32.885117731629066,24.49724055944914,68.39715021440519 +2024-10-23 12:00:00,4,9.771334208342617,22.40381880756622,62.64259534323286 +2024-10-23 13:00:00,4,13.89418227354326,20.033924325808464,52.82643255662208 +2024-10-23 14:00:00,4,21.352267426284516,24.458823169565644,40.14139517002731 +2024-10-23 15:00:00,4,16.533291597956627,25.64378197230508,44.879473415698406 +2024-10-23 16:00:00,4,24.764749030261136,19.808846228886534,46.27236748126486 +2024-10-23 17:00:00,4,26.807282394461403,26.967352725653807,57.92628623023713 +2024-10-23 18:00:00,4,26.502638518732287,24.92374325036253,50.41947920109706 +2024-10-23 19:00:00,4,27.114685163078782,13.892643015994741,46.32617051429817 +2024-10-23 20:00:00,4,19.215058334367587,23.90227632111092,71.50059145456022 +2024-10-23 21:00:00,4,26.393408844923943,30.816074476409874,41.96654684214118 +2024-10-23 22:00:00,4,27.83438167192419,25.46007294150179,53.09906649822543 +2024-10-23 23:00:00,4,25.284208923757575,28.456199552963497,52.30424342681484 +2024-10-24 00:00:00,4,26.450418134422698,19.32973561978402,50.599998875360136 +2024-10-24 01:00:00,4,20.35001081763869,20.42783530177831,45.21349710642062 +2024-10-24 02:00:00,4,23.63093573463681,24.027390847192553,49.70515967472859 +2024-10-24 03:00:00,4,22.80926270380781,24.738921633696947,45.06001871050246 +2024-10-24 04:00:00,4,25.243376058888362,21.45051578079166,57.104117072712825 +2024-10-24 05:00:00,4,18.642864891722738,24.9267652828219,51.312952753205224 +2024-10-24 06:00:00,4,13.460905122230255,25.609479804505973,53.21388036039121 +2024-10-24 07:00:00,4,17.14704786085545,19.789782131964586,55.982593465767685 +2024-10-24 08:00:00,4,15.787148601916195,21.97776564743937,54.302408219730054 +2024-10-24 09:00:00,4,10.218415917002867,17.566082412272085,56.851606994773746 +2024-10-24 10:00:00,4,25.47790923290342,24.96904306074405,52.58344897728797 +2024-10-24 11:00:00,4,42.17207970739098,23.124932668088203,63.361619301035006 +2024-10-24 12:00:00,4,29.886709304730683,26.014751982321403,56.27571832480519 +2024-10-24 13:00:00,4,17.05073431270646,21.985930319209718,60.497752832323364 +2024-10-24 14:00:00,4,32.70647860344236,19.600760851741658,48.133026199545036 +2024-10-24 15:00:00,4,7.85555287802428,22.9647921365068,62.6174079205544 +2024-10-24 16:00:00,4,31.777518438945158,23.415980518458017,57.72895330176694 +2024-10-24 17:00:00,4,21.893575168152445,22.242658514392474,45.73935113197856 +2024-10-24 18:00:00,4,24.106822511072,28.894912366224155,56.076241902268926 +2024-10-24 19:00:00,4,22.094418858458553,22.187977068316048,57.151849836329156 +2024-10-24 20:00:00,4,29.48106071467171,23.92917871566559,45.00144223110892 +2024-10-24 21:00:00,4,17.485700489093666,24.785983209081646,41.27120792994197 +2024-10-24 22:00:00,4,24.453453245573968,26.128069786885103,58.56466011812541 +2024-10-24 23:00:00,4,37.17566396120279,25.129392553639313,52.20613568019956 +2024-10-25 00:00:00,4,26.862282050054937,21.820422783494607,56.61118502144926 +2024-10-25 01:00:00,4,31.233464051295925,19.32396717400775,47.51008603576348 +2024-10-25 02:00:00,4,32.14934968819178,22.16637981320218,51.51322434946135 +2024-10-25 03:00:00,4,45.688691163053676,21.998933285699575,68.82967940399365 +2024-10-25 04:00:00,4,29.55689126735364,25.87811879886338,47.077792191345644 +2024-10-25 05:00:00,4,26.119167571885615,28.727896077719535,52.6206505190348 +2024-10-25 06:00:00,4,12.175615113735754,20.606167040141536,54.57467650455125 +2024-10-25 07:00:00,4,39.113179788515744,20.367055996517202,60.88505040426619 +2024-10-25 08:00:00,4,21.234838448131136,20.026718659385836,59.18761883188931 +2024-10-25 09:00:00,4,37.47456985061899,22.070618320129558,42.079907490821796 +2024-10-25 10:00:00,4,19.110086494070174,21.414021616325233,62.78510802262333 +2024-10-25 11:00:00,4,16.097643836006526,25.015257194721077,55.33641236586213 +2024-10-25 12:00:00,4,32.179553971698496,22.815150910666294,46.926066433677065 +2024-10-25 13:00:00,4,21.174545074007707,25.151599266732877,63.181871996479686 +2024-10-25 14:00:00,4,29.943610317531366,15.036085011451267,37.998762159308626 +2024-10-25 15:00:00,4,11.952141677749882,28.826352203731076,51.562942007136876 +2024-10-25 16:00:00,4,29.847102983763776,17.65706304246293,62.67872547107291 +2024-10-25 17:00:00,4,21.9266321632835,29.29431653717204,39.3456181748989 +2024-10-25 18:00:00,4,19.99777053240986,30.74481581367067,55.910499177706946 +2024-10-25 19:00:00,4,22.641848641144023,25.98637835191548,33.94088081009962 +2024-10-25 20:00:00,4,22.8245042973951,24.550733494039108,46.71795185150074 +2024-10-25 21:00:00,4,31.693320376710442,28.45679163405598,49.56336800710464 +2024-10-25 22:00:00,4,38.2032131571805,29.2456236459431,48.20163441501872 +2024-10-25 23:00:00,4,23.976394082372636,34.27163717996846,56.10832985566869 +2024-10-26 00:00:00,4,18.440863594234557,23.762244427505156,47.52192750697283 +2024-10-26 01:00:00,4,25.34380866955809,26.605403068654894,66.33984095091347 +2024-10-26 02:00:00,4,30.798664657295415,20.58253953714833,61.46712458255197 +2024-10-26 03:00:00,4,34.28386613982251,19.415376279311058,58.68598373953065 +2024-10-26 04:00:00,4,26.575547892433832,19.201881558368665,52.57549686616242 +2024-10-26 05:00:00,4,22.25179019612421,25.075983597106266,66.32576191413237 +2024-10-26 06:00:00,4,14.233882820963197,16.770419889967926,47.77088777190054 +2024-10-26 07:00:00,4,12.64870806772648,27.654838404367844,72.55936131484842 +2024-10-26 08:00:00,4,25.48937384775351,17.666560804647034,58.07793067888081 +2024-10-26 09:00:00,4,22.075104867078313,20.97535921522322,61.37594762886027 +2024-10-26 10:00:00,4,18.257964264762226,25.759044302580158,50.606883142440516 +2024-10-26 11:00:00,4,11.851169552902096,20.373869399218677,51.89876515279581 +2024-10-26 12:00:00,4,12.838880179897993,19.8643524982447,48.418180433972125 +2024-10-26 13:00:00,4,2.336662665788598,23.2812509312688,63.49881970564238 +2024-10-26 14:00:00,4,6.50692005212224,22.171213831706048,52.91116960647769 +2024-10-26 15:00:00,4,10.770366994702453,26.506126583805464,57.119890847527266 +2024-10-26 16:00:00,4,11.169349620134383,24.075514956961772,31.727408830165377 +2024-10-26 17:00:00,4,9.534260339577301,25.967791079963504,41.25975296272501 +2024-10-26 18:00:00,4,20.34961116583103,24.334199366380055,63.17556143176961 +2024-10-26 19:00:00,4,16.55462263430724,30.191237337489373,69.04782585590607 +2024-10-26 20:00:00,4,31.538747189031458,31.61264566032881,63.704603757405835 +2024-10-26 21:00:00,4,24.160962459833737,24.03554474404211,66.98150568312279 +2024-10-26 22:00:00,4,37.78023418669314,30.016574036544768,52.48241264764549 +2024-10-26 23:00:00,4,17.580343343457717,24.215414298393444,41.0684981148723 +2024-10-27 00:00:00,4,26.23032203607526,26.1709787391015,77.15825163328853 +2024-10-27 01:00:00,4,18.93566880480936,23.397842719231008,37.78595368022299 +2024-10-27 02:00:00,4,18.350764383020646,25.329308008626427,38.98176760834023 +2024-10-27 03:00:00,4,28.327621151426253,23.62469927238981,62.12179163540093 +2024-10-27 04:00:00,4,24.461741609340557,25.11575487420037,54.62410212376804 +2024-10-27 05:00:00,4,24.73861036327618,24.039460522907707,76.92858617482875 +2024-10-27 06:00:00,4,31.136532697816616,22.019946382872007,58.46377492591795 +2024-10-27 07:00:00,4,5.773463821989402,26.268045673086394,50.44971937236242 +2024-10-27 08:00:00,4,34.87367364634795,28.184175714295073,60.042623886699914 +2024-10-27 09:00:00,4,21.590482330143754,10.256085539947305,50.66983741034177 +2024-10-27 10:00:00,4,22.94929316118383,23.53833807152809,59.49095284639762 +2024-10-27 11:00:00,4,18.611090933511335,20.373097524454455,55.22564986439464 +2024-10-27 12:00:00,4,12.919677051775711,21.944920825785168,59.209934043609216 +2024-10-27 13:00:00,4,20.13554946999349,26.049793481819695,53.73156484191706 +2024-10-27 14:00:00,4,11.858816782612253,21.520793935588078,56.46297287307033 +2024-10-27 15:00:00,4,34.38522792039902,27.865200991845132,52.008871694299614 +2024-10-27 16:00:00,4,31.97897022123799,26.849496345886084,66.66232954454972 +2024-10-27 17:00:00,4,24.584178176412053,27.866852461279596,61.57371004549755 +2024-10-27 18:00:00,4,26.758192619423532,26.276280810592283,60.54874812944725 +2024-10-27 19:00:00,4,32.53141529894775,23.895278221390797,50.361515841112485 +2024-10-27 20:00:00,4,21.733742173102733,21.807624464005585,77.2447565567616 +2024-10-27 21:00:00,4,28.02934456054671,24.28717574266352,57.84809025731298 +2024-10-27 22:00:00,4,20.420799892508324,28.220112826399518,46.32821041941171 +2024-10-27 23:00:00,4,39.39481699655607,22.578291606363134,48.325436653052776 +2024-10-28 00:00:00,4,22.499652562125547,21.484252062401797,46.138720082878756 +2024-10-28 01:00:00,4,17.169888137670057,23.416891902354276,60.69284197105792 +2024-10-28 02:00:00,4,33.87186019739841,26.705667310203495,68.52848226784509 +2024-10-28 03:00:00,4,16.452039599880102,23.219764710074372,54.50808445242391 +2024-10-28 04:00:00,4,18.758305899506873,18.456438175226186,46.56299382069825 +2024-10-28 05:00:00,4,48.5051454412064,21.154107690975227,44.369085950799644 +2024-10-28 06:00:00,4,32.267489974457206,21.896755787577533,63.82682266847398 +2024-10-28 07:00:00,4,34.35968665873948,20.3074390774038,54.05851371086585 +2024-10-28 08:00:00,4,18.832758782187486,28.761847617263445,62.39148323387586 +2024-10-28 09:00:00,4,34.3961237657582,25.55922956775728,50.16564398396006 +2024-10-28 10:00:00,4,25.011856892830405,23.399065340123165,51.2730873818047 +2024-10-28 11:00:00,4,28.76165621024872,26.469313382985888,41.99909587864053 +2024-10-28 12:00:00,4,38.759477547950254,23.608420291731765,59.60648768958693 +2024-10-28 13:00:00,4,28.784302607476068,19.185320633293674,46.511413041186664 +2024-10-28 14:00:00,4,26.034931392561695,20.355765229471775,49.28423606339636 +2024-10-28 15:00:00,4,29.46560070258591,23.550846371306122,61.148886181515074 +2024-10-28 16:00:00,4,17.950538196780812,21.169819796556926,42.78849076048164 +2024-10-28 17:00:00,4,19.9293709142146,22.436174517588416,62.504921165105095 +2024-10-28 18:00:00,4,25.100732605011498,32.462311043218264,60.95391845246768 +2024-10-28 19:00:00,4,19.891869000531408,22.74229048331902,61.36625091648212 +2024-10-28 20:00:00,4,22.696336438867696,22.31346250241372,74.55617074196013 +2024-10-28 21:00:00,4,10.98671274437647,25.818299660224106,47.479735374496535 +2024-10-28 22:00:00,4,23.472168123658356,24.01231029961479,43.99220938651484 +2024-10-28 23:00:00,4,24.37495229879592,26.360357868053818,63.348523599861196 +2024-10-29 00:00:00,4,10.165112529001853,24.26793177937,62.59100554909212 +2024-10-29 01:00:00,4,12.08328813483573,22.218581480156534,69.0773280794273 +2024-10-29 02:00:00,4,41.39280655840922,24.72186340645888,48.63402347571097 +2024-10-29 03:00:00,4,18.722370387378987,26.319509987952554,68.35068988858129 +2024-10-29 04:00:00,4,34.63108010412314,21.195134444380198,64.45646667371192 +2024-10-29 05:00:00,4,24.823757739033905,26.54294348082976,43.251793591152335 +2024-10-29 06:00:00,4,24.65759784900397,26.19854782970313,55.98290598984948 +2024-10-29 07:00:00,4,21.416640373423757,22.870782472211715,67.60708775103191 +2024-10-29 08:00:00,4,25.75018499198041,16.16168488464669,48.61513977881067 +2024-10-29 09:00:00,4,36.53328743878971,28.251856497795906,65.53515627987984 +2024-10-29 10:00:00,4,26.255687378683586,25.625404826180954,66.68228518386954 +2024-10-29 11:00:00,4,20.353014696529577,19.2962019809957,51.93614150309863 +2024-10-29 12:00:00,4,33.09712747278691,23.116300292190836,66.44136619532652 +2024-10-29 13:00:00,4,25.965156560214137,21.569688538903147,60.57603455352934 +2024-10-29 14:00:00,4,13.350207334538204,19.73221668894591,59.657792236046724 +2024-10-29 15:00:00,4,7.057781782885602,22.524349410827988,55.46732088230797 +2024-10-29 16:00:00,4,11.5032586824926,25.353551407100746,61.91724989544474 +2024-10-29 17:00:00,4,20.075977413622347,21.62470158010887,55.35623554043562 +2024-10-29 18:00:00,4,28.640340307109785,24.43477945029796,37.80494398629694 +2024-10-29 19:00:00,4,19.86230264538769,17.939229642056315,51.80776668352057 +2024-10-29 20:00:00,4,13.571439074167928,27.63919290963762,60.95826932219287 +2024-10-29 21:00:00,4,29.893622404847562,28.66938737911877,60.97826619811946 +2024-10-29 22:00:00,4,36.12634582266032,24.072080411733225,48.8884783258455 +2024-10-29 23:00:00,4,23.49721480620109,25.614588142863937,58.0274034679088 +2024-10-30 00:00:00,4,43.827507201770445,26.182058659328924,53.81859775788521 +2024-10-30 01:00:00,4,27.199113780627183,29.646020847987007,41.55435953804889 +2024-10-30 02:00:00,4,3.549896700162229,26.302812952568534,52.654557365081374 +2024-10-30 03:00:00,4,41.319297515178526,15.741026401690966,64.52850208238539 +2024-10-30 04:00:00,4,39.995032163743915,24.77456773913269,53.42350463222819 +2024-10-30 05:00:00,4,40.76000312273078,20.797364536862453,69.09816793349708 +2024-10-30 06:00:00,4,35.482653896235036,26.159007237562417,60.062303613676775 +2024-10-30 07:00:00,4,18.375210138363023,25.68054686682475,61.002385362528784 +2024-10-30 08:00:00,4,27.64086308404151,27.671447661592453,56.641962428447464 +2024-10-30 09:00:00,4,22.609962043070674,23.193587735880474,55.845128664207834 +2024-10-30 10:00:00,4,15.740899026926655,24.717697535872972,55.85218420612062 +2024-10-30 11:00:00,4,40.38983233279089,21.171040118537835,60.0910819636623 +2024-10-30 12:00:00,4,32.34499997305666,23.631944602783108,49.112229935878844 +2024-10-30 13:00:00,4,23.394755862028365,20.50065394782276,54.535720013363246 +2024-10-30 14:00:00,4,26.88197386679519,19.42286375541202,63.061791192932475 +2024-10-30 15:00:00,4,26.7065701570202,22.43545287772689,47.07845000304423 +2024-10-30 16:00:00,4,15.027639886400788,23.930455231177778,64.1773390490546 +2024-10-30 17:00:00,4,13.564661607324995,22.026715060480566,63.8174335218572 +2024-10-30 18:00:00,4,25.747692821872022,24.71864148752635,49.20833644239935 +2024-10-30 19:00:00,4,13.829987066313892,22.805586575283826,51.42628634580137 +2024-10-30 20:00:00,4,34.28520085848618,18.480501154465227,43.907361557629585 +2024-10-30 21:00:00,4,17.45754698292972,25.19353894042616,72.24522308731406 +2024-10-30 22:00:00,4,23.23838622628368,25.775422710199244,63.06250345449507 +2024-10-30 23:00:00,4,34.465173170126974,24.83813648351714,62.58755869808887 +2024-10-31 00:00:00,4,29.57869447042532,23.174166857171166,71.11739948517831 +2024-10-31 01:00:00,4,20.413791715125743,30.130958904815845,51.44520504084578 +2024-10-31 02:00:00,4,22.699025475522106,29.208373018819564,45.31268448107955 +2024-10-31 03:00:00,4,34.700701844718104,23.401444956326596,66.25946898585637 +2024-10-31 04:00:00,4,22.14802538881816,26.323448103226962,50.90987109383808 +2024-10-31 05:00:00,4,13.371028574381807,22.84645690489068,56.10195119157069 +2024-10-31 06:00:00,4,20.06442967900064,24.204251665795617,50.49920169409149 +2024-10-31 07:00:00,4,27.93366616800574,19.017042072849442,73.05888279936224 +2024-10-31 08:00:00,4,15.75885267473112,18.625206002888653,56.124141334303985 +2024-10-31 09:00:00,4,29.63533097926061,22.318539253401273,76.49059556757261 +2024-10-31 10:00:00,4,19.241322980184858,18.412605541760108,51.74801182299874 +2024-10-31 11:00:00,4,21.09118341703502,26.516989295344935,56.274439198136605 +2024-10-31 12:00:00,4,25.513332546675766,19.615963525289402,53.16889570661054 +2024-10-31 13:00:00,4,22.546642822131865,24.39818064468568,51.41492471974562 +2024-10-31 14:00:00,4,24.391432405770114,19.55307512579524,57.153707109334704 +2024-10-31 15:00:00,4,17.87086265036019,25.26068813139442,51.26765429527469 +2024-10-31 16:00:00,4,26.860309804135216,27.559729536612167,70.30012924046707 +2024-10-31 17:00:00,4,24.9760082406114,26.854141320061636,62.41759702331511 +2024-10-31 18:00:00,4,18.957297702916918,30.900946168521983,56.942011756769375 +2024-10-31 19:00:00,4,15.493726101957986,19.976065013188165,64.98390529512623 +2024-10-31 20:00:00,4,5.533577884282568,21.28715055800941,48.20867216496023 +2024-10-31 21:00:00,4,33.34759920655176,26.664235956242006,57.916357704411375 +2024-10-31 22:00:00,4,25.82877542627785,26.761120821447015,60.22744766536874 +2024-10-31 23:00:00,4,17.997312222023265,27.313147420041787,67.53379828539961 +2024-11-01 00:00:00,4,22.721941691581875,23.247416873217542,68.75622868747783 +2024-11-01 01:00:00,4,37.12376682552983,19.113558293086985,52.91851751582487 +2024-11-01 02:00:00,4,22.533795745092004,21.88262717669827,68.25548491404987 +2024-11-01 03:00:00,4,21.5016312441053,20.393927002304384,51.812910129020125 +2024-11-01 04:00:00,4,40.27120388981126,21.179776711053957,58.75759786039494 +2024-11-01 05:00:00,4,7.914806505545396,24.619113815890167,48.500716824570276 +2024-11-01 06:00:00,4,16.921894047443118,24.556009461431515,60.87101157261524 +2024-11-01 07:00:00,4,20.833852574451353,26.80097425556626,69.36426268565798 +2024-11-01 08:00:00,4,39.788871905736826,19.867773186788142,53.36912085099499 +2024-11-01 09:00:00,4,23.65753483852845,28.212764378024907,43.77937370965326 +2024-11-01 10:00:00,4,26.84918686748788,26.400175851136293,68.28681018972587 +2024-11-01 11:00:00,4,26.132359484792612,21.719405817951333,58.29750898062332 +2024-11-01 12:00:00,4,43.19182540178993,19.70540407443085,48.681867111922095 +2024-11-01 13:00:00,4,12.145207926871228,23.888603584904185,54.33507196557128 +2024-11-01 14:00:00,4,12.841750559365293,26.67615113607392,47.2197086926078 +2024-11-01 15:00:00,4,20.71398338345638,24.492818543424264,47.525756741188665 +2024-11-01 16:00:00,4,12.702423536262282,27.908601348531654,31.764992752514708 +2024-11-01 17:00:00,4,16.940395518532192,23.45027925215497,53.60897280918854 +2024-11-01 18:00:00,4,46.70546717315975,26.162803744869336,50.157635115823844 +2024-11-01 19:00:00,4,25.150615669068255,30.206732958145004,38.86400965829142 +2024-11-01 20:00:00,4,5.992596052597406,23.101293109046146,32.35541103345482 +2024-11-01 21:00:00,4,30.600636237935227,22.103686904062478,57.38553857739906 +2024-11-01 22:00:00,4,18.29937739415245,25.408267230908535,58.885908747044574 +2024-11-01 23:00:00,4,21.934956531950757,25.378923302679894,43.021995717533855 +2024-11-02 00:00:00,4,32.81873810073393,22.695409675661864,48.18074868418476 +2024-11-02 01:00:00,4,33.74123311230753,20.90550873877503,59.70954061692365 +2024-11-02 02:00:00,4,22.210898759947085,26.053750436049533,52.251087432884106 +2024-11-02 03:00:00,4,43.865467775743426,26.349061557899393,53.592982552703276 +2024-11-02 04:00:00,4,31.36306815069787,22.26887125422318,64.11232471501958 +2024-08-04 05:00:00,5,19.13961494587332,23.307744669123537,39.56923344754625 +2024-08-04 06:00:00,5,29.76567309742262,20.62279305448877,60.62315081537717 +2024-08-04 07:00:00,5,31.663256433040615,23.056723288733853,57.09176488741136 +2024-08-04 08:00:00,5,19.307227583426215,18.755128683143646,51.84276754282599 +2024-08-04 09:00:00,5,19.06949186689727,27.23347729969134,41.7282025785955 +2024-08-04 10:00:00,5,15.639604192314513,28.5572417165638,61.571409596133456 +2024-08-04 11:00:00,5,28.151718081766887,27.931149255173192,36.90348093440366 +2024-08-04 12:00:00,5,20.31309686460837,26.089532790615507,54.76218211523415 +2024-08-04 13:00:00,5,23.57286655024669,25.03580440622213,66.20819216675471 +2024-08-04 14:00:00,5,21.718416967556685,25.625881073094828,57.55635923697245 +2024-08-04 15:00:00,5,14.603118592421588,17.479475024515466,64.43584067658381 +2024-08-04 16:00:00,5,25.889022653661833,17.598457969958748,53.30717972201106 +2024-08-04 17:00:00,5,29.569994550987538,26.963470076760746,60.67279839997746 +2024-08-04 18:00:00,5,19.679198411721128,25.070213700341817,62.37270359348666 +2024-08-04 19:00:00,5,16.542127821612564,18.13310870912168,51.35140658928083 +2024-08-04 20:00:00,5,41.97186657114884,25.04365403319893,61.21097339063024 +2024-08-04 21:00:00,5,20.889698007584034,26.986034661723572,49.998319512074865 +2024-08-04 22:00:00,5,20.320819316256692,25.37543611173354,49.711689980586506 +2024-08-04 23:00:00,5,30.675248770639488,22.45619572308162,59.85422589870122 +2024-08-05 00:00:00,5,6.974119994792863,21.98504899996379,47.40684680615247 +2024-08-05 01:00:00,5,37.99516270661606,24.048928757297563,58.373992576290334 +2024-08-05 02:00:00,5,18.18733993355155,20.986068167955874,48.33603963496555 +2024-08-05 03:00:00,5,30.12280916671049,25.255791771461503,59.67814144992882 +2024-08-05 04:00:00,5,25.42308346354032,22.62770203591428,54.627168288231864 +2024-08-05 05:00:00,5,4.098820813063615,18.52822326548825,41.295320814966495 +2024-08-05 06:00:00,5,28.461462003177505,23.56098926819573,64.28214203520321 +2024-08-05 07:00:00,5,37.22948663230856,23.518322337395542,49.59029125459098 +2024-08-05 08:00:00,5,13.536043150746528,20.274222914055283,50.3858483254013 +2024-08-05 09:00:00,5,23.405069563432075,24.453548753269164,44.275747514671046 +2024-08-05 10:00:00,5,37.12880756027636,17.166025664457482,48.571980741719855 +2024-08-05 11:00:00,5,28.972046653480586,26.540165106203673,64.64101016298497 +2024-08-05 12:00:00,5,34.33898355823099,29.95383946626772,53.01830693250813 +2024-08-05 13:00:00,5,19.313212817768992,30.710491814490037,57.09346969104929 +2024-08-05 14:00:00,5,23.307125805182437,31.010221711092065,69.10585263806853 +2024-08-05 15:00:00,5,23.94031137097999,26.056963498529125,70.68130112011505 +2024-08-05 16:00:00,5,15.978612801893194,22.833184162365104,56.44276460934498 +2024-08-05 17:00:00,5,32.96305746759701,25.01001526692221,56.70713351365709 +2024-08-05 18:00:00,5,13.425225858375901,24.03752662738218,60.68180298482009 +2024-08-05 19:00:00,5,29.50551400649783,26.4754667070623,61.469894381203794 +2024-08-05 20:00:00,5,32.955899069385374,24.38221748738004,56.149125176785816 +2024-08-05 21:00:00,5,20.624802642214078,19.516696637470293,58.79792208902346 +2024-08-05 22:00:00,5,13.787976503387476,25.09225898711332,48.38605442596892 +2024-08-05 23:00:00,5,19.195499098837914,24.42931828502627,50.87606647677054 +2024-08-06 00:00:00,5,10.971354545915498,24.14280144213511,59.312128181551145 +2024-08-06 01:00:00,5,20.793762985691078,23.817027691947565,58.51775430862351 +2024-08-06 02:00:00,5,22.542222402469136,26.370479820487112,46.09431731970216 +2024-08-06 03:00:00,5,7.866744491851373,19.01050718601011,58.921712239143176 +2024-08-06 04:00:00,5,9.21251678045249,20.700748982327525,57.56838365396733 +2024-08-06 05:00:00,5,26.121954140262407,26.818274628570386,56.57816453428696 +2024-08-06 06:00:00,5,5.714693897830806,19.71359340842388,49.940808334073765 +2024-08-06 07:00:00,5,16.033800891085917,22.83835489915847,49.96971776188252 +2024-08-06 08:00:00,5,19.381751367910816,22.979599391738937,48.10410619103625 +2024-08-06 09:00:00,5,32.465315699603714,24.190960637060964,55.980044851778864 +2024-08-06 10:00:00,5,21.24104225678156,28.484426134525783,48.68804045785693 +2024-08-06 11:00:00,5,30.116417855564023,21.785592236050256,44.37976117649631 +2024-08-06 12:00:00,5,26.67027136226371,28.770910481719227,42.99385056587646 +2024-08-06 13:00:00,5,21.367022074013434,20.40375391959038,43.37166515496755 +2024-08-06 14:00:00,5,35.92540350095534,30.544067183375965,38.9827566153306 +2024-08-06 15:00:00,5,35.64207590135991,28.4753587723997,62.16502723505443 +2024-08-06 16:00:00,5,18.42827612363237,23.130241183358844,62.660213661993936 +2024-08-06 17:00:00,5,20.230656682360284,28.112640874520608,49.826189028200496 +2024-08-06 18:00:00,5,14.094439469951837,22.970013534580648,55.946217398992296 +2024-08-06 19:00:00,5,29.15868762664343,27.78165545505822,56.69429906416749 +2024-08-06 20:00:00,5,17.75568566395953,24.259394840303678,57.468245003967944 +2024-08-06 21:00:00,5,37.9529794109368,27.113170319470232,49.602476780605706 +2024-08-06 22:00:00,5,19.34033608796478,24.31716380083698,45.083964150785484 +2024-08-06 23:00:00,5,6.5775243351988095,31.761934282408085,52.68956418320277 +2024-08-07 00:00:00,5,14.865506064831838,21.202740490746404,46.85416163399061 +2024-08-07 01:00:00,5,0.0,21.300682511772948,67.67960636996227 +2024-08-07 02:00:00,5,7.870359657219042,24.03481504040869,54.754414926301116 +2024-08-07 03:00:00,5,8.629219911831449,20.770252205076883,35.840846047006345 +2024-08-07 04:00:00,5,28.776884484597957,21.556445827423452,47.577756019199114 +2024-08-07 05:00:00,5,28.21407036945226,26.20333068465379,44.4159584982135 +2024-08-07 06:00:00,5,44.147332977527526,23.515479131757296,43.17869220019124 +2024-08-07 07:00:00,5,24.789670506317865,27.76990050589327,50.726542884582464 +2024-08-07 08:00:00,5,10.771145401874668,26.926353899298995,62.07714310648409 +2024-08-07 09:00:00,5,31.962987685366897,30.148069374927687,47.71825220542827 +2024-08-07 10:00:00,5,31.8247809430136,20.648303321156746,25.96306079642717 +2024-08-07 11:00:00,5,18.00664244906128,20.696020526580423,45.974611467964436 +2024-08-07 12:00:00,5,30.260007135649055,21.327670039910803,40.293390004705124 +2024-08-07 13:00:00,5,22.87025493576446,25.304562029343806,60.98138759565824 +2024-08-07 14:00:00,5,31.84492349251796,25.13155353365719,56.6765891889631 +2024-08-07 15:00:00,5,32.04098753636512,26.276960286013885,50.56242655011627 +2024-08-07 16:00:00,5,22.65069132782191,21.002312957037127,44.07894390086236 +2024-08-07 17:00:00,5,17.416286754710203,21.925298263903546,41.903986622016404 +2024-08-07 18:00:00,5,29.290711109145224,23.508349280966275,53.25301618402361 +2024-08-07 19:00:00,5,20.586239199663503,25.132107949097357,54.96035891839232 +2024-08-07 20:00:00,5,34.28651612624499,27.736448166860633,64.1926000724474 +2024-08-07 21:00:00,5,28.659871706620514,24.213223410180028,68.78042447443481 +2024-08-07 22:00:00,5,39.28209425709139,29.252855145157532,47.47258252153517 +2024-08-07 23:00:00,5,21.407725239292127,28.908413273289426,56.37161744477112 +2024-08-08 00:00:00,5,24.04731799427383,22.00055827061649,52.37825854157484 +2024-08-08 01:00:00,5,22.55044549048947,20.365051637927174,52.084824807256595 +2024-08-08 02:00:00,5,16.881704443492506,22.143704723785287,43.662563054148976 +2024-08-08 03:00:00,5,27.772005321470864,25.214453059748116,64.54571113134485 +2024-08-08 04:00:00,5,17.493029714570817,21.415755531868488,52.98967084058523 +2024-08-08 05:00:00,5,31.130011788354608,19.456168159515926,48.259883879010786 +2024-08-08 06:00:00,5,22.957174567848647,19.525513973089446,39.5355118350122 +2024-08-08 07:00:00,5,4.877540158665777,19.348417243747594,47.69720753405932 +2024-08-08 08:00:00,5,23.25808568192546,28.629029748240804,48.41163483666611 +2024-08-08 09:00:00,5,33.93824652020683,25.962801603603896,31.851506164149274 +2024-08-08 10:00:00,5,31.522899831362647,18.688681943739063,60.38029925158302 +2024-08-08 11:00:00,5,24.709027057173714,17.692919225262425,46.365509910502034 +2024-08-08 12:00:00,5,15.007097772865864,23.511239816769738,65.77879399208005 +2024-08-08 13:00:00,5,23.364902841445854,25.08267539788538,51.17963509414965 +2024-08-08 14:00:00,5,36.23632627264156,24.711879651169244,51.24346873825389 +2024-08-08 15:00:00,5,19.077077921408836,28.417504812108913,61.74051479193699 +2024-08-08 16:00:00,5,33.367286224646996,28.239292985094004,56.36541702024703 +2024-08-08 17:00:00,5,21.952771764389052,22.20915088776335,61.567725766778736 +2024-08-08 18:00:00,5,21.581665658133172,24.245586982480887,59.54707783629098 +2024-08-08 19:00:00,5,25.91509484332579,18.96158536212267,67.55545999045873 +2024-08-08 20:00:00,5,18.80147509999657,29.147034909349802,45.572333919342356 +2024-08-08 21:00:00,5,37.722738010182695,24.002020420012386,66.85061056007892 +2024-08-08 22:00:00,5,28.882450616113925,27.72597123989502,44.333818445108896 +2024-08-08 23:00:00,5,21.099194201816097,21.463210891282962,61.49255864953997 +2024-08-09 00:00:00,5,24.87378797149776,23.428392615433864,47.998297826326514 +2024-08-09 01:00:00,5,14.820132273322356,22.77546098391716,52.57241345698718 +2024-08-09 02:00:00,5,27.474204365092213,25.152677900348873,56.0729993374195 +2024-08-09 03:00:00,5,21.368996221773653,24.46719275557941,54.08636295900715 +2024-08-09 04:00:00,5,27.111673660544625,23.686191299339743,40.816325690271704 +2024-08-09 05:00:00,5,18.15965634102043,28.361191985257918,49.23139768635973 +2024-08-09 06:00:00,5,2.1314301981615635,28.099623050585517,65.0082360231164 +2024-08-09 07:00:00,5,28.509859288284684,25.523653886233493,60.632897869836995 +2024-08-09 08:00:00,5,18.579796869040432,21.722958793126057,37.559978784513774 +2024-08-09 09:00:00,5,19.95212555975747,22.928854507382688,48.35794876467038 +2024-08-09 10:00:00,5,19.14368377069526,17.3081176657915,67.37038978489645 +2024-08-09 11:00:00,5,18.76102440356412,25.976155638154818,44.25195995080709 +2024-08-09 12:00:00,5,31.06647485246789,23.36706923580522,68.09554350423774 +2024-08-09 13:00:00,5,26.115794900051327,27.72897949157952,62.44908230060055 +2024-08-09 14:00:00,5,20.302863634881017,24.428022138577038,64.84830934079405 +2024-08-09 15:00:00,5,19.85547511763366,21.85555311224142,72.89909621594035 +2024-08-09 16:00:00,5,16.078818987359494,25.878459944311853,61.6417573273762 +2024-08-09 17:00:00,5,20.620382245882556,18.04288898318275,61.34530666927173 +2024-08-09 18:00:00,5,22.491220293028785,29.934709475831234,46.68651461058956 +2024-08-09 19:00:00,5,48.2113996218872,21.864872699765268,36.94039692231483 +2024-08-09 20:00:00,5,26.825299298022028,21.125013267198142,59.95050256634165 +2024-08-09 21:00:00,5,25.181858151718824,24.618241700344768,53.553527248745844 +2024-08-09 22:00:00,5,25.765715331052828,27.284354291553214,47.11233372428689 +2024-08-09 23:00:00,5,36.97731051593712,19.2848360809938,61.71286956306085 +2024-08-10 00:00:00,5,33.13193895212195,18.786971926424616,62.07778615122755 +2024-08-10 01:00:00,5,29.106883362922176,17.069291436705832,60.44589982559649 +2024-08-10 02:00:00,5,33.13747342644081,24.05353582404418,46.93951712789497 +2024-08-10 03:00:00,5,20.53058750620943,20.252128925166673,45.41064863778374 +2024-08-10 04:00:00,5,29.804416582158154,22.165356585168432,40.88419289825035 +2024-08-10 05:00:00,5,21.240355764130655,27.39178785305472,58.303602276836266 +2024-08-10 06:00:00,5,22.550491066411517,17.506192101564984,67.79668780771509 +2024-08-10 07:00:00,5,21.327703647902435,27.058901871063384,52.783675318174865 +2024-08-10 08:00:00,5,30.663571687833993,21.192547018925687,51.2684243258744 +2024-08-10 09:00:00,5,25.599533622364152,21.233132843516483,61.06607492758653 +2024-08-10 10:00:00,5,20.868450430468904,22.626193689174183,53.69229240973459 +2024-08-10 11:00:00,5,20.84400939579867,23.126665186820674,48.89531788651764 +2024-08-10 12:00:00,5,22.913695653135154,25.681851212559042,64.4894264040268 +2024-08-10 13:00:00,5,13.452231957796696,26.949275505286593,56.0370884288153 +2024-08-10 14:00:00,5,19.14564659597407,17.390090976352926,65.07129300333011 +2024-08-10 15:00:00,5,24.131391663444326,21.243487816527487,66.57392698974714 +2024-08-10 16:00:00,5,36.4615263270797,25.108476863752607,45.41576598578176 +2024-08-10 17:00:00,5,15.021217754346155,25.178093121712646,62.01625374679611 +2024-08-10 18:00:00,5,14.504813528293157,25.910086691172484,46.566945709745866 +2024-08-10 19:00:00,5,21.071647826503238,24.1975334873715,54.04976159544886 +2024-08-10 20:00:00,5,25.05651226607238,21.730032616345156,51.71572440544598 +2024-08-10 21:00:00,5,17.74789290510884,27.75540061131279,65.3141889821122 +2024-08-10 22:00:00,5,16.829497631910094,20.984959678098328,57.037349941351444 +2024-08-10 23:00:00,5,18.338783049023284,19.586701689012123,46.50623521983293 +2024-08-11 00:00:00,5,25.002747285369814,26.119621857085033,49.68301846280296 +2024-08-11 01:00:00,5,22.383219993073332,21.527117615913298,45.286672454064934 +2024-08-11 02:00:00,5,9.974282214607763,20.383323483313486,50.1202407592539 +2024-08-11 03:00:00,5,11.452798646116157,21.742187252646232,56.447025660536205 +2024-08-11 04:00:00,5,21.196352762853763,22.459179518838493,50.093966767215306 +2024-08-11 05:00:00,5,30.387937305989347,18.019319313151122,50.50323428869143 +2024-08-11 06:00:00,5,9.393569766525012,27.11132156906163,63.98754814494029 +2024-08-11 07:00:00,5,26.82998763168125,30.77917614996389,55.042943517811544 +2024-08-11 08:00:00,5,21.140616997025813,21.325851400652567,34.08455960591444 +2024-08-11 09:00:00,5,41.828637080054705,29.11510009380659,50.13610584837301 +2024-08-11 10:00:00,5,34.9214502086528,20.855915408496642,63.78573570526558 +2024-08-11 11:00:00,5,25.84440811883662,20.380302093746025,50.000108771103314 +2024-08-11 12:00:00,5,41.55055868098644,22.893272181218535,48.31105470749051 +2024-08-11 13:00:00,5,30.200114160788477,26.170840252795802,44.8068030620897 +2024-08-11 14:00:00,5,12.480173542644,17.78312783652926,51.080642077597695 +2024-08-11 15:00:00,5,26.894980635719236,29.469519733814728,57.83917255633499 +2024-08-11 16:00:00,5,28.779616176465787,28.15818341880975,67.14815829254786 +2024-08-11 17:00:00,5,3.351318144654055,23.990845022307393,57.55770660920263 +2024-08-11 18:00:00,5,31.55032920739275,27.06555999387749,52.51962220743984 +2024-08-11 19:00:00,5,32.728465434229314,26.54556928329191,51.904476958088495 +2024-08-11 20:00:00,5,25.24102463666715,24.06195470406215,67.00056300971822 +2024-08-11 21:00:00,5,20.616676171751656,22.831377560821817,61.98579462329582 +2024-08-11 22:00:00,5,25.22256048259679,26.829025570135116,56.133181345532 +2024-08-11 23:00:00,5,21.043377916615047,24.58006699268449,54.069821606598126 +2024-08-12 00:00:00,5,7.076818424129106,19.892154114850275,61.16579236791806 +2024-08-12 01:00:00,5,34.04838802801936,23.7478445469293,44.08603834159248 +2024-08-12 02:00:00,5,17.51435911323274,26.497814303235213,73.91669787483652 +2024-08-12 03:00:00,5,36.69718985050653,23.70365008262153,57.96760576625291 +2024-08-12 04:00:00,5,14.967152614471171,22.493673105826307,66.04413505980217 +2024-08-12 05:00:00,5,12.8871200098689,23.952049395145373,56.02930334337266 +2024-08-12 06:00:00,5,31.847994353448787,24.266434314584203,54.398194050711524 +2024-08-12 07:00:00,5,15.578382713553555,26.060164471682345,47.95708269927095 +2024-08-12 08:00:00,5,30.48084793949586,28.805296589895562,50.077798460224194 +2024-08-12 09:00:00,5,25.863652698840276,25.063257207329094,56.56160842612063 +2024-08-12 10:00:00,5,34.45654968914667,27.69632620823829,65.44463612433073 +2024-08-12 11:00:00,5,44.7282966279606,24.192069001019426,56.280446549115254 +2024-08-12 12:00:00,5,22.57056418125942,17.514949530669163,64.00123903306786 +2024-08-12 13:00:00,5,29.65416075813513,26.124593117062712,47.94704955564798 +2024-08-12 14:00:00,5,7.124072799711893,23.332855451210143,54.159971763732585 +2024-08-12 15:00:00,5,27.218998141199418,23.63393258041002,54.39475987893366 +2024-08-12 16:00:00,5,20.36932859512561,25.66591562608014,67.12990659579313 +2024-08-12 17:00:00,5,24.861109716549464,21.412438489645915,73.37602293150012 +2024-08-12 18:00:00,5,19.6973271018849,21.907570721934473,32.88862550317291 +2024-08-12 19:00:00,5,12.861076352608157,27.129235938436103,58.46582052780924 +2024-08-12 20:00:00,5,17.313184879217584,31.13195376465548,53.830247992084125 +2024-08-12 21:00:00,5,32.73101338984611,21.413420446534346,49.674420955529946 +2024-08-12 22:00:00,5,19.712098650144814,21.4306999195484,43.20717918552421 +2024-08-12 23:00:00,5,13.972801880518542,23.48501130528439,39.197199180620814 +2024-08-13 00:00:00,5,28.72199988793445,17.284266933406542,60.45826821231248 +2024-08-13 01:00:00,5,27.020767744887667,19.398134730144317,51.1126953562468 +2024-08-13 02:00:00,5,21.562980109661424,25.85813195914547,51.392716251662485 +2024-08-13 03:00:00,5,22.590268756817572,24.223258290533852,47.09039590205285 +2024-08-13 04:00:00,5,17.25467326589603,25.657609674379476,70.98731475148196 +2024-08-13 05:00:00,5,30.007099527926854,20.598726992209365,65.33406755558421 +2024-08-13 06:00:00,5,28.08936915864067,30.407634650994908,56.02984045933508 +2024-08-13 07:00:00,5,25.1816210152465,27.265935785128132,59.906161795501475 +2024-08-13 08:00:00,5,20.099547689525117,18.843940405826864,57.68371247612982 +2024-08-13 09:00:00,5,27.8938055736862,23.86988298211644,51.569183083372444 +2024-08-13 10:00:00,5,20.26116657837789,30.17423469907689,50.33585558405591 +2024-08-13 11:00:00,5,49.612354667977726,22.300867479920665,38.05345807989555 +2024-08-13 12:00:00,5,19.17138378312779,23.231024878481318,51.20451649483049 +2024-08-13 13:00:00,5,21.16419231592708,17.53575264711668,62.115763241659536 +2024-08-13 14:00:00,5,33.70411922370397,21.60450796118495,65.8414918402856 +2024-08-13 15:00:00,5,21.20267455895071,23.329309759173697,59.253994871472365 +2024-08-13 16:00:00,5,25.716678731358783,17.97414550336374,68.07831854097354 +2024-08-13 17:00:00,5,13.159108178028816,27.99149974874986,54.27703896088632 +2024-08-13 18:00:00,5,18.7199986057009,22.660520983208787,53.073820546860155 +2024-08-13 19:00:00,5,41.64486276999156,19.349367195987337,59.964434603656926 +2024-08-13 20:00:00,5,18.19828454930906,21.597569136884466,51.54219449155927 +2024-08-13 21:00:00,5,38.17795855999749,21.605938542336602,67.32795813911775 +2024-08-13 22:00:00,5,12.410587168994443,31.833865246583414,65.88818087872943 +2024-08-13 23:00:00,5,27.866290659802253,25.99465954556774,55.07630455017695 +2024-08-14 00:00:00,5,22.562497085339235,26.07408038849502,39.15352735597375 +2024-08-14 01:00:00,5,25.86936142310525,22.587700471581314,62.41166362241746 +2024-08-14 02:00:00,5,10.8055828559015,25.423715688841934,41.069608418585986 +2024-08-14 03:00:00,5,34.67128840904815,19.18965152020914,58.50266134130476 +2024-08-14 04:00:00,5,26.358634900470115,27.55819114651372,60.40189506931014 +2024-08-14 05:00:00,5,6.411177733003896,26.907067156886963,57.23643678253047 +2024-08-14 06:00:00,5,36.03258143339862,18.808334749634042,52.618746110527894 +2024-08-14 07:00:00,5,19.404255690334963,22.271722613158992,43.16194295587023 +2024-08-14 08:00:00,5,29.119307579081887,25.412359516126504,45.66616900195324 +2024-08-14 09:00:00,5,22.93555353981498,27.102558562733954,41.958653751485876 +2024-08-14 10:00:00,5,8.45340494184953,24.588415964676834,51.230229346087036 +2024-08-14 11:00:00,5,29.602662089319207,23.24434597940892,46.809403420390616 +2024-08-14 12:00:00,5,22.963636147441598,23.79336017975223,53.67363177573914 +2024-08-14 13:00:00,5,24.020317733188662,24.90288953127138,54.796883885140375 +2024-08-14 14:00:00,5,40.36499890593872,15.92398839775397,40.78574921785881 +2024-08-14 15:00:00,5,21.31367936238428,26.39236909539513,52.72730616643706 +2024-08-14 16:00:00,5,20.257673804537852,25.36146335292269,65.43801860667423 +2024-08-14 17:00:00,5,14.128111706342194,27.00465372479164,52.842803738811064 +2024-08-14 18:00:00,5,17.82448146873797,18.575381591244913,55.47962035784151 +2024-08-14 19:00:00,5,25.34441968812932,25.87732589240154,47.03040870472326 +2024-08-14 20:00:00,5,29.721694352798046,25.754891126651238,55.547462358108575 +2024-08-14 21:00:00,5,17.574934958942677,22.941194020863897,48.87144088370319 +2024-08-14 22:00:00,5,36.37789526942469,28.46570981101048,48.47108249143884 +2024-08-14 23:00:00,5,19.05386707048605,29.76187509328858,56.427934013468565 +2024-08-15 00:00:00,5,15.424697359522503,22.653765468976477,58.08671243132403 +2024-08-15 01:00:00,5,43.5088254952702,23.926958785088527,41.94375635651201 +2024-08-15 02:00:00,5,19.58299285317932,27.358301315626186,58.685478001348415 +2024-08-15 03:00:00,5,20.262775724818976,22.641158104752524,56.2589766604848 +2024-08-15 04:00:00,5,17.885055307431916,20.61192210027332,55.37968324573617 +2024-08-15 05:00:00,5,21.662803729631566,23.57039940126711,53.14203831868596 +2024-08-15 06:00:00,5,21.52296918505967,23.493862384182155,49.29063558399582 +2024-08-15 07:00:00,5,22.630558905930705,28.653899573757734,53.55401552022091 +2024-08-15 08:00:00,5,23.786470355973535,25.02412790331173,37.26212301627139 +2024-08-15 09:00:00,5,41.0911381036185,22.69398651251739,62.194652149969684 +2024-08-15 10:00:00,5,33.02757613616158,23.77099288636956,34.87963988661568 +2024-08-15 11:00:00,5,38.33459913074692,21.73493247884474,55.17817725019085 +2024-08-15 12:00:00,5,7.59275781766134,23.21840569974657,48.72282526720974 +2024-08-15 13:00:00,5,29.996432501830327,17.101145297973712,66.79668284622676 +2024-08-15 14:00:00,5,32.308410727219574,26.467369842250193,54.45393247781435 +2024-08-15 15:00:00,5,21.714579032645666,21.369859826347717,70.88718653714267 +2024-08-15 16:00:00,5,10.636218509518672,25.167833496001293,39.0626081164204 +2024-08-15 17:00:00,5,21.076000797216068,27.74304367055463,49.01693660865536 +2024-08-15 18:00:00,5,17.834915902793625,29.102137200933566,73.38845326064757 +2024-08-15 19:00:00,5,27.932408555046102,21.72818284851852,64.56369115777588 +2024-08-15 20:00:00,5,26.386976140618287,25.168243665917114,58.98945855028916 +2024-08-15 21:00:00,5,27.33724497377135,29.06121722963214,41.954852591975104 +2024-08-15 22:00:00,5,26.671165958924476,27.703648091818724,50.61453511367344 +2024-08-15 23:00:00,5,20.487310038617927,25.15223830017275,61.538698513896996 +2024-08-16 00:00:00,5,24.587290895178434,18.172337441297188,54.209585364449126 +2024-08-16 01:00:00,5,5.4943383142720545,19.88913489531319,52.71235669671376 +2024-08-16 02:00:00,5,14.777074657425446,17.89093196072132,42.723798461672395 +2024-08-16 03:00:00,5,21.47427256313976,20.410956858219784,46.42295821971634 +2024-08-16 04:00:00,5,25.345684799433453,19.40966803238674,38.55061164267395 +2024-08-16 05:00:00,5,24.936201096987688,26.749082805303583,52.98289224592231 +2024-08-16 06:00:00,5,18.64426658854464,18.72669681525015,59.32007810402871 +2024-08-16 07:00:00,5,21.818110314546725,20.32153790020063,41.444683227003594 +2024-08-16 08:00:00,5,12.089545459675524,24.0931338648985,40.81776758525902 +2024-08-16 09:00:00,5,26.933788444011064,23.70047945476323,51.35722359445069 +2024-08-16 10:00:00,5,32.0955803508798,23.46507876329147,49.510560983698944 +2024-08-16 11:00:00,5,15.50689749290674,26.04733818758192,55.67265754953537 +2024-08-16 12:00:00,5,39.82039389630316,26.973725661085044,60.336178603418 +2024-08-16 13:00:00,5,23.01507360315613,28.594829810302457,41.628789027876245 +2024-08-16 14:00:00,5,24.52175854243369,23.004763150502068,70.72716606762305 +2024-08-16 15:00:00,5,22.238613251392003,30.14749140009917,57.38761446372972 +2024-08-16 16:00:00,5,11.997056113394123,17.94840367680598,54.817865763499306 +2024-08-16 17:00:00,5,26.40575382235304,22.16965621379797,42.19430371097863 +2024-08-16 18:00:00,5,28.949114573840472,23.302061569618317,59.36375503563724 +2024-08-16 19:00:00,5,11.78228472567716,24.91630257248099,47.51824981072651 +2024-08-16 20:00:00,5,31.72689558067367,21.471699316909792,51.82877271321609 +2024-08-16 21:00:00,5,26.68754628113446,25.73355008879125,43.747704039275256 +2024-08-16 22:00:00,5,31.936298391499733,23.727476651434042,55.83102606771616 +2024-08-16 23:00:00,5,18.400232272797627,25.807075805442913,56.49114904339387 +2024-08-17 00:00:00,5,22.634791892427863,18.746904012881775,37.4760012505313 +2024-08-17 01:00:00,5,30.46542983406923,19.270444964045847,61.726768089901945 +2024-08-17 02:00:00,5,22.48399618420288,28.147010436697585,58.329040953382275 +2024-08-17 03:00:00,5,6.939729931309424,22.643157712060283,43.227857772894126 +2024-08-17 04:00:00,5,11.736554651519354,26.241328014008314,49.15898733088754 +2024-08-17 05:00:00,5,35.02722054540192,20.087589346505865,39.84407847802541 +2024-08-17 06:00:00,5,23.102434044390733,21.50230219207836,57.287402309723625 +2024-08-17 07:00:00,5,19.108612653721387,25.634857480957,62.32477242353102 +2024-08-17 08:00:00,5,34.95299835981603,17.117142361554656,48.318966169290974 +2024-08-17 09:00:00,5,24.57999263726038,27.359031517134987,47.41409214128373 +2024-08-17 10:00:00,5,36.08397883033268,14.293569214296632,71.06655372516447 +2024-08-17 11:00:00,5,17.621968998197197,29.799652222045115,55.31324532636084 +2024-08-17 12:00:00,5,28.401999088398107,25.294818186756782,50.260896048099454 +2024-08-17 13:00:00,5,13.290080817300684,18.343609553339295,46.70892325926882 +2024-08-17 14:00:00,5,29.286134411713533,22.181436095284475,66.183926888017 +2024-08-17 15:00:00,5,13.336538409866677,25.142436262275655,62.78337297587855 +2024-08-17 16:00:00,5,30.478989098386513,20.931508372398056,67.40824127394662 +2024-08-17 17:00:00,5,28.54681266810401,24.331406723538127,54.828733969248454 +2024-08-17 18:00:00,5,15.153322883843618,29.326425325787334,55.52875302296961 +2024-08-17 19:00:00,5,25.256759436998422,26.64663484249206,61.009869643138465 +2024-08-17 20:00:00,5,22.839503106469547,25.50561600104595,53.80691934456577 +2024-08-17 21:00:00,5,8.226773373803299,24.371905719842434,46.24922704581718 +2024-08-17 22:00:00,5,39.98255420057518,28.060749677470724,45.21955796296444 +2024-08-17 23:00:00,5,16.80524674256336,24.42029735007084,67.83133459116559 +2024-08-18 00:00:00,5,14.126919127874821,24.044659723831376,63.638739393412884 +2024-08-18 01:00:00,5,24.875899241320074,26.710004559381634,48.3454435397539 +2024-08-18 02:00:00,5,19.972167501308135,19.97313640984073,61.99829649019058 +2024-08-18 03:00:00,5,25.472642071105625,20.93078136633108,43.10946222672408 +2024-08-18 04:00:00,5,32.67254719804397,26.5666078884343,58.32237766918519 +2024-08-18 05:00:00,5,31.606196238633217,23.38255069904024,59.22600208749071 +2024-08-18 06:00:00,5,3.0061692782472704,25.81452820009814,43.8271565718816 +2024-08-18 07:00:00,5,17.268002992204217,21.97754584929374,62.63082588785031 +2024-08-18 08:00:00,5,24.577128969587076,20.09125407392386,71.02356007074525 +2024-08-18 09:00:00,5,21.807068679985257,26.35434942960409,56.936592086703605 +2024-08-18 10:00:00,5,27.949061658892873,26.873790575589883,60.55122873987206 +2024-08-18 11:00:00,5,35.03167405549566,24.794612696907386,33.68601411820842 +2024-08-18 12:00:00,5,27.960139527849815,23.671660773441783,65.78015252539639 +2024-08-18 13:00:00,5,12.251328031888994,24.656202187042165,43.43920220955493 +2024-08-18 14:00:00,5,25.33444831800361,20.194546349135784,57.59383696583955 +2024-08-18 15:00:00,5,9.332347704236035,26.72569290857482,55.06162837102239 +2024-08-18 16:00:00,5,14.904228465537914,25.173751944322348,60.860237829845595 +2024-08-18 17:00:00,5,7.536519854971365,26.115260540395262,63.27659874211272 +2024-08-18 18:00:00,5,18.964391502316488,25.045841612043546,49.643815505539266 +2024-08-18 19:00:00,5,32.397546712186774,22.89451988880532,69.98325311737693 +2024-08-18 20:00:00,5,24.599008328728505,22.454209132444902,59.966910310509626 +2024-08-18 21:00:00,5,21.010367705684477,24.569609590593622,59.94888955355718 +2024-08-18 22:00:00,5,17.124471106058003,26.143026840872864,56.72904207564928 +2024-08-18 23:00:00,5,20.496809335235895,26.40741051338939,57.315611814720796 +2024-08-19 00:00:00,5,25.225901571825055,25.873237818504066,69.07726584924902 +2024-08-19 01:00:00,5,40.354240155068894,27.060477080122297,63.311801664027215 +2024-08-19 02:00:00,5,23.559360965040142,25.635199621702032,59.74656566172267 +2024-08-19 03:00:00,5,32.32072794020369,23.61346874581911,42.55107020584934 +2024-08-19 04:00:00,5,23.063454793351212,24.32334155785832,55.02600792325926 +2024-08-19 05:00:00,5,21.600076573759146,22.664483741534216,54.25237257432765 +2024-08-19 06:00:00,5,27.574820262989302,23.00749636903741,39.42959005969068 +2024-08-19 07:00:00,5,27.51935443955474,24.62761256533596,67.72581901349375 +2024-08-19 08:00:00,5,34.01240234492822,23.09662210168784,66.14901655581463 +2024-08-19 09:00:00,5,29.981253978999238,25.083257208805176,44.056008312889894 +2024-08-19 10:00:00,5,28.815864885645713,22.57256947538264,66.25112450163279 +2024-08-19 11:00:00,5,17.234943300151052,28.604191691564456,56.62972974436465 +2024-08-19 12:00:00,5,25.259861253335544,23.806528387850737,48.871372884512716 +2024-08-19 13:00:00,5,28.458745272111464,24.9502785701997,64.03120376311067 +2024-08-19 14:00:00,5,18.51830881512958,24.850160077261023,72.62883244941102 +2024-08-19 15:00:00,5,23.957229119601525,22.761951506911583,55.761191077121545 +2024-08-19 16:00:00,5,18.15986739787236,22.874480933319944,60.21767263582831 +2024-08-19 17:00:00,5,17.52928394815512,26.518916293723066,78.61961150842039 +2024-08-19 18:00:00,5,25.71752482815997,27.829133954908606,40.964969634048316 +2024-08-19 19:00:00,5,26.61105808491763,24.27534815726612,42.18160636452426 +2024-08-19 20:00:00,5,21.868789622515454,25.33561701226689,57.40004404206483 +2024-08-19 21:00:00,5,24.401703392666672,29.371564557039687,66.19046336234655 +2024-08-19 22:00:00,5,26.611505849111328,27.44499912314539,59.69276905894416 +2024-08-19 23:00:00,5,37.19454698094634,24.769279365407066,39.40924537757759 +2024-08-20 00:00:00,5,34.120885449349494,24.93990291341147,48.149177390469376 +2024-08-20 01:00:00,5,18.42801027222834,23.236144709231837,60.96235131851953 +2024-08-20 02:00:00,5,32.035009093844984,22.363601109873382,55.595444057883334 +2024-08-20 03:00:00,5,35.89707295137315,23.525461396555666,38.47191724896726 +2024-08-20 04:00:00,5,19.265152340567834,28.33323646335255,55.67070564668443 +2024-08-20 05:00:00,5,38.28894442690087,19.652225714995254,51.87980554374342 +2024-08-20 06:00:00,5,9.006110732680703,22.693821404142234,60.83121041362007 +2024-08-20 07:00:00,5,20.20206893194411,18.03890300539456,60.7066636792241 +2024-08-20 08:00:00,5,55.25817280060481,24.816347110188243,53.94318987093869 +2024-08-20 09:00:00,5,9.412878783986407,24.342495838797344,51.175487049654976 +2024-08-20 10:00:00,5,32.123604710199075,24.11785934349225,50.457873283362034 +2024-08-20 11:00:00,5,25.72387572502867,21.59599630553735,68.1453564230922 +2024-08-20 12:00:00,5,18.891110173158296,25.446378018711854,74.88616405694505 +2024-08-20 13:00:00,5,9.968299460827,25.468236462198448,56.35771982315537 +2024-08-20 14:00:00,5,25.450419728075673,25.660855747319108,57.078779296512316 +2024-08-20 15:00:00,5,23.4829700565883,25.950588585712072,69.32318288736883 +2024-08-20 16:00:00,5,14.409933329993041,21.68861950441364,59.576266710805896 +2024-08-20 17:00:00,5,25.692949642432136,23.69128895748585,60.61254793255092 +2024-08-20 18:00:00,5,21.478819927403183,24.70839894364442,65.87903947825512 +2024-08-20 19:00:00,5,21.65110480829685,31.822891128978736,52.92009147266537 +2024-08-20 20:00:00,5,31.03909367911738,27.965710872362138,48.01487423400226 +2024-08-20 21:00:00,5,30.699157696768957,22.64963563774878,51.25340510758913 +2024-08-20 22:00:00,5,29.47116302182939,23.895205610443007,55.720574397631545 +2024-08-20 23:00:00,5,30.659014914262247,25.946401049234588,44.080780087112785 +2024-08-21 00:00:00,5,19.285109098062286,21.15249683494412,64.58038136012041 +2024-08-21 01:00:00,5,19.792331908742394,27.35152596465849,67.07264769818637 +2024-08-21 02:00:00,5,25.751833710335514,32.23822919122701,50.411897750981616 +2024-08-21 03:00:00,5,28.308679977312725,25.644388661432053,54.94116627719098 +2024-08-21 04:00:00,5,14.211889651824226,24.422910818445203,54.84370587937287 +2024-08-21 05:00:00,5,10.097247932937282,27.18584308186876,46.13488449149166 +2024-08-21 06:00:00,5,23.256371170103687,25.256659491319954,59.10124507873232 +2024-08-21 07:00:00,5,24.607718432319203,19.871447909622134,35.680959531462584 +2024-08-21 08:00:00,5,11.72438649320364,20.135563307678847,43.5743346266863 +2024-08-21 09:00:00,5,26.62091357286179,28.865365167991413,36.29975716689686 +2024-08-21 10:00:00,5,30.059431691731444,20.927258586351453,49.245101201558086 +2024-08-21 11:00:00,5,38.42307981699068,27.07625585052201,55.17606170140032 +2024-08-21 12:00:00,5,19.658119015354337,21.111703982139712,48.484586371564056 +2024-08-21 13:00:00,5,20.33860126757595,21.87541540754639,61.31945736754391 +2024-08-21 14:00:00,5,22.128782989101907,26.714547266802146,55.579428880304995 +2024-08-21 15:00:00,5,30.112067660044087,26.187879116892734,57.293379989651214 +2024-08-21 16:00:00,5,31.635129741061846,27.971947264639038,53.21137562918037 +2024-08-21 17:00:00,5,30.103295336421372,23.6363171909684,63.50575670752289 +2024-08-21 18:00:00,5,31.930272564598567,26.213686863502158,51.722143126904705 +2024-08-21 19:00:00,5,27.139285718988802,14.994110462157634,61.91594818663795 +2024-08-21 20:00:00,5,25.682001051037727,29.042994954819427,49.2156664621071 +2024-08-21 21:00:00,5,19.732835890923482,23.271817342137116,44.805113654882945 +2024-08-21 22:00:00,5,20.966921762098302,28.490198574269577,64.84632629436653 +2024-08-21 23:00:00,5,13.899627918058325,29.33275189234015,56.63313045442389 +2024-08-22 00:00:00,5,19.707940630061497,26.963613833423068,60.559483046596384 +2024-08-22 01:00:00,5,25.380466533977785,21.835555041358713,54.49894614939156 +2024-08-22 02:00:00,5,15.375220053332296,21.465995958683205,53.82911204340019 +2024-08-22 03:00:00,5,22.5272181164681,22.50572765423749,56.65105287865545 +2024-08-22 04:00:00,5,22.217133638341416,27.21745979215422,43.5704634165871 +2024-08-22 05:00:00,5,26.65698269446712,21.638692349675452,52.10064732652235 +2024-08-22 06:00:00,5,48.511690986327466,22.36114908609753,51.22304284891588 +2024-08-22 07:00:00,5,10.778676859593647,22.23120019262513,64.04305208679088 +2024-08-22 08:00:00,5,26.346178999698985,23.91984766469015,62.890453834112876 +2024-08-22 09:00:00,5,36.25000492389209,28.655996321336108,49.63677458651758 +2024-08-22 10:00:00,5,23.388263268712862,20.187662614762534,46.9065261639599 +2024-08-22 11:00:00,5,24.11901205225116,29.659632349779663,53.45866913686811 +2024-08-22 12:00:00,5,9.586852772283546,28.566254406751963,54.5476764071494 +2024-08-22 13:00:00,5,4.720191579543837,25.76782799486431,60.26570503654284 +2024-08-22 14:00:00,5,23.452303808625075,21.770038729705284,53.05613638778074 +2024-08-22 15:00:00,5,46.330972949427505,23.62655450569415,72.5356022672678 +2024-08-22 16:00:00,5,10.309404540191201,27.14630531118263,48.56798999336498 +2024-08-22 17:00:00,5,26.129691198361385,25.23905266979389,69.87453230867398 +2024-08-22 18:00:00,5,22.485381473938936,25.150532284346937,54.64483953230058 +2024-08-22 19:00:00,5,9.663936007598297,31.335435594404967,62.22983241781781 +2024-08-22 20:00:00,5,14.926285218499084,26.639383020965404,60.12368044812444 +2024-08-22 21:00:00,5,16.449817628890752,23.023424636375008,64.74183316735059 +2024-08-22 22:00:00,5,25.211065303531548,25.427072178148634,54.27488114253036 +2024-08-22 23:00:00,5,42.16433000024155,20.256073940454243,47.07557866569161 +2024-08-23 00:00:00,5,22.692822139914632,19.781647198110793,47.531809875155815 +2024-08-23 01:00:00,5,27.914676709128408,24.664247348689457,47.410339740835965 +2024-08-23 02:00:00,5,11.218290361695365,25.722465297213077,61.50235768903931 +2024-08-23 03:00:00,5,25.340066205048796,25.121300573888817,46.072852453169965 +2024-08-23 04:00:00,5,21.80521251951561,24.619722732864336,41.747899683372665 +2024-08-23 05:00:00,5,4.3933823902213724,18.43146408625478,51.06313640188105 +2024-08-23 06:00:00,5,22.826538441928406,22.280900744854705,51.99932445032985 +2024-08-23 07:00:00,5,23.7106589498775,29.172254569751892,59.30286215933326 +2024-08-23 08:00:00,5,10.884161469047315,20.87307176216403,65.88930864197906 +2024-08-23 09:00:00,5,19.139414766329665,20.191474530949797,62.1607550285527 +2024-08-23 10:00:00,5,17.99486027963025,24.271845867889763,29.499741753514527 +2024-08-23 11:00:00,5,17.623360556588146,24.019656527417514,47.552882421693766 +2024-08-23 12:00:00,5,37.14599367703681,22.596786917199577,46.212457460860136 +2024-08-23 13:00:00,5,26.260168372504012,27.486114058310903,68.419767739906 +2024-08-23 14:00:00,5,33.3596511341879,23.935403176227368,47.99136891063193 +2024-08-23 15:00:00,5,23.27201366284147,23.793324920109423,51.081685979094765 +2024-08-23 16:00:00,5,30.890251400757883,24.95255503691014,48.918415663754786 +2024-08-23 17:00:00,5,18.835639913611306,29.056639494945557,64.45634799324824 +2024-08-23 18:00:00,5,14.152304413429963,28.824812083491743,50.880867134597935 +2024-08-23 19:00:00,5,20.56367525033979,25.66293526230603,56.365709052814545 +2024-08-23 20:00:00,5,27.091344531033606,21.524709217290162,52.00037636745623 +2024-08-23 21:00:00,5,19.063223810222183,24.236929868167714,63.088624369993525 +2024-08-23 22:00:00,5,32.29228751841045,26.12269401569353,62.63021421577672 +2024-08-23 23:00:00,5,25.977170600143296,28.82839689162149,61.34070896817561 +2024-08-24 00:00:00,5,20.00009404636875,24.315411626818697,56.29675287022104 +2024-08-24 01:00:00,5,28.121796995444626,17.918673780093656,42.41036089102599 +2024-08-24 02:00:00,5,29.982993310736838,20.74138803679619,68.26635994447545 +2024-08-24 03:00:00,5,21.982985852487992,27.074612571343174,50.032490807721786 +2024-08-24 04:00:00,5,31.304874581658574,24.273734316388314,39.08469169445621 +2024-08-24 05:00:00,5,18.468243907790026,21.963622060411154,44.03356639330438 +2024-08-24 06:00:00,5,14.075116168010052,22.61517575273063,44.48526185963048 +2024-08-24 07:00:00,5,26.670259552021413,16.886001152106946,46.680454305573704 +2024-08-24 08:00:00,5,25.40154768182701,22.290263631715234,53.118053253533766 +2024-08-24 09:00:00,5,39.77679466323285,26.339959127776666,48.10789931292744 +2024-08-24 10:00:00,5,32.07242968518943,25.7576768900286,49.76312247747794 +2024-08-24 11:00:00,5,43.06322037699543,25.25561531503733,57.05035101620955 +2024-08-24 12:00:00,5,31.36967047745627,22.59929486396851,50.45043444003334 +2024-08-24 13:00:00,5,20.092866954193063,22.198425287628723,50.27295484675307 +2024-08-24 14:00:00,5,32.4789784405002,24.71721131917878,71.10058428601907 +2024-08-24 15:00:00,5,30.46404924955431,22.66011562399582,50.94089093749703 +2024-08-24 16:00:00,5,17.737364795220813,22.16928813253181,56.28091341383508 +2024-08-24 17:00:00,5,21.526338523930676,25.877038945965666,66.5661922670628 +2024-08-24 18:00:00,5,23.848183877000437,25.229840135161634,59.90961559990296 +2024-08-24 19:00:00,5,12.507917377709543,27.578053985467175,83.0654145340735 +2024-08-24 20:00:00,5,8.691730211660301,28.80708930557139,67.20397183143814 +2024-08-24 21:00:00,5,32.157662000638204,24.789128000805448,67.225869664456 +2024-08-24 22:00:00,5,24.33424823830986,25.322643356128292,62.855124955922925 +2024-08-24 23:00:00,5,20.252731119463597,27.91502698533104,35.363393724020085 +2024-08-25 00:00:00,5,34.287284592137595,21.51841653248904,47.037327244912106 +2024-08-25 01:00:00,5,25.380385710952993,19.70064566340319,42.045319782179234 +2024-08-25 02:00:00,5,13.198341964279924,22.058582550626944,44.706341291325444 +2024-08-25 03:00:00,5,15.44817178716736,24.28785865733031,49.24159797852285 +2024-08-25 04:00:00,5,23.819086301352556,17.472862094235335,58.760987653776965 +2024-08-25 05:00:00,5,26.613155218226467,25.608705627123676,55.50463141246112 +2024-08-25 06:00:00,5,25.020327627271545,25.26798376541028,59.07202274448501 +2024-08-25 07:00:00,5,36.360817103192275,24.710113669146498,55.20861438965369 +2024-08-25 08:00:00,5,31.82943250146704,22.02066710631815,43.298638134870345 +2024-08-25 09:00:00,5,14.939362053788527,21.880350678844525,50.77453875758177 +2024-08-25 10:00:00,5,33.196913521213105,28.23761488941811,57.25238426400618 +2024-08-25 11:00:00,5,21.730223513981304,23.87902667017648,50.4744559143941 +2024-08-25 12:00:00,5,28.167202691637847,22.938599610915112,55.75465044369185 +2024-08-25 13:00:00,5,24.463073692079494,21.460575169152165,31.14520809503634 +2024-08-25 14:00:00,5,29.694353639903877,17.649339718711204,50.60248946405586 +2024-08-25 15:00:00,5,29.983351659911698,31.565544918018052,75.01663229030775 +2024-08-25 16:00:00,5,22.47924862500302,24.108534400686622,61.71576292741839 +2024-08-25 17:00:00,5,19.156536565464002,27.671829839858233,54.30613064672601 +2024-08-25 18:00:00,5,7.388821922967633,23.223830200410184,58.03521791398108 +2024-08-25 19:00:00,5,17.82916228836579,29.17997380753997,56.267063282885665 +2024-08-25 20:00:00,5,23.19701875156028,23.60054265878221,63.61722921042356 +2024-08-25 21:00:00,5,25.727696078117365,31.598169803910135,61.78589391266144 +2024-08-25 22:00:00,5,22.158355612661897,18.399262721051436,52.76645842614863 +2024-08-25 23:00:00,5,23.479631634934485,30.7554436986702,52.76515738332419 +2024-08-26 00:00:00,5,14.213261013261441,27.509750196489502,29.324204216667248 +2024-08-26 01:00:00,5,20.409867557434737,18.201210675327257,59.29831048866572 +2024-08-26 02:00:00,5,20.785759402244555,24.19595704388291,68.5391652376338 +2024-08-26 03:00:00,5,28.7321550905728,22.93819757444113,68.0138782729211 +2024-08-26 04:00:00,5,12.968683107957556,23.980586598372987,60.68734447174906 +2024-08-26 05:00:00,5,22.72802950263625,25.068921498464924,54.92905550665909 +2024-08-26 06:00:00,5,15.863279459071283,21.00880153309721,61.27501928683301 +2024-08-26 07:00:00,5,33.475873933778686,20.512075877900987,53.41044233178499 +2024-08-26 08:00:00,5,26.820809049034757,25.720891676500322,61.63025061284449 +2024-08-26 09:00:00,5,37.33677547731806,25.07654349657708,54.84370540067988 +2024-08-26 10:00:00,5,22.06248736534102,21.351258967491987,42.177036477900565 +2024-08-26 11:00:00,5,33.84713778006843,23.642474014789276,63.31587524464838 +2024-08-26 12:00:00,5,22.18662497985382,18.0186782882979,63.63691169309262 +2024-08-26 13:00:00,5,38.218339846930014,28.194758943172044,51.11267963689018 +2024-08-26 14:00:00,5,13.814707122733603,21.83785574223862,47.96361168310933 +2024-08-26 15:00:00,5,33.4762318225218,19.050496755638335,56.68548793022349 +2024-08-26 16:00:00,5,21.799020130821877,22.612637726489645,60.95622744469457 +2024-08-26 17:00:00,5,27.128140485047517,17.08144245851731,55.840620671233005 +2024-08-26 18:00:00,5,23.777712950058888,27.278470299327015,60.07204203649231 +2024-08-26 19:00:00,5,13.072054397104257,28.01662473311957,67.14855002143213 +2024-08-26 20:00:00,5,3.6918063184711976,23.4672236486061,59.054704903164264 +2024-08-26 21:00:00,5,38.97133866412586,31.183833514599442,69.72650244909914 +2024-08-26 22:00:00,5,28.568782894793003,29.124687041961845,50.7659213071088 +2024-08-26 23:00:00,5,24.963640856559532,28.099402034066713,51.111393443074874 +2024-08-27 00:00:00,5,13.501166601743316,20.612298476069615,56.50021411520787 +2024-08-27 01:00:00,5,17.49230562371678,26.439558386999256,55.115560524763566 +2024-08-27 02:00:00,5,34.14090160323223,23.60871948797444,55.600876768916336 +2024-08-27 03:00:00,5,22.36537560820522,22.553410008918675,59.815669154330116 +2024-08-27 04:00:00,5,21.92674479415393,22.11409761192958,48.75331622515229 +2024-08-27 05:00:00,5,22.90643356674539,24.182086236704578,36.03940394159653 +2024-08-27 06:00:00,5,32.888772905553296,19.719877526388448,51.10978442602831 +2024-08-27 07:00:00,5,37.6088830520613,25.552996481241852,52.42900506047594 +2024-08-27 08:00:00,5,28.582831499594292,20.023914098111973,49.683652798486186 +2024-08-27 09:00:00,5,20.431889063939607,26.543300905347117,48.99069072320486 +2024-08-27 10:00:00,5,21.534628460256833,25.580427760895112,47.712452373551756 +2024-08-27 11:00:00,5,16.09136563433369,23.697317974382294,53.04090743460032 +2024-08-27 12:00:00,5,20.37527635249347,20.53611190274806,56.685233272856216 +2024-08-27 13:00:00,5,28.37160613823488,22.5878384405155,52.288678848746244 +2024-08-27 14:00:00,5,23.02276119215891,24.728359395417574,39.570799262134614 +2024-08-27 15:00:00,5,31.572772997449896,27.833295106976678,40.9208681409778 +2024-08-27 16:00:00,5,42.60418749427386,27.605825542437362,51.03412324214609 +2024-08-27 17:00:00,5,30.288788451591486,31.242159533898047,50.89137060184944 +2024-08-27 18:00:00,5,25.446509495869474,23.443943550485766,58.1579191685933 +2024-08-27 19:00:00,5,7.8217188453360205,18.876722012478723,64.31792656947158 +2024-08-27 20:00:00,5,28.75468889160999,29.473211475175948,44.085102844434545 +2024-08-27 21:00:00,5,32.790636069703154,26.341688510753848,50.439526739714715 +2024-08-27 22:00:00,5,22.308556334843484,30.141788397553167,62.119658545536645 +2024-08-27 23:00:00,5,34.97823300810871,23.270879585080017,54.5899808704878 +2024-08-28 00:00:00,5,19.521155756189636,24.855241165520223,49.53670015603963 +2024-08-28 01:00:00,5,22.31797732908262,22.437763824586987,55.08782542376955 +2024-08-28 02:00:00,5,41.63886556370399,21.005745351037287,57.2563316518048 +2024-08-28 03:00:00,5,21.534112382078092,25.044530233211514,42.76876010108887 +2024-08-28 04:00:00,5,31.697638028239083,27.7884068200531,41.96576171066778 +2024-08-28 05:00:00,5,23.809194086973626,23.85746842573808,48.3809564473554 +2024-08-28 06:00:00,5,20.352886690963153,24.114373050545314,56.4236447572177 +2024-08-28 07:00:00,5,28.97096881114386,19.478591601073553,50.37670594180838 +2024-08-28 08:00:00,5,8.289791977897124,18.673464377386978,60.425631162594 +2024-08-28 09:00:00,5,31.658334165473672,26.88129090855647,57.461917395713996 +2024-08-28 10:00:00,5,28.718731125135264,26.160400801825922,55.18663515906707 +2024-08-28 11:00:00,5,38.10163857880326,28.034694063856215,71.9953047914245 +2024-08-28 12:00:00,5,27.87444984576426,20.372616136292617,43.36180818210339 +2024-08-28 13:00:00,5,19.116778675056235,27.19251379553064,56.172570340217405 +2024-08-28 14:00:00,5,18.1992323996122,24.43684144064077,71.2256423615157 +2024-08-28 15:00:00,5,26.167705879218392,28.254112615455547,63.66213380743115 +2024-08-28 16:00:00,5,28.20937396665207,19.239095869080124,64.37467736290735 +2024-08-28 17:00:00,5,33.63921481128182,22.36468203818136,59.274736532429586 +2024-08-28 18:00:00,5,15.705019307222546,24.10874139101036,44.642056026009726 +2024-08-28 19:00:00,5,21.99145666190496,18.85089676757099,49.84003322718786 +2024-08-28 20:00:00,5,34.377255941461975,28.950702984696957,38.61841229476418 +2024-08-28 21:00:00,5,20.926177492859804,21.860997384845454,60.74488893692515 +2024-08-28 22:00:00,5,27.413375464268857,30.889099167572084,59.548922463799535 +2024-08-28 23:00:00,5,12.102585333919672,26.740145958731876,36.220986168116525 +2024-08-29 00:00:00,5,21.08532935640767,25.004248173420514,44.84958699822958 +2024-08-29 01:00:00,5,18.339865944051812,21.393569991675218,55.93550545944915 +2024-08-29 02:00:00,5,25.279860193431286,26.38090797539416,52.348580089004706 +2024-08-29 03:00:00,5,25.988689538840738,27.81886085706706,57.36079349448619 +2024-08-29 04:00:00,5,19.660406523377,15.561356905644114,65.06710673647835 +2024-08-29 05:00:00,5,29.055431888905552,24.88184512841928,58.97859099073891 +2024-08-29 06:00:00,5,38.3187200889587,22.95659128674998,57.32045871516624 +2024-08-29 07:00:00,5,30.353678441734065,22.433249738477226,44.15432091451609 +2024-08-29 08:00:00,5,27.58694758577023,25.161937890179722,49.228447923848364 +2024-08-29 09:00:00,5,9.952929501605649,19.315254976605644,52.87924816341426 +2024-08-29 10:00:00,5,30.636102465606456,25.965848141346555,72.32874043512498 +2024-08-29 11:00:00,5,21.135498576222467,25.067768291118323,58.85498919078113 +2024-08-29 12:00:00,5,25.21346484824374,25.041941025581977,64.69975454652985 +2024-08-29 13:00:00,5,16.461548708605413,25.287404549718588,56.53820470442636 +2024-08-29 14:00:00,5,21.67171324931184,28.924575993253253,56.21333277263536 +2024-08-29 15:00:00,5,19.950310827410956,19.99114539809036,44.71177728919427 +2024-08-29 16:00:00,5,28.372075575622475,27.234990167208885,62.9810889574087 +2024-08-29 17:00:00,5,19.487479903899754,24.80893509300959,64.65538437191545 +2024-08-29 18:00:00,5,23.51630803690294,19.410999695231837,51.64070800656958 +2024-08-29 19:00:00,5,16.13828141266332,25.47868072347325,56.68687913859216 +2024-08-29 20:00:00,5,19.751789180240507,22.439763291463034,60.139046447356264 +2024-08-29 21:00:00,5,23.592157078821053,25.19910010335453,63.033197092365235 +2024-08-29 22:00:00,5,26.243926797418897,34.393228444458984,54.46192778358018 +2024-08-29 23:00:00,5,44.67088927741976,28.53724101918604,59.10895185335127 +2024-08-30 00:00:00,5,9.807102261104625,23.60581893638461,40.67890747993782 +2024-08-30 01:00:00,5,28.052253938191537,20.42835156857029,50.59915535394932 +2024-08-30 02:00:00,5,9.879587168089083,19.400220147241875,46.82206983104244 +2024-08-30 03:00:00,5,29.9508113786159,28.677000068639607,51.04976274437672 +2024-08-30 04:00:00,5,20.176545667142328,21.267445407308784,71.05009717315866 +2024-08-30 05:00:00,5,29.08955268283639,22.119047775508406,47.83476876359052 +2024-08-30 06:00:00,5,17.259557771115183,22.600973913943225,46.516731267605294 +2024-08-30 07:00:00,5,23.661508452299454,25.448387836204372,70.29964139563201 +2024-08-30 08:00:00,5,23.581701009496236,21.53555507586172,65.09097835520535 +2024-08-30 09:00:00,5,24.399537467525636,20.666288273998262,32.281325950292405 +2024-08-30 10:00:00,5,35.88496148756475,22.887900244361155,53.83176047411389 +2024-08-30 11:00:00,5,30.588722509578787,21.250942554064025,61.75677289044022 +2024-08-30 12:00:00,5,13.47963774170129,21.49407110186729,69.87150666616063 +2024-08-30 13:00:00,5,18.135097947626008,17.813630065583943,56.70441196890079 +2024-08-30 14:00:00,5,22.52048160842057,25.72265267641529,44.53096922468037 +2024-08-30 15:00:00,5,13.985513428562985,33.98135769956983,56.078655033708756 +2024-08-30 16:00:00,5,24.457791730560373,24.19226269724369,58.95406496729463 +2024-08-30 17:00:00,5,21.462273251331926,22.814657109677654,57.73285240489041 +2024-08-30 18:00:00,5,10.560847467167788,21.88896340189272,71.7988691365854 +2024-08-30 19:00:00,5,25.6176235030697,29.958000491448928,66.70904647935374 +2024-08-30 20:00:00,5,24.64194078937442,25.936672505562864,57.54313634898937 +2024-08-30 21:00:00,5,28.542791293635297,24.852809120998007,52.45861102479863 +2024-08-30 22:00:00,5,33.579785732339396,27.695675721014165,46.5378002621322 +2024-08-30 23:00:00,5,26.528704533095706,26.678608808980908,77.6470690636149 +2024-08-31 00:00:00,5,20.46312126303149,21.810479347103602,52.249207595377754 +2024-08-31 01:00:00,5,19.829163504827694,27.490192059013037,59.15540548896419 +2024-08-31 02:00:00,5,23.917758214496047,27.454097129980163,58.13886658379392 +2024-08-31 03:00:00,5,23.2914219898871,23.543921789104377,56.849723717593335 +2024-08-31 04:00:00,5,18.29767393444344,22.001723906207104,58.95650747503055 +2024-08-31 05:00:00,5,25.07787162331246,19.25911915552185,47.17520313302204 +2024-08-31 06:00:00,5,18.18305525721313,22.50664743329143,64.8117019754542 +2024-08-31 07:00:00,5,28.279229845000312,22.755998174220544,77.32505544015558 +2024-08-31 08:00:00,5,24.009778464790525,21.393872856698206,57.356777208052065 +2024-08-31 09:00:00,5,21.762843693913435,21.6432126277237,46.02640140722608 +2024-08-31 10:00:00,5,23.466951648087658,24.798335158166708,69.98837021429799 +2024-08-31 11:00:00,5,20.465977292743762,23.671298761303557,55.43385178498555 +2024-08-31 12:00:00,5,25.82219943125573,27.349632539950907,61.74337084804791 +2024-08-31 13:00:00,5,21.893057604373187,26.27721502364551,54.21328142519209 +2024-08-31 14:00:00,5,22.756294914773843,23.291278301711454,48.95978096846443 +2024-08-31 15:00:00,5,11.971940294630324,26.896607244591344,51.38544801159353 +2024-08-31 16:00:00,5,36.94520148642848,23.947435651187973,44.984590829947116 +2024-08-31 17:00:00,5,29.07007878333227,25.435521201548017,72.75015857871244 +2024-08-31 18:00:00,5,18.47398267215217,27.991430428621147,79.49401285687358 +2024-08-31 19:00:00,5,27.05690519741044,23.117615064969733,62.75315990860565 +2024-08-31 20:00:00,5,35.151173603466525,21.57272999186311,61.681429441166664 +2024-08-31 21:00:00,5,16.986992673934555,24.74142925776838,62.7130741065361 +2024-08-31 22:00:00,5,22.189657587617887,25.718326369692786,42.54590302615741 +2024-08-31 23:00:00,5,23.39435580928362,19.99394405816492,62.88681125170502 +2024-09-01 00:00:00,5,20.418838898807707,18.230509613171915,65.61035064615393 +2024-09-01 01:00:00,5,18.53227045427275,22.75952363451606,68.92059468058086 +2024-09-01 02:00:00,5,34.32760916810893,18.66679226839447,49.04011284851225 +2024-09-01 03:00:00,5,2.8150240635454544,25.551927766802965,53.90316736377099 +2024-09-01 04:00:00,5,38.74896690372657,25.86408042751042,49.62659128443155 +2024-09-01 05:00:00,5,16.301452049412518,26.869244486049855,51.58395828990042 +2024-09-01 06:00:00,5,30.30121647410895,22.377623833797198,51.025555307297346 +2024-09-01 07:00:00,5,24.861538636317654,24.97430404420062,39.519779610053625 +2024-09-01 08:00:00,5,13.290282739050124,22.178619779335882,59.796381937685574 +2024-09-01 09:00:00,5,29.371396709071256,22.791396906661845,62.247100648114184 +2024-09-01 10:00:00,5,33.36247396602601,27.686731078056557,53.26686693200262 +2024-09-01 11:00:00,5,41.397846660345515,20.489763679196898,48.691279113737764 +2024-09-01 12:00:00,5,48.66664685573421,25.305530138722503,45.21040456608978 +2024-09-01 13:00:00,5,25.916955253212535,31.649909831610486,65.35758445260598 +2024-09-01 14:00:00,5,8.359574032149848,26.10032090493957,60.47494435975078 +2024-09-01 15:00:00,5,26.065336575375113,21.389649970994473,50.36117685467816 +2024-09-01 16:00:00,5,21.285252939532793,25.233367965809386,54.56003018663526 +2024-09-01 17:00:00,5,35.461347018290155,29.83994750572479,64.38729101011043 +2024-09-01 18:00:00,5,35.69994394129649,28.420540915271374,54.99626442612024 +2024-09-01 19:00:00,5,18.264526316754633,29.35198548495657,59.0212506335149 +2024-09-01 20:00:00,5,27.908965133384292,19.217207393005936,51.78728202906234 +2024-09-01 21:00:00,5,11.143667024250416,30.72631157144778,59.04691414241715 +2024-09-01 22:00:00,5,43.480546373856555,28.911462658342224,36.09484699888861 +2024-09-01 23:00:00,5,30.499093396793292,24.687396885250017,54.869291164212264 +2024-09-02 00:00:00,5,20.824824641966078,19.774093930043072,43.2284691054087 +2024-09-02 01:00:00,5,22.996178727605276,18.90291615225301,52.51162528238191 +2024-09-02 02:00:00,5,10.498516970534153,21.463621481345363,42.88378788387698 +2024-09-02 03:00:00,5,27.5625726765601,19.084971632763942,54.6165122943439 +2024-09-02 04:00:00,5,23.79795640002964,16.21794377978428,41.61252376347838 +2024-09-02 05:00:00,5,9.259400954493902,29.443545772462613,43.420514449635675 +2024-09-02 06:00:00,5,35.53585437652311,20.75991868663803,53.751870384720455 +2024-09-02 07:00:00,5,23.686736612066912,24.60527125723577,50.62461601050715 +2024-09-02 08:00:00,5,30.839100781636514,25.62327179865142,45.84406137508435 +2024-09-02 09:00:00,5,19.88406776976546,21.1601911369057,62.63111099788983 +2024-09-02 10:00:00,5,31.054460197691043,23.41400501915935,48.98747782901955 +2024-09-02 11:00:00,5,23.637163648251082,22.861486493479497,58.750462627753315 +2024-09-02 12:00:00,5,42.974781822731686,20.796451924403005,64.7797827687281 +2024-09-02 13:00:00,5,29.668146710741468,26.75312749521901,43.72753884443313 +2024-09-02 14:00:00,5,19.119676531125233,21.67207135613627,64.7318803282968 +2024-09-02 15:00:00,5,10.369871414939421,22.693295414699403,62.10405689814449 +2024-09-02 16:00:00,5,18.9589617944711,19.759071563028797,45.603750679625065 +2024-09-02 17:00:00,5,26.069007973520463,26.406128618304148,71.23688980982206 +2024-09-02 18:00:00,5,36.182078013257154,24.289737624786234,67.27015972855791 +2024-09-02 19:00:00,5,15.417894146751355,26.70714123068756,62.69113954104303 +2024-09-02 20:00:00,5,39.57251580626492,19.024089697169515,47.28577884279696 +2024-09-02 21:00:00,5,28.930808341210486,24.974854480926066,56.06863998352131 +2024-09-02 22:00:00,5,27.199599563328334,18.59569653905775,70.17273213360099 +2024-09-02 23:00:00,5,27.368640805084873,22.294980764706327,55.718834172783424 +2024-09-03 00:00:00,5,26.55829999317156,23.206663161791475,45.419132872607136 +2024-09-03 01:00:00,5,17.532468871513636,22.323017038983867,46.45594942596555 +2024-09-03 02:00:00,5,23.719852452861495,22.909632430302953,71.82530473808598 +2024-09-03 03:00:00,5,11.518459507723332,21.895356766780978,50.40468684191954 +2024-09-03 04:00:00,5,22.601863169520914,23.91477113768229,59.038155022254486 +2024-09-03 05:00:00,5,22.14772841599516,27.742506756823676,53.722218879323016 +2024-09-03 06:00:00,5,22.95317744626126,25.311923695932613,49.99020482916704 +2024-09-03 07:00:00,5,45.91695603027138,28.82546525466272,62.162901181590996 +2024-09-03 08:00:00,5,29.031090575682338,23.61663187351765,50.021824302321086 +2024-09-03 09:00:00,5,25.727831478452426,24.990502630553284,52.4150877910647 +2024-09-03 10:00:00,5,14.769327198287705,24.973651985326672,53.42426943246865 +2024-09-03 11:00:00,5,11.631706780918543,28.963141081444654,56.71438113291485 +2024-09-03 12:00:00,5,17.050107697671997,22.892687667038334,52.31871913410195 +2024-09-03 13:00:00,5,33.692638035545215,19.762800390781027,54.02536362331197 +2024-09-03 14:00:00,5,21.706459012427278,16.573353079706813,53.053088939862604 +2024-09-03 15:00:00,5,25.316973541095326,26.133523797162702,50.42073501028083 +2024-09-03 16:00:00,5,28.123385028214486,19.900759435052084,49.38546826442093 +2024-09-03 17:00:00,5,29.647201634666096,25.981964577113658,64.7868943783068 +2024-09-03 18:00:00,5,34.5831503252639,23.64956756030088,55.697231418104856 +2024-09-03 19:00:00,5,35.46555793318182,25.74976333038982,64.02100564441028 +2024-09-03 20:00:00,5,15.612858558567892,24.51250384334067,52.208000450637485 +2024-09-03 21:00:00,5,13.155420906559314,26.88183384725992,65.17847547120994 +2024-09-03 22:00:00,5,35.50390535677281,22.8889261935471,53.084065279418745 +2024-09-03 23:00:00,5,24.510192018513546,24.93317247729064,72.69259152817622 +2024-09-04 00:00:00,5,28.028764871887507,23.851793530503947,53.067840327535606 +2024-09-04 01:00:00,5,24.895637836459635,23.271592804522722,45.2488559169072 +2024-09-04 02:00:00,5,31.33645408223392,25.645595549906325,60.2162085326523 +2024-09-04 03:00:00,5,24.139293738356635,24.264468914948193,54.73668306389868 +2024-09-04 04:00:00,5,20.694924935112134,18.611060638974337,57.36737645061324 +2024-09-04 05:00:00,5,5.46863200908026,20.566550054047976,57.90980401765829 +2024-09-04 06:00:00,5,19.77655295672769,19.000502539761374,54.57069870931653 +2024-09-04 07:00:00,5,6.0185872791772255,20.001966252423856,62.62070310723429 +2024-09-04 08:00:00,5,26.925921179724288,21.940442649108224,60.213154160386864 +2024-09-04 09:00:00,5,23.761217610264524,22.400613910875855,62.4902873625907 +2024-09-04 10:00:00,5,19.710265838144917,25.84253974696377,32.120984263043596 +2024-09-04 11:00:00,5,31.981419489692247,26.943108726782206,57.847566170431165 +2024-09-04 12:00:00,5,19.386136255928907,21.718851917397892,53.727935734877114 +2024-09-04 13:00:00,5,24.8792151685922,20.168544439198946,42.68934927842648 +2024-09-04 14:00:00,5,27.926146788246157,20.45402933545308,40.29345605182547 +2024-09-04 15:00:00,5,19.499160322205178,16.050309207556793,46.862941147061925 +2024-09-04 16:00:00,5,24.19901818020237,23.583070682710087,64.78625233718887 +2024-09-04 17:00:00,5,21.26594639387524,22.57733563149199,60.2754645208923 +2024-09-04 18:00:00,5,18.61203215275279,21.81890501511872,57.33872521478478 +2024-09-04 19:00:00,5,30.382379284397725,24.10827741080836,48.87336245777986 +2024-09-04 20:00:00,5,24.389252797769473,23.848807182070434,61.67511760540536 +2024-09-04 21:00:00,5,18.479143473285642,33.22539559140331,69.27076141379422 +2024-09-04 22:00:00,5,28.66813683512,27.141584967515282,43.13255601490445 +2024-09-04 23:00:00,5,18.540297428452867,22.7221005662075,67.92480413172974 +2024-09-05 00:00:00,5,24.589800879683995,22.378127383810465,44.2640855166108 +2024-09-05 01:00:00,5,20.42100858011596,21.116725823435726,51.79214376845052 +2024-09-05 02:00:00,5,16.21540509776142,18.333225827516987,53.7658978314069 +2024-09-05 03:00:00,5,24.732066940434446,24.13978372496059,50.5166623455096 +2024-09-05 04:00:00,5,33.315606659617714,22.06683109509859,68.21199983038059 +2024-09-05 05:00:00,5,23.58280450329778,20.600354271063445,39.304089482258064 +2024-09-05 06:00:00,5,33.90301018057239,25.441258107684966,76.997241978241 +2024-09-05 07:00:00,5,12.887263752651561,24.303197389369473,43.017973640635915 +2024-09-05 08:00:00,5,19.65988206901371,25.59357983870858,42.939858151051844 +2024-09-05 09:00:00,5,16.491879762412594,24.219619976392373,48.57927663324086 +2024-09-05 10:00:00,5,22.056408739081718,22.97087792099162,64.83301989199448 +2024-09-05 11:00:00,5,39.98963557460593,27.023906400382533,54.662538283219625 +2024-09-05 12:00:00,5,29.0380175308374,28.498232859149706,48.88014713006462 +2024-09-05 13:00:00,5,26.043804514816372,22.715615870898553,61.48687408471251 +2024-09-05 14:00:00,5,34.88986029238006,20.24181323739547,68.32656903067912 +2024-09-05 15:00:00,5,18.487784451677367,21.778634139075095,57.52298498923255 +2024-09-05 16:00:00,5,13.219843937201446,28.992820734083896,63.22262706396624 +2024-09-05 17:00:00,5,37.34451264220914,26.4081067628526,62.14762225729861 +2024-09-05 18:00:00,5,31.289360159142,27.460306946834876,51.50174630549731 +2024-09-05 19:00:00,5,30.89038513508653,25.593548554846336,70.53937080593607 +2024-09-05 20:00:00,5,21.928641442369827,27.305280422185533,42.73690138909771 +2024-09-05 21:00:00,5,36.42430693770105,29.102179732600952,49.78166683541933 +2024-09-05 22:00:00,5,21.153350443223754,26.455611686463634,48.93718870699718 +2024-09-05 23:00:00,5,27.127565034882796,23.181546525468175,51.31104661441638 +2024-09-06 00:00:00,5,27.634650369977145,23.667006734107968,43.71641152043089 +2024-09-06 01:00:00,5,7.497110330292372,23.36417387788442,63.70957873200914 +2024-09-06 02:00:00,5,20.96311408023476,22.119479543504173,48.60881514702793 +2024-09-06 03:00:00,5,25.365828154856125,20.65205824621721,40.06196634933939 +2024-09-06 04:00:00,5,17.833651468783334,28.965456379995377,56.895092673574155 +2024-09-06 05:00:00,5,2.859114641808876,26.47712466313701,64.84362054896668 +2024-09-06 06:00:00,5,10.406691621533884,19.765218557959273,37.82470418654605 +2024-09-06 07:00:00,5,41.26119748429174,22.779381122517172,52.10930951403149 +2024-09-06 08:00:00,5,22.634704166518663,23.707406953163403,54.229723977207826 +2024-09-06 09:00:00,5,26.328265391417958,19.46643463019255,65.50237388482655 +2024-09-06 10:00:00,5,19.093788985803357,25.10585285912922,57.98288154332184 +2024-09-06 11:00:00,5,32.24714426554617,21.784955792604247,55.09130724516234 +2024-09-06 12:00:00,5,26.709895125468847,22.39559364234611,51.83843443395167 +2024-09-06 13:00:00,5,25.43636654017598,28.921535067534673,54.899864086788206 +2024-09-06 14:00:00,5,20.49398945840842,23.25287662002276,53.86270955699972 +2024-09-06 15:00:00,5,27.45091894555727,26.425115105999513,57.59241589154062 +2024-09-06 16:00:00,5,25.424296728277383,22.309422310460835,62.68498246952855 +2024-09-06 17:00:00,5,6.910519228030164,28.097171155216014,48.24451828485991 +2024-09-06 18:00:00,5,24.252553968157635,26.831000733721314,67.73107943413707 +2024-09-06 19:00:00,5,25.588571977509982,26.285318121590386,53.23067474178642 +2024-09-06 20:00:00,5,25.115711460744755,23.92225316668393,59.46930482077469 +2024-09-06 21:00:00,5,7.494154752913062,24.585245352636438,46.42243674805692 +2024-09-06 22:00:00,5,16.148821523470783,21.224225742971193,70.38794104696395 +2024-09-06 23:00:00,5,23.975486663786885,22.971118600578514,61.437315470161366 +2024-09-07 00:00:00,5,28.03835903786585,18.16539863034924,53.659446296365815 +2024-09-07 01:00:00,5,7.616674053561654,25.992961003748228,57.16795199207422 +2024-09-07 02:00:00,5,31.29356866701679,21.66239300080246,58.3024611083571 +2024-09-07 03:00:00,5,16.202988266942,24.005377357531888,54.82632906938778 +2024-09-07 04:00:00,5,38.03181590773205,22.434435358135076,57.764602201312854 +2024-09-07 05:00:00,5,26.221380391531557,28.727459879498095,60.55850966186672 +2024-09-07 06:00:00,5,20.894421136976266,29.00108151630053,64.53890862227593 +2024-09-07 07:00:00,5,22.281580390544963,22.30254482810404,49.06681685656887 +2024-09-07 08:00:00,5,25.716130473360085,18.45399602513162,49.26228115755979 +2024-09-07 09:00:00,5,18.97016280302262,22.62402786569085,47.951891598807066 +2024-09-07 10:00:00,5,21.688872731172076,23.636423730201184,57.35247596943377 +2024-09-07 11:00:00,5,14.62114443032676,25.96740424072412,46.12718113292276 +2024-09-07 12:00:00,5,20.373193770990664,19.96469810575654,47.02538812072482 +2024-09-07 13:00:00,5,16.7928706248311,27.446901048513887,46.677990674728846 +2024-09-07 14:00:00,5,29.811631963350855,19.258485051720495,60.930392230993085 +2024-09-07 15:00:00,5,26.556744053527737,21.988983954027187,62.2008441916734 +2024-09-07 16:00:00,5,14.762041079035003,21.242161817234965,58.69133494665559 +2024-09-07 17:00:00,5,13.588281473219075,26.137249253035737,59.47175585414124 +2024-09-07 18:00:00,5,29.597111390184686,27.516306491587645,56.675336335011174 +2024-09-07 19:00:00,5,26.24098643474891,31.61619341660719,48.26629218871762 +2024-09-07 20:00:00,5,15.761236807926888,20.794977074278165,48.92553378068095 +2024-09-07 21:00:00,5,41.18117103281138,26.48410392500267,60.75597811874596 +2024-09-07 22:00:00,5,24.291220823452065,26.683985751993205,46.499163347066784 +2024-09-07 23:00:00,5,17.381856726090756,27.73736078807152,44.902545116637036 +2024-09-08 00:00:00,5,42.127934289127396,27.166414443339644,44.19326728297948 +2024-09-08 01:00:00,5,9.300040824437382,22.50784337397736,53.201955092986395 +2024-09-08 02:00:00,5,24.18166581706835,19.330335096907188,29.096117019819314 +2024-09-08 03:00:00,5,38.660051951454925,26.571652002079137,46.660240619119854 +2024-09-08 04:00:00,5,19.60766512978295,28.600388193851504,65.32900729468768 +2024-09-08 05:00:00,5,36.432733708570815,30.89208572903058,60.33492531227324 +2024-09-08 06:00:00,5,15.25103589589622,18.188585874103815,55.83574390441973 +2024-09-08 07:00:00,5,28.06318562414209,20.189664866151386,46.131754567710615 +2024-09-08 08:00:00,5,17.691844012712018,23.509159186805224,45.60875087801003 +2024-09-08 09:00:00,5,23.460102837586653,27.021587435930556,40.960948258861045 +2024-09-08 10:00:00,5,26.50472306698877,22.269355441154797,51.97581036060104 +2024-09-08 11:00:00,5,18.293966494564415,19.20542431997206,54.52469900646632 +2024-09-08 12:00:00,5,16.709393133883736,22.72654887974813,57.22101150117111 +2024-09-08 13:00:00,5,16.482860066809337,22.06494909338179,51.72975578998983 +2024-09-08 14:00:00,5,30.390272377944264,26.383864891325526,52.669308296443916 +2024-09-08 15:00:00,5,49.13182928952758,19.896856270715862,48.51649922340008 +2024-09-08 16:00:00,5,15.287038096296916,21.0454435367564,45.85984576579371 +2024-09-08 17:00:00,5,26.351707833597523,26.44612795345125,63.816254185828136 +2024-09-08 18:00:00,5,29.670362694310672,29.982382090081302,76.09065243125514 +2024-09-08 19:00:00,5,30.81495185488463,22.844446424706867,51.6131894179779 +2024-09-08 20:00:00,5,15.724843842682194,16.878045975437054,63.39041823647851 +2024-09-08 21:00:00,5,15.644850381398602,22.96617702527802,46.02203639468345 +2024-09-08 22:00:00,5,30.101539512060903,25.793813657187,40.20907436279321 +2024-09-08 23:00:00,5,16.673935731536353,20.527722986429612,45.33582991735455 +2024-09-09 00:00:00,5,32.34313309667339,24.24678587558416,56.430016525041175 +2024-09-09 01:00:00,5,12.341288619389445,30.800678519591813,62.467039024788875 +2024-09-09 02:00:00,5,35.37174019237003,27.616015538364667,57.593637636296066 +2024-09-09 03:00:00,5,32.21897442629284,26.43560283748528,71.30562125361801 +2024-09-09 04:00:00,5,28.240037022036127,25.97362886623552,39.19396428257751 +2024-09-09 05:00:00,5,9.017420297299065,21.73204582542054,50.126646438487526 +2024-09-09 06:00:00,5,36.14689785308101,21.48855447593406,44.86542629209061 +2024-09-09 07:00:00,5,26.767642457220624,27.812057697707434,80.53744961261292 +2024-09-09 08:00:00,5,20.318510759448603,24.60246502202183,27.444695890363867 +2024-09-09 09:00:00,5,23.936197628502278,21.808205705250543,42.27843209762969 +2024-09-09 10:00:00,5,12.374711401993732,21.655690769616356,59.934166759483475 +2024-09-09 11:00:00,5,25.316832985710306,15.12411207715787,53.784930468059315 +2024-09-09 12:00:00,5,25.627283369493703,18.03829392071708,44.43651168533382 +2024-09-09 13:00:00,5,15.807879729497659,24.55461195611841,64.07180036690696 +2024-09-09 14:00:00,5,28.819160958583932,26.327211778383298,47.294140005728465 +2024-09-09 15:00:00,5,38.13731633478654,26.29745232089041,51.192145171576065 +2024-09-09 16:00:00,5,0.022041090508455596,31.123464309615315,43.02015579263288 +2024-09-09 17:00:00,5,29.49245138621758,19.530515765918892,56.2378057273998 +2024-09-09 18:00:00,5,24.693272452791742,22.694816146511503,39.30535213167836 +2024-09-09 19:00:00,5,17.42984243552045,25.721991986064857,57.270675025649254 +2024-09-09 20:00:00,5,18.853119767341852,27.93110993813844,58.30935507160489 +2024-09-09 21:00:00,5,7.774724104444854,24.383133176535374,37.65495416172205 +2024-09-09 22:00:00,5,25.300255932642724,26.808132212267815,52.96150461984597 +2024-09-09 23:00:00,5,18.00839793309202,28.06504957764217,51.477769402878934 +2024-09-10 00:00:00,5,19.39081323353558,20.53303718556828,57.455778816977215 +2024-09-10 01:00:00,5,26.316063059295992,18.96599578551351,48.609009539287904 +2024-09-10 02:00:00,5,29.155544511403605,25.172168196737864,53.0622764179175 +2024-09-10 03:00:00,5,32.35058137869072,23.99711154969658,45.90835677828395 +2024-09-10 04:00:00,5,23.14946295701497,20.09458679850841,57.93276161941543 +2024-09-10 05:00:00,5,30.67446692151442,26.14687893486178,62.91863096382877 +2024-09-10 06:00:00,5,33.50868900918616,22.751834687997942,58.53859352084124 +2024-09-10 07:00:00,5,28.381546749199956,26.96902619199091,31.899895845019635 +2024-09-10 08:00:00,5,45.243686420728864,24.370596836618102,45.01447999833761 +2024-09-10 09:00:00,5,41.19474399161039,24.979238854714026,55.30448114175875 +2024-09-10 10:00:00,5,25.764772245963965,25.519243183919652,48.16575674841167 +2024-09-10 11:00:00,5,21.47100862051868,22.370800038489033,45.861840633675406 +2024-09-10 12:00:00,5,16.201790876752344,23.272604390837945,53.707231648783186 +2024-09-10 13:00:00,5,21.211032799261822,25.988243567352463,35.81650308259546 +2024-09-10 14:00:00,5,23.327740588152967,27.182245107208534,45.599930854624986 +2024-09-10 15:00:00,5,26.126980037199566,20.89604288867704,49.70043843471498 +2024-09-10 16:00:00,5,31.28326515540728,20.331788211563726,68.65225557619532 +2024-09-10 17:00:00,5,14.689030214788097,24.712226116147306,62.348247276168486 +2024-09-10 18:00:00,5,10.970477031486443,26.505279595952313,71.04662674796629 +2024-09-10 19:00:00,5,9.648262584292171,23.35049064681983,47.45756109480815 +2024-09-10 20:00:00,5,15.645347943149034,25.229209558409256,57.90841872432454 +2024-09-10 21:00:00,5,26.363964518650562,25.623144420169613,64.61029910571851 +2024-09-10 22:00:00,5,26.59916887941466,29.561337667592767,52.25909566837398 +2024-09-10 23:00:00,5,27.555801069923096,22.579703547842993,59.30080202403543 +2024-09-11 00:00:00,5,30.247916359574926,20.542292308797645,42.41938549502612 +2024-09-11 01:00:00,5,28.709188268161128,14.752809831411366,46.84495052935323 +2024-09-11 02:00:00,5,32.289278913885795,22.989154367479127,47.174000418031014 +2024-09-11 03:00:00,5,25.356687764205248,24.142605500352772,68.69053687317997 +2024-09-11 04:00:00,5,36.115667623414126,22.43778922949568,52.74393550223654 +2024-09-11 05:00:00,5,9.781450579827414,23.59852225317517,55.00963056689214 +2024-09-11 06:00:00,5,12.251082491924173,32.101711836344734,58.39376740843928 +2024-09-11 07:00:00,5,13.850341661755373,19.00650762129888,63.91458811283826 +2024-09-11 08:00:00,5,20.5961171553886,26.140902234990538,70.34837592029575 +2024-09-11 09:00:00,5,25.296765209794813,26.715193213479232,51.87157405843868 +2024-09-11 10:00:00,5,34.20479686886087,24.750381969011976,56.07805758893472 +2024-09-11 11:00:00,5,37.15384438775257,27.794715381680078,51.25788822256919 +2024-09-11 12:00:00,5,18.114492509526524,22.195032743306825,45.41677039129068 +2024-09-11 13:00:00,5,17.91112767167592,25.948409176108093,55.14547591472235 +2024-09-11 14:00:00,5,28.294407560146183,29.857065607672485,59.6616278394308 +2024-09-11 15:00:00,5,12.91482441057378,31.568368125288295,58.68968805672854 +2024-09-11 16:00:00,5,26.727037128026424,27.242091954852476,67.70270688813046 +2024-09-11 17:00:00,5,26.663304765221415,28.508115567340337,50.50087625356759 +2024-09-11 18:00:00,5,29.398959035020816,31.274337112847245,38.52112045871202 +2024-09-11 19:00:00,5,36.66009992946463,21.76623368886022,56.985738155479666 +2024-09-11 20:00:00,5,26.809461641691815,18.450405300735234,56.7004813563118 +2024-09-11 21:00:00,5,19.19527715563678,22.174664661171857,53.83691147916216 +2024-09-11 22:00:00,5,31.878389870097635,24.548907003297632,67.68457383465933 +2024-09-11 23:00:00,5,28.17974999897254,26.309888402422775,69.41879822292302 +2024-09-12 00:00:00,5,16.352332170340752,22.0420145738882,51.84907557558812 +2024-09-12 01:00:00,5,27.161282768006846,23.100703358956487,65.73198686550289 +2024-09-12 02:00:00,5,31.214737019470515,16.232873780343425,61.44359844730701 +2024-09-12 03:00:00,5,5.448249954806236,24.4771903296758,50.60675840355664 +2024-09-12 04:00:00,5,27.868354474577213,24.257245489350925,39.079770274293736 +2024-09-12 05:00:00,5,17.96477465260205,21.794852481447162,53.04781086517268 +2024-09-12 06:00:00,5,14.368790926406213,21.361113046799407,59.6221020349341 +2024-09-12 07:00:00,5,22.936939810139187,25.58565485379352,65.21150283515853 +2024-09-12 08:00:00,5,31.390865352165363,22.918182061388446,65.84135131792776 +2024-09-12 09:00:00,5,18.934428892472987,24.66788649859995,56.77773539129313 +2024-09-12 10:00:00,5,17.5237276441675,22.115507622937848,69.79275319283221 +2024-09-12 11:00:00,5,12.587589824242164,24.065849863444768,44.85770117814066 +2024-09-12 12:00:00,5,25.813829941027766,31.241787021307207,33.73503387335286 +2024-09-12 13:00:00,5,17.904305726399894,21.686045246531805,74.64012162699319 +2024-09-12 14:00:00,5,29.60966023927434,28.322207608337713,52.078653665401674 +2024-09-12 15:00:00,5,38.180920645616816,19.720414719079553,59.43716002564457 +2024-09-12 16:00:00,5,23.020109965178882,26.028249936498074,59.79131789499045 +2024-09-12 17:00:00,5,20.337984611900758,27.6758566805589,52.86894635578635 +2024-09-12 18:00:00,5,35.15470912717555,24.551812969428454,48.10562574196693 +2024-09-12 19:00:00,5,27.970062464049672,26.20951455801599,58.817662596031305 +2024-09-12 20:00:00,5,15.664714384031974,27.52118906720056,50.63584817323703 +2024-09-12 21:00:00,5,17.661644349937184,27.6186072964405,60.17682278211447 +2024-09-12 22:00:00,5,21.709908569916443,24.952324923497684,62.5419513371106 +2024-09-12 23:00:00,5,19.41491034943103,24.33888929982659,63.37595945793903 +2024-09-13 00:00:00,5,29.198276329698437,21.616492281839808,48.73977636939989 +2024-09-13 01:00:00,5,33.644180651257194,25.800209432228563,39.12569437770944 +2024-09-13 02:00:00,5,21.159582654607387,28.39293909062501,64.86816281872335 +2024-09-13 03:00:00,5,18.745244283498913,22.544227731933848,44.581432944713725 +2024-09-13 04:00:00,5,26.868745698157973,17.914577622134324,55.638223701765796 +2024-09-13 05:00:00,5,23.295018072402023,19.040405665443682,70.106972251425 +2024-09-13 06:00:00,5,34.596013957891834,22.00262797632652,63.74501329479936 +2024-09-13 07:00:00,5,29.830788144764544,28.048953266577545,46.46216185425966 +2024-09-13 08:00:00,5,40.81315122415805,17.852363969288273,46.78070791363294 +2024-09-13 09:00:00,5,23.3942189777398,23.33335881443076,54.59084561228794 +2024-09-13 10:00:00,5,16.038927090019946,25.34857511751392,38.85165212416073 +2024-09-13 11:00:00,5,21.356948828050225,25.548289024532007,59.27024109692393 +2024-09-13 12:00:00,5,31.186417393669192,21.064411331176803,46.825767230781395 +2024-09-13 13:00:00,5,30.71426164463532,27.96567261899748,40.60955534229306 +2024-09-13 14:00:00,5,16.092487217887783,26.798718895777146,51.69555936227812 +2024-09-13 15:00:00,5,22.294553629465867,28.444871215796034,42.82933604095768 +2024-09-13 16:00:00,5,4.258792373721132,28.168085399506957,61.51726261358736 +2024-09-13 17:00:00,5,37.88658358403116,26.386394142539615,63.7140372524557 +2024-09-13 18:00:00,5,42.76266764621335,26.646752138941775,58.7875851337042 +2024-09-13 19:00:00,5,35.707786692197196,28.58969413054436,52.10090177851046 +2024-09-13 20:00:00,5,19.727647592881105,31.50618800116778,65.53938805661707 +2024-09-13 21:00:00,5,16.064931298632843,25.550238453376632,74.43015568579848 +2024-09-13 22:00:00,5,25.453498700135434,32.57839744638674,59.74361106139006 +2024-09-13 23:00:00,5,34.70217392368271,31.374363756325696,50.34852484683628 +2024-09-14 00:00:00,5,31.16509096751752,21.995436158308614,58.81130834320399 +2024-09-14 01:00:00,5,38.15972753701061,25.794390949128204,51.35760134078085 +2024-09-14 02:00:00,5,13.088811028381869,25.419780651186457,48.490840755891526 +2024-09-14 03:00:00,5,20.82732061411269,21.906072394180548,55.89405748934004 +2024-09-14 04:00:00,5,32.53124495108612,30.735391898417916,64.15238743480872 +2024-09-14 05:00:00,5,32.23660754843871,27.051403625946868,60.84703809716914 +2024-09-14 06:00:00,5,21.86165057198634,23.759795495478677,54.92861489068669 +2024-09-14 07:00:00,5,6.783354697866297,22.523273415231,49.69110126805885 +2024-09-14 08:00:00,5,29.952811351107034,29.695215094467933,59.34763432011642 +2024-09-14 09:00:00,5,8.949846355051417,26.733927718719194,64.8575321609622 +2024-09-14 10:00:00,5,14.321397001290988,23.64720387314508,39.70344624400926 +2024-09-14 11:00:00,5,22.730782449794557,22.648254939484715,65.1543710645636 +2024-09-14 12:00:00,5,20.158456412803748,23.926991883819053,65.0371293246503 +2024-09-14 13:00:00,5,29.681466666562972,32.32913493494028,61.0027621040117 +2024-09-14 14:00:00,5,18.99172621554066,28.41107303664851,52.948008527524095 +2024-09-14 15:00:00,5,26.01075057946357,27.52197206834583,49.76841967677385 +2024-09-14 16:00:00,5,28.162812085673295,24.985386323945644,59.49860577614576 +2024-09-14 17:00:00,5,14.810755411780066,28.178101743358173,52.92026891194265 +2024-09-14 18:00:00,5,18.508585361141897,31.569087368420337,52.4394283217306 +2024-09-14 19:00:00,5,26.11667822967357,26.147013743158844,54.621157971850586 +2024-09-14 20:00:00,5,21.08582976777782,22.39131912076457,49.23421990085655 +2024-09-14 21:00:00,5,32.39182859808787,29.00278278619053,58.797561029625314 +2024-09-14 22:00:00,5,17.072990441761096,29.71484586331149,58.41824058867067 +2024-09-14 23:00:00,5,20.138542161414993,22.26044117396947,66.99527099143278 +2024-09-15 00:00:00,5,27.070899866215214,24.51809004268005,68.48400267877628 +2024-09-15 01:00:00,5,32.808039863362055,19.499758287824108,50.276631082897 +2024-09-15 02:00:00,5,16.091799078682534,22.957980879808115,55.56082865532465 +2024-09-15 03:00:00,5,20.468525915158864,14.498782443508619,60.15047973436361 +2024-09-15 04:00:00,5,16.514477865419295,21.277414519660358,47.56040086172011 +2024-09-15 05:00:00,5,34.28855628180177,28.253358260861198,47.48751293214136 +2024-09-15 06:00:00,5,14.666050105484056,27.567945365998014,55.13977382478727 +2024-09-15 07:00:00,5,18.664330351630362,25.037077817410033,48.78239867231089 +2024-09-15 08:00:00,5,14.536911602081364,21.580857715913442,59.64568398275445 +2024-09-15 09:00:00,5,32.17036016813331,26.696892803053732,63.37919124940941 +2024-09-15 10:00:00,5,23.605676595825496,23.223769340434583,46.41838618909532 +2024-09-15 11:00:00,5,21.56816470388391,23.532224486394757,65.66582761405249 +2024-09-15 12:00:00,5,26.607633027659233,27.873442298627104,55.640618097982156 +2024-09-15 13:00:00,5,25.033664805990348,23.192826116519242,63.38752401822466 +2024-09-15 14:00:00,5,22.754403681994543,27.95722631838642,44.78555480253907 +2024-09-15 15:00:00,5,21.742906073690527,26.852146621662836,52.575509260013185 +2024-09-15 16:00:00,5,17.15549865430947,15.420421148864792,61.69618539887545 +2024-09-15 17:00:00,5,21.00689859089128,22.713372674123434,58.5858540922719 +2024-09-15 18:00:00,5,14.486916358878537,24.494512207941803,57.378310623056265 +2024-09-15 19:00:00,5,24.598163706034594,28.880835854628888,53.61230008974152 +2024-09-15 20:00:00,5,33.265321682058115,23.8510759702251,66.05983224675718 +2024-09-15 21:00:00,5,27.287327115517293,25.982111076994762,50.04368592911384 +2024-09-15 22:00:00,5,31.19713338111452,19.96165130809152,46.60147668910807 +2024-09-15 23:00:00,5,36.744024637189604,20.735974579579317,48.53041343614428 +2024-09-16 00:00:00,5,40.43802758478702,21.151548557600794,48.25805675450097 +2024-09-16 01:00:00,5,24.338698795730295,19.70528244915077,53.36322864190606 +2024-09-16 02:00:00,5,27.35037918999555,26.164203273426256,54.03091197373265 +2024-09-16 03:00:00,5,35.737556944432576,31.145347019853542,46.789323053912014 +2024-09-16 04:00:00,5,20.93607656921751,24.526738343658284,55.93056922998578 +2024-09-16 05:00:00,5,35.476758961993404,27.22773240674859,43.09298261002341 +2024-09-16 06:00:00,5,10.071889434937257,21.555793648698476,61.59174508593708 +2024-09-16 07:00:00,5,29.08907498528245,22.075019134097488,47.334280582267866 +2024-09-16 08:00:00,5,33.844441118996066,26.299595097843667,43.25956400081844 +2024-09-16 09:00:00,5,26.753335319918932,26.459932335840197,61.48545018100319 +2024-09-16 10:00:00,5,25.32856855143775,27.09099287805343,60.1149883715494 +2024-09-16 11:00:00,5,20.425993408738357,21.130875703142607,61.61969866059582 +2024-09-16 12:00:00,5,28.190855741410008,27.641296828248322,58.8453100259031 +2024-09-16 13:00:00,5,24.561837025318383,25.458349468627308,46.43782451885704 +2024-09-16 14:00:00,5,30.78293195941708,19.751808915745837,33.20514627363205 +2024-09-16 15:00:00,5,18.383527771000754,22.04559789375713,63.93194841164754 +2024-09-16 16:00:00,5,20.284042701176755,31.168070212019508,70.19923479101081 +2024-09-16 17:00:00,5,29.446279740688855,18.126354501091217,60.085797354650495 +2024-09-16 18:00:00,5,10.623979832105105,24.341772730035558,52.16860271746664 +2024-09-16 19:00:00,5,24.343974895915462,21.716851340581748,59.54976051090965 +2024-09-16 20:00:00,5,25.25236411990544,28.93525823635575,54.65462425947638 +2024-09-16 21:00:00,5,13.056001381470436,24.74680793501699,61.23431011560385 +2024-09-16 22:00:00,5,40.39846678372911,25.76539682389385,59.379204976187786 +2024-09-16 23:00:00,5,20.503911809615857,25.796873166920193,55.95135197137709 +2024-09-17 00:00:00,5,19.224222302077393,26.95919400477876,57.600216294785966 +2024-09-17 01:00:00,5,0.0,16.27223882236776,62.21318979976198 +2024-09-17 02:00:00,5,44.97449610138844,26.357930615478818,46.91044076965776 +2024-09-17 03:00:00,5,9.896093317688898,21.963343118013835,53.80870251699171 +2024-09-17 04:00:00,5,10.826204920014948,15.251808951123499,47.09820968563421 +2024-09-17 05:00:00,5,27.67342947010988,19.36665865745908,58.35155154557125 +2024-09-17 06:00:00,5,36.23978719426258,25.094783779095565,48.01713920563734 +2024-09-17 07:00:00,5,37.353104025876405,27.240860789673754,52.74344094189536 +2024-09-17 08:00:00,5,28.00873860996047,20.07121846958366,53.38743859518792 +2024-09-17 09:00:00,5,17.763753173452855,19.102886120744056,45.58860693021291 +2024-09-17 10:00:00,5,27.56682824055535,22.369608404441983,56.60436831178732 +2024-09-17 11:00:00,5,37.48316252121863,25.529130776139056,51.06336039385679 +2024-09-17 12:00:00,5,16.987237888262733,22.136541106036457,47.14060095089134 +2024-09-17 13:00:00,5,16.28650045597987,23.93385407921055,49.254416327884805 +2024-09-17 14:00:00,5,34.54236429299485,22.470227481040432,61.82287892471544 +2024-09-17 15:00:00,5,22.660625989922345,28.962967223994553,48.285992634487975 +2024-09-17 16:00:00,5,23.570730739922052,22.320179481513843,58.132266406983625 +2024-09-17 17:00:00,5,27.28092470409812,21.367927874779507,49.74019510280978 +2024-09-17 18:00:00,5,35.48805503169446,31.004716313889485,45.55337333920735 +2024-09-17 19:00:00,5,20.634271771477657,24.668762032470234,61.812191070553645 +2024-09-17 20:00:00,5,25.053476647078877,22.102495001249586,59.35788001243551 +2024-09-17 21:00:00,5,6.27263049332333,22.806869322994892,63.96985938283956 +2024-09-17 22:00:00,5,26.112961522660854,28.754353849714356,51.415175694606646 +2024-09-17 23:00:00,5,12.353419535185637,21.923795177933584,70.72108362605786 +2024-09-18 00:00:00,5,27.752453866856953,26.409489168537437,57.50168135254284 +2024-09-18 01:00:00,5,26.029067246331266,29.036775243886318,44.12178674943565 +2024-09-18 02:00:00,5,28.960212331978823,22.734586626948524,54.35619568874532 +2024-09-18 03:00:00,5,5.278078250574193,20.935424550058368,48.365871589211835 +2024-09-18 04:00:00,5,35.626740435412984,20.183109936828444,58.68090967677621 +2024-09-18 05:00:00,5,28.735811153493003,19.42276010842712,42.04995239641616 +2024-09-18 06:00:00,5,20.817420907379876,22.443805861300397,50.49094841573529 +2024-09-18 07:00:00,5,40.300923855941264,26.883791251367605,49.34280359889388 +2024-09-18 08:00:00,5,28.376732734421562,20.971179119379205,57.87377356383206 +2024-09-18 09:00:00,5,30.711017415484577,22.194856175033458,60.50961286712868 +2024-09-18 10:00:00,5,21.69141831318224,24.058972203491507,67.97463701611856 +2024-09-18 11:00:00,5,22.386744195720468,27.07624143940918,52.79427758241769 +2024-09-18 12:00:00,5,21.8317928079001,29.151996844786552,57.1615152415777 +2024-09-18 13:00:00,5,32.35427721014196,27.24942386309904,45.54211306823618 +2024-09-18 14:00:00,5,20.04687681052192,21.282371601565703,62.15463107704386 +2024-09-18 15:00:00,5,24.807575433620734,24.478855757773843,53.85202998424862 +2024-09-18 16:00:00,5,16.531263698353,21.94380749464417,71.14208713979535 +2024-09-18 17:00:00,5,14.636777027408835,24.61323022440363,65.15488965185122 +2024-09-18 18:00:00,5,22.068921252582644,28.499504007622324,65.07511246231218 +2024-09-18 19:00:00,5,18.97087001553203,24.943409434238525,46.96526122296596 +2024-09-18 20:00:00,5,25.5016401625225,25.06176378238395,69.7864890578046 +2024-09-18 21:00:00,5,15.628912598370908,27.511234399780395,49.877976223632814 +2024-09-18 22:00:00,5,12.318979078296316,26.492700198193784,52.398578918499155 +2024-09-18 23:00:00,5,12.969202856855,25.003129124267307,53.908644151566534 +2024-09-19 00:00:00,5,29.667465799348694,18.94147811342261,52.07471031368508 +2024-09-19 01:00:00,5,6.59054351909513,20.680636202171875,53.056439984516935 +2024-09-19 02:00:00,5,5.662040861868613,15.706707109231967,44.624181268367 +2024-09-19 03:00:00,5,0.9594846526767213,27.117586986611983,59.75577894758609 +2024-09-19 04:00:00,5,31.16383032233661,18.877641638779597,52.914309139108234 +2024-09-19 05:00:00,5,32.99107564689934,23.195539134234977,57.432181911984415 +2024-09-19 06:00:00,5,27.78259684259047,19.16997134442106,58.363625533965745 +2024-09-19 07:00:00,5,24.008066785465445,23.59421590572136,50.52057232947641 +2024-09-19 08:00:00,5,32.176457456648684,25.552950407471016,48.677103950717466 +2024-09-19 09:00:00,5,33.39247617968283,21.342224433683416,57.491017695338634 +2024-09-19 10:00:00,5,38.373502922448026,26.214023485257783,41.88221677150452 +2024-09-19 11:00:00,5,30.838165610086442,25.431808846229174,39.79346392117789 +2024-09-19 12:00:00,5,18.470574364377654,27.095843619246427,51.466537526450175 +2024-09-19 13:00:00,5,36.105031497106815,21.72533614026643,63.647930629599635 +2024-09-19 14:00:00,5,26.645440642693423,23.92741076525404,49.93080775838315 +2024-09-19 15:00:00,5,28.022667529729638,25.86059660290882,59.2637089896647 +2024-09-19 16:00:00,5,8.875749226029415,19.505554440818727,62.87727515950732 +2024-09-19 17:00:00,5,21.408832809448903,27.88232695367088,64.86415341290069 +2024-09-19 18:00:00,5,21.549363552718425,24.71725741605151,55.8592555893241 +2024-09-19 19:00:00,5,21.849568515432814,25.90146247532302,59.108966734964895 +2024-09-19 20:00:00,5,17.010763969713675,23.71958628889294,47.91791613685129 +2024-09-19 21:00:00,5,24.568564161763998,19.870468566144265,43.859894928949544 +2024-09-19 22:00:00,5,21.523007633095983,19.685863949509677,68.78463172764866 +2024-09-19 23:00:00,5,19.10446405782608,26.556288901119707,51.17029917685101 +2024-09-20 00:00:00,5,58.058131457577204,21.160338320650773,52.6777414054233 +2024-09-20 01:00:00,5,20.06536347830646,19.848734791109074,60.889029048050446 +2024-09-20 02:00:00,5,21.48178814655598,23.146121619758727,52.93991856820201 +2024-09-20 03:00:00,5,26.806264092459113,24.31530958110405,43.86960071847211 +2024-09-20 04:00:00,5,17.11311193771389,24.48854171045306,51.26931678153524 +2024-09-20 05:00:00,5,13.450304950493974,25.445726902433393,66.1727797889415 +2024-09-20 06:00:00,5,16.417290726730137,27.59799007385391,44.99364562702064 +2024-09-20 07:00:00,5,25.76668692570441,22.888551677398528,32.044050769944604 +2024-09-20 08:00:00,5,39.166887989013766,26.46665829439508,55.22978601447038 +2024-09-20 09:00:00,5,33.28647089701232,21.329540398117828,35.081927162945064 +2024-09-20 10:00:00,5,31.762090270134536,31.197896463705035,55.44428297702006 +2024-09-20 11:00:00,5,31.926951275360246,24.529866150174065,53.888155272907525 +2024-09-20 12:00:00,5,22.598462073556757,18.50479638313003,56.50081886075629 +2024-09-20 13:00:00,5,30.980814285320843,21.716841999825036,65.46904001988952 +2024-09-20 14:00:00,5,23.5324387025447,25.03831725192035,55.90008372964758 +2024-09-20 15:00:00,5,14.703118634725891,27.4321269775225,53.22520625937469 +2024-09-20 16:00:00,5,30.690087689625937,23.01421807485223,55.53780556761598 +2024-09-20 17:00:00,5,24.731060496656738,24.394110701833583,64.70530218008062 +2024-09-20 18:00:00,5,21.272839781725118,25.245737088264516,54.48435184639215 +2024-09-20 19:00:00,5,18.676080333296454,24.320458601193778,55.83467794717326 +2024-09-20 20:00:00,5,17.674965228281923,24.034017734456928,71.69195120456133 +2024-09-20 21:00:00,5,12.603912616304996,21.843700859290045,53.47336742575004 +2024-09-20 22:00:00,5,27.0658435819284,19.29690930766593,79.8853576101074 +2024-09-20 23:00:00,5,24.74641445641724,20.138093479446297,46.66784092924971 +2024-09-21 00:00:00,5,17.068455954350988,24.23320200682894,48.87439302187131 +2024-09-21 01:00:00,5,16.28808768292559,28.2544297115996,50.76065563143305 +2024-09-21 02:00:00,5,19.45369558163812,17.05998243828908,42.18847207656829 +2024-09-21 03:00:00,5,19.913937137586167,26.88788286992786,51.28963253667248 +2024-09-21 04:00:00,5,19.545765072679295,19.22869511179875,44.30971434911833 +2024-09-21 05:00:00,5,34.297919332089336,24.956646498241533,56.100023634333134 +2024-09-21 06:00:00,5,26.52939916459159,21.98120970598365,42.24256191813923 +2024-09-21 07:00:00,5,24.133881961243667,28.619280977349987,45.325970652837285 +2024-09-21 08:00:00,5,19.80751190001691,19.689218349889586,41.007227102029454 +2024-09-21 09:00:00,5,20.729697828803157,20.446349933742695,64.57420827323799 +2024-09-21 10:00:00,5,27.833649593283386,18.401220194385758,47.12119981539363 +2024-09-21 11:00:00,5,17.409814187178487,28.16831782377489,56.381102414173206 +2024-09-21 12:00:00,5,25.033155518465367,29.566175576685715,47.94679310521181 +2024-09-21 13:00:00,5,30.646819539416242,23.65654076157098,58.85776391212367 +2024-09-21 14:00:00,5,17.328633994173153,20.589128949060235,42.57133109528917 +2024-09-21 15:00:00,5,28.876389365746093,24.257153642219425,66.80740125787153 +2024-09-21 16:00:00,5,26.504950910352033,28.911942678928597,66.15888967521474 +2024-09-21 17:00:00,5,23.740167867692975,23.93177707322389,40.20759661922315 +2024-09-21 18:00:00,5,31.13461275273526,21.955071035740644,53.58769115434193 +2024-09-21 19:00:00,5,25.041882210825747,21.60561406309113,63.14506172640098 +2024-09-21 20:00:00,5,20.068949164386883,23.089795187892253,59.91850536443896 +2024-09-21 21:00:00,5,24.71654051034623,27.702496712152158,45.56188571167667 +2024-09-21 22:00:00,5,16.80034358632981,26.36919237375443,56.721730547035655 +2024-09-21 23:00:00,5,43.259210703987335,26.371700240181063,61.625859707945736 +2024-09-22 00:00:00,5,20.348964120068878,22.164493234629234,49.11261153566994 +2024-09-22 01:00:00,5,12.518020876375468,28.442185175022065,45.56706301348178 +2024-09-22 02:00:00,5,31.066067312311375,23.891934326314068,54.316150851104716 +2024-09-22 03:00:00,5,14.12076408232505,18.77260755640708,52.68713815635786 +2024-09-22 04:00:00,5,12.602300764967929,26.120417350482672,38.23462193214635 +2024-09-22 05:00:00,5,30.693994332000145,26.7564091717945,40.227175955264244 +2024-09-22 06:00:00,5,18.595145438451446,24.849170306853186,54.57722432493331 +2024-09-22 07:00:00,5,18.24174715456675,22.357023413496535,42.42054111084688 +2024-09-22 08:00:00,5,19.164709843606783,26.42136657633808,27.995937112034312 +2024-09-22 09:00:00,5,20.8749257258107,19.96278045743187,46.07875735873237 +2024-09-22 10:00:00,5,21.094971291217664,24.610109370279275,60.6227325536843 +2024-09-22 11:00:00,5,33.64705654149928,24.261595087965254,57.064240098616224 +2024-09-22 12:00:00,5,28.318581102005666,21.441538224379762,61.41807503608112 +2024-09-22 13:00:00,5,20.59755038289547,22.115878036388516,60.229233099195525 +2024-09-22 14:00:00,5,38.34652273672621,23.38912760808003,56.95824571593068 +2024-09-22 15:00:00,5,23.8317532665012,18.850028038533164,70.65883904334608 +2024-09-22 16:00:00,5,14.144206858299349,24.058290422973712,47.587954858179955 +2024-09-22 17:00:00,5,8.017448943592642,20.66555806472712,60.78847872524861 +2024-09-22 18:00:00,5,16.740397597952363,27.784080309983157,53.18112901919517 +2024-09-22 19:00:00,5,32.078216505719766,30.485644860018297,40.09153145820735 +2024-09-22 20:00:00,5,3.4048976591884923,28.750417129938405,59.7770025609774 +2024-09-22 21:00:00,5,28.755666018873406,20.067081122210517,58.74301405628423 +2024-09-22 22:00:00,5,27.977421554164074,26.857679396898003,79.7380357076889 +2024-09-22 23:00:00,5,17.353341796846937,25.30675806740639,61.93580554597868 +2024-09-23 00:00:00,5,22.11654633010973,25.222976220245748,57.53547485321939 +2024-09-23 01:00:00,5,18.61400197268209,19.27665835241646,45.128624745931354 +2024-09-23 02:00:00,5,16.816245167228796,23.542985186529634,56.494434541166534 +2024-09-23 03:00:00,5,25.4555748559316,17.65866274157235,46.315818561680565 +2024-09-23 04:00:00,5,13.45806526858895,19.93379186718656,55.220525541067914 +2024-09-23 05:00:00,5,37.09313274495609,23.09417780193752,50.78704842306085 +2024-09-23 06:00:00,5,28.033759224734972,24.839020923537785,54.69186661714503 +2024-09-23 07:00:00,5,20.27220264548278,24.766329592209335,55.67734175725089 +2024-09-23 08:00:00,5,31.302877685642112,26.676832862044606,37.482218668284894 +2024-09-23 09:00:00,5,32.39758773821073,25.01835006363445,72.69789026172697 +2024-09-23 10:00:00,5,23.50022441024612,22.30699656015298,64.62681522378254 +2024-09-23 11:00:00,5,23.024306152413427,25.241798323251484,53.93874260288466 +2024-09-23 12:00:00,5,22.24789662006586,24.504510769881687,57.151942923720604 +2024-09-23 13:00:00,5,26.101399509006587,20.019567175563708,48.05614428652909 +2024-09-23 14:00:00,5,22.846238873909755,27.12418301583306,51.623494469783054 +2024-09-23 15:00:00,5,24.27462096348184,24.67540837128439,60.72938097241635 +2024-09-23 16:00:00,5,25.007757933495913,17.077739652602073,55.73885634248836 +2024-09-23 17:00:00,5,31.05068919328187,23.641037538322657,61.61881270458346 +2024-09-23 18:00:00,5,21.140152862450176,24.17758698726152,53.19356887123704 +2024-09-23 19:00:00,5,16.635218066548955,22.5438679426926,57.28052384206683 +2024-09-23 20:00:00,5,26.441090509367633,22.938426688757495,62.00231025844288 +2024-09-23 21:00:00,5,23.443853815543243,26.830391645185657,63.01803509636395 +2024-09-23 22:00:00,5,31.678199917797976,25.78225183813781,56.176782284884 +2024-09-23 23:00:00,5,18.364840095787983,25.374389698427116,70.44458494520016 +2024-09-24 00:00:00,5,9.342430449939103,18.11281299308249,68.80560235025301 +2024-09-24 01:00:00,5,19.363300784394443,20.719088400171636,57.43116293780553 +2024-09-24 02:00:00,5,25.770717975209035,30.836403873509603,43.87822903287523 +2024-09-24 03:00:00,5,36.7434199967084,27.145933839057847,62.17428795413479 +2024-09-24 04:00:00,5,18.88539202616632,21.674150087641923,47.576581314761015 +2024-09-24 05:00:00,5,21.820079082638333,22.44002778089693,54.83518204699321 +2024-09-24 06:00:00,5,25.807766940562512,28.95628765706909,46.73795584740021 +2024-09-24 07:00:00,5,25.940796448954238,19.99732813288835,52.17809952897163 +2024-09-24 08:00:00,5,9.867702777279108,24.27370850975493,53.347952477864816 +2024-09-24 09:00:00,5,18.912480032762435,18.167200053425514,53.65351143736675 +2024-09-24 10:00:00,5,30.59122474966943,28.114759011867406,46.97270611934154 +2024-09-24 11:00:00,5,29.843141568431964,22.216487986066657,53.563141238307196 +2024-09-24 12:00:00,5,11.683372121466904,23.37599531062435,60.37560706439628 +2024-09-24 13:00:00,5,22.366885502730888,17.45950761659739,69.01726776445395 +2024-09-24 14:00:00,5,25.562762696023142,19.19095730440099,37.65099287412106 +2024-09-24 15:00:00,5,16.559499101295195,26.360768387509324,63.80659475320545 +2024-09-24 16:00:00,5,18.95690749049852,24.01730981961468,54.903488464166834 +2024-09-24 17:00:00,5,27.947484122047236,21.45188032405516,75.02921946593237 +2024-09-24 18:00:00,5,31.11542287276464,21.39220300313737,69.92310714078263 +2024-09-24 19:00:00,5,21.54485211559237,22.15516767163282,69.61743010408192 +2024-09-24 20:00:00,5,17.712892322874914,26.906063708538255,63.04020659543943 +2024-09-24 21:00:00,5,31.804511772462842,24.95893033121381,71.94445051095782 +2024-09-24 22:00:00,5,16.081224079270793,26.04865915812995,44.10954911258795 +2024-09-24 23:00:00,5,25.351253201305898,25.260362336001467,59.94614133705756 +2024-09-25 00:00:00,5,13.252394226057724,20.05830988573345,55.718096044660655 +2024-09-25 01:00:00,5,36.26951594389581,20.463681648178717,52.50920638383119 +2024-09-25 02:00:00,5,14.021994179044615,23.891783902065868,62.63268010494727 +2024-09-25 03:00:00,5,0.0,21.64265752851548,70.32395366686674 +2024-09-25 04:00:00,5,2.052576610189444,21.416967982521847,47.43821270037708 +2024-09-25 05:00:00,5,29.863817803840682,18.46750136978668,67.60171535363781 +2024-09-25 06:00:00,5,34.13136254160963,14.966785176097863,46.45739751219008 +2024-09-25 07:00:00,5,32.13085603068449,21.6478238593156,40.19907462981649 +2024-09-25 08:00:00,5,31.91330221964165,21.06391428020882,57.0837167799498 +2024-09-25 09:00:00,5,21.52204151952332,31.715117414511525,53.8666296091152 +2024-09-25 10:00:00,5,27.605910279817003,22.212612958244197,53.787150236593995 +2024-09-25 11:00:00,5,19.469065259857715,28.406939094429156,64.2068439810913 +2024-09-25 12:00:00,5,30.722680668854256,24.826926772243873,59.93653529088773 +2024-09-25 13:00:00,5,24.592888808588874,29.386142703097565,59.18004914427223 +2024-09-25 14:00:00,5,6.973449380944231,20.376006270416514,59.01256690528024 +2024-09-25 15:00:00,5,32.96088488509412,21.89353130314802,73.43087710491682 +2024-09-25 16:00:00,5,19.34455050019578,22.139715451698706,57.87692830642922 +2024-09-25 17:00:00,5,32.37178313863731,28.122378117052236,67.02900762290938 +2024-09-25 18:00:00,5,27.12104619737159,24.10735887662772,43.64643447559511 +2024-09-25 19:00:00,5,30.97129759716117,30.033073992850984,46.77248914674449 +2024-09-25 20:00:00,5,7.885019601147519,23.20880598230191,62.2386429865827 +2024-09-25 21:00:00,5,10.755904260108496,22.75835355924041,56.83574563769664 +2024-09-25 22:00:00,5,27.259131720485982,18.39862914627072,56.869159203992595 +2024-09-25 23:00:00,5,31.519810739064255,32.80480479340357,57.317341575352735 +2024-09-26 00:00:00,5,44.671276834093675,14.510783595521973,60.60672386171488 +2024-09-26 01:00:00,5,18.94081165909938,22.13464763674684,56.96574532964406 +2024-09-26 02:00:00,5,27.54781213275622,26.81848807308335,44.678920272841694 +2024-09-26 03:00:00,5,11.233507124789156,29.483071467841953,54.54761624655332 +2024-09-26 04:00:00,5,21.894899548764496,24.602201837658523,47.676870968982634 +2024-09-26 05:00:00,5,35.497307553114844,23.023621824309522,61.629461498250066 +2024-09-26 06:00:00,5,18.163023111990924,26.040115228056067,65.11154832723919 +2024-09-26 07:00:00,5,28.620056183476283,25.40448183867887,56.84480647294624 +2024-09-26 08:00:00,5,23.342663876572743,26.427908920201414,47.734329347406494 +2024-09-26 09:00:00,5,25.164077413779033,23.791841360389636,65.84535439586618 +2024-09-26 10:00:00,5,11.111592118572023,22.657877888799746,60.921414149944646 +2024-09-26 11:00:00,5,31.62432390252325,16.676320890244288,50.13488346551047 +2024-09-26 12:00:00,5,27.738104335307604,24.232475494686355,53.89082221459551 +2024-09-26 13:00:00,5,33.62873701468415,28.255209123491866,62.52113259368389 +2024-09-26 14:00:00,5,21.29357035159062,32.843766008251734,41.87997924488435 +2024-09-26 15:00:00,5,28.875598255372097,26.014104145217043,56.80517731276416 +2024-09-26 16:00:00,5,29.322499480767966,21.892925791587245,63.87927326231078 +2024-09-26 17:00:00,5,21.275829744220477,23.697569874269348,49.85825224712674 +2024-09-26 18:00:00,5,38.895846328983765,18.734096893770154,53.176018879541424 +2024-09-26 19:00:00,5,19.057921092503978,27.228973197846944,48.53338508057825 +2024-09-26 20:00:00,5,29.39056967254283,23.255108218695263,48.486560921106665 +2024-09-26 21:00:00,5,7.304355323240319,33.30981656692823,48.72080576451874 +2024-09-26 22:00:00,5,30.030771835465604,26.435689556942577,63.57442655659026 +2024-09-26 23:00:00,5,12.534490378714622,29.963606724150846,39.0130263946018 +2024-09-27 00:00:00,5,17.77486624920096,26.415812502084655,46.60746634352598 +2024-09-27 01:00:00,5,28.517362736991792,22.07635804091445,62.292782023491355 +2024-09-27 02:00:00,5,29.542329581180358,18.207070987727633,66.90725843473967 +2024-09-27 03:00:00,5,25.1210923695742,23.190428761105192,56.208265218058756 +2024-09-27 04:00:00,5,18.18230741133796,23.91044720304132,45.082312647432005 +2024-09-27 05:00:00,5,17.513209325704473,22.885785838743537,59.83478489526625 +2024-09-27 06:00:00,5,11.390011908246445,23.090488092054937,62.532433131193756 +2024-09-27 07:00:00,5,40.86139389100395,29.75461735954602,45.15477483928034 +2024-09-27 08:00:00,5,27.606264677195117,25.556786906745568,69.8034103649108 +2024-09-27 09:00:00,5,16.343609582766334,20.707402802065747,50.618775592284706 +2024-09-27 10:00:00,5,21.721996136853765,21.632880375129112,58.70442207397082 +2024-09-27 11:00:00,5,14.899082723645774,23.51723454329827,61.33438480146014 +2024-09-27 12:00:00,5,33.078604610424094,24.417882789616204,31.342448486066615 +2024-09-27 13:00:00,5,19.846634304521302,18.70490719239387,56.37564067365267 +2024-09-27 14:00:00,5,23.10830961009168,27.243419249067443,61.44110116300334 +2024-09-27 15:00:00,5,22.961607419940346,24.6397587902883,43.871522571196294 +2024-09-27 16:00:00,5,44.77943263470084,22.941094936983575,57.415458728536905 +2024-09-27 17:00:00,5,23.423903814025188,26.949560059770672,61.92646190276705 +2024-09-27 18:00:00,5,21.971063353588484,22.475867639262876,58.74420187114414 +2024-09-27 19:00:00,5,15.832922689961643,21.07338569775781,67.21343748977117 +2024-09-27 20:00:00,5,40.099187036499146,24.272374112793056,46.9362942486216 +2024-09-27 21:00:00,5,25.02433646191298,21.08088344010379,57.34872192974046 +2024-09-27 22:00:00,5,8.732157808296046,28.83693389018654,57.98519108860257 +2024-09-27 23:00:00,5,19.863943098760277,22.8463531220312,63.685804646148334 +2024-09-28 00:00:00,5,30.115196127169014,26.16041462939556,48.69126680362153 +2024-09-28 01:00:00,5,16.74427741032995,20.754502778328646,55.46917232686302 +2024-09-28 02:00:00,5,30.803054992674035,23.885217469757734,66.47075547624972 +2024-09-28 03:00:00,5,30.571419387974814,23.176339842618734,61.696575973646176 +2024-09-28 04:00:00,5,20.539655713222068,21.573653435603397,32.18763236246677 +2024-09-28 05:00:00,5,24.642919189902084,24.984797009090645,50.86524422079315 +2024-09-28 06:00:00,5,21.241134260578598,20.47658106060404,44.34074548445086 +2024-09-28 07:00:00,5,10.701280612264418,15.556397064974362,38.69562920577512 +2024-09-28 08:00:00,5,30.81553755161267,24.645310445445894,60.134624036387514 +2024-09-28 09:00:00,5,36.87210237027867,19.5707918659871,66.32212993988166 +2024-09-28 10:00:00,5,34.22176216098909,21.142695609437908,57.221694281841685 +2024-09-28 11:00:00,5,27.711775091848516,28.055461114317975,68.6532056946383 +2024-09-28 12:00:00,5,20.070395217872953,26.932455439883707,46.60227680549343 +2024-09-28 13:00:00,5,28.854239153156684,24.705815137067514,59.70018865123511 +2024-09-28 14:00:00,5,28.571407297375305,20.587689862298145,60.32861581620811 +2024-09-28 15:00:00,5,32.825916525341796,22.107911936647476,62.63913048359606 +2024-09-28 16:00:00,5,28.00197373298795,26.9354209203491,64.15079515044644 +2024-09-28 17:00:00,5,30.23121556395877,30.544246512096947,41.15724102019992 +2024-09-28 18:00:00,5,8.675168079580798,25.83349743342451,55.320572436257045 +2024-09-28 19:00:00,5,22.372948297150398,26.17996777494007,60.467611285260475 +2024-09-28 20:00:00,5,28.290894021105434,22.32032787829599,65.13859248817612 +2024-09-28 21:00:00,5,48.517785090339814,20.733776838204697,54.67483576598959 +2024-09-28 22:00:00,5,25.418976088153784,28.09309826223594,58.59943887143575 +2024-09-28 23:00:00,5,28.862672676359857,21.98124632538768,63.493965561090086 +2024-09-29 00:00:00,5,23.44984881405044,28.123889370391403,63.66790696192929 +2024-09-29 01:00:00,5,23.776465165366176,15.137640618941203,44.90430668821507 +2024-09-29 02:00:00,5,14.34520715429856,23.418911811387087,54.76799927825813 +2024-09-29 03:00:00,5,13.095380190622183,17.430428983548662,49.93583362286452 +2024-09-29 04:00:00,5,14.965756539272945,27.131478272908247,58.66033384625853 +2024-09-29 05:00:00,5,29.159751908100567,20.125548140959737,76.69228376011282 +2024-09-29 06:00:00,5,15.406930150437658,24.915437820957607,42.56843509498469 +2024-09-29 07:00:00,5,13.990181763283445,19.675028236358614,42.49537959713673 +2024-09-29 08:00:00,5,30.164833954992943,20.77262167342745,64.2629213600419 +2024-09-29 09:00:00,5,11.118288132046388,20.612677527449954,49.43729371307377 +2024-09-29 10:00:00,5,31.036532682576496,24.39365414043249,49.20780182805223 +2024-09-29 11:00:00,5,36.61954447179377,27.75530735614192,49.133629477499404 +2024-09-29 12:00:00,5,40.91216216666434,19.506686807883042,52.187366455407336 +2024-09-29 13:00:00,5,30.8703585775375,21.647360273113474,46.68426773092605 +2024-09-29 14:00:00,5,32.155452808052964,26.91182433142048,52.516977575977585 +2024-09-29 15:00:00,5,21.481079543733713,23.321441074539276,29.63617380742326 +2024-09-29 16:00:00,5,18.11962274034497,18.62297656660971,71.61028054118451 +2024-09-29 17:00:00,5,32.741444715146756,24.35889997441432,58.07402361069767 +2024-09-29 18:00:00,5,15.691010232927244,27.782174209694844,52.652247376964226 +2024-09-29 19:00:00,5,19.655637316358963,22.92790029194708,69.40595405498478 +2024-09-29 20:00:00,5,31.80679076975153,26.291786300871085,60.68746950656236 +2024-09-29 21:00:00,5,42.77847739982124,21.363532988232258,48.021484134692116 +2024-09-29 22:00:00,5,33.529409264117106,27.435508290260575,50.85436304984072 +2024-09-29 23:00:00,5,18.315285523897195,22.57474123400617,53.38175153751847 +2024-09-30 00:00:00,5,21.25319920951899,25.530589974106803,43.74971431805425 +2024-09-30 01:00:00,5,26.822441222209783,20.4393461246064,44.45759573763975 +2024-09-30 02:00:00,5,21.52602762628919,23.6448389837544,55.83933819668174 +2024-09-30 03:00:00,5,18.738966421061622,17.70781524441696,57.831752762564506 +2024-09-30 04:00:00,5,17.449897542026854,20.101852016917615,52.533008383600176 +2024-09-30 05:00:00,5,25.288062475419547,15.248113991607124,38.221335532594985 +2024-09-30 06:00:00,5,21.50797148557847,26.351459620894506,64.0456825330191 +2024-09-30 07:00:00,5,24.93583844345097,17.41357897945264,54.64651780979858 +2024-09-30 08:00:00,5,20.82029353622881,20.916305891814417,55.04181970582699 +2024-09-30 09:00:00,5,34.46601033548745,24.01586180410898,37.84797231249345 +2024-09-30 10:00:00,5,22.56285293374252,22.58793789918633,58.356554390419184 +2024-09-30 11:00:00,5,26.68297820178825,24.85855637700644,58.511602933317775 +2024-09-30 12:00:00,5,35.04417173869228,27.27412768862277,73.4543568082412 +2024-09-30 13:00:00,5,19.615352115791943,21.218069385646256,41.809258271638996 +2024-09-30 14:00:00,5,14.071595269604966,16.23910574590996,67.40484770983764 +2024-09-30 15:00:00,5,14.123634018725758,29.285399860269393,53.4490571960932 +2024-09-30 16:00:00,5,30.722700908737377,26.41080290708809,60.16663430032029 +2024-09-30 17:00:00,5,29.642249489951105,25.3939102843933,49.790309728543434 +2024-09-30 18:00:00,5,6.757985022727869,28.238823095888122,60.20808990911493 +2024-09-30 19:00:00,5,22.531464315485653,23.942825597898207,41.40308264290971 +2024-09-30 20:00:00,5,28.54957427596319,26.505866911949376,62.72638141094166 +2024-09-30 21:00:00,5,20.751961138585514,20.138320212022748,64.87530319892797 +2024-09-30 22:00:00,5,31.347118530912965,27.68247917000019,47.36714592026226 +2024-09-30 23:00:00,5,39.3664247432791,29.550124491393852,48.98581008470282 +2024-10-01 00:00:00,5,21.524139528135706,17.342010752898474,70.42624043029363 +2024-10-01 01:00:00,5,27.29248994010582,26.77221771183075,56.322232723711984 +2024-10-01 02:00:00,5,23.31198332492319,26.39513380239801,56.87470548889735 +2024-10-01 03:00:00,5,25.959587877595826,25.47507754112935,48.91829425022003 +2024-10-01 04:00:00,5,13.765322325327578,21.995526688362727,54.23145181426693 +2024-10-01 05:00:00,5,38.174818431427994,23.929604132350455,55.66000056297576 +2024-10-01 06:00:00,5,7.242996384239429,28.738575384040892,66.78079913693622 +2024-10-01 07:00:00,5,16.360970961227537,18.597228829870513,50.79317174875061 +2024-10-01 08:00:00,5,30.877593596178265,21.42848210015503,50.85783949518159 +2024-10-01 09:00:00,5,24.10511688299052,18.209113826053155,51.63732539790223 +2024-10-01 10:00:00,5,31.216521431681212,18.58186691714959,58.452562264399475 +2024-10-01 11:00:00,5,27.76390784091595,22.021559750572095,50.03359980952067 +2024-10-01 12:00:00,5,29.907669132522223,22.26426593775055,52.773861162467604 +2024-10-01 13:00:00,5,28.59997062493743,30.469942196306494,46.01230718105547 +2024-10-01 14:00:00,5,31.987172921894604,24.705019090243436,60.34860368871889 +2024-10-01 15:00:00,5,27.066004501523487,24.244941140892532,65.12468946153031 +2024-10-01 16:00:00,5,13.306645103754445,21.274703438868286,51.174760213970686 +2024-10-01 17:00:00,5,27.714856470032295,27.048730404780517,38.61224699721601 +2024-10-01 18:00:00,5,33.80621669018256,21.684541331721015,56.704965070217725 +2024-10-01 19:00:00,5,36.63338009251808,19.34491372335028,73.7525654064677 +2024-10-01 20:00:00,5,29.953875258255298,25.248926642049874,44.01643694611678 +2024-10-01 21:00:00,5,24.870642668241125,27.15074965892227,61.985590541858706 +2024-10-01 22:00:00,5,23.61149633841377,34.73607902742583,41.07635010743337 +2024-10-01 23:00:00,5,13.559195277387005,30.429580403479925,50.71798469740096 +2024-10-02 00:00:00,5,16.989170469518236,26.576501444470445,51.219027904484065 +2024-10-02 01:00:00,5,20.30402316727291,15.4326909989022,55.41297545890055 +2024-10-02 02:00:00,5,32.40435822379641,20.829495187073242,54.458706394263245 +2024-10-02 03:00:00,5,28.36088157749797,22.524788964676773,47.194216776437614 +2024-10-02 04:00:00,5,26.59417993702703,21.051527786304227,55.60248079584245 +2024-10-02 05:00:00,5,26.130318732293343,24.95019908818022,52.4746656945338 +2024-10-02 06:00:00,5,21.59841310158403,20.643931309531215,55.10036002877915 +2024-10-02 07:00:00,5,23.20530797762772,19.949750986001483,71.4529914885926 +2024-10-02 08:00:00,5,28.977973277465683,23.33830750605441,51.49001657846558 +2024-10-02 09:00:00,5,33.502515720945965,22.736135576687207,55.206194394458905 +2024-10-02 10:00:00,5,12.691411918071278,22.95159780445266,67.81205614863188 +2024-10-02 11:00:00,5,39.096079449510704,25.908113535651708,52.629580277903734 +2024-10-02 12:00:00,5,35.797902447439306,24.958621207293458,57.39807535557285 +2024-10-02 13:00:00,5,35.59502927355895,22.96770778047741,49.79458611259829 +2024-10-02 14:00:00,5,18.55961885005383,20.411109988935266,69.03504599766248 +2024-10-02 15:00:00,5,24.549276470760137,30.171067838067387,57.38477528423186 +2024-10-02 16:00:00,5,25.976549645481217,23.672695820247192,54.79563266489543 +2024-10-02 17:00:00,5,21.003375091542694,23.269754472393117,59.42224651686732 +2024-10-02 18:00:00,5,38.04110332358829,24.376651054664364,61.47874792707558 +2024-10-02 19:00:00,5,13.753107449683878,27.957180494872222,61.22646913760998 +2024-10-02 20:00:00,5,2.0340418106204936,26.17593028209413,51.26186715210004 +2024-10-02 21:00:00,5,26.62633105017022,29.78322087158046,72.0221153436563 +2024-10-02 22:00:00,5,20.7503015796689,25.02964793794256,50.86546918964062 +2024-10-02 23:00:00,5,32.87420159558085,27.51131608372963,40.49066453941223 +2024-10-03 00:00:00,5,16.02580559223992,19.355160072445198,58.70111534681062 +2024-10-03 01:00:00,5,22.718237218088557,22.617197250934684,50.52130279778148 +2024-10-03 02:00:00,5,44.73276398584069,20.992710331848762,65.81868850045021 +2024-10-03 03:00:00,5,37.40114134190999,19.629881222244833,47.57690022639399 +2024-10-03 04:00:00,5,15.768343826281672,28.693626830738854,58.35216274720693 +2024-10-03 05:00:00,5,21.233063458512103,24.285907354228655,57.69024681906304 +2024-10-03 06:00:00,5,21.406150672859567,22.027167935847977,32.19465976732931 +2024-10-03 07:00:00,5,17.098671897194983,27.179906257908687,59.084430179620696 +2024-10-03 08:00:00,5,21.34303989480831,15.945898455465636,63.23236841051615 +2024-10-03 09:00:00,5,21.758413218841806,25.3900139823705,53.32653667546647 +2024-10-03 10:00:00,5,30.590678206150827,25.21296209766482,42.59203324264223 +2024-10-03 11:00:00,5,30.484510343113936,28.54714925442814,44.86698654768574 +2024-10-03 12:00:00,5,20.435366445410427,22.37475351133179,70.23692089919714 +2024-10-03 13:00:00,5,12.258734558534956,24.00497553379704,65.95223953076871 +2024-10-03 14:00:00,5,31.072776769517343,25.604488234845906,49.5941163833731 +2024-10-03 15:00:00,5,26.53234213482134,23.101246252369528,58.26979281902519 +2024-10-03 16:00:00,5,4.635276468780059,22.705256885867108,63.79410793293042 +2024-10-03 17:00:00,5,9.85448251376773,25.937022798167867,62.37966304091395 +2024-10-03 18:00:00,5,32.962665388760904,25.621797119601464,57.696079885462524 +2024-10-03 19:00:00,5,28.229509084341792,25.28906967050893,58.157671355385574 +2024-10-03 20:00:00,5,30.158384631872835,24.09716978851926,56.61657902917601 +2024-10-03 21:00:00,5,33.68070991542383,25.542242206096297,54.66047539317055 +2024-10-03 22:00:00,5,29.201960648479485,24.81428915992002,45.377024050758294 +2024-10-03 23:00:00,5,29.883388191283835,29.471371728311823,60.47835829956435 +2024-10-04 00:00:00,5,20.080689574415022,23.984470340479888,58.29114357424223 +2024-10-04 01:00:00,5,13.152418689092244,22.935107010418445,78.07367342332618 +2024-10-04 02:00:00,5,30.077007171683363,22.45290448589446,51.92849133131733 +2024-10-04 03:00:00,5,13.52626495579291,21.303503843157287,56.60053069252335 +2024-10-04 04:00:00,5,26.708974245683617,23.53656199751541,55.49791826642136 +2024-10-04 05:00:00,5,27.03900067962544,18.993276194110912,63.03813965179749 +2024-10-04 06:00:00,5,21.71666347925938,22.314229012960872,27.73032940913764 +2024-10-04 07:00:00,5,32.27330808149714,26.11342469304109,42.4215735107275 +2024-10-04 08:00:00,5,23.734733719578564,24.02611001969457,43.32623681753383 +2024-10-04 09:00:00,5,19.24403169608063,20.59097002092401,34.89484667740315 +2024-10-04 10:00:00,5,14.681645133448646,20.997036925803947,52.305066375720564 +2024-10-04 11:00:00,5,23.356242033904017,29.47455998779029,56.175102263442845 +2024-10-04 12:00:00,5,19.236336003792335,21.088805860882253,47.448446939802736 +2024-10-04 13:00:00,5,28.27828678166252,27.479016617550414,50.87221398895586 +2024-10-04 14:00:00,5,22.52354722646326,22.38312124920934,47.034260979321246 +2024-10-04 15:00:00,5,23.077699053103384,27.597882376819374,47.93913511926641 +2024-10-04 16:00:00,5,22.583690313965494,27.961745749565843,62.857297194281834 +2024-10-04 17:00:00,5,19.724606977695142,22.929435247930726,73.25797336471318 +2024-10-04 18:00:00,5,42.39523427283859,19.830785897784413,55.051704045954935 +2024-10-04 19:00:00,5,34.565206307406825,25.569882714490923,50.44469696582249 +2024-10-04 20:00:00,5,19.90923181019214,24.84758279112613,56.74502639690477 +2024-10-04 21:00:00,5,27.056684920905,18.525134368240263,74.89498362549065 +2024-10-04 22:00:00,5,25.632643796819888,26.140594773741167,57.91005559236657 +2024-10-04 23:00:00,5,28.509055862273087,27.8454704750631,53.01731550076552 +2024-10-05 00:00:00,5,24.79100762248664,23.20551637781695,44.1927444051131 +2024-10-05 01:00:00,5,26.47134776981784,22.10797931915388,58.23191121334811 +2024-10-05 02:00:00,5,22.01117113594905,22.530920166836545,60.051608712216016 +2024-10-05 03:00:00,5,25.501547754601727,21.40314949659781,45.522754276602406 +2024-10-05 04:00:00,5,25.058277245744364,18.398288088965543,55.98506249660331 +2024-10-05 05:00:00,5,30.807801734064412,17.933139879614334,41.04269405413792 +2024-10-05 06:00:00,5,34.27249037117361,22.462789743551934,36.01267259969881 +2024-10-05 07:00:00,5,18.605830965382786,21.644424011780586,34.204930932694445 +2024-10-05 08:00:00,5,27.31922607523515,21.880125885578202,64.07284682876912 +2024-10-05 09:00:00,5,26.059920094021948,24.77481864075998,57.65548811005178 +2024-10-05 10:00:00,5,22.659549908345877,21.83818927492898,51.479305151494295 +2024-10-05 11:00:00,5,24.12450797516913,24.40919920092947,54.30057792869106 +2024-10-05 12:00:00,5,26.42246444066321,30.38154210220789,57.20464834047799 +2024-10-05 13:00:00,5,8.127291127085847,18.582693133041815,56.34538382423462 +2024-10-05 14:00:00,5,29.462091344340884,25.43603130390077,63.97544224756506 +2024-10-05 15:00:00,5,41.03176682512856,26.87313140822237,53.9172985761892 +2024-10-05 16:00:00,5,33.71215539393678,22.90644608161609,59.4710393180361 +2024-10-05 17:00:00,5,18.84418530547029,28.641407224066675,67.62812920017629 +2024-10-05 18:00:00,5,28.70439879466933,23.111511426075246,50.04857828485871 +2024-10-05 19:00:00,5,30.37243607296864,21.866712606527884,50.12092063432353 +2024-10-05 20:00:00,5,39.48389794108519,24.848018660308707,63.750768632976126 +2024-10-05 21:00:00,5,11.474876820929651,19.174261090700337,40.65732727901697 +2024-10-05 22:00:00,5,31.62060410484657,20.88134010122916,41.9114745677788 +2024-10-05 23:00:00,5,25.263654261811,21.597858221107387,67.84106031743681 +2024-10-06 00:00:00,5,13.52480159859296,20.999443828759542,49.1851668577488 +2024-10-06 01:00:00,5,18.962548996383354,20.707908276401835,51.833665605623914 +2024-10-06 02:00:00,5,30.959388612612507,23.931148848545632,46.80463277989206 +2024-10-06 03:00:00,5,19.125614901863596,19.39181980644574,51.024558815534924 +2024-10-06 04:00:00,5,26.233492812393266,18.86109916705027,53.31247260284715 +2024-10-06 05:00:00,5,15.275333447426288,27.96442023896162,50.431324340416104 +2024-10-06 06:00:00,5,17.479593840648896,23.10956093993411,45.9521313722679 +2024-10-06 07:00:00,5,31.053402140235413,19.48147392604632,65.44885716542763 +2024-10-06 08:00:00,5,27.568056110931263,24.81106709173609,57.8915486682921 +2024-10-06 09:00:00,5,35.09707033976091,20.283532574970586,41.47424134720181 +2024-10-06 10:00:00,5,26.96868865026683,25.929926731494874,53.85983879430069 +2024-10-06 11:00:00,5,30.267522035203395,29.377551901351644,65.60377710937979 +2024-10-06 12:00:00,5,37.428163633781395,21.524775236977426,39.24977086131668 +2024-10-06 13:00:00,5,24.05558100886189,22.40678787732671,35.74350677204246 +2024-10-06 14:00:00,5,7.590071622239744,18.257423725112773,53.29918078256103 +2024-10-06 15:00:00,5,26.7568446322225,21.13730742656445,61.781017138233366 +2024-10-06 16:00:00,5,28.23849809071343,29.173879476134147,61.061603313560994 +2024-10-06 17:00:00,5,23.06769182883908,29.835163410592006,56.541833391164104 +2024-10-06 18:00:00,5,20.73773398941476,30.418880268910677,55.399120029990534 +2024-10-06 19:00:00,5,27.93832417368414,32.11797624065947,54.414930140775944 +2024-10-06 20:00:00,5,26.32798674324167,23.65234861441104,55.361352328221024 +2024-10-06 21:00:00,5,25.47730802617603,24.59239588954276,63.450266529069175 +2024-10-06 22:00:00,5,29.510043535901072,24.073651561001636,62.09807612793491 +2024-10-06 23:00:00,5,14.732674565846558,21.271998680998294,43.43935601932969 +2024-10-07 00:00:00,5,33.312973223247226,26.36556798734609,30.01160614236635 +2024-10-07 01:00:00,5,39.77406136983139,25.996993044258023,37.02100857461059 +2024-10-07 02:00:00,5,22.583566721773696,28.48832620076093,57.13546223725372 +2024-10-07 03:00:00,5,20.16579692716111,21.726852176738525,58.93534275235625 +2024-10-07 04:00:00,5,24.83025172532,24.096288414153953,52.13984838853513 +2024-10-07 05:00:00,5,21.509209956282376,22.914971015596887,53.99477575966684 +2024-10-07 06:00:00,5,26.286953087646125,20.305119869828204,67.26284481659117 +2024-10-07 07:00:00,5,15.532872399967863,24.399938056120533,45.53249626577363 +2024-10-07 08:00:00,5,23.708005961444726,23.177113160739133,59.67618696596846 +2024-10-07 09:00:00,5,23.552697320543075,24.5388448252798,57.27987011547792 +2024-10-07 10:00:00,5,27.88757647251643,25.84122428625418,52.15479746922975 +2024-10-07 11:00:00,5,36.888359172391944,23.709864840946864,57.67381481678986 +2024-10-07 12:00:00,5,24.360527835340235,18.642909598985838,44.233527005539884 +2024-10-07 13:00:00,5,15.995881306426234,23.668794615335823,61.070655856014206 +2024-10-07 14:00:00,5,22.407770873414055,27.55504365803368,65.97292564558575 +2024-10-07 15:00:00,5,29.33793454982602,22.10911470516495,45.37856032571479 +2024-10-07 16:00:00,5,39.99092989014441,20.23856602819573,57.753705999111595 +2024-10-07 17:00:00,5,15.577979254946513,23.933361914147696,50.68950060993765 +2024-10-07 18:00:00,5,14.008052033657416,26.22420241061948,58.137819355998325 +2024-10-07 19:00:00,5,18.52152529078909,24.634148273282293,69.03974270056344 +2024-10-07 20:00:00,5,24.6108238058153,25.146376425008942,58.85580418943174 +2024-10-07 21:00:00,5,37.73926934338326,26.94647998082715,63.01688374203807 +2024-10-07 22:00:00,5,29.327239659948873,22.826722866584763,63.4670098451509 +2024-10-07 23:00:00,5,15.334989545572883,32.54430011117782,48.78187819685159 +2024-10-08 00:00:00,5,28.422836597841275,19.701594440520868,37.1706416964757 +2024-10-08 01:00:00,5,35.190752863653245,16.951647524148672,55.50209737735683 +2024-10-08 02:00:00,5,28.428126941847097,30.837519623936675,38.236838806436495 +2024-10-08 03:00:00,5,20.745085369070072,20.44824461364132,51.30785647558925 +2024-10-08 04:00:00,5,34.349905654673336,20.874081018550328,68.63290069070322 +2024-10-08 05:00:00,5,38.62638081872368,26.38749604558005,47.99397196382445 +2024-10-08 06:00:00,5,22.747493701461725,23.440660142854103,47.707297367782004 +2024-10-08 07:00:00,5,13.312515313993359,29.093387800211694,47.345824491241615 +2024-10-08 08:00:00,5,17.73790066702361,23.131737929494204,63.07349798941414 +2024-10-08 09:00:00,5,26.62751980131454,26.974917615784445,34.66290799073494 +2024-10-08 10:00:00,5,19.067709256361677,27.050221476316786,57.63524470207031 +2024-10-08 11:00:00,5,21.474090641547882,22.866690930215462,40.65802299742527 +2024-10-08 12:00:00,5,19.060001964964304,21.98031739781748,53.664003150917665 +2024-10-08 13:00:00,5,4.165360414005086,26.922849987867053,63.65766920245072 +2024-10-08 14:00:00,5,36.11925541900293,28.28180422835565,57.16120190420461 +2024-10-08 15:00:00,5,9.4158254436999,23.487845818512543,54.02387069980827 +2024-10-08 16:00:00,5,26.47834425119821,28.195164602633078,63.48909897989177 +2024-10-08 17:00:00,5,30.614109520256747,25.759848185280976,62.3212940812169 +2024-10-08 18:00:00,5,21.50051978430169,29.94985634532834,32.14808001023397 +2024-10-08 19:00:00,5,21.79553594231169,30.143238502552407,46.56037012586703 +2024-10-08 20:00:00,5,17.04219344885795,25.93164350898263,55.677074526565356 +2024-10-08 21:00:00,5,3.6939977096135372,29.12220186031899,39.93888060601587 +2024-10-08 22:00:00,5,26.74338440053378,25.62058256892758,60.48984344541711 +2024-10-08 23:00:00,5,26.112955510198578,22.03323541839035,40.37510341901227 +2024-10-09 00:00:00,5,34.31209424457274,21.27562257386981,59.176603212129145 +2024-10-09 01:00:00,5,26.66421226249307,19.907131311970833,47.671358003011385 +2024-10-09 02:00:00,5,20.689697664174528,24.72545640864603,47.99165768857805 +2024-10-09 03:00:00,5,3.3768807682444404,25.71371536365194,52.380953648408834 +2024-10-09 04:00:00,5,10.767150453944659,19.302999726000632,45.45932525403597 +2024-10-09 05:00:00,5,27.492078613927,27.876244398380244,40.27735201336352 +2024-10-09 06:00:00,5,15.019679412011167,22.933050863549273,55.763038681936806 +2024-10-09 07:00:00,5,26.450339494513717,17.140037940126508,55.32013314987911 +2024-10-09 08:00:00,5,19.490044016895656,22.59224529088715,45.021201048921725 +2024-10-09 09:00:00,5,27.39331356892876,27.88588005796572,53.53501180237129 +2024-10-09 10:00:00,5,31.72021066473246,26.17575229578324,49.92783058583914 +2024-10-09 11:00:00,5,15.56672997311765,17.40220659272311,67.82922202041905 +2024-10-09 12:00:00,5,25.376164547723455,25.193429655208604,47.50771632111278 +2024-10-09 13:00:00,5,40.11641136167239,27.234033242313785,51.698740110847815 +2024-10-09 14:00:00,5,27.695360325255674,26.717961665523173,66.40423565205762 +2024-10-09 15:00:00,5,39.464783417882515,29.669266748963793,68.07773930187763 +2024-10-09 16:00:00,5,24.396489906474212,28.14878593125392,65.04591241969977 +2024-10-09 17:00:00,5,38.7864799260372,19.38125328289032,54.773658503151886 +2024-10-09 18:00:00,5,28.988339560481965,23.467150007919265,55.732260181160655 +2024-10-09 19:00:00,5,29.354085824578533,26.331817791565133,65.58730886967781 +2024-10-09 20:00:00,5,27.32827796931182,27.304889224188464,79.0571894814556 +2024-10-09 21:00:00,5,35.97382367870408,24.14126949592252,63.781874755692265 +2024-10-09 22:00:00,5,24.45847302178947,24.644338837748343,61.52968592669523 +2024-10-09 23:00:00,5,10.589451977034535,22.568881198664283,45.95775032472939 +2024-10-10 00:00:00,5,20.36010912382555,21.945598722759772,52.5098763130897 +2024-10-10 01:00:00,5,31.00775668863993,22.49597925600733,45.76399888647239 +2024-10-10 02:00:00,5,19.894501150429576,28.384182543436047,45.2756766033487 +2024-10-10 03:00:00,5,19.864639013679696,20.521585120761458,47.9457433694342 +2024-10-10 04:00:00,5,10.823849235129053,22.61588081039113,43.46398372070206 +2024-10-10 05:00:00,5,11.686654724363017,21.94197602656746,59.35764590666492 +2024-10-10 06:00:00,5,0.0,27.479127933631677,43.28772274352379 +2024-10-10 07:00:00,5,21.17394274804264,18.568163469820004,52.51646181147497 +2024-10-10 08:00:00,5,39.08982956813086,21.34617178038106,51.408979510178895 +2024-10-10 09:00:00,5,40.439607544763525,24.786758322983218,57.67450640497807 +2024-10-10 10:00:00,5,20.483255358373054,28.732025089316522,49.71005921805687 +2024-10-10 11:00:00,5,41.91667463860048,17.457208796465142,60.42671237756057 +2024-10-10 12:00:00,5,25.924663427208,22.64349640630243,44.14712655271292 +2024-10-10 13:00:00,5,22.94997169969325,24.106297703505323,51.0667734065063 +2024-10-10 14:00:00,5,19.815154920254216,18.73615258649438,47.48098320047943 +2024-10-10 15:00:00,5,24.423806147551435,24.888948630700853,62.76177450914667 +2024-10-10 16:00:00,5,19.459527812151954,19.534710800861088,63.47087009657247 +2024-10-10 17:00:00,5,19.380981570454153,29.679392623899748,70.52215441496296 +2024-10-10 18:00:00,5,24.159738049922122,28.781010827618505,75.34649773074699 +2024-10-10 19:00:00,5,30.332173487043192,29.29650118622995,55.12959766924337 +2024-10-10 20:00:00,5,44.29589300595863,20.115744262923407,58.701549240906004 +2024-10-10 21:00:00,5,11.203007824465441,28.616843686618857,41.391356321404984 +2024-10-10 22:00:00,5,29.403024861575915,22.739707272686267,58.21515678537567 +2024-10-10 23:00:00,5,41.172000188110424,25.11859644428296,51.96652264032561 +2024-10-11 00:00:00,5,19.195813934707235,22.32964645052568,41.07211640511356 +2024-10-11 01:00:00,5,13.224193210978088,22.11818758548756,61.51867526575108 +2024-10-11 02:00:00,5,27.803267113261146,21.692008290309793,48.847560555018376 +2024-10-11 03:00:00,5,22.404211623988118,22.76587521526057,60.26311713812 +2024-10-11 04:00:00,5,31.548108878230032,18.990114168728788,54.105880940683186 +2024-10-11 05:00:00,5,19.724790006789952,21.091348819031307,47.96047606039747 +2024-10-11 06:00:00,5,18.279155810047666,26.387537090264793,64.4958608490603 +2024-10-11 07:00:00,5,31.67422640869506,28.175543712566437,60.85228650442159 +2024-10-11 08:00:00,5,17.471453672483953,27.623931449179274,47.95491625477506 +2024-10-11 09:00:00,5,26.76573945875613,21.124436098399983,48.20039877367086 +2024-10-11 10:00:00,5,23.865465788101094,21.607933314341494,50.22118037402883 +2024-10-11 11:00:00,5,14.764879769971811,19.625508295159328,49.06408924877899 +2024-10-11 12:00:00,5,19.5072458184437,25.527794324949106,67.15907947700055 +2024-10-11 13:00:00,5,22.338086905751037,22.675015478651215,53.71900702228658 +2024-10-11 14:00:00,5,31.092636725362784,19.130379547030074,49.32676233341804 +2024-10-11 15:00:00,5,38.21560542327157,24.536861052366266,59.18491239081144 +2024-10-11 16:00:00,5,27.9139269306714,27.364787445241948,55.379706182596834 +2024-10-11 17:00:00,5,17.764592138450467,27.137076772488353,56.66304112490779 +2024-10-11 18:00:00,5,19.459092397428442,14.69830442795969,60.292065170508195 +2024-10-11 19:00:00,5,13.026963109928902,25.92660246106163,58.22961915414956 +2024-10-11 20:00:00,5,19.35272766870856,22.911180669991754,55.21226988620494 +2024-10-11 21:00:00,5,34.75733269540863,21.50473492692194,53.751851610557 +2024-10-11 22:00:00,5,27.993549721609668,23.805829853650383,66.31167869606371 +2024-10-11 23:00:00,5,16.772701139382633,31.35154855345545,48.89738194254335 +2024-10-12 00:00:00,5,9.048098480984514,19.051117418045692,46.953123666339124 +2024-10-12 01:00:00,5,40.974222100871,25.362023178721735,56.691933498573974 +2024-10-12 02:00:00,5,17.226196823152875,17.753271823698476,61.57893950768151 +2024-10-12 03:00:00,5,18.009131124561264,21.710405340641667,51.774340545613235 +2024-10-12 04:00:00,5,38.13600605118995,25.412207594697417,61.13979119742543 +2024-10-12 05:00:00,5,32.81036026548721,21.14059097868546,41.168576634941694 +2024-10-12 06:00:00,5,17.365482119733578,27.792313506213773,50.692420856922766 +2024-10-12 07:00:00,5,23.936963381173236,20.66769853581137,67.5646471497736 +2024-10-12 08:00:00,5,20.438575903494772,30.158305912522643,40.36600427511315 +2024-10-12 09:00:00,5,17.170574144462982,27.652109524914273,42.591248669802695 +2024-10-12 10:00:00,5,32.665055548046496,31.254262924838958,35.97255989370697 +2024-10-12 11:00:00,5,32.488583035058696,17.911480271535268,51.86556761227502 +2024-10-12 12:00:00,5,34.03408457839451,25.477846252628325,53.59040809665351 +2024-10-12 13:00:00,5,21.434272587923836,20.204182850845513,62.61778927365726 +2024-10-12 14:00:00,5,26.288892496449368,27.474332954764908,51.97299565623335 +2024-10-12 15:00:00,5,20.25129143685268,24.782914795702325,52.38779117890513 +2024-10-12 16:00:00,5,19.64647995837612,25.04264829528128,57.393645871222695 +2024-10-12 17:00:00,5,24.637870985043627,22.048231463449227,50.90921684328883 +2024-10-12 18:00:00,5,30.039394849678736,21.69219570359754,60.18534226612597 +2024-10-12 19:00:00,5,0.7979250482940756,25.832842104169693,62.86974255824684 +2024-10-12 20:00:00,5,31.214711908894188,27.258960747726256,40.71432559484256 +2024-10-12 21:00:00,5,10.651700731854115,25.829937272834975,52.1648261551848 +2024-10-12 22:00:00,5,30.378033791350997,27.61593061488365,49.83973892763745 +2024-10-12 23:00:00,5,26.196515174560744,27.90016469044225,62.28073479591069 +2024-10-13 00:00:00,5,5.559137731576685,20.23844500718462,53.255340170351104 +2024-10-13 01:00:00,5,33.56775354488592,16.749624155647453,47.87240523526291 +2024-10-13 02:00:00,5,22.90631206465333,27.871324978929437,62.36817819647541 +2024-10-13 03:00:00,5,15.811565397960411,20.187510579294578,45.85130964200598 +2024-10-13 04:00:00,5,28.10522265685571,27.240404018346254,48.76092974074733 +2024-10-13 05:00:00,5,22.636911065503767,25.049596186259432,58.08686298446235 +2024-10-13 06:00:00,5,12.233135122145981,23.501077094455677,56.49610705497172 +2024-10-13 07:00:00,5,19.680632959222283,23.96344029873309,72.48836023737188 +2024-10-13 08:00:00,5,21.258133897882452,21.38665791632736,50.385515986830924 +2024-10-13 09:00:00,5,13.62060880049059,26.55813473609402,61.36514721861319 +2024-10-13 10:00:00,5,34.713172834065205,25.070222292353428,60.68917944831139 +2024-10-13 11:00:00,5,28.647432100938403,25.73610045546017,59.12855928714563 +2024-10-13 12:00:00,5,22.872736489675763,31.455128338238865,49.41805812966299 +2024-10-13 13:00:00,5,17.394455954485366,21.040220995930998,45.097865689163385 +2024-10-13 14:00:00,5,12.212380697830215,27.161882800774812,68.57508729933167 +2024-10-13 15:00:00,5,21.29781160844327,26.477500900485683,47.33959390296086 +2024-10-13 16:00:00,5,16.282687001965634,20.62040869650947,59.90655920230992 +2024-10-13 17:00:00,5,27.90562174912035,22.11477741074493,58.328708838916796 +2024-10-13 18:00:00,5,14.46435642338068,26.370488492817987,51.18865526726041 +2024-10-13 19:00:00,5,33.37612963615724,29.099039346421346,56.21984465321868 +2024-10-13 20:00:00,5,23.449234537992304,20.91780077746109,61.87810369569726 +2024-10-13 21:00:00,5,22.99182175715422,25.02148397457937,61.823754639670305 +2024-10-13 22:00:00,5,19.512860655817356,26.56459506012537,59.50383901477716 +2024-10-13 23:00:00,5,30.4126824577322,26.378247163969636,56.66161899127759 +2024-10-14 00:00:00,5,23.67894809263955,25.25908481415731,45.594118171776174 +2024-10-14 01:00:00,5,14.620856422094935,17.950946699082763,77.36789068470196 +2024-10-14 02:00:00,5,28.592989397957346,22.718533960148772,69.928873397751 +2024-10-14 03:00:00,5,36.86838826096307,20.09818162735771,60.00916699225363 +2024-10-14 04:00:00,5,23.47266128700138,18.618652628192038,39.157231348359396 +2024-10-14 05:00:00,5,27.024524730493546,22.264856263684134,55.45174474204215 +2024-10-14 06:00:00,5,15.656962235238634,23.295169040499605,41.47832991851637 +2024-10-14 07:00:00,5,23.8591640518143,23.689116052248234,52.336643061263196 +2024-10-14 08:00:00,5,35.848338089290735,14.811186643428956,48.108857494872915 +2024-10-14 09:00:00,5,34.19504719249517,20.64440932148421,69.05143044765344 +2024-10-14 10:00:00,5,29.217459503137675,24.251878033823868,79.70308846455046 +2024-10-14 11:00:00,5,18.226773229081942,20.16206763105987,61.701891835588825 +2024-10-14 12:00:00,5,22.80592824952927,27.532562408887074,38.58490267918262 +2024-10-14 13:00:00,5,26.640059182383325,20.580339663921304,45.817437595636385 +2024-10-14 14:00:00,5,23.01877968411521,28.985139142106252,63.27624204380668 +2024-10-14 15:00:00,5,38.84372046537345,27.737965622896915,56.33941074343241 +2024-10-14 16:00:00,5,15.175187589560352,25.69167743535543,77.11867111541993 +2024-10-14 17:00:00,5,9.237628386234336,27.176650976376255,55.292560850937065 +2024-10-14 18:00:00,5,21.64833817585604,18.956511109270075,61.26031556278966 +2024-10-14 19:00:00,5,22.224180007637088,24.122042484218237,62.88131076096255 +2024-10-14 20:00:00,5,12.221888778544015,26.731352198176975,73.82912839946613 +2024-10-14 21:00:00,5,21.916992101607992,24.084117522301245,39.0233309169366 +2024-10-14 22:00:00,5,24.06559686190969,21.901856046100065,62.89273366369714 +2024-10-14 23:00:00,5,22.43081749524968,22.14694511361793,66.74047486270429 +2024-10-15 00:00:00,5,16.11414809605514,24.21398481771811,62.93176952692163 +2024-10-15 01:00:00,5,17.716804797512147,28.022659484232065,45.507199363538305 +2024-10-15 02:00:00,5,6.034323278937247,25.21712709511645,41.62917270743182 +2024-10-15 03:00:00,5,32.36218143123774,20.637550954947727,76.97557530073476 +2024-10-15 04:00:00,5,29.750984298988197,22.804989539277962,50.761769379384496 +2024-10-15 05:00:00,5,13.454677321682679,23.177923604707154,58.329299071498816 +2024-10-15 06:00:00,5,24.852822367761394,25.11239434250068,64.26396101727862 +2024-10-15 07:00:00,5,15.428133049724064,22.551852027123495,45.872612766059675 +2024-10-15 08:00:00,5,37.21028888915061,27.891293860265748,62.305977627301125 +2024-10-15 09:00:00,5,15.896531120737672,23.42951888181328,44.30719852431501 +2024-10-15 10:00:00,5,26.929697393493118,20.97886978986482,50.0890752554753 +2024-10-15 11:00:00,5,36.085880280634726,23.158299518816218,48.47813266133062 +2024-10-15 12:00:00,5,31.84061473590444,23.34174600157336,46.81008109431858 +2024-10-15 13:00:00,5,29.44627397948337,15.931508252361944,49.75254410106713 +2024-10-15 14:00:00,5,33.46236583385678,27.02037403808994,43.15372943941591 +2024-10-15 15:00:00,5,25.154568092003114,26.41370157643499,57.297304252458005 +2024-10-15 16:00:00,5,21.816890556495096,22.584213050530188,46.33590767134481 +2024-10-15 17:00:00,5,31.206152306395367,20.364010756223568,58.201549838262906 +2024-10-15 18:00:00,5,28.494111716412252,30.726740965731953,48.284021547626374 +2024-10-15 19:00:00,5,21.8608080991412,28.285758156134932,58.81908766936383 +2024-10-15 20:00:00,5,23.796034276109005,24.92054747853535,48.216228940337324 +2024-10-15 21:00:00,5,21.610806660567228,20.864181201171885,61.97341566232241 +2024-10-15 22:00:00,5,24.43418005803096,25.994560640036507,43.14899819253138 +2024-10-15 23:00:00,5,25.15231439938908,27.146586183310305,47.996304438588076 +2024-10-16 00:00:00,5,14.921287957624731,22.454843838622732,40.529323514873454 +2024-10-16 01:00:00,5,25.895664320590868,20.284302885825667,45.127747965017015 +2024-10-16 02:00:00,5,23.432018859728906,24.58922993032887,55.25614082113284 +2024-10-16 03:00:00,5,17.013153581396644,26.003273188059474,55.74548061674477 +2024-10-16 04:00:00,5,28.95140413643975,24.68566068774679,50.03445696140471 +2024-10-16 05:00:00,5,20.08470061577582,15.806032003783404,46.022971727818714 +2024-10-16 06:00:00,5,30.332748637622892,21.7856134232929,72.58852884265785 +2024-10-16 07:00:00,5,26.492993994270122,21.086379939551644,39.75877875116234 +2024-10-16 08:00:00,5,31.399518872164865,21.150515245835074,52.90583966922389 +2024-10-16 09:00:00,5,14.19009585993715,24.25397322980269,61.45841379800228 +2024-10-16 10:00:00,5,23.993524962273952,19.90647817310189,61.87262351730814 +2024-10-16 11:00:00,5,28.901424449979153,26.564016648265373,44.424057203809795 +2024-10-16 12:00:00,5,23.840096860399125,25.637984165220335,56.8100396811045 +2024-10-16 13:00:00,5,28.872745155642015,20.420148891571657,43.59949949556957 +2024-10-16 14:00:00,5,28.290317043396293,25.23841032521751,40.18281046024721 +2024-10-16 15:00:00,5,26.972121421433464,20.08006092375191,55.71784027556722 +2024-10-16 16:00:00,5,23.816230576749422,22.377789394096588,56.069263128450025 +2024-10-16 17:00:00,5,25.2593071630808,23.002106763395897,67.05019200339504 +2024-10-16 18:00:00,5,36.081114870656265,26.27302978159199,71.88670644243228 +2024-10-16 19:00:00,5,19.33264174531188,25.498904944910237,54.52503712103722 +2024-10-16 20:00:00,5,14.856342147980936,32.86350313376151,44.645550437123084 +2024-10-16 21:00:00,5,12.943913272935248,22.613222667000088,49.308244295608624 +2024-10-16 22:00:00,5,15.866214663237239,29.449902010378207,62.66679693095945 +2024-10-16 23:00:00,5,26.593400608314788,20.25727570585522,63.321139101698165 +2024-10-17 00:00:00,5,22.854599616185425,23.75319573974518,33.455002627150016 +2024-10-17 01:00:00,5,19.54879762985297,18.291566523158842,56.803603180199836 +2024-10-17 02:00:00,5,21.07431186108905,22.772593538054185,68.02203284894743 +2024-10-17 03:00:00,5,23.421636280285338,20.77094007323292,52.51335695497768 +2024-10-17 04:00:00,5,22.154130985900824,24.535767502150385,44.71064587163047 +2024-10-17 05:00:00,5,12.856955846352296,17.69525504397808,68.75966161851382 +2024-10-17 06:00:00,5,24.479690362847997,23.181488597003433,50.495188080288145 +2024-10-17 07:00:00,5,38.51577989281085,22.360859142242887,54.701360916433075 +2024-10-17 08:00:00,5,19.415639772857173,23.527128472622042,52.08632191425415 +2024-10-17 09:00:00,5,21.17947060673896,23.56396658794999,38.5687130751134 +2024-10-17 10:00:00,5,30.073285914803087,16.73555035062009,51.59457989869817 +2024-10-17 11:00:00,5,26.861672795869186,20.03415692168516,46.43653839133131 +2024-10-17 12:00:00,5,10.30755015643804,28.299340376586986,56.879524237125366 +2024-10-17 13:00:00,5,20.526302784756115,25.03146706542156,52.85001512046529 +2024-10-17 14:00:00,5,20.271800849425247,28.776159783400107,46.33284178912544 +2024-10-17 15:00:00,5,26.733799200654335,23.132247819855035,51.960556626239715 +2024-10-17 16:00:00,5,39.76926290785363,25.556728587931808,49.45667384506129 +2024-10-17 17:00:00,5,32.32651551866704,24.9168373885181,39.709275292050535 +2024-10-17 18:00:00,5,16.7946339909158,25.941128469438933,54.92431437132788 +2024-10-17 19:00:00,5,28.930851819862873,17.898724477260714,39.731653982008424 +2024-10-17 20:00:00,5,41.682165321833736,27.895895714388878,65.24663438028178 +2024-10-17 21:00:00,5,16.03273898906285,23.175608190600286,61.73642381654666 +2024-10-17 22:00:00,5,20.523875882293886,26.041068580534183,61.89906996636979 +2024-10-17 23:00:00,5,19.800370333661025,22.86427097884299,61.449635908418585 +2024-10-18 00:00:00,5,17.158704299420112,21.169703774088248,51.19086042303982 +2024-10-18 01:00:00,5,11.4907445075863,18.946043924780525,62.103868701423735 +2024-10-18 02:00:00,5,23.788297272406908,25.536906453990756,54.734691115120725 +2024-10-18 03:00:00,5,23.964221565875143,25.235287263249752,53.023503331860255 +2024-10-18 04:00:00,5,5.103082705979563,20.106645763455116,57.95239876715328 +2024-10-18 05:00:00,5,23.815792318678,24.916299293543446,64.9653624052527 +2024-10-18 06:00:00,5,34.59163945644249,22.83865990052728,52.55821471229526 +2024-10-18 07:00:00,5,33.00903478761967,22.63425085641991,67.8159026718993 +2024-10-18 08:00:00,5,15.934676959700443,27.748175155497318,50.286965219186655 +2024-10-18 09:00:00,5,25.894311403191722,23.78520066387191,33.591670467975746 +2024-10-18 10:00:00,5,35.24026429204362,27.4811247200759,60.69660848736291 +2024-10-18 11:00:00,5,11.177130066086663,24.73836573273503,53.67571219318795 +2024-10-18 12:00:00,5,25.95924470828484,24.59904762022304,60.00069482300379 +2024-10-18 13:00:00,5,31.553167174178423,23.465729112194264,53.13773809527063 +2024-10-18 14:00:00,5,26.81121424746993,25.385605153320284,70.30467362447565 +2024-10-18 15:00:00,5,28.144014252507453,22.163337366454922,54.097980500474456 +2024-10-18 16:00:00,5,16.231865327449114,22.26749483329652,60.69045623747233 +2024-10-18 17:00:00,5,25.372100604302723,26.206961981988417,68.74258431133447 +2024-10-18 18:00:00,5,29.32105927285405,35.20423311048159,67.05105177981125 +2024-10-18 19:00:00,5,12.157948448672443,18.8979080969417,49.539869031570554 +2024-10-18 20:00:00,5,17.8334319983432,23.962125553051894,59.803828825909875 +2024-10-18 21:00:00,5,19.983638033576646,27.22528140372807,53.485453012003134 +2024-10-18 22:00:00,5,15.65808487158787,27.644995421149133,52.54152819555346 +2024-10-18 23:00:00,5,32.562091316131806,26.03100008873585,57.927847934234 +2024-10-19 00:00:00,5,29.010681330611934,14.201351452306001,56.599414427946776 +2024-10-19 01:00:00,5,29.07286345514795,23.767028506175322,42.82311502885147 +2024-10-19 02:00:00,5,20.145653832617082,21.041158844788225,57.82703524251882 +2024-10-19 03:00:00,5,32.54756898196917,27.559282905560405,53.34591114306543 +2024-10-19 04:00:00,5,27.613442478525116,24.616258927534776,58.94199768471356 +2024-10-19 05:00:00,5,14.55293652195455,24.737123756485573,76.51471748687163 +2024-10-19 06:00:00,5,18.917346961977913,24.13308460273617,46.567730920274855 +2024-10-19 07:00:00,5,13.656502775236897,25.66733594480084,38.81600637027269 +2024-10-19 08:00:00,5,25.647398067107055,19.804317532455926,55.39862242898713 +2024-10-19 09:00:00,5,39.30505077487672,21.98816828787544,45.19350089788631 +2024-10-19 10:00:00,5,35.59633090525898,23.283232660446426,48.77457257246 +2024-10-19 11:00:00,5,16.776631965562846,18.7021450135694,57.880999354226184 +2024-10-19 12:00:00,5,26.310947748393758,24.092724937539305,43.78029909042508 +2024-10-19 13:00:00,5,38.11306898483352,27.34442032338269,61.555462459091494 +2024-10-19 14:00:00,5,45.61857823245473,27.78933042099603,71.14137354254339 +2024-10-19 15:00:00,5,31.35757278874838,29.926014247949972,43.35137680568998 +2024-10-19 16:00:00,5,28.703342765755348,23.68112435618144,46.79901969143624 +2024-10-19 17:00:00,5,25.550235909106046,26.40253680746173,54.64687212308295 +2024-10-19 18:00:00,5,18.868615842050815,25.615321357357793,52.75018185974913 +2024-10-19 19:00:00,5,20.710608365269866,24.9390751087586,36.689983730958375 +2024-10-19 20:00:00,5,22.346251788545004,21.378274740397725,60.07323595992895 +2024-10-19 21:00:00,5,24.63989600415439,19.532788550365144,56.647628232304776 +2024-10-19 22:00:00,5,25.120210138911943,28.123858306841456,48.26226515097874 +2024-10-19 23:00:00,5,10.525479423106553,20.762574583726746,61.76835269926882 +2024-10-20 00:00:00,5,35.905146435522624,27.58991327231687,59.361611239273074 +2024-10-20 01:00:00,5,16.42473884896961,20.760270701508443,67.30247885996565 +2024-10-20 02:00:00,5,5.624692261568718,23.826669805132475,46.72757801057414 +2024-10-20 03:00:00,5,21.95783456874704,18.01157927713509,54.10986323787099 +2024-10-20 04:00:00,5,40.716014450656154,23.61450368121612,49.65888829059198 +2024-10-20 05:00:00,5,28.242291495205613,18.02322327660044,49.28341282875253 +2024-10-20 06:00:00,5,35.93680390733134,17.775867912611936,54.53603695344519 +2024-10-20 07:00:00,5,31.324800629335886,23.82996072785522,64.3097677113138 +2024-10-20 08:00:00,5,21.957610360466695,26.745687795700288,55.43992244618232 +2024-10-20 09:00:00,5,36.88248063081457,24.828890488515068,53.743738812378076 +2024-10-20 10:00:00,5,16.128294003293078,24.714592997739267,47.24606792385291 +2024-10-20 11:00:00,5,9.544560027286003,25.12594666444266,56.37193239175779 +2024-10-20 12:00:00,5,30.290967539956487,25.06879121939942,75.0351843121639 +2024-10-20 13:00:00,5,35.406605532151744,25.888625022197445,64.56490408483491 +2024-10-20 14:00:00,5,23.08667677769737,28.0772363875645,52.407567138197244 +2024-10-20 15:00:00,5,38.10731052368451,25.558662857066675,51.620862002383355 +2024-10-20 16:00:00,5,13.629019964067309,25.021142159629534,57.65941776913842 +2024-10-20 17:00:00,5,11.27717848428088,27.460324698592963,57.96646944780439 +2024-10-20 18:00:00,5,36.318654737848064,28.387764326990528,42.17882436910949 +2024-10-20 19:00:00,5,25.117226663993467,21.569426601527642,52.872200502965654 +2024-10-20 20:00:00,5,25.735367508079445,32.01802804117553,45.05168474804873 +2024-10-20 21:00:00,5,38.63927602264504,27.531332744082558,37.01895949151031 +2024-10-20 22:00:00,5,30.55082072741143,27.504919807516238,44.889475320124795 +2024-10-20 23:00:00,5,22.811117292027056,21.725374756428288,47.12367602570153 +2024-10-21 00:00:00,5,12.113034050581168,18.340563298839378,51.02180037500242 +2024-10-21 01:00:00,5,32.3854218949395,22.491689703063024,60.398811849338884 +2024-10-21 02:00:00,5,27.09781868171448,22.350689718475426,59.627772786605654 +2024-10-21 03:00:00,5,18.981636949738462,22.6397427588401,41.19782263854969 +2024-10-21 04:00:00,5,27.644169643715504,21.223346255505586,49.03977646669328 +2024-10-21 05:00:00,5,38.04182986205843,28.770848008008677,47.87788244706883 +2024-10-21 06:00:00,5,13.579046551083275,26.313805261837427,51.38151898171544 +2024-10-21 07:00:00,5,28.350890542915288,28.456647332636905,57.54220091175623 +2024-10-21 08:00:00,5,27.450803666820693,27.165331268826375,47.63587226235034 +2024-10-21 09:00:00,5,23.277814475141476,19.003536464316454,54.55758359024612 +2024-10-21 10:00:00,5,34.50486710164252,21.915569446633345,52.99533418181921 +2024-10-21 11:00:00,5,28.62575815805175,26.966893146702656,63.11577094490859 +2024-10-21 12:00:00,5,23.610051159883778,26.167150585603256,50.81292220303757 +2024-10-21 13:00:00,5,41.48432964939237,20.430853484183768,72.19637438499879 +2024-10-21 14:00:00,5,22.264939181825802,24.833346156831567,63.207627506093765 +2024-10-21 15:00:00,5,18.25042711855993,21.27420937791837,49.990441367957814 +2024-10-21 16:00:00,5,23.533366116251685,23.59391631899756,55.423701992623606 +2024-10-21 17:00:00,5,18.635162848773675,23.927171543063253,46.18899242120038 +2024-10-21 18:00:00,5,12.154019461688701,25.624147302019004,54.9079701877133 +2024-10-21 19:00:00,5,10.83806087685128,21.983947451711643,50.80807155070675 +2024-10-21 20:00:00,5,28.811697978541723,22.017057701370938,51.088464188386475 +2024-10-21 21:00:00,5,16.770955395687498,24.758035396153034,51.03542856749147 +2024-10-21 22:00:00,5,21.07109312555815,17.989956504785912,54.91738403440015 +2024-10-21 23:00:00,5,28.727156846793058,25.523574996834675,36.437906499144006 +2024-10-22 00:00:00,5,24.652827703276884,27.51875624891363,43.77386326261498 +2024-10-22 01:00:00,5,13.442775377669443,17.38166643374327,50.48659855060724 +2024-10-22 02:00:00,5,24.944780801950387,23.302236054116985,63.093434335149034 +2024-10-22 03:00:00,5,17.161549830194836,15.856135200831602,56.058639162967474 +2024-10-22 04:00:00,5,28.116171310914364,20.444254832124958,60.637022632164644 +2024-10-22 05:00:00,5,35.86848365361162,26.405390002544078,50.436708476728754 +2024-10-22 06:00:00,5,12.862272252339874,21.67758133501179,56.960676190354356 +2024-10-22 07:00:00,5,29.418233552410012,28.448356129509513,55.48379257464494 +2024-10-22 08:00:00,5,21.745684686129366,24.742586654378727,52.18909612702534 +2024-10-22 09:00:00,5,25.513611326288213,21.728370748454008,54.815406177788866 +2024-10-22 10:00:00,5,18.481445345285636,23.993614642926698,45.70883781691382 +2024-10-22 11:00:00,5,31.249249990858466,23.988696144613304,60.19258731497644 +2024-10-22 12:00:00,5,21.687728615978106,22.59454165061953,51.156359421725035 +2024-10-22 13:00:00,5,31.7599624074241,29.516904377583923,53.47546198189097 +2024-10-22 14:00:00,5,39.82770376027663,20.17514473649972,46.40294630634354 +2024-10-22 15:00:00,5,20.521200831997813,21.918887965362742,54.423569423492395 +2024-10-22 16:00:00,5,18.31412295385259,32.16257951000777,52.533629795056555 +2024-10-22 17:00:00,5,2.068518099801775,23.057018830877414,52.91289149528378 +2024-10-22 18:00:00,5,23.984439615593843,24.66301713434671,57.45766948114231 +2024-10-22 19:00:00,5,23.4192054148188,20.962741417970012,57.34172966173474 +2024-10-22 20:00:00,5,16.524620379844116,25.942606429537058,61.16017289608029 +2024-10-22 21:00:00,5,32.131865636357254,22.054975023242882,63.08393980656474 +2024-10-22 22:00:00,5,34.70457695850792,21.234425205406392,59.454633865554634 +2024-10-22 23:00:00,5,44.06385320106738,28.06967612393872,69.69556365581197 +2024-10-23 00:00:00,5,31.943127236677093,21.582091379502284,55.60665325196138 +2024-10-23 01:00:00,5,22.4684523774305,22.75312640544429,54.337981050468045 +2024-10-23 02:00:00,5,37.49328471560981,21.009063925694466,66.05892767810798 +2024-10-23 03:00:00,5,19.404381206367173,18.508780770270167,42.99246050565142 +2024-10-23 04:00:00,5,22.540903307349673,15.904987355578626,54.749476413575444 +2024-10-23 05:00:00,5,19.270604334049246,22.30052312803748,58.127064067418786 +2024-10-23 06:00:00,5,19.720043249680977,21.637024530939936,57.89805623028073 +2024-10-23 07:00:00,5,26.14048130453383,24.25919772361951,47.048825842928615 +2024-10-23 08:00:00,5,34.44596841969545,19.94689454655771,61.563567604656114 +2024-10-23 09:00:00,5,12.145359307452432,20.039964355514062,49.64500330602559 +2024-10-23 10:00:00,5,23.16878754159528,21.294646805186133,48.992293029798304 +2024-10-23 11:00:00,5,22.82330793950627,25.582434044663536,56.69363777562959 +2024-10-23 12:00:00,5,25.0554880086681,22.844538075680116,59.94223665041853 +2024-10-23 13:00:00,5,17.90836068355738,28.29549085848949,50.81635815428606 +2024-10-23 14:00:00,5,15.823148793844124,23.020341461141783,54.47381274035874 +2024-10-23 15:00:00,5,25.33674625986359,20.561928788738022,60.90474721742069 +2024-10-23 16:00:00,5,32.317630344730524,24.933191001755382,70.8840695742621 +2024-10-23 17:00:00,5,40.586852427352255,25.90166388180442,54.514253241731126 +2024-10-23 18:00:00,5,18.744320443585437,26.22122730936941,48.192075098623086 +2024-10-23 19:00:00,5,19.85691124005607,25.052519053911535,53.591126672940135 +2024-10-23 20:00:00,5,32.78398052059024,26.01190604800145,48.296835490326885 +2024-10-23 21:00:00,5,34.36651580799757,24.841527953565354,61.631772350533886 +2024-10-23 22:00:00,5,30.566601028374166,25.475533615772346,38.66681787938698 +2024-10-23 23:00:00,5,18.26500116674481,25.0481493919813,50.61890074678956 +2024-10-24 00:00:00,5,24.534339713345762,22.13753026561337,51.46839154092714 +2024-10-24 01:00:00,5,27.986086972900036,22.22853472319387,52.35488245133968 +2024-10-24 02:00:00,5,19.498467368720657,21.07865255320578,55.15686089429858 +2024-10-24 03:00:00,5,30.07068436954659,20.762743524293732,46.3884129808163 +2024-10-24 04:00:00,5,17.707136527220754,23.636037407128757,58.621334935581224 +2024-10-24 05:00:00,5,12.890805975337946,21.41506377191584,57.794825820425736 +2024-10-24 06:00:00,5,16.223435617930708,21.11032288994914,69.83415382296681 +2024-10-24 07:00:00,5,14.896147289970438,21.494438015644434,50.53703361116018 +2024-10-24 08:00:00,5,24.101466942419645,24.589877380836242,55.38070501377076 +2024-10-24 09:00:00,5,16.313331382492017,24.743464166086234,55.36367145929354 +2024-10-24 10:00:00,5,20.307233950226294,21.38272854966163,56.28840669250392 +2024-10-24 11:00:00,5,30.495034697982078,20.38775524552595,47.23328333609905 +2024-10-24 12:00:00,5,34.67855493084047,22.100671503299743,60.08174291865107 +2024-10-24 13:00:00,5,25.395714084351845,25.870129785035466,64.31269095511209 +2024-10-24 14:00:00,5,11.451418019493309,23.320417900747632,54.36465221390056 +2024-10-24 15:00:00,5,9.258339739996753,27.52893968811173,69.1410236767685 +2024-10-24 16:00:00,5,22.416504546416732,29.722293457338772,56.720198698057374 +2024-10-24 17:00:00,5,27.9665365896013,25.39637023220816,67.34347384082672 +2024-10-24 18:00:00,5,29.610516644407817,22.32647721812338,59.137734742081385 +2024-10-24 19:00:00,5,13.671986364288664,28.08008716955716,50.96454664602608 +2024-10-24 20:00:00,5,33.666186114785866,22.425956162308864,55.27934720734156 +2024-10-24 21:00:00,5,38.430292711325464,23.437089932024588,51.518973664292766 +2024-10-24 22:00:00,5,43.18135581200835,27.4876294796022,40.596360359199906 +2024-10-24 23:00:00,5,22.233636800935365,22.323318976650956,55.18200729343584 +2024-10-25 00:00:00,5,29.048389159245076,26.060401277259793,46.98836534458712 +2024-10-25 01:00:00,5,29.49251699150095,24.942987703727223,49.44541381804586 +2024-10-25 02:00:00,5,24.094929863412077,24.833846917440013,60.959781378388755 +2024-10-25 03:00:00,5,22.13315395319966,28.133592369761473,34.44055947031335 +2024-10-25 04:00:00,5,27.052960346689716,21.543508570769273,52.24565424051809 +2024-10-25 05:00:00,5,36.40827442916601,25.95192609523198,55.48574790866768 +2024-10-25 06:00:00,5,5.941641959339901,21.973379809620585,57.67292087455674 +2024-10-25 07:00:00,5,38.482979243456256,20.795192371854743,59.25676454841631 +2024-10-25 08:00:00,5,27.2864501738725,19.251705489835494,56.30733762718763 +2024-10-25 09:00:00,5,26.163492991012305,26.72113187675736,57.49827828644962 +2024-10-25 10:00:00,5,9.695866122435813,23.227327701199844,54.592891963049325 +2024-10-25 11:00:00,5,18.594073300339954,22.261549805493104,49.905256235063845 +2024-10-25 12:00:00,5,22.495249632418975,25.686601608295746,55.4835507348569 +2024-10-25 13:00:00,5,24.764175821529296,24.045870219214535,62.51076928029109 +2024-10-25 14:00:00,5,12.409986931630728,27.15972430804643,48.77886381922452 +2024-10-25 15:00:00,5,42.85992677151917,29.606117704773947,49.42507637528894 +2024-10-25 16:00:00,5,15.62532849429273,21.76411603600654,49.203327051423365 +2024-10-25 17:00:00,5,32.72037045686691,27.49856526994675,48.36897701435946 +2024-10-25 18:00:00,5,26.072078708809904,21.735486240738148,59.589461901996785 +2024-10-25 19:00:00,5,12.936486278648298,23.969261766598617,57.1028012079865 +2024-10-25 20:00:00,5,8.01419279314881,25.579049396636517,65.48002015597324 +2024-10-25 21:00:00,5,21.5135811738553,29.59805511675131,60.00053476664114 +2024-10-25 22:00:00,5,20.817492953376547,23.817160881586673,39.187244563107996 +2024-10-25 23:00:00,5,28.474866532862876,25.967458005580692,67.9248919482573 +2024-10-26 00:00:00,5,27.608131603846918,21.25770473238164,48.95268910558254 +2024-10-26 01:00:00,5,24.097358338963154,28.685394008592958,62.84475799511772 +2024-10-26 02:00:00,5,24.93905829860746,23.850836179959217,69.42662795824212 +2024-10-26 03:00:00,5,11.186915293498751,21.008603610459645,62.376963073723516 +2024-10-26 04:00:00,5,30.839555594638416,22.576779080741378,54.37459154139938 +2024-10-26 05:00:00,5,32.177253909232064,23.666771253959183,58.74901028280083 +2024-10-26 06:00:00,5,25.032406618470425,21.638322099275573,51.27826459054363 +2024-10-26 07:00:00,5,24.588190637833822,25.704945097541206,49.6790115981571 +2024-10-26 08:00:00,5,21.767357373544044,27.166935054704044,65.38918721090533 +2024-10-26 09:00:00,5,20.405012953476817,22.543935809406726,43.89900113561616 +2024-10-26 10:00:00,5,15.464822060544677,26.86428657351484,60.896106557763886 +2024-10-26 11:00:00,5,25.152412112222663,23.13223542226915,52.10194665771503 +2024-10-26 12:00:00,5,24.46991991897414,24.457060617299685,54.9400228242569 +2024-10-26 13:00:00,5,26.236274849393396,22.914046323773917,53.07151255604029 +2024-10-26 14:00:00,5,27.71188788860983,27.58173585549177,58.5063716196731 +2024-10-26 15:00:00,5,18.46335488307671,28.3500118539216,58.17987560615465 +2024-10-26 16:00:00,5,9.720644365110665,26.914244243906136,43.95912232710573 +2024-10-26 17:00:00,5,38.08219834907061,24.276109330687405,51.82844827618318 +2024-10-26 18:00:00,5,18.000743396737388,26.98994925054456,65.34225694901217 +2024-10-26 19:00:00,5,20.90031097709835,27.308062505725225,50.115995395699684 +2024-10-26 20:00:00,5,22.71774844586645,23.04944525031637,57.21674202104241 +2024-10-26 21:00:00,5,16.064733795880215,26.514763516215737,49.02059435067609 +2024-10-26 22:00:00,5,28.98875169907413,23.77860559346935,60.60361902843033 +2024-10-26 23:00:00,5,11.10084089517835,29.203877376971384,69.28667302767231 +2024-10-27 00:00:00,5,23.10705712654989,22.740998014583955,61.456841369702474 +2024-10-27 01:00:00,5,20.904357614626825,25.07003112641445,47.40935158117549 +2024-10-27 02:00:00,5,25.67398377562234,24.715079693519524,53.57497020528056 +2024-10-27 03:00:00,5,27.95090437573132,18.224298973808107,61.00407266149182 +2024-10-27 04:00:00,5,22.24769634382488,20.01104954991409,65.74466409536664 +2024-10-27 05:00:00,5,25.11813931800264,25.811685473036253,51.01292807869348 +2024-10-27 06:00:00,5,26.09534925498518,28.854959057333282,54.63351029556962 +2024-10-27 07:00:00,5,29.330141024331805,23.90574532670253,58.98245579546086 +2024-10-27 08:00:00,5,33.57272952718397,21.86445226399626,41.77359617233144 +2024-10-27 09:00:00,5,20.59260003932519,21.926185070019706,68.31068069307702 +2024-10-27 10:00:00,5,26.812338152421134,27.395433008232708,45.97469563593424 +2024-10-27 11:00:00,5,29.570637582537103,28.95790610098729,50.43124858718789 +2024-10-27 12:00:00,5,36.38400518330029,22.962945367490953,43.79959158145946 +2024-10-27 13:00:00,5,29.16294277188394,21.214631898199496,54.61564465007901 +2024-10-27 14:00:00,5,30.77586501280271,18.18895630209687,69.77472924373775 +2024-10-27 15:00:00,5,14.804773875503598,29.591087095046714,52.37604275436732 +2024-10-27 16:00:00,5,25.067819939326522,28.900716427182918,70.72688828656936 +2024-10-27 17:00:00,5,38.77590355752909,22.842410376704365,54.76558763296936 +2024-10-27 18:00:00,5,22.329087849548475,22.360953697933592,65.86042006089568 +2024-10-27 19:00:00,5,34.918070864312334,29.9527444606095,42.27064683914793 +2024-10-27 20:00:00,5,25.49662645763213,25.316954154738774,62.32970462732851 +2024-10-27 21:00:00,5,28.55461803610819,20.734774734912055,48.69770360579815 +2024-10-27 22:00:00,5,24.03584868988577,27.153812579978098,67.66165041110946 +2024-10-27 23:00:00,5,26.226081970098324,21.435638940192753,43.95316353415889 +2024-10-28 00:00:00,5,20.220999495683657,12.640922218956911,68.91078549767799 +2024-10-28 01:00:00,5,14.260314328862819,25.855510381944082,41.884278734942 +2024-10-28 02:00:00,5,26.57364937794274,26.013641494347688,56.774968187514666 +2024-10-28 03:00:00,5,26.61124007320521,18.612429659997268,51.81660710389285 +2024-10-28 04:00:00,5,33.04718191572647,31.02846528090138,62.88737385645388 +2024-10-28 05:00:00,5,27.81482355679873,26.088439815529323,56.83775145133292 +2024-10-28 06:00:00,5,24.430377410946843,31.676630261180875,57.68298287242762 +2024-10-28 07:00:00,5,7.267560521320284,28.084850892643082,50.443113506416935 +2024-10-28 08:00:00,5,23.751138933897277,26.653974782206706,59.33604802405084 +2024-10-28 09:00:00,5,33.873211010773844,23.64976671703795,49.30978549835663 +2024-10-28 10:00:00,5,20.612386645321383,26.89799921800095,54.004902869832364 +2024-10-28 11:00:00,5,12.150449418848925,24.974036702423078,52.62618673663619 +2024-10-28 12:00:00,5,27.089090951268567,20.648959029213387,57.29517622584329 +2024-10-28 13:00:00,5,27.738909317948824,23.7714663149169,52.23215070507161 +2024-10-28 14:00:00,5,1.3282844693256912,23.685201587325317,62.33424789370184 +2024-10-28 15:00:00,5,32.15088235061556,22.840943455680492,63.59799022592719 +2024-10-28 16:00:00,5,22.58689592787107,25.475724412526407,55.517622166745916 +2024-10-28 17:00:00,5,19.99213457144221,30.593480684284444,59.25275716316737 +2024-10-28 18:00:00,5,23.26221290531962,25.43171842914071,53.81653093370263 +2024-10-28 19:00:00,5,28.312812614103095,25.209758978831143,48.65265939625016 +2024-10-28 20:00:00,5,21.895074181119874,25.563854513601036,58.02046971325772 +2024-10-28 21:00:00,5,28.229171524701073,28.42630401602506,48.53860076422675 +2024-10-28 22:00:00,5,19.971859226558465,23.765787012487845,46.93728445895948 +2024-10-28 23:00:00,5,29.08253507765929,28.294552110094823,65.63019804161607 +2024-10-29 00:00:00,5,17.95404686170802,21.428324145365536,37.89824497829926 +2024-10-29 01:00:00,5,0.0,22.973849393648916,50.14407931301569 +2024-10-29 02:00:00,5,15.906717878121682,14.864142336741603,37.040134022455916 +2024-10-29 03:00:00,5,14.628192494474126,27.58628418133063,57.3946313540312 +2024-10-29 04:00:00,5,22.39903350796754,27.92292901076519,52.4878527696879 +2024-10-29 05:00:00,5,14.339659578988504,21.99387939063244,45.365533010118085 +2024-10-29 06:00:00,5,14.950866761543242,24.958658354503264,49.718390890730404 +2024-10-29 07:00:00,5,37.01294262947198,21.995341191401774,35.42623880254594 +2024-10-29 08:00:00,5,32.307134037987026,23.002133979362647,43.832821687704474 +2024-10-29 09:00:00,5,30.339411885681606,22.6453816612096,36.79286387336685 +2024-10-29 10:00:00,5,22.45202896726239,26.091692043217993,50.79299016797419 +2024-10-29 11:00:00,5,17.63127305745578,25.79257631900157,41.92549231013367 +2024-10-29 12:00:00,5,17.794503332911344,29.176647747008154,55.28465827079183 +2024-10-29 13:00:00,5,29.26311531066613,24.63881420771793,65.13128151122802 +2024-10-29 14:00:00,5,21.438786554945462,21.90191615529151,49.162522435645926 +2024-10-29 15:00:00,5,27.826766775063213,20.501123740146497,48.580104528764004 +2024-10-29 16:00:00,5,38.2975513645962,28.49321439070178,63.90371231671353 +2024-10-29 17:00:00,5,22.675951968894076,23.187468932870644,32.79850116845089 +2024-10-29 18:00:00,5,27.28320125518293,18.563184012735142,66.707751722248 +2024-10-29 19:00:00,5,26.508262669518544,22.37504074171694,58.36781976778086 +2024-10-29 20:00:00,5,29.230349130533988,25.76867018870578,51.73072256825552 +2024-10-29 21:00:00,5,36.05926835586746,24.508747157920897,49.96685073092543 +2024-10-29 22:00:00,5,20.420391473466616,24.20008363985563,74.76271420184662 +2024-10-29 23:00:00,5,32.690598418387374,25.241624744452196,46.50973473093937 +2024-10-30 00:00:00,5,15.83085442673502,19.560085425592284,60.69712415663086 +2024-10-30 01:00:00,5,14.969117802516983,24.46127287897479,53.56213601099712 +2024-10-30 02:00:00,5,15.65832849309749,24.19813134890712,40.425203557061785 +2024-10-30 03:00:00,5,32.07393283758527,20.843883698644618,50.252410742831906 +2024-10-30 04:00:00,5,27.300523135291662,23.61068573906212,45.876871417123986 +2024-10-30 05:00:00,5,19.271602372329312,22.099692622228886,47.53836357156562 +2024-10-30 06:00:00,5,30.548397338306557,21.977259768873427,65.45715119750562 +2024-10-30 07:00:00,5,33.42912553132561,24.25221508827401,48.890484945999 +2024-10-30 08:00:00,5,11.964266243405167,22.544049772096653,52.88266964662858 +2024-10-30 09:00:00,5,33.80370208854227,29.896649799760368,63.857865573174834 +2024-10-30 10:00:00,5,14.385058866948409,23.94188403371581,46.437332960887026 +2024-10-30 11:00:00,5,26.254645983880884,20.65346611301274,41.40414055547313 +2024-10-30 12:00:00,5,25.699170856379872,23.640599992822473,47.4428491118642 +2024-10-30 13:00:00,5,21.214248896743406,28.032743828511176,62.122454480068626 +2024-10-30 14:00:00,5,20.732322220109605,22.363165152906774,40.391341037838046 +2024-10-30 15:00:00,5,12.200829544079435,26.929759606842012,48.028307529066765 +2024-10-30 16:00:00,5,27.73459208090877,26.864593206009104,51.408536141623486 +2024-10-30 17:00:00,5,34.419075327342924,27.305848568595238,53.928623281727795 +2024-10-30 18:00:00,5,37.62128223699758,26.644364034824594,54.820560084621434 +2024-10-30 19:00:00,5,28.445565381490916,25.959987914027266,56.00531358166602 +2024-10-30 20:00:00,5,15.089977302147764,24.679914059312093,59.09945006059516 +2024-10-30 21:00:00,5,23.7187221399922,28.413964755125267,54.065898960720915 +2024-10-30 22:00:00,5,23.569595656416215,30.22701616007535,51.80601011517888 +2024-10-30 23:00:00,5,19.981452520247693,23.919291409955346,56.587310832482416 +2024-10-31 00:00:00,5,31.318789862605932,22.70188240957139,56.26970333184208 +2024-10-31 01:00:00,5,16.198697613856243,23.427400183584403,47.758316294688356 +2024-10-31 02:00:00,5,12.453866478153536,27.037013496077257,49.00496195856855 +2024-10-31 03:00:00,5,28.722179953935882,24.78546863071324,49.86518531299069 +2024-10-31 04:00:00,5,12.378796699504228,27.144226349926395,39.44881504361131 +2024-10-31 05:00:00,5,20.49622494443257,23.6547813355252,55.02497496405462 +2024-10-31 06:00:00,5,27.358121912648688,26.569725477083395,52.7344426690589 +2024-10-31 07:00:00,5,15.52557048507133,24.720076917250317,73.02660505435747 +2024-10-31 08:00:00,5,42.089518753736826,25.086239493756782,61.490316104548405 +2024-10-31 09:00:00,5,33.42486355789818,22.274425647982774,50.64179550349746 +2024-10-31 10:00:00,5,30.01404051726505,22.248091711265054,55.2054331521 +2024-10-31 11:00:00,5,22.192489300712147,24.359863841105206,48.16216559138498 +2024-10-31 12:00:00,5,21.41647387492749,24.356125213121405,36.970609293130565 +2024-10-31 13:00:00,5,21.21391657745419,22.12726816309033,31.096951392933324 +2024-10-31 14:00:00,5,21.66847724308404,23.48397660701547,68.3861577092595 +2024-10-31 15:00:00,5,26.661012144364985,24.838250616478277,58.83674071909496 +2024-10-31 16:00:00,5,4.724311743104039,27.844996571185035,48.85350050360182 +2024-10-31 17:00:00,5,18.2120680764572,25.357626339395825,41.098330113452356 +2024-10-31 18:00:00,5,28.23769243410343,28.00008959023582,51.32317184909677 +2024-10-31 19:00:00,5,9.854653192071659,23.646248203750403,54.47420416008253 +2024-10-31 20:00:00,5,25.05579011373215,24.983163174911564,58.08080225992575 +2024-10-31 21:00:00,5,20.449985101348307,20.824326542872033,49.31694594774203 +2024-10-31 22:00:00,5,47.06849273502745,27.433372239128786,54.362380964960835 +2024-10-31 23:00:00,5,32.52250187917183,22.32685941047832,53.310640021960715 +2024-11-01 00:00:00,5,25.949286022765563,18.604672210633478,65.90420394330575 +2024-11-01 01:00:00,5,15.334367580066996,26.12770967861203,39.21376612298528 +2024-11-01 02:00:00,5,22.550502938353507,21.62564367413064,42.870212021930996 +2024-11-01 03:00:00,5,14.20969743280551,25.92523902868739,57.050080292100304 +2024-11-01 04:00:00,5,17.87868169874079,28.95043885021523,45.28601028991652 +2024-11-01 05:00:00,5,19.35080801527081,20.551294928292407,60.842766710502886 +2024-11-01 06:00:00,5,13.347748092878225,18.865070203199974,58.94249055892854 +2024-11-01 07:00:00,5,32.325164602801124,22.07780560051907,68.79453283317403 +2024-11-01 08:00:00,5,14.924580554720427,26.659269183682447,60.97847573105437 +2024-11-01 09:00:00,5,27.85016330488169,26.104378925432652,54.63846018702264 +2024-11-01 10:00:00,5,28.54407295195349,28.458344595735962,35.81898055633134 +2024-11-01 11:00:00,5,18.193590212221892,24.35776702075796,63.95209233835155 +2024-11-01 12:00:00,5,36.410970271400544,28.190177997797637,62.5965280371839 +2024-11-01 13:00:00,5,27.484742818218983,21.00877015778635,65.43945532277272 +2024-11-01 14:00:00,5,1.1971654367106055,20.67377779840524,52.068558046945675 +2024-11-01 15:00:00,5,18.64406024046021,26.568773730518593,59.48283032467567 +2024-11-01 16:00:00,5,24.495620247852514,22.989098893937367,45.53189222919388 +2024-11-01 17:00:00,5,25.521852820996507,26.604055387636745,60.6308771584149 +2024-11-01 18:00:00,5,14.03988551235807,14.80663590897489,44.99198299411234 +2024-11-01 19:00:00,5,34.034813648503395,27.061899339593733,70.77227507016906 +2024-11-01 20:00:00,5,14.104305598856666,21.463766144921998,63.802018799414974 +2024-11-01 21:00:00,5,20.757170042023645,20.454405178860444,49.160178210253996 +2024-11-01 22:00:00,5,27.844963015951386,24.759767891317217,46.114695242351765 +2024-11-01 23:00:00,5,32.48221035761355,29.476111210968625,57.35044782550099 +2024-11-02 00:00:00,5,27.237833327863747,26.298459765338034,58.15038076671175 +2024-11-02 01:00:00,5,25.754936970880692,27.115143812905522,60.011406565128034 +2024-11-02 02:00:00,5,12.419018051206008,19.5527221967776,61.852340870411076 +2024-11-02 03:00:00,5,30.345954803307233,24.434289334916514,50.76272496966092 +2024-11-02 04:00:00,5,24.57128777879341,24.216612266241015,52.286731171730175 +2024-08-04 05:00:00,6,22.399881753135315,13.144673336244384,53.87401725962101 +2024-08-04 06:00:00,6,12.324017984012311,22.112705906380075,67.85437417668601 +2024-08-04 07:00:00,6,23.32027988142518,20.764393291287845,60.45540414084564 +2024-08-04 08:00:00,6,24.960199054543324,23.348942666413954,60.327874862982924 +2024-08-04 09:00:00,6,27.03829279214776,23.871246752681632,51.54133642370674 +2024-08-04 10:00:00,6,34.860587227345555,23.856846733087515,58.56497987706358 +2024-08-04 11:00:00,6,21.941820880953983,25.962126810184895,31.45986946724206 +2024-08-04 12:00:00,6,27.99559888650252,28.998315148638383,58.09327726555205 +2024-08-04 13:00:00,6,21.737191435080657,24.811187433546802,54.94315301423277 +2024-08-04 14:00:00,6,37.10170458719995,26.37125086833699,52.013714975225696 +2024-08-04 15:00:00,6,41.40491223333257,19.748486398728602,48.796004887186214 +2024-08-04 16:00:00,6,24.367199392978637,21.419331932031916,57.447963749004906 +2024-08-04 17:00:00,6,11.474074443551537,25.740544991905722,52.21787221715621 +2024-08-04 18:00:00,6,31.947440633132274,21.131445501008738,62.62215051705253 +2024-08-04 19:00:00,6,19.427451855820884,21.778345236170537,45.966829920454664 +2024-08-04 20:00:00,6,38.65394726561646,24.25111089129423,49.07959862467954 +2024-08-04 21:00:00,6,42.87166820189714,26.686130687628452,56.17469531441998 +2024-08-04 22:00:00,6,29.124112278055154,23.473006548225243,46.3187129627044 +2024-08-04 23:00:00,6,26.565047833626107,23.463433868872247,39.07142045384791 +2024-08-05 00:00:00,6,33.566680150981796,21.791428030077178,60.605834667831736 +2024-08-05 01:00:00,6,12.807976410839235,21.754919958711344,63.76597875873081 +2024-08-05 02:00:00,6,14.101488669438512,20.033037557513005,45.42010927517639 +2024-08-05 03:00:00,6,21.952196005186746,20.847902130629496,54.378365943405356 +2024-08-05 04:00:00,6,19.825303057799182,24.51949168513506,64.9783840573835 +2024-08-05 05:00:00,6,31.637583910337813,27.44741944068023,49.25212793083908 +2024-08-05 06:00:00,6,32.42010050051219,20.61140777980571,57.21221309012688 +2024-08-05 07:00:00,6,32.14291521431105,28.321015033797682,48.64269642680534 +2024-08-05 08:00:00,6,18.713296160561256,21.946456901562083,58.076338401936354 +2024-08-05 09:00:00,6,26.313514136417254,18.915085203275783,59.6873045128822 +2024-08-05 10:00:00,6,26.73269473743943,30.012372864835932,69.43303768742871 +2024-08-05 11:00:00,6,23.522829356254398,28.69478540029828,54.74539291765773 +2024-08-05 12:00:00,6,26.25667054740842,24.56988912394026,30.62062790864385 +2024-08-05 13:00:00,6,32.99416340093654,23.291924382524556,50.50299699564295 +2024-08-05 14:00:00,6,29.295849579065685,25.466882586684296,56.53615374291278 +2024-08-05 15:00:00,6,10.631942344158496,23.915146731945207,50.02546775113508 +2024-08-05 16:00:00,6,28.166813152112567,23.069735293681784,55.6015414212223 +2024-08-05 17:00:00,6,21.87942243208109,25.499116438543904,64.92485817988816 +2024-08-05 18:00:00,6,18.472353406564164,23.788074705139483,50.99205179923845 +2024-08-05 19:00:00,6,27.96621646768279,28.248625574424615,52.74003230549596 +2024-08-05 20:00:00,6,17.21375899661385,35.06600137049083,52.91091642075274 +2024-08-05 21:00:00,6,18.24337657811607,20.39917676965367,61.13745592678914 +2024-08-05 22:00:00,6,15.578702756697133,25.30746257611286,45.19538957962192 +2024-08-05 23:00:00,6,34.96000996896163,18.396696917317513,47.63256495072724 +2024-08-06 00:00:00,6,36.958097498114945,27.654410068995688,49.74372924183272 +2024-08-06 01:00:00,6,27.72131984932274,25.391905953473444,72.20461331112574 +2024-08-06 02:00:00,6,27.794244453376734,22.325412953988707,62.83936835207367 +2024-08-06 03:00:00,6,23.602439611790025,17.49530809769342,53.096899761642995 +2024-08-06 04:00:00,6,23.371163067268867,21.717175711003524,57.49216982897525 +2024-08-06 05:00:00,6,24.916086575893495,24.79880366111585,67.44415892633526 +2024-08-06 06:00:00,6,16.69630574914015,25.622805773745498,44.72031936783257 +2024-08-06 07:00:00,6,18.672109592925842,22.573791560856385,59.5310407686791 +2024-08-06 08:00:00,6,25.93889148921616,28.198817860903212,56.96066331937638 +2024-08-06 09:00:00,6,29.942964088201766,20.688807858693472,49.92891435693925 +2024-08-06 10:00:00,6,24.203009165351602,24.218712798904352,58.962931995979964 +2024-08-06 11:00:00,6,20.766742897777526,23.147694310184935,67.78177652193429 +2024-08-06 12:00:00,6,28.86933293627784,20.69077082313236,62.1872266368889 +2024-08-06 13:00:00,6,32.103698304418856,25.02229078207744,67.60193033990794 +2024-08-06 14:00:00,6,23.25734612006695,19.720607878178686,37.77258056631394 +2024-08-06 15:00:00,6,32.37735979336581,21.607720993579786,53.50878084402056 +2024-08-06 16:00:00,6,26.821334813192376,26.057487157518317,59.21667922593831 +2024-08-06 17:00:00,6,33.11750048151208,28.41004553651296,49.27913367078075 +2024-08-06 18:00:00,6,38.109271007063796,28.644969600613653,66.6085434728636 +2024-08-06 19:00:00,6,14.794913397240174,20.704496515511003,45.14847191478903 +2024-08-06 20:00:00,6,33.188279580338836,25.495635554634216,53.00254180375478 +2024-08-06 21:00:00,6,34.57589043115005,26.96525793767359,58.659767849810464 +2024-08-06 22:00:00,6,32.08180969230644,26.182536826640607,42.80950666684102 +2024-08-06 23:00:00,6,31.5295772261996,21.56356134862746,59.99862100183546 +2024-08-07 00:00:00,6,25.827079215972415,22.067659285095118,70.60399767666385 +2024-08-07 01:00:00,6,20.947015109449868,25.487006314883214,54.09215594256895 +2024-08-07 02:00:00,6,25.18634667642958,19.082617849121128,45.73402706699415 +2024-08-07 03:00:00,6,31.516837730467714,20.11318953425247,55.06958277997937 +2024-08-07 04:00:00,6,22.44948320557529,29.299367271601913,61.62589941022566 +2024-08-07 05:00:00,6,11.510253825915498,16.794920878021212,69.8031802246489 +2024-08-07 06:00:00,6,21.87381068268725,18.546956994662963,47.88312869038894 +2024-08-07 07:00:00,6,23.016739967807574,20.24441174778912,53.422089116815094 +2024-08-07 08:00:00,6,16.64905735997408,22.159478066115412,57.656580961237985 +2024-08-07 09:00:00,6,24.757883086222574,20.542696597069657,52.4856707258504 +2024-08-07 10:00:00,6,19.059943098654415,23.016173703195353,51.98261790915713 +2024-08-07 11:00:00,6,33.72302046537001,29.25924804195775,55.667378430954436 +2024-08-07 12:00:00,6,38.29377418329877,21.06859997328765,55.08673135229823 +2024-08-07 13:00:00,6,33.56447337337107,25.287740238956108,58.14605931205754 +2024-08-07 14:00:00,6,40.46407309138146,22.471544354957246,56.12313434505979 +2024-08-07 15:00:00,6,22.87009642507134,21.228443008707835,48.941852375167684 +2024-08-07 16:00:00,6,33.53470566335015,23.131404359573306,48.66288296826035 +2024-08-07 17:00:00,6,32.63179958141251,23.872458903412266,58.057802245865545 +2024-08-07 18:00:00,6,30.636256519122096,22.139815313090793,45.16599190116624 +2024-08-07 19:00:00,6,31.046030757132716,18.10440057931684,54.689535826120725 +2024-08-07 20:00:00,6,24.52109213922558,24.54132879583129,50.67609140898135 +2024-08-07 21:00:00,6,37.6550563744106,27.894311733248816,49.37218049060746 +2024-08-07 22:00:00,6,27.996643437028595,23.420141941736954,55.26054792051413 +2024-08-07 23:00:00,6,27.510204428013306,25.97155666141427,49.03899287479284 +2024-08-08 00:00:00,6,30.151147944455477,23.90589427933045,48.7782311129618 +2024-08-08 01:00:00,6,19.066626713374266,26.107274823639028,55.49340549939948 +2024-08-08 02:00:00,6,22.540677513894693,23.766449545622955,57.871043919968855 +2024-08-08 03:00:00,6,28.664203248870677,18.671229865477994,47.16494395693263 +2024-08-08 04:00:00,6,11.350306665362572,25.329292118735715,47.34154741875598 +2024-08-08 05:00:00,6,33.29340159804498,24.62574042460383,60.80457870654377 +2024-08-08 06:00:00,6,11.493812829797774,20.575956447822307,57.22813192091268 +2024-08-08 07:00:00,6,12.5383315405641,24.392488734312256,64.24423176013201 +2024-08-08 08:00:00,6,14.658385880539322,23.5878373153314,55.1535614785433 +2024-08-08 09:00:00,6,37.06110607517582,21.621803252453898,54.61720935703395 +2024-08-08 10:00:00,6,27.92194571772752,18.76147504709484,51.760189600398995 +2024-08-08 11:00:00,6,21.548124290590643,21.575457481131632,60.5151929550922 +2024-08-08 12:00:00,6,22.714773164891458,22.54883106672599,52.901050093248905 +2024-08-08 13:00:00,6,31.958645219301896,20.240892231918213,42.69383647749876 +2024-08-08 14:00:00,6,25.612608970272206,21.074702120235585,56.73550472528058 +2024-08-08 15:00:00,6,13.084006020669433,23.571609386200734,42.67235137092173 +2024-08-08 16:00:00,6,29.12933081072405,25.374933697296456,47.44032293540352 +2024-08-08 17:00:00,6,20.369911364587608,18.65334495240969,40.697319590662396 +2024-08-08 18:00:00,6,31.261371000790014,24.737239027666828,53.51157580390994 +2024-08-08 19:00:00,6,29.003816486089345,21.547654826745017,53.98959965946206 +2024-08-08 20:00:00,6,31.501598800239822,23.344430857517192,44.15262597146685 +2024-08-08 21:00:00,6,35.14054003667265,21.514416208017618,64.00968841356988 +2024-08-08 22:00:00,6,13.670767398584648,24.265146585260798,47.41645806118094 +2024-08-08 23:00:00,6,24.647069666852705,20.2337920372507,46.879834978092994 +2024-08-09 00:00:00,6,18.072139257076717,24.82819962110557,56.123585270242316 +2024-08-09 01:00:00,6,33.581458560814525,16.517834604879212,58.08648375036796 +2024-08-09 02:00:00,6,19.88932585197168,24.90780795647426,58.9885184163952 +2024-08-09 03:00:00,6,27.18641753262163,25.67716630109719,39.05443300845033 +2024-08-09 04:00:00,6,42.98624938495202,23.68999035450747,49.137346010582235 +2024-08-09 05:00:00,6,27.04412132130961,18.266921298263146,65.79530448291936 +2024-08-09 06:00:00,6,28.791172815226552,20.515334208693623,45.62939100889731 +2024-08-09 07:00:00,6,32.888535843570416,23.634939490428962,30.01950299145783 +2024-08-09 08:00:00,6,25.34974890000879,22.386553552991344,47.21823882989951 +2024-08-09 09:00:00,6,33.02588876312358,27.48202424879412,49.15468443622446 +2024-08-09 10:00:00,6,19.329332722528367,19.742655455669997,52.07507326232687 +2024-08-09 11:00:00,6,29.33361338636567,25.845413751582424,61.63416761103182 +2024-08-09 12:00:00,6,27.69687326418511,24.466797674978217,71.10044676449704 +2024-08-09 13:00:00,6,38.775033964521896,23.65409845028408,48.6677071969181 +2024-08-09 14:00:00,6,28.937583988430685,28.159663609386975,60.32316572746874 +2024-08-09 15:00:00,6,22.684764417568793,27.64319551404976,62.91641391442242 +2024-08-09 16:00:00,6,18.436793990905677,26.237723292548438,56.932299556030515 +2024-08-09 17:00:00,6,38.695065231670895,22.43614574157599,65.00390723389386 +2024-08-09 18:00:00,6,22.133551295250555,24.11906806265815,37.51573096911886 +2024-08-09 19:00:00,6,24.98141296429577,28.455787422339018,52.0731614529259 +2024-08-09 20:00:00,6,39.89057528441447,29.768056759460745,66.54057900799377 +2024-08-09 21:00:00,6,24.65769753062353,22.869148611839357,56.009368593152175 +2024-08-09 22:00:00,6,36.03377050605354,19.857630828107183,52.13128685665337 +2024-08-09 23:00:00,6,22.88598100209126,15.357019276005936,54.0822643550838 +2024-08-10 00:00:00,6,23.03516829055426,27.56478948746999,55.18109511847749 +2024-08-10 01:00:00,6,32.500944457515175,18.450410293083905,65.08719329121664 +2024-08-10 02:00:00,6,24.038370656443476,17.92320045651033,47.015573743965916 +2024-08-10 03:00:00,6,11.712999548175219,24.888020135267347,41.46917212351464 +2024-08-10 04:00:00,6,10.400612600939159,25.01012244002694,61.98014158330953 +2024-08-10 05:00:00,6,24.850243257864314,29.6414087681444,74.93595603088013 +2024-08-10 06:00:00,6,30.398752458017412,25.384433521511184,45.078449714250226 +2024-08-10 07:00:00,6,29.036996807774948,24.834909376508126,48.09101477937009 +2024-08-10 08:00:00,6,41.96235497188961,25.66385547375543,57.025763371064365 +2024-08-10 09:00:00,6,25.49879214211958,28.291513257750772,56.290798219965005 +2024-08-10 10:00:00,6,23.134185595324887,21.35980280617541,58.5454288075118 +2024-08-10 11:00:00,6,28.2536788443378,15.775299009727624,56.5834677537807 +2024-08-10 12:00:00,6,30.520662192299522,18.557694386598,45.797436850750536 +2024-08-10 13:00:00,6,33.93236615370304,20.360805194696173,47.8535523639628 +2024-08-10 14:00:00,6,59.031700928798216,23.640103169111907,56.41998567935853 +2024-08-10 15:00:00,6,43.82000765208434,16.16031175742444,57.654977287137164 +2024-08-10 16:00:00,6,30.367183311754076,21.528625192318806,45.500675086821914 +2024-08-10 17:00:00,6,30.73582589780195,21.877390468645682,71.12134819245621 +2024-08-10 18:00:00,6,20.037842287188184,22.817155624166226,44.01717761022618 +2024-08-10 19:00:00,6,8.518042329133454,23.367056571467266,46.35150695411435 +2024-08-10 20:00:00,6,21.054381658083244,20.153747631978163,49.827477041327526 +2024-08-10 21:00:00,6,21.052915938234023,20.133856854166893,46.9061647547469 +2024-08-10 22:00:00,6,32.70773453463021,26.381276118521296,45.163684541270285 +2024-08-10 23:00:00,6,15.716994253863191,20.283309595238254,60.706935868270584 +2024-08-11 00:00:00,6,13.467292084589875,21.738187401307584,64.50176035697375 +2024-08-11 01:00:00,6,25.09079147903364,22.78287907681323,58.1513554046188 +2024-08-11 02:00:00,6,14.14925057309381,24.509328948594234,56.34531957373343 +2024-08-11 03:00:00,6,14.66403011241541,25.96986232741957,59.781747512289364 +2024-08-11 04:00:00,6,13.459442088968235,24.883880416978887,58.29037071987302 +2024-08-11 05:00:00,6,8.983659627643021,28.680973166482318,41.29462560612593 +2024-08-11 06:00:00,6,13.578131763800721,20.460388969686296,50.205133439268856 +2024-08-11 07:00:00,6,21.5932698110864,18.27528371054376,61.17877140195755 +2024-08-11 08:00:00,6,24.941735417108465,27.000358153127337,57.627453015603926 +2024-08-11 09:00:00,6,29.927891222919385,29.25790225854125,65.72686362013908 +2024-08-11 10:00:00,6,19.575170189966045,22.869392672956174,64.39867381890143 +2024-08-11 11:00:00,6,14.795726368750858,25.573509657091794,59.64797450157264 +2024-08-11 12:00:00,6,22.22691034613363,27.55679511077782,57.20173347892172 +2024-08-11 13:00:00,6,21.885611274841988,25.0640443006427,44.69863931051292 +2024-08-11 14:00:00,6,23.05619949882775,29.51516467275945,51.19909350240641 +2024-08-11 15:00:00,6,23.93845623623261,25.019547104800214,37.88103399091721 +2024-08-11 16:00:00,6,35.742546712677516,20.086638583796088,59.75309602540956 +2024-08-11 17:00:00,6,12.115447542735991,20.91959976028727,48.13224287799343 +2024-08-11 18:00:00,6,18.149001640270725,21.99013992941164,64.07837252873249 +2024-08-11 19:00:00,6,33.231370780162244,19.89712422397484,50.49388700324959 +2024-08-11 20:00:00,6,26.18651974460406,26.868709212782115,54.84621996862838 +2024-08-11 21:00:00,6,32.29848473562615,26.869739847501688,47.373662018769025 +2024-08-11 22:00:00,6,33.321927475193014,22.15020633107399,49.03847232186577 +2024-08-11 23:00:00,6,24.19693203262947,17.559866110900224,64.86117710802579 +2024-08-12 00:00:00,6,21.907953051457355,29.526035658434168,57.459429767134225 +2024-08-12 01:00:00,6,27.10011455482674,22.745860436678996,41.21542094589354 +2024-08-12 02:00:00,6,6.334995579481202,23.907210308755655,45.64232417748349 +2024-08-12 03:00:00,6,19.080055830664012,23.049824690866803,53.78854817663435 +2024-08-12 04:00:00,6,26.699071783147183,19.947105159435772,55.25083102552183 +2024-08-12 05:00:00,6,23.54685781230026,24.38334657949806,52.98814830263197 +2024-08-12 06:00:00,6,20.647629629508337,21.456842484097212,39.671607017344606 +2024-08-12 07:00:00,6,18.97123044322759,21.34143320254995,49.00094218455713 +2024-08-12 08:00:00,6,19.65675696385293,21.58872272472315,44.09806634824555 +2024-08-12 09:00:00,6,21.44738733996143,28.68088863160974,50.12331112964301 +2024-08-12 10:00:00,6,34.93254120212664,22.71701548666867,55.017329647578755 +2024-08-12 11:00:00,6,24.969403948951406,22.292487486814714,43.21222624119147 +2024-08-12 12:00:00,6,35.98419856054512,23.54879917601973,41.14592018147002 +2024-08-12 13:00:00,6,28.041025308983667,21.82702546211076,46.857788399503534 +2024-08-12 14:00:00,6,10.997216214834499,29.78705539543246,62.94966601011988 +2024-08-12 15:00:00,6,18.701744370316135,26.45748355765892,56.744316334666514 +2024-08-12 16:00:00,6,37.87178615970972,27.52580765735386,61.50645344714618 +2024-08-12 17:00:00,6,25.885813871690086,23.13810724742477,55.61473004148848 +2024-08-12 18:00:00,6,36.38645602898693,24.80999272034039,47.49296724094287 +2024-08-12 19:00:00,6,30.709432399165728,26.40326992227081,47.38163995294925 +2024-08-12 20:00:00,6,26.800616849072743,22.662883223745443,49.87619147275531 +2024-08-12 21:00:00,6,24.675844796305785,23.01361083180209,69.31027932826758 +2024-08-12 22:00:00,6,30.245386668113426,23.11241998620184,48.51628375827838 +2024-08-12 23:00:00,6,30.22413737480923,19.668214236011806,59.67190038390236 +2024-08-13 00:00:00,6,26.67574899662625,24.30023800457129,57.96947075587372 +2024-08-13 01:00:00,6,25.46950325397855,19.82547469367231,51.59965801538008 +2024-08-13 02:00:00,6,21.329023035988296,23.999151254907478,46.36473784802089 +2024-08-13 03:00:00,6,25.509772301731772,22.709955482941048,54.93273566515043 +2024-08-13 04:00:00,6,34.045470238447805,24.52231658628152,60.17514963270114 +2024-08-13 05:00:00,6,13.96579880303697,15.731167877609803,48.19132922366424 +2024-08-13 06:00:00,6,29.595427340211668,26.5813609232442,52.03569616814075 +2024-08-13 07:00:00,6,24.338818409304476,21.431970849290852,49.46836153570074 +2024-08-13 08:00:00,6,25.119593420499392,28.22952985258621,41.70572105146711 +2024-08-13 09:00:00,6,32.58974247703559,21.495676656151353,54.59632320026767 +2024-08-13 10:00:00,6,45.61467002938059,25.065080461715745,64.81637481018383 +2024-08-13 11:00:00,6,30.715533118259074,21.70234651304552,60.33660668332123 +2024-08-13 12:00:00,6,20.10355927137687,23.328443669905266,56.919464141980214 +2024-08-13 13:00:00,6,25.00335378459322,24.233631865879794,52.384658485965026 +2024-08-13 14:00:00,6,18.14680235918762,30.7690339368962,50.703989959765735 +2024-08-13 15:00:00,6,31.84152585258334,26.388609851564283,67.6542952618241 +2024-08-13 16:00:00,6,21.74172889112308,21.907233885240995,54.75834240000238 +2024-08-13 17:00:00,6,17.897955640257408,26.308931766709712,44.5167323718011 +2024-08-13 18:00:00,6,35.92963173464128,18.055920104487075,57.43325228306708 +2024-08-13 19:00:00,6,18.1530582122573,31.777501829513763,60.62980898576988 +2024-08-13 20:00:00,6,12.433306910332673,19.945048161169556,59.14402993739776 +2024-08-13 21:00:00,6,28.792199034121257,24.41018146145162,35.20949105643122 +2024-08-13 22:00:00,6,28.95598840762161,23.20623726736338,50.69589087627955 +2024-08-13 23:00:00,6,27.486287913812355,24.67200893007956,37.97587255508843 +2024-08-14 00:00:00,6,18.617178682991934,16.10436471724211,50.58323358537342 +2024-08-14 01:00:00,6,29.56564722790798,24.972814401999486,50.775106731701634 +2024-08-14 02:00:00,6,22.886013918259977,18.604200301708985,57.10205957191829 +2024-08-14 03:00:00,6,21.358906010042883,18.362409144555656,44.078870381394964 +2024-08-14 04:00:00,6,14.642403727565632,14.262789529514297,56.11542759393292 +2024-08-14 05:00:00,6,19.112935536928056,22.54221300158642,64.07897060757998 +2024-08-14 06:00:00,6,17.701993874574704,23.6895889373958,64.91102166492101 +2024-08-14 07:00:00,6,14.765937304486929,22.667616277098052,37.748484731491665 +2024-08-14 08:00:00,6,33.13998461038037,22.04291671800297,59.51198240510715 +2024-08-14 09:00:00,6,32.144856224598094,20.36220569052216,56.908566358338845 +2024-08-14 10:00:00,6,18.872868069495077,31.310404878232887,76.5419017531582 +2024-08-14 11:00:00,6,37.45450458453075,20.10772100112013,37.986582255633415 +2024-08-14 12:00:00,6,29.726241396293553,22.435409335616562,63.91254864633702 +2024-08-14 13:00:00,6,37.629311686001095,26.62951572561853,39.884308994647874 +2024-08-14 14:00:00,6,23.11012680425361,24.83884370092799,54.11273991316124 +2024-08-14 15:00:00,6,22.437874080382024,28.161574542768637,50.38411365842485 +2024-08-14 16:00:00,6,24.103196246249556,22.258506670512087,56.06797932617254 +2024-08-14 17:00:00,6,34.20086382372117,23.453749393133894,63.15229235989027 +2024-08-14 18:00:00,6,15.676540158395973,24.65320961401056,40.472330888530806 +2024-08-14 19:00:00,6,46.792848645536566,22.000195453646153,52.39973429694137 +2024-08-14 20:00:00,6,18.33907533570405,24.589631510414307,63.38118834960304 +2024-08-14 21:00:00,6,18.720388729833925,20.948153257788007,49.15361404213953 +2024-08-14 22:00:00,6,26.65575714438666,27.674586208560918,50.781946028786855 +2024-08-14 23:00:00,6,20.06659345483417,21.04432417227565,62.6041760347482 +2024-08-15 00:00:00,6,11.921675244403943,18.516713372757444,54.53745132890393 +2024-08-15 01:00:00,6,6.135280909410913,26.508348899489754,41.081154951592104 +2024-08-15 02:00:00,6,21.731876601085148,25.84660603414702,67.36187238123627 +2024-08-15 03:00:00,6,21.379962417656206,21.14860346206347,42.492320693609514 +2024-08-15 04:00:00,6,29.592165188753505,18.185205890509536,48.828781185166115 +2024-08-15 05:00:00,6,6.315037326233176,20.336208808190168,42.01992766542701 +2024-08-15 06:00:00,6,39.52463937525304,23.372414653903494,59.170943208726165 +2024-08-15 07:00:00,6,1.6068256598098074,26.882356687817676,44.708808116074536 +2024-08-15 08:00:00,6,28.090202561463574,26.334777491239635,58.045408073436896 +2024-08-15 09:00:00,6,21.56445138259936,27.812375067241117,75.23609195233897 +2024-08-15 10:00:00,6,28.428739296717346,21.988406229194673,48.34765794178297 +2024-08-15 11:00:00,6,22.322838213812354,24.5293353879938,52.77576737204122 +2024-08-15 12:00:00,6,21.15964574037015,25.511009577239477,56.03251497173744 +2024-08-15 13:00:00,6,19.11954764493177,26.786557324348223,43.12993322433881 +2024-08-15 14:00:00,6,45.33650457040015,17.607074441039842,62.856730234678906 +2024-08-15 15:00:00,6,16.073237106460063,24.881973637549667,68.11605516607146 +2024-08-15 16:00:00,6,34.53543496449227,24.235583543621168,48.944561003340304 +2024-08-15 17:00:00,6,28.577834894619865,19.7120007828661,54.4541552562071 +2024-08-15 18:00:00,6,34.04992679844904,20.47839547000438,42.884832847048415 +2024-08-15 19:00:00,6,29.048590466893415,21.574197409370996,52.23064676490438 +2024-08-15 20:00:00,6,11.753498118794557,19.381815099327888,57.771358400715656 +2024-08-15 21:00:00,6,26.586768972031884,21.084813060192708,48.2349241765004 +2024-08-15 22:00:00,6,27.450875239322404,30.689331151218635,51.76703806778092 +2024-08-15 23:00:00,6,24.222843092334205,20.33794823884877,79.43380402863242 +2024-08-16 00:00:00,6,20.699768920876846,28.415645159077744,41.947761180828465 +2024-08-16 01:00:00,6,20.279598813611436,22.29990664547741,41.7352913641248 +2024-08-16 02:00:00,6,21.530459314730503,23.785983459545875,57.5913123440919 +2024-08-16 03:00:00,6,33.9352961385557,24.76917564180097,50.232882716467344 +2024-08-16 04:00:00,6,21.74549201289289,23.33152601778589,51.82778006618212 +2024-08-16 05:00:00,6,15.88006253829516,21.13165428204268,60.661924747610854 +2024-08-16 06:00:00,6,42.62247433362124,22.93830646986992,59.80715930090192 +2024-08-16 07:00:00,6,11.683633430462958,25.41085364529465,61.053398612173865 +2024-08-16 08:00:00,6,14.004261078231938,21.34397088148104,44.01684083660683 +2024-08-16 09:00:00,6,28.327658748890563,22.570978241304008,53.416651876491756 +2024-08-16 10:00:00,6,28.5832209154674,16.177071981477418,53.861684599297575 +2024-08-16 11:00:00,6,25.55435593252647,24.326039274704566,79.16067601311366 +2024-08-16 12:00:00,6,34.09247969217996,23.10422320819123,60.097369775408715 +2024-08-16 13:00:00,6,28.68222445441146,22.580368604608612,71.43201868009729 +2024-08-16 14:00:00,6,22.98117037535184,19.15695431226563,60.46639966631638 +2024-08-16 15:00:00,6,44.702287070660674,18.437956887845466,48.506680952637616 +2024-08-16 16:00:00,6,16.1802877272383,28.20506038239506,43.87344067355581 +2024-08-16 17:00:00,6,20.52521282483014,28.96169215511613,61.162950150719986 +2024-08-16 18:00:00,6,39.362420267483714,24.64975940292071,52.56640264098895 +2024-08-16 19:00:00,6,31.144158254103633,20.38664464021367,51.34841204032288 +2024-08-16 20:00:00,6,29.932783794395156,26.336216636873033,55.022776403979194 +2024-08-16 21:00:00,6,21.811378997378412,23.404815439864663,59.23627985202845 +2024-08-16 22:00:00,6,7.733137498141048,33.948853499299354,57.219660589239105 +2024-08-16 23:00:00,6,23.915024378939428,20.862795136997576,62.62549786223852 +2024-08-17 00:00:00,6,31.78703502749548,20.91972772868598,65.93675084616378 +2024-08-17 01:00:00,6,26.926456785614967,23.19901956447627,54.17659964428008 +2024-08-17 02:00:00,6,31.683073222584532,24.39202037949538,42.98909193456102 +2024-08-17 03:00:00,6,20.465425888518645,22.39052699943861,68.58869896485308 +2024-08-17 04:00:00,6,17.29739593950521,22.31326363515693,48.861087988948334 +2024-08-17 05:00:00,6,23.796887151949534,28.93925522482594,61.12989364060312 +2024-08-17 06:00:00,6,30.6695077694539,22.366173444084964,44.77640200009836 +2024-08-17 07:00:00,6,22.34501203956886,27.83827406279336,46.28716451916356 +2024-08-17 08:00:00,6,28.073524376047455,24.361120472015255,51.53363478385252 +2024-08-17 09:00:00,6,31.300293869839482,24.552405537184555,78.95333383296847 +2024-08-17 10:00:00,6,39.461038859725086,21.47101296280889,61.849332109919345 +2024-08-17 11:00:00,6,40.91966977755945,23.696769016773455,55.35445691009059 +2024-08-17 12:00:00,6,27.448990526406234,19.427125042237332,59.090764370690806 +2024-08-17 13:00:00,6,20.238792310640758,23.366389475101577,57.347039553293115 +2024-08-17 14:00:00,6,31.39525992189511,23.332653633496566,70.90633305995253 +2024-08-17 15:00:00,6,10.932064004879745,21.09915835419442,51.74849422356911 +2024-08-17 16:00:00,6,30.635103992164805,32.14457593717485,60.30826623656612 +2024-08-17 17:00:00,6,35.70576492728118,23.940098913005418,64.25571988134072 +2024-08-17 18:00:00,6,30.116807959783,30.00536412828565,54.927960067842584 +2024-08-17 19:00:00,6,30.591390166390934,21.184381721966673,48.4070745442345 +2024-08-17 20:00:00,6,39.97174177457438,24.523581235295044,53.7565578368229 +2024-08-17 21:00:00,6,35.720942899051146,25.996979141768556,54.62337338434886 +2024-08-17 22:00:00,6,19.411334042474806,28.17948006292363,49.252769616274804 +2024-08-17 23:00:00,6,28.69853251957513,24.88249856414576,58.88713469735931 +2024-08-18 00:00:00,6,31.703830567373853,17.274708359418145,53.98258866310175 +2024-08-18 01:00:00,6,29.139819165455055,24.222924943715082,64.80463373160615 +2024-08-18 02:00:00,6,7.83085318631332,20.992278003447797,44.508292363148655 +2024-08-18 03:00:00,6,35.00095811764325,28.14178987813203,50.223767979136035 +2024-08-18 04:00:00,6,15.52309509482105,23.431054480245766,70.40592692032092 +2024-08-18 05:00:00,6,19.519749269472204,22.754316021119838,53.925653664221315 +2024-08-18 06:00:00,6,40.00317800566808,21.220161619949753,56.12716936661517 +2024-08-18 07:00:00,6,20.155688708679023,18.302399149898818,47.28261084196227 +2024-08-18 08:00:00,6,34.352915667271205,22.920000732565814,62.6519693424345 +2024-08-18 09:00:00,6,34.230422400782786,21.830657685614344,63.30321235942811 +2024-08-18 10:00:00,6,36.734099122669214,20.0089890227868,53.26734628701216 +2024-08-18 11:00:00,6,24.497080059466043,19.407327352711196,60.30841952714589 +2024-08-18 12:00:00,6,27.82770838484956,24.70192356038453,50.00892945103537 +2024-08-18 13:00:00,6,25.58637593701564,23.53163649510558,62.91195172577248 +2024-08-18 14:00:00,6,34.2160456680124,24.443441419221287,64.91318460408893 +2024-08-18 15:00:00,6,30.26408960275073,18.561988497552843,61.16894084542272 +2024-08-18 16:00:00,6,30.17957071474433,22.68138901188114,61.13213388468021 +2024-08-18 17:00:00,6,17.482488163084298,24.62577183812752,54.05923944942665 +2024-08-18 18:00:00,6,18.193514572004354,18.904337362517346,49.88823126909996 +2024-08-18 19:00:00,6,24.621749302967824,19.268573361287785,46.532481778409434 +2024-08-18 20:00:00,6,27.908842464693087,22.5878943650259,44.98169140041375 +2024-08-18 21:00:00,6,35.64282299326764,24.09749811906219,50.11350894627322 +2024-08-18 22:00:00,6,27.869097875368023,21.213580413507426,53.15116441436583 +2024-08-18 23:00:00,6,27.815835789503218,21.436141269648715,46.66018856537329 +2024-08-19 00:00:00,6,18.135108012689884,23.183663431595708,54.457322175448816 +2024-08-19 01:00:00,6,28.601129488120208,21.431663988618766,75.77406417211424 +2024-08-19 02:00:00,6,34.71051211473227,25.553351868795122,45.06912667000411 +2024-08-19 03:00:00,6,7.419005360528249,30.405578856134547,41.24274287039015 +2024-08-19 04:00:00,6,18.55048186834292,22.32692597471236,60.61240773524145 +2024-08-19 05:00:00,6,10.979893462340888,23.629288450233325,57.19251439403091 +2024-08-19 06:00:00,6,30.071311654872755,20.10941587963161,62.55470044305615 +2024-08-19 07:00:00,6,0.0,28.911559170825242,60.261369123674115 +2024-08-19 08:00:00,6,28.22696774326202,26.671542788232923,68.33223146880741 +2024-08-19 09:00:00,6,19.82082460492041,25.086514829811378,49.914525274522795 +2024-08-19 10:00:00,6,17.727460153902157,21.160024485602083,59.76481703393906 +2024-08-19 11:00:00,6,40.737954057103316,28.88087055476723,65.7290035991769 +2024-08-19 12:00:00,6,37.76247475975386,22.91340764040558,45.55255156024182 +2024-08-19 13:00:00,6,27.367554506842954,27.547396666996434,47.374781846465666 +2024-08-19 14:00:00,6,45.356977944262795,23.22820404467834,55.853900038464666 +2024-08-19 15:00:00,6,34.617224814315534,21.658819728674086,59.30309868330403 +2024-08-19 16:00:00,6,11.368245371358693,19.633451882358024,51.90572903657944 +2024-08-19 17:00:00,6,4.504259778094017,18.561820891499778,60.64653909291467 +2024-08-19 18:00:00,6,38.67587993538997,23.794115043245878,50.41088987486357 +2024-08-19 19:00:00,6,21.084097032952638,24.68678421831688,58.58216253821193 +2024-08-19 20:00:00,6,14.242082631954217,20.653998482198897,43.67282478653909 +2024-08-19 21:00:00,6,17.542554690489908,23.41894434312618,25.74208309544004 +2024-08-19 22:00:00,6,29.244517898917195,27.457707478229377,62.37423828840867 +2024-08-19 23:00:00,6,21.41002472393891,18.58831070936377,45.58938007707614 +2024-08-20 00:00:00,6,28.160125823487697,19.708685820668823,65.87518931404574 +2024-08-20 01:00:00,6,23.244458115619896,22.092714116842505,54.523459385044056 +2024-08-20 02:00:00,6,13.859939185332538,25.25331786851187,59.714193608735584 +2024-08-20 03:00:00,6,24.454131100107595,23.925260691675476,66.18857400122428 +2024-08-20 04:00:00,6,9.614563909141504,24.220355634392224,57.65608380466096 +2024-08-20 05:00:00,6,24.22325189611649,25.722607198969282,72.66074884508913 +2024-08-20 06:00:00,6,35.278347860863086,24.25412264586891,52.9267675019412 +2024-08-20 07:00:00,6,29.14826569046908,25.87661875872019,52.04320358619408 +2024-08-20 08:00:00,6,21.862616367628707,22.116548936327813,58.307905178973606 +2024-08-20 09:00:00,6,24.253008358285804,16.69814814703588,42.65519568223763 +2024-08-20 10:00:00,6,36.0386741142157,36.75000571909273,66.24188416797541 +2024-08-20 11:00:00,6,12.801087170715515,23.131313234938858,59.73035917634493 +2024-08-20 12:00:00,6,36.69837798116276,21.53855912644785,58.22597809520806 +2024-08-20 13:00:00,6,34.419282241016184,17.694283316600135,66.64319939280944 +2024-08-20 14:00:00,6,33.157362324107424,28.00102583616732,54.854752263978654 +2024-08-20 15:00:00,6,27.869172817105692,17.977078420911617,59.8733007640397 +2024-08-20 16:00:00,6,18.51404555509695,23.628609421831996,43.48484052957091 +2024-08-20 17:00:00,6,26.355574856676082,24.62280059799378,56.107212573339396 +2024-08-20 18:00:00,6,50.78080802947967,19.97758231625017,80.23500347328127 +2024-08-20 19:00:00,6,5.6000983494999055,24.114471460639926,70.30260246438412 +2024-08-20 20:00:00,6,13.851073296799209,24.65114775767036,60.29896191162855 +2024-08-20 21:00:00,6,14.649817636978721,21.334862255874256,41.70517874966106 +2024-08-20 22:00:00,6,29.47335273482311,21.141444827805646,48.256260614603306 +2024-08-20 23:00:00,6,26.003583378489424,25.774309849315838,50.00956158023927 +2024-08-21 00:00:00,6,37.46805486101434,22.0790001112636,39.864694188936966 +2024-08-21 01:00:00,6,10.920380936650897,20.40320114651648,52.21851452058269 +2024-08-21 02:00:00,6,21.694112545391945,17.542937708388706,50.932642463898326 +2024-08-21 03:00:00,6,19.08587333351013,22.93168664851026,51.90763076082448 +2024-08-21 04:00:00,6,16.281847823186464,21.41062604632402,61.695007556956696 +2024-08-21 05:00:00,6,23.519802789875925,22.66355579778031,47.82268721736604 +2024-08-21 06:00:00,6,22.86072177298542,25.87045543804259,49.58608624037694 +2024-08-21 07:00:00,6,33.2274861247939,26.18583032787519,58.83170014198641 +2024-08-21 08:00:00,6,31.535397653831353,27.92787058854569,69.07604675524765 +2024-08-21 09:00:00,6,34.516943364030055,17.84196006207378,60.447888088626335 +2024-08-21 10:00:00,6,49.21260272509581,24.032090062328642,52.951043361169525 +2024-08-21 11:00:00,6,30.673288608927784,27.328349419816398,55.626022128393025 +2024-08-21 12:00:00,6,19.60223072480523,23.86878366409227,56.38621893600931 +2024-08-21 13:00:00,6,35.16737738221353,19.078548878816267,35.9300324938406 +2024-08-21 14:00:00,6,23.91560678045333,23.22107840983582,51.69190535928748 +2024-08-21 15:00:00,6,27.510390906608453,20.992833618365776,45.42117885536891 +2024-08-21 16:00:00,6,19.760672413172102,31.05800085579229,58.18555219891265 +2024-08-21 17:00:00,6,16.487321036718836,25.328777293268054,57.429225185081684 +2024-08-21 18:00:00,6,34.67903778440009,24.039920746716255,47.60752131012648 +2024-08-21 19:00:00,6,37.66536871485296,19.59698630058652,46.84074383368951 +2024-08-21 20:00:00,6,17.46367694883395,24.299648934822653,39.07900594712916 +2024-08-21 21:00:00,6,22.02495170781375,25.81633566815998,53.04097952690836 +2024-08-21 22:00:00,6,30.88460449698408,24.976223286359346,52.21038826000541 +2024-08-21 23:00:00,6,34.02078700825086,24.056602949783525,45.35030960941431 +2024-08-22 00:00:00,6,18.429611861085224,16.67080835648772,28.203100082812295 +2024-08-22 01:00:00,6,20.926717923388647,24.287390281182496,73.97480177856626 +2024-08-22 02:00:00,6,16.78355792675982,26.344657409449045,63.02518483222726 +2024-08-22 03:00:00,6,22.965358200128197,19.41437106691273,57.25919510870057 +2024-08-22 04:00:00,6,37.09468597472262,21.07031157866568,49.53941431794016 +2024-08-22 05:00:00,6,17.696120318551223,23.124600080161564,44.32147672761602 +2024-08-22 06:00:00,6,18.307102979567016,21.725734179997268,51.46321332585798 +2024-08-22 07:00:00,6,25.80829469412386,24.478605642702572,57.78771557000735 +2024-08-22 08:00:00,6,42.20303131530371,22.157793228877054,66.39862539064025 +2024-08-22 09:00:00,6,24.59106768626058,26.28833158152053,82.56436299042127 +2024-08-22 10:00:00,6,20.451207609749744,25.63615483943078,41.7283501413647 +2024-08-22 11:00:00,6,36.853093639287906,28.61246295150196,55.1695134895683 +2024-08-22 12:00:00,6,18.96163969240365,30.566633638300747,59.848870082013356 +2024-08-22 13:00:00,6,25.160667958340763,22.834809819869015,52.59465180657939 +2024-08-22 14:00:00,6,17.15794816429397,22.551546807385805,64.00120218929362 +2024-08-22 15:00:00,6,30.86583085141179,13.100104083375689,65.33599908810217 +2024-08-22 16:00:00,6,12.86328864014031,24.12375230722251,60.32407172830252 +2024-08-22 17:00:00,6,27.27101552886257,30.225404515144234,57.209069538778714 +2024-08-22 18:00:00,6,21.06213570838508,23.485203269030702,71.79300510289838 +2024-08-22 19:00:00,6,18.113417043227535,18.835347792365052,50.63491883865972 +2024-08-22 20:00:00,6,24.15798607917916,27.512666467454807,43.84510908639581 +2024-08-22 21:00:00,6,33.63970775219114,26.876465786487934,77.63854533017712 +2024-08-22 22:00:00,6,14.326960587733616,22.362534769779447,45.30159098314927 +2024-08-22 23:00:00,6,18.32999096670134,21.204933708877718,46.93485875120548 +2024-08-23 00:00:00,6,25.414255131366673,20.312436489024464,63.77766032800033 +2024-08-23 01:00:00,6,25.25376837706804,19.66516133160772,49.32638890324631 +2024-08-23 02:00:00,6,18.3734470561079,20.306883535822784,37.756702027711505 +2024-08-23 03:00:00,6,13.292794495052245,23.257771452054932,43.43489553267693 +2024-08-23 04:00:00,6,14.319299509433886,21.675748585071975,42.77631827316576 +2024-08-23 05:00:00,6,16.193803526673452,21.666065886640364,55.206399246957304 +2024-08-23 06:00:00,6,35.152805796343266,33.254321856500084,53.990639274404 +2024-08-23 07:00:00,6,31.660231062476573,26.476111203771236,51.36834902395244 +2024-08-23 08:00:00,6,22.208289614802098,29.39817744913359,53.214543671379225 +2024-08-23 09:00:00,6,32.12859322176821,20.468360688828387,73.83987718632432 +2024-08-23 10:00:00,6,25.065910413282033,22.101704845743743,57.1567117169479 +2024-08-23 11:00:00,6,34.64883183840046,21.433586786850228,53.34929788312643 +2024-08-23 12:00:00,6,31.430206276599165,27.532703133344985,51.04775751596681 +2024-08-23 13:00:00,6,35.14080071884877,23.04321772602067,53.54375341754568 +2024-08-23 14:00:00,6,24.225060824672024,25.59371688788185,60.63733021108533 +2024-08-23 15:00:00,6,24.346573822650985,19.39543765190806,72.7356191504603 +2024-08-23 16:00:00,6,24.910950041221618,20.27465466422045,50.646037459862434 +2024-08-23 17:00:00,6,31.77082606328444,27.41758613694109,53.29931067820274 +2024-08-23 18:00:00,6,23.42428613400928,27.491396222266303,53.666715248150595 +2024-08-23 19:00:00,6,45.37939850204245,24.460316000803196,58.876095877856194 +2024-08-23 20:00:00,6,22.696609903741372,22.53636076280498,47.86404161798208 +2024-08-23 21:00:00,6,26.291955441587856,26.81780499231334,43.91657771268077 +2024-08-23 22:00:00,6,28.65604275204043,18.62215322440039,52.243820854871856 +2024-08-23 23:00:00,6,29.5150659066765,24.480403220085545,49.334305858105004 +2024-08-24 00:00:00,6,26.354739329525096,8.433616863826096,52.736340083957124 +2024-08-24 01:00:00,6,23.633369935053167,22.33485305618313,62.86708664097583 +2024-08-24 02:00:00,6,19.81642164317416,24.887670200746836,51.93081669737462 +2024-08-24 03:00:00,6,23.05503226529463,18.193884711629014,49.799929361835254 +2024-08-24 04:00:00,6,21.599098016204984,24.801309132120405,47.12177637708128 +2024-08-24 05:00:00,6,20.356196463614616,27.638559462127724,62.19233054823741 +2024-08-24 06:00:00,6,11.79953080823232,22.801580905515436,63.0105810202391 +2024-08-24 07:00:00,6,31.031298200229866,23.8607692616655,47.758288509337056 +2024-08-24 08:00:00,6,39.662410446717004,27.280050668618347,55.42326236261876 +2024-08-24 09:00:00,6,17.974962969406263,18.9435398598381,66.92622044571335 +2024-08-24 10:00:00,6,31.485712778437176,25.53014473299319,66.43283548348157 +2024-08-24 11:00:00,6,29.03631118411188,28.105416846205983,59.00690918828299 +2024-08-24 12:00:00,6,27.600203993650386,19.072467264348624,54.84918322618329 +2024-08-24 13:00:00,6,30.750872767889547,27.243294180431906,67.31082148169777 +2024-08-24 14:00:00,6,40.09941027947292,23.243544824012904,55.78687110054889 +2024-08-24 15:00:00,6,28.4019884910685,23.146684690673432,62.53819649349076 +2024-08-24 16:00:00,6,46.50800387413188,17.427922341730838,58.22387826939314 +2024-08-24 17:00:00,6,6.689677608361283,28.633556027367348,47.101537371770775 +2024-08-24 18:00:00,6,25.706546747491196,18.39665822790395,44.99280297518057 +2024-08-24 19:00:00,6,19.567048552664634,19.874871771147493,60.13599778282854 +2024-08-24 20:00:00,6,20.50136992100946,23.956390784987597,64.6622559393552 +2024-08-24 21:00:00,6,21.94755492124237,25.380933624229176,46.597564505423044 +2024-08-24 22:00:00,6,23.305738266047406,21.431016563783167,67.58865838288133 +2024-08-24 23:00:00,6,28.774159680076924,26.824286617618547,47.63257481504966 +2024-08-25 00:00:00,6,13.36331216265799,19.55778250928802,47.413044886509006 +2024-08-25 01:00:00,6,15.816009807359439,22.327256104236486,35.94176580040186 +2024-08-25 02:00:00,6,32.84423867515626,20.53473103270666,49.02254326215327 +2024-08-25 03:00:00,6,38.812564710483315,24.0082249329484,48.3109590769529 +2024-08-25 04:00:00,6,0.0,20.965182207937932,60.244606084141445 +2024-08-25 05:00:00,6,19.453465083508846,22.85016292958007,64.75258036608363 +2024-08-25 06:00:00,6,21.95540065948175,24.193128515562716,61.03718354584638 +2024-08-25 07:00:00,6,20.72809370837041,22.196856566594324,49.828073455582484 +2024-08-25 08:00:00,6,21.86951189359235,28.19269724481696,71.72694284031797 +2024-08-25 09:00:00,6,20.582022030386163,24.477207485747766,49.772127103574405 +2024-08-25 10:00:00,6,24.772049438096403,25.338916755491322,28.226257728359226 +2024-08-25 11:00:00,6,29.421598928076502,21.588284579164416,58.70011096359739 +2024-08-25 12:00:00,6,28.52229608876831,21.29184277072435,50.189048741782884 +2024-08-25 13:00:00,6,32.2555071198806,26.416479413365945,55.861721059051696 +2024-08-25 14:00:00,6,26.904164301994395,23.406139979591355,52.49159901928659 +2024-08-25 15:00:00,6,23.978098298610178,25.30083974830894,72.51770952926316 +2024-08-25 16:00:00,6,30.304446440928757,20.401603904699122,40.05014455241435 +2024-08-25 17:00:00,6,23.752611237021036,15.951794571326056,59.571627711507716 +2024-08-25 18:00:00,6,19.33372546662313,23.646175372912666,45.883232937024175 +2024-08-25 19:00:00,6,23.961119099748192,30.105951939564072,44.533796320246665 +2024-08-25 20:00:00,6,33.00111263462776,23.852627114617842,39.89131626151351 +2024-08-25 21:00:00,6,24.107158655279804,16.23342262918259,58.44118008258017 +2024-08-25 22:00:00,6,25.641169669523,27.546300816383397,46.359992803055924 +2024-08-25 23:00:00,6,26.71551289919005,26.64690099163355,44.360725876329035 +2024-08-26 00:00:00,6,18.479165511008684,26.464537475225384,50.766117957477306 +2024-08-26 01:00:00,6,13.028019585640639,21.02971876979816,50.16952672046208 +2024-08-26 02:00:00,6,23.74984564081459,16.403012558275183,49.83573537901802 +2024-08-26 03:00:00,6,21.069490477080365,18.30943217528731,59.151380692146994 +2024-08-26 04:00:00,6,21.53064429365197,21.609227585637434,43.948822795224984 +2024-08-26 05:00:00,6,27.130835575678205,26.423191165062544,53.30993942699278 +2024-08-26 06:00:00,6,42.4203450290075,20.968535463630417,61.1568630530856 +2024-08-26 07:00:00,6,20.923543114809885,21.929428473814216,54.09736446448795 +2024-08-26 08:00:00,6,28.173266260615442,25.474795464673143,59.87085393354746 +2024-08-26 09:00:00,6,25.003959737770533,29.0219561837185,60.78992295305561 +2024-08-26 10:00:00,6,36.1713201232197,17.397801319152066,65.99865419568685 +2024-08-26 11:00:00,6,30.112392517669658,32.131863403288314,73.71782196286154 +2024-08-26 12:00:00,6,30.808038067040933,22.065980503097446,63.07995947124683 +2024-08-26 13:00:00,6,17.59605031095468,22.081096064501743,52.43363534553761 +2024-08-26 14:00:00,6,8.43731319192619,23.234121230449134,50.253603754466745 +2024-08-26 15:00:00,6,34.295313529902714,21.899365903598653,60.17824525556849 +2024-08-26 16:00:00,6,21.291418418034937,23.587921427752804,49.13831761050622 +2024-08-26 17:00:00,6,24.169563441293057,19.871445181128518,54.93042793693064 +2024-08-26 18:00:00,6,41.14704274026633,24.841209396533696,54.9028014332507 +2024-08-26 19:00:00,6,36.79323170952128,21.87483635062103,34.14459618032478 +2024-08-26 20:00:00,6,32.187387810097185,24.220203840587224,40.042729900529125 +2024-08-26 21:00:00,6,38.85479109380171,30.181680619380074,58.65795395761248 +2024-08-26 22:00:00,6,36.95374014157942,24.557203872403612,57.51877280689034 +2024-08-26 23:00:00,6,31.420617591456114,25.82653793736287,62.09170186746828 +2024-08-27 00:00:00,6,34.20079507747263,24.369723233128326,55.621072150626404 +2024-08-27 01:00:00,6,32.71027802581962,23.36491523903488,56.65401289182343 +2024-08-27 02:00:00,6,20.637470536869422,24.948711487838864,61.68221796938703 +2024-08-27 03:00:00,6,30.724767000728285,25.823246405703973,53.5334314872504 +2024-08-27 04:00:00,6,17.108302719454784,21.20703675126618,61.71789525909959 +2024-08-27 05:00:00,6,15.07409980208831,22.76139768918825,68.69957243500737 +2024-08-27 06:00:00,6,11.410184424235899,15.762284545991275,65.6485207679285 +2024-08-27 07:00:00,6,30.796772885066414,32.12579177650295,42.20608929366536 +2024-08-27 08:00:00,6,28.02798769601718,24.350908421125038,58.195826059113394 +2024-08-27 09:00:00,6,41.04600890322579,26.880182640164865,62.72723224399357 +2024-08-27 10:00:00,6,29.165937855819635,23.99725376737808,67.15133284254529 +2024-08-27 11:00:00,6,30.06311225848037,26.393685475003462,54.5010649563549 +2024-08-27 12:00:00,6,38.649983367796345,26.39545288572686,42.67656356646035 +2024-08-27 13:00:00,6,29.6878318706162,25.992004185592492,49.58055239312977 +2024-08-27 14:00:00,6,30.837755658097095,19.378359176931312,49.791701150543695 +2024-08-27 15:00:00,6,29.844599487617845,20.86538386418094,68.54133185334794 +2024-08-27 16:00:00,6,26.361187054577606,21.324941052614452,66.09320602451612 +2024-08-27 17:00:00,6,16.463580301685287,28.52092044950464,56.74610703878338 +2024-08-27 18:00:00,6,34.2358889339855,25.593935427500462,61.901030655664115 +2024-08-27 19:00:00,6,18.896039436060384,26.192716635143764,50.63339491212341 +2024-08-27 20:00:00,6,25.85199113904574,26.08452796388336,44.377112970354695 +2024-08-27 21:00:00,6,25.37207679552367,26.533279973293574,58.82681546386069 +2024-08-27 22:00:00,6,22.943579967989052,21.295092670852757,58.11236675409848 +2024-08-27 23:00:00,6,24.667666194225454,22.836548290107764,57.05710288334981 +2024-08-28 00:00:00,6,24.027449329516788,18.960319288802907,47.06178825749151 +2024-08-28 01:00:00,6,16.434873177654502,24.87444663207706,57.22646127987544 +2024-08-28 02:00:00,6,16.212397364293842,28.230391706056288,61.03108064974444 +2024-08-28 03:00:00,6,33.07588968908053,21.505337329533454,49.307893374525385 +2024-08-28 04:00:00,6,30.676657569923023,15.117345264227987,37.929821913503744 +2024-08-28 05:00:00,6,32.28855815957711,30.654919779323073,49.37686706597062 +2024-08-28 06:00:00,6,19.931348338828524,22.760703648711527,49.02451475667318 +2024-08-28 07:00:00,6,19.111897105938475,21.677817909667134,53.100932377232695 +2024-08-28 08:00:00,6,29.220394794841464,22.608323432583592,62.05860851151137 +2024-08-28 09:00:00,6,30.412692010899747,25.11030286462137,57.51445326027646 +2024-08-28 10:00:00,6,34.181045179114236,21.29976783168342,52.490249122215346 +2024-08-28 11:00:00,6,26.4154762485224,28.800385334688258,43.81910002581067 +2024-08-28 12:00:00,6,30.64312086820271,19.38091216519061,63.06337860115752 +2024-08-28 13:00:00,6,17.02663845963663,28.117650433294372,35.71023927470216 +2024-08-28 14:00:00,6,11.430454669826283,25.78984630997694,56.59378873815034 +2024-08-28 15:00:00,6,19.045048852764907,20.397597003094923,56.93728050620394 +2024-08-28 16:00:00,6,35.42788432615922,24.10202389753155,52.188381452900586 +2024-08-28 17:00:00,6,14.22127755853908,26.802411134696154,54.463367695811996 +2024-08-28 18:00:00,6,20.805622300141657,27.789744202722957,44.06364027050555 +2024-08-28 19:00:00,6,40.291159689505655,24.48448283260928,48.310821835672876 +2024-08-28 20:00:00,6,33.839282416173965,21.148003139240522,44.21629857816084 +2024-08-28 21:00:00,6,20.199649973156795,12.732702752835918,43.631405731893096 +2024-08-28 22:00:00,6,34.09309543789628,21.612807870035784,56.76384909258337 +2024-08-28 23:00:00,6,26.09642109591812,22.87646779584911,52.07176925004991 +2024-08-29 00:00:00,6,9.586841437922157,20.605516440546694,52.507526842970854 +2024-08-29 01:00:00,6,24.692489259890237,18.737417972570448,71.38609382461914 +2024-08-29 02:00:00,6,21.350105055065843,27.734742582697308,66.02034918015029 +2024-08-29 03:00:00,6,10.095458947561696,20.010609528542314,45.55576310133634 +2024-08-29 04:00:00,6,8.430204772010809,24.21974613619984,55.15685286033915 +2024-08-29 05:00:00,6,25.288507860745003,13.312114596964122,44.17476804251356 +2024-08-29 06:00:00,6,29.73254364071493,21.394799164519963,64.26497135288568 +2024-08-29 07:00:00,6,30.696222628240214,21.937098333149784,59.764651742773864 +2024-08-29 08:00:00,6,36.704485247412734,26.601574466488273,62.599365550986384 +2024-08-29 09:00:00,6,29.14894604365368,21.83198422780507,60.30745070762227 +2024-08-29 10:00:00,6,28.31205338851782,26.017064351214927,50.647889141358654 +2024-08-29 11:00:00,6,27.06989542420526,18.457214569940728,57.27772560152124 +2024-08-29 12:00:00,6,28.538814046580555,22.592942610937595,60.96786726389491 +2024-08-29 13:00:00,6,16.998550696337965,19.77048582514487,50.11487210083989 +2024-08-29 14:00:00,6,14.602368297185539,19.429298551560336,53.035008832805744 +2024-08-29 15:00:00,6,12.245196738217771,21.552239354199344,38.83439544886876 +2024-08-29 16:00:00,6,14.953655243799979,20.223171594526626,46.165884797509484 +2024-08-29 17:00:00,6,16.553266662008333,23.34500538155905,60.15647978518585 +2024-08-29 18:00:00,6,13.519425213737374,27.338759326309727,48.670401582186344 +2024-08-29 19:00:00,6,33.37118737567839,27.90358536567934,47.69087008585583 +2024-08-29 20:00:00,6,23.522741169327638,30.1218462170785,52.089882920075496 +2024-08-29 21:00:00,6,27.521790812712382,24.252765203704545,49.93034149609105 +2024-08-29 22:00:00,6,27.964548434509762,18.698644048717625,50.83050460052378 +2024-08-29 23:00:00,6,37.94382657862563,26.723152943558123,64.67516664698746 +2024-08-30 00:00:00,6,16.39816966208045,21.824807755512545,43.76995062111222 +2024-08-30 01:00:00,6,21.41162859959934,21.24652759047229,62.481277719767036 +2024-08-30 02:00:00,6,15.565265947300333,28.13258125082644,65.6170252301817 +2024-08-30 03:00:00,6,27.613141297876137,20.06161567000857,61.715454662893386 +2024-08-30 04:00:00,6,9.163850534499282,21.0230252856304,64.79979482105074 +2024-08-30 05:00:00,6,11.46025121611639,21.21712509312806,37.12434835756649 +2024-08-30 06:00:00,6,11.299664346105246,22.143599311760358,62.862524991007824 +2024-08-30 07:00:00,6,17.38407948286715,22.35748442829718,44.42227824845505 +2024-08-30 08:00:00,6,28.022684870136988,29.940877236039903,63.536192920477426 +2024-08-30 09:00:00,6,28.623159353160915,20.514873255110036,58.01771296341232 +2024-08-30 10:00:00,6,41.867469341712074,26.978738714227056,57.042568388370334 +2024-08-30 11:00:00,6,30.23369513368568,22.892828200656517,27.94968056775515 +2024-08-30 12:00:00,6,14.798133663860426,30.20385948545993,43.460713546928815 +2024-08-30 13:00:00,6,31.302876854196022,22.885645919519973,60.54879599525344 +2024-08-30 14:00:00,6,36.639418404472394,18.508964606000283,44.39608191099633 +2024-08-30 15:00:00,6,33.13137779876448,22.36898710746895,52.70853231268225 +2024-08-30 16:00:00,6,19.781296243261465,28.614533832744364,53.361280555435705 +2024-08-30 17:00:00,6,28.41940979892675,20.453597134538867,42.3860422595642 +2024-08-30 18:00:00,6,29.43120149859036,23.742519216467855,48.41582470666212 +2024-08-30 19:00:00,6,34.28001051454389,26.547218589283112,69.52429612187288 +2024-08-30 20:00:00,6,34.03722534816563,21.42387702950831,51.66720197377726 +2024-08-30 21:00:00,6,32.95446693158394,20.969039693609012,39.847958736157395 +2024-08-30 22:00:00,6,16.08597668900699,22.945619245736818,57.77417964275425 +2024-08-30 23:00:00,6,13.07475737022497,20.925250771951827,62.61873991071208 +2024-08-31 00:00:00,6,32.809835156142704,26.112041968813898,50.685804921357345 +2024-08-31 01:00:00,6,37.972508314554204,23.672366054408087,52.359187613588084 +2024-08-31 02:00:00,6,15.410590006246464,25.022747896548836,52.887243543445635 +2024-08-31 03:00:00,6,22.891801563063538,18.303715862384653,46.81810607638875 +2024-08-31 04:00:00,6,25.53171699837383,28.22439506575692,59.816984790789796 +2024-08-31 05:00:00,6,20.043497649797132,15.200789764701339,50.691501121382295 +2024-08-31 06:00:00,6,36.906266586555304,14.209644183742945,53.91953093352368 +2024-08-31 07:00:00,6,27.73297167185617,20.62913808283299,49.09171081242451 +2024-08-31 08:00:00,6,11.442578162429054,28.188949017407797,53.596896051872555 +2024-08-31 09:00:00,6,36.13526967822973,29.147349378406737,46.50807809137281 +2024-08-31 10:00:00,6,22.257049855467013,28.603962454841533,50.58426642110759 +2024-08-31 11:00:00,6,25.88338695229826,25.393489615967635,66.99465081499032 +2024-08-31 12:00:00,6,25.117484431328364,18.08322955587525,75.27989339532766 +2024-08-31 13:00:00,6,27.836169720997436,29.491799870533832,51.01676320358108 +2024-08-31 14:00:00,6,16.504679762314645,17.98852115269242,47.82671128966075 +2024-08-31 15:00:00,6,32.330311436344985,19.110477696038856,69.47181930959653 +2024-08-31 16:00:00,6,33.866889739461634,20.70849762019799,62.173358031735354 +2024-08-31 17:00:00,6,16.493410064987415,23.66364743911114,64.02614606500816 +2024-08-31 18:00:00,6,26.933003368156704,22.457611183715436,53.99876919258805 +2024-08-31 19:00:00,6,34.608592016000735,22.636015899418798,71.2351726324557 +2024-08-31 20:00:00,6,24.147660288754377,20.17612866437208,45.09155277386286 +2024-08-31 21:00:00,6,36.63519967084596,25.047463855282846,33.70202968747556 +2024-08-31 22:00:00,6,35.585820197633026,22.829609699222065,55.26731141150359 +2024-08-31 23:00:00,6,34.33371309157997,24.898251930890325,60.518076073129826 +2024-09-01 00:00:00,6,21.64302245981938,25.096222834274236,55.08950774650234 +2024-09-01 01:00:00,6,20.21082983260434,24.41483184612973,40.464631312625755 +2024-09-01 02:00:00,6,13.45234634978555,18.873955577368832,51.72758163696831 +2024-09-01 03:00:00,6,11.524550184054984,19.765207084100624,67.9244176128593 +2024-09-01 04:00:00,6,16.897249325224777,20.733673108034864,66.8819246810348 +2024-09-01 05:00:00,6,19.09684704342535,24.450069944264836,50.20024087495815 +2024-09-01 06:00:00,6,21.34263429963067,28.055490403933398,59.14902763848881 +2024-09-01 07:00:00,6,25.791640830116055,21.65195494935641,60.6968104860638 +2024-09-01 08:00:00,6,20.296785903486878,31.006293230193293,45.09632207093726 +2024-09-01 09:00:00,6,36.5251834264155,22.04907935654115,47.785153529493 +2024-09-01 10:00:00,6,22.4255575359485,30.50369724313325,57.24203619107375 +2024-09-01 11:00:00,6,19.292827258869934,25.691725878545135,46.72650202654046 +2024-09-01 12:00:00,6,43.46013284863187,21.46711872867132,46.470066377633444 +2024-09-01 13:00:00,6,24.518314758268435,21.85331748162755,53.2818833320893 +2024-09-01 14:00:00,6,30.9917142026318,19.503417142872163,62.6986214523585 +2024-09-01 15:00:00,6,23.421443852999325,19.5944708733047,37.9528739494185 +2024-09-01 16:00:00,6,23.075466997068247,24.24508846300295,45.862603127199066 +2024-09-01 17:00:00,6,19.85097184933022,20.146762245377026,57.6236476128761 +2024-09-01 18:00:00,6,28.039674570205058,23.24964556176279,62.81773969613656 +2024-09-01 19:00:00,6,24.365704847215824,23.154914976432135,49.420742951047394 +2024-09-01 20:00:00,6,26.40565157697483,23.91882850911881,41.02000429908939 +2024-09-01 21:00:00,6,17.47085761728829,19.984713610530942,55.33472214563757 +2024-09-01 22:00:00,6,10.6226631208221,18.196562301266383,50.045462761738094 +2024-09-01 23:00:00,6,28.17128462093712,28.71792469131702,57.94603116282714 +2024-09-02 00:00:00,6,9.057788750017982,24.933748351670523,34.63911099836369 +2024-09-02 01:00:00,6,20.958649538818133,25.68167619961438,57.409503056758915 +2024-09-02 02:00:00,6,39.98088922439988,18.635867845132193,47.644301577169045 +2024-09-02 03:00:00,6,20.488699000655952,16.421409199870318,54.06375297756186 +2024-09-02 04:00:00,6,21.078352911297692,31.981023815862464,51.63344658732588 +2024-09-02 05:00:00,6,17.80416846082841,22.66322383270096,48.880230277603054 +2024-09-02 06:00:00,6,14.960292303990133,20.45537503642845,51.973647797202325 +2024-09-02 07:00:00,6,18.95941612028268,23.51160736525615,43.381108963348225 +2024-09-02 08:00:00,6,28.83669013572785,19.8305373411605,68.18276047943888 +2024-09-02 09:00:00,6,22.90194597705149,21.633357032555594,66.96453990617266 +2024-09-02 10:00:00,6,32.47659957343387,29.264369905181923,58.77000960318045 +2024-09-02 11:00:00,6,31.197636184581377,18.346242100946945,71.133759679699 +2024-09-02 12:00:00,6,27.974210724075434,21.27600376691776,66.32210933573607 +2024-09-02 13:00:00,6,36.175992656085235,15.916916618814888,67.7086170380833 +2024-09-02 14:00:00,6,30.85195135571038,20.80606348502316,58.68100847323606 +2024-09-02 15:00:00,6,30.17760263262726,26.051946130115827,57.5969982198243 +2024-09-02 16:00:00,6,40.77815443707291,20.547750985180286,53.40285656865344 +2024-09-02 17:00:00,6,20.740058011367843,23.408224786404496,57.58765162274532 +2024-09-02 18:00:00,6,35.882567013703515,22.45838503612064,34.93878577230779 +2024-09-02 19:00:00,6,22.903076897439007,21.480004082815956,65.29039173377576 +2024-09-02 20:00:00,6,23.450924439673685,18.894202609384507,46.95735756636943 +2024-09-02 21:00:00,6,45.937432908931314,23.363507203189815,62.77945059784933 +2024-09-02 22:00:00,6,21.53891907205643,27.797005720108082,56.64857344583708 +2024-09-02 23:00:00,6,23.648664819305825,24.923340141763802,65.3245252992235 +2024-09-03 00:00:00,6,21.271556669468733,21.73113555151299,49.592901909154065 +2024-09-03 01:00:00,6,26.147317457881226,26.86258448618549,57.95575986340605 +2024-09-03 02:00:00,6,25.73056649944438,19.170373528049335,58.68891632820239 +2024-09-03 03:00:00,6,17.590575525163445,18.71464018563644,55.581614414965394 +2024-09-03 04:00:00,6,34.00268012910517,18.58158455233243,46.967756440622644 +2024-09-03 05:00:00,6,11.516992848348897,23.394159702901437,69.35134405700764 +2024-09-03 06:00:00,6,9.049602597757898,27.932583963703692,38.08151072364388 +2024-09-03 07:00:00,6,23.097650402553978,21.35121547962809,52.19923484673892 +2024-09-03 08:00:00,6,28.16180992128008,27.363907206415853,46.54457957463565 +2024-09-03 09:00:00,6,29.184524531073613,27.287043129119045,63.017071555283096 +2024-09-03 10:00:00,6,32.27686592251231,29.701913002780962,35.70448647670851 +2024-09-03 11:00:00,6,26.99845639364569,22.435864594883615,36.12559153329626 +2024-09-03 12:00:00,6,35.948846791414574,33.01586033291286,49.64750578194808 +2024-09-03 13:00:00,6,39.17537555476417,18.20136236565108,57.08073612309199 +2024-09-03 14:00:00,6,14.007899304513979,20.548152239986468,59.30517761108583 +2024-09-03 15:00:00,6,18.857609693214656,18.72461437737401,62.564330947758016 +2024-09-03 16:00:00,6,29.214707631741966,22.74391818246056,60.27808480420467 +2024-09-03 17:00:00,6,43.941285526709436,18.988525450706767,59.2302922314798 +2024-09-03 18:00:00,6,24.172646823713325,26.015114458350304,63.344248000056965 +2024-09-03 19:00:00,6,30.770929900122297,24.8367183457666,48.01307793116449 +2024-09-03 20:00:00,6,16.81607034605181,23.299226733289427,55.94011512909965 +2024-09-03 21:00:00,6,19.008745107428574,23.13675454482384,55.27288868650275 +2024-09-03 22:00:00,6,47.14656995988909,26.337913899607127,52.692483797328876 +2024-09-03 23:00:00,6,28.963257169680222,22.232742401359733,54.88839501177432 +2024-09-04 00:00:00,6,12.615389440090306,28.24641423245463,53.341711287819564 +2024-09-04 01:00:00,6,13.459098413359792,19.464338448246465,66.78852422282569 +2024-09-04 02:00:00,6,23.909131397465007,21.219856245938356,50.19321359823143 +2024-09-04 03:00:00,6,28.045599474792922,15.323187443970276,44.604004168939426 +2024-09-04 04:00:00,6,35.54296434259365,25.783783249227376,53.23804512677099 +2024-09-04 05:00:00,6,21.840973132460743,16.796371310836697,58.824767548923845 +2024-09-04 06:00:00,6,22.812396334221155,23.7989819191804,42.68781882420874 +2024-09-04 07:00:00,6,15.074338097771873,24.481064343597918,49.12283306577009 +2024-09-04 08:00:00,6,30.762587609232508,20.25049059963117,63.19342447789896 +2024-09-04 09:00:00,6,25.639506791693083,27.217041050521903,60.25215959350992 +2024-09-04 10:00:00,6,29.757403128447795,26.16948662168563,58.83397180446526 +2024-09-04 11:00:00,6,26.57400523899492,17.86910592388698,45.660542429104225 +2024-09-04 12:00:00,6,18.167051480989244,20.73771367151453,56.821634043643925 +2024-09-04 13:00:00,6,17.0107599551572,22.099946607239737,52.07249900302541 +2024-09-04 14:00:00,6,18.159664916902774,17.978579085973138,50.92048020141183 +2024-09-04 15:00:00,6,28.891609845098397,20.556546365803953,49.88528710763449 +2024-09-04 16:00:00,6,21.282114976234524,27.24484165767496,48.37049435150456 +2024-09-04 17:00:00,6,32.66595800343115,22.661889638079618,64.11570778956481 +2024-09-04 18:00:00,6,28.448797132349636,18.821483290304734,53.55321356174525 +2024-09-04 19:00:00,6,16.422275933179584,22.872579373056844,58.307522323888115 +2024-09-04 20:00:00,6,38.75698608847726,24.940331823011732,66.95186560415067 +2024-09-04 21:00:00,6,39.08743546350233,22.929978738042816,45.18041499358572 +2024-09-04 22:00:00,6,21.63686644302355,19.74781674754474,72.49458435917984 +2024-09-04 23:00:00,6,36.56935433415324,27.704763822755176,55.961490028034085 +2024-09-05 00:00:00,6,29.72544214493368,22.370439564428448,64.61563769218102 +2024-09-05 01:00:00,6,18.252288795406706,21.743716355159677,72.43216846978041 +2024-09-05 02:00:00,6,33.29267425265831,22.854064062972235,49.34678143443743 +2024-09-05 03:00:00,6,28.77711921071085,23.400020252464156,45.058795997891316 +2024-09-05 04:00:00,6,27.455866664615105,16.62525459864682,44.88049862745451 +2024-09-05 05:00:00,6,11.398109240763782,21.45999243557582,48.23190257915276 +2024-09-05 06:00:00,6,16.403015090844594,24.89695900425082,57.90499845160112 +2024-09-05 07:00:00,6,27.320931678842285,23.08446261817536,52.67773082364475 +2024-09-05 08:00:00,6,17.74516791768022,18.42092017710667,49.508375618693 +2024-09-05 09:00:00,6,24.398956398618264,27.382326974079774,58.47059455674208 +2024-09-05 10:00:00,6,23.21008496770622,23.38272167347758,58.50961166565505 +2024-09-05 11:00:00,6,41.65231624869346,26.029397405573892,49.3822396735091 +2024-09-05 12:00:00,6,32.81795997709949,26.02050241005535,61.805346900146944 +2024-09-05 13:00:00,6,23.826680594102356,23.010653745822296,55.28486574745872 +2024-09-05 14:00:00,6,26.476483675428028,24.421643142508803,61.18408103182148 +2024-09-05 15:00:00,6,20.955095587382495,28.97235219078598,37.25136712788262 +2024-09-05 16:00:00,6,28.643490248508954,18.57515526632465,60.13121033581243 +2024-09-05 17:00:00,6,20.705064506507135,18.947253708759416,43.835708699703986 +2024-09-05 18:00:00,6,13.381611329348585,28.265217381701344,67.74996553428268 +2024-09-05 19:00:00,6,17.668338722979612,22.87618272888847,52.8258613339503 +2024-09-05 20:00:00,6,22.723159264282884,18.55635975663337,57.031306940794046 +2024-09-05 21:00:00,6,33.29775098125702,28.073110835153216,59.497964603637485 +2024-09-05 22:00:00,6,26.879544322795795,25.929892061799595,69.16964569378189 +2024-09-05 23:00:00,6,25.8936002792041,18.584032747217663,71.06118335336416 +2024-09-06 00:00:00,6,24.263965802501474,16.544155381790894,42.04198916797053 +2024-09-06 01:00:00,6,37.00191012497008,20.228169990260312,63.37436416293454 +2024-09-06 02:00:00,6,13.692524878811923,19.75838959579725,61.09111333942407 +2024-09-06 03:00:00,6,10.311002454200617,25.36401725197265,55.63187025016294 +2024-09-06 04:00:00,6,17.86997839019623,23.397239519935905,63.12851869240851 +2024-09-06 05:00:00,6,27.019699162322134,25.499368245482113,64.62856051475828 +2024-09-06 06:00:00,6,32.268110404963956,18.989813891685756,49.738576452438636 +2024-09-06 07:00:00,6,35.53910455945844,24.33626850499258,52.07184093542354 +2024-09-06 08:00:00,6,8.13852154358753,28.785340057331812,46.54618459871809 +2024-09-06 09:00:00,6,34.70634306706212,29.2554864296475,69.10003115502846 +2024-09-06 10:00:00,6,6.961383436443217,28.77653485412433,51.10449292440469 +2024-09-06 11:00:00,6,25.47428839530018,27.945086197475245,59.19595580581751 +2024-09-06 12:00:00,6,36.496481573890506,19.575911427473635,56.08708743277329 +2024-09-06 13:00:00,6,19.56797084031115,17.637545416582718,56.74755119699221 +2024-09-06 14:00:00,6,32.323553690732815,26.92683480214249,46.68540389410989 +2024-09-06 15:00:00,6,11.26714316989059,22.824044203505725,60.47711665505963 +2024-09-06 16:00:00,6,16.14406237071531,27.87135439593208,55.41593671625214 +2024-09-06 17:00:00,6,38.29595115403977,20.794004736084776,61.19507982355748 +2024-09-06 18:00:00,6,12.336867245473735,22.89352928230376,54.069151676598764 +2024-09-06 19:00:00,6,24.7004395337182,21.303641917893298,47.966641328163874 +2024-09-06 20:00:00,6,26.57241680691211,28.284194405257317,55.39165542126002 +2024-09-06 21:00:00,6,29.98142342334117,21.685932748320713,45.93669627164352 +2024-09-06 22:00:00,6,37.52889639814214,22.652026389694797,59.492855498499445 +2024-09-06 23:00:00,6,23.912100603912876,21.1558387581156,52.04222748406984 +2024-09-07 00:00:00,6,17.357221782334406,21.002499580391554,51.42721136944945 +2024-09-07 01:00:00,6,22.33660928998092,24.239831918793744,47.04579935936953 +2024-09-07 02:00:00,6,18.846509003622554,18.280417900576936,55.492986422485814 +2024-09-07 03:00:00,6,21.495182977184903,23.66151563147783,68.0581219566267 +2024-09-07 04:00:00,6,18.928415846954017,21.99970865237027,40.321754690383685 +2024-09-07 05:00:00,6,45.345306290534715,27.438987444429813,54.676161899062656 +2024-09-07 06:00:00,6,21.66600454520386,24.1213201104015,41.35872718406964 +2024-09-07 07:00:00,6,38.32680027209125,20.327318990158346,58.74179998765496 +2024-09-07 08:00:00,6,25.24551367778881,30.069500367442494,42.47589714027892 +2024-09-07 09:00:00,6,15.238907676872872,26.532679596258212,59.41157803347687 +2024-09-07 10:00:00,6,42.364576926782355,32.0084660532855,50.87295240416799 +2024-09-07 11:00:00,6,15.61446322301818,27.94077944073892,60.540884952357366 +2024-09-07 12:00:00,6,21.896558956740545,27.08551186890408,49.509676276441375 +2024-09-07 13:00:00,6,31.86855069594612,18.933108134355294,54.718985024381276 +2024-09-07 14:00:00,6,18.407752117291572,23.185043345452137,59.695074934643166 +2024-09-07 15:00:00,6,19.939483635446525,23.741487970377754,44.72375652743793 +2024-09-07 16:00:00,6,22.57875399282836,27.049818048852458,46.23603978854014 +2024-09-07 17:00:00,6,18.667058667902513,22.506087821749933,50.99347790118556 +2024-09-07 18:00:00,6,42.34175598543918,26.308315661568557,62.90084243969902 +2024-09-07 19:00:00,6,13.844224613949715,21.52040805250642,54.80720352905079 +2024-09-07 20:00:00,6,24.546161303279934,27.767768679170068,59.57897045073883 +2024-09-07 21:00:00,6,8.676124261284283,24.77601611099472,46.81353207183323 +2024-09-07 22:00:00,6,36.80402567057476,23.65994823186536,44.256942805749176 +2024-09-07 23:00:00,6,39.88217043044732,22.093568971870006,54.531171844275704 +2024-09-08 00:00:00,6,28.25579331204562,25.67143106919267,49.54379105734044 +2024-09-08 01:00:00,6,32.95594738864743,21.014377041941685,54.13109443286671 +2024-09-08 02:00:00,6,20.96687171203147,20.485298912108046,47.42271830309641 +2024-09-08 03:00:00,6,10.0911317060144,23.52347646942823,57.75678638329923 +2024-09-08 04:00:00,6,26.301981417560548,24.644916992818455,44.20403262746567 +2024-09-08 05:00:00,6,17.68130762083421,19.641001998952255,49.643425659062295 +2024-09-08 06:00:00,6,28.923526834956277,28.853269873036346,54.79068829810681 +2024-09-08 07:00:00,6,32.201945339201494,19.486779692480706,61.778080814796 +2024-09-08 08:00:00,6,32.635609175960255,28.25645254238956,57.906936187148965 +2024-09-08 09:00:00,6,24.010115173403975,23.56965364242093,62.12394352159121 +2024-09-08 10:00:00,6,33.86835769839996,21.29641816238042,64.68536747360375 +2024-09-08 11:00:00,6,20.86166903744504,27.754796879580415,63.034006001599614 +2024-09-08 12:00:00,6,29.46439627842425,13.8367252229818,57.769989180739614 +2024-09-08 13:00:00,6,34.13460589616097,20.291521706269492,53.1985580738802 +2024-09-08 14:00:00,6,29.506314562529546,18.00183495613618,61.05448736915723 +2024-09-08 15:00:00,6,39.88289777952081,16.88752789964217,71.41236043654301 +2024-09-08 16:00:00,6,18.669216830549793,20.262735772383547,62.046276318844235 +2024-09-08 17:00:00,6,38.8691657673625,30.23741119185767,52.31746163980305 +2024-09-08 18:00:00,6,24.873607254928668,26.154033112777974,44.63670434085096 +2024-09-08 19:00:00,6,29.600478294114875,24.955396945207944,61.27984074360924 +2024-09-08 20:00:00,6,32.930064087834495,26.372151425189088,55.737941490156516 +2024-09-08 21:00:00,6,32.99028284137611,18.888726494727337,53.7007668912668 +2024-09-08 22:00:00,6,22.32437142999989,23.622958538534167,58.33790805393869 +2024-09-08 23:00:00,6,28.299338985420054,25.859586951857363,63.803912334075605 +2024-09-09 00:00:00,6,35.04629414176525,25.212817295120466,46.107983958678375 +2024-09-09 01:00:00,6,23.311555590070274,21.71611407625684,36.09969380145492 +2024-09-09 02:00:00,6,20.615358759130526,29.109360222716315,37.008237245166725 +2024-09-09 03:00:00,6,35.73484309376483,28.26042097603255,54.06279433617197 +2024-09-09 04:00:00,6,20.438437317540636,22.782355828337696,47.879518942682644 +2024-09-09 05:00:00,6,27.240377067686257,21.988949167192622,56.524828425047765 +2024-09-09 06:00:00,6,20.350574260227756,23.586410882840166,60.777895973432464 +2024-09-09 07:00:00,6,29.807726348438393,19.0346286222046,54.31828854242384 +2024-09-09 08:00:00,6,32.08659852996831,27.696348388670305,58.97014232872175 +2024-09-09 09:00:00,6,19.91097864614548,18.19064682881438,47.8655872593483 +2024-09-09 10:00:00,6,37.28898215023953,24.11702525992517,61.96708283246702 +2024-09-09 11:00:00,6,27.611250823095524,23.59375214700733,43.338664556198914 +2024-09-09 12:00:00,6,31.910516050035678,21.88913694760034,72.51188503558576 +2024-09-09 13:00:00,6,22.883976224852187,27.675049671549434,52.057769245492516 +2024-09-09 14:00:00,6,23.840892596163037,24.450603166950337,66.11696154130036 +2024-09-09 15:00:00,6,32.46684489009199,22.62462007535414,44.66004628321758 +2024-09-09 16:00:00,6,36.82861246653613,28.31378775646128,54.62914814848692 +2024-09-09 17:00:00,6,26.558609931728874,24.51094241286732,56.70220333696248 +2024-09-09 18:00:00,6,24.583593349882978,18.47802102834357,51.13607514283202 +2024-09-09 19:00:00,6,22.258962235824793,25.086467705228362,69.22900953479622 +2024-09-09 20:00:00,6,24.87222929767635,25.317019863907888,56.60819384115512 +2024-09-09 21:00:00,6,18.500594331065592,26.450657215388834,57.73991099218716 +2024-09-09 22:00:00,6,24.321335485497848,27.037502568377413,63.57184445316993 +2024-09-09 23:00:00,6,29.450751761588744,23.69541951185305,43.58906081804552 +2024-09-10 00:00:00,6,34.17186069124147,22.46455808091782,48.30335616910519 +2024-09-10 01:00:00,6,11.639728356374635,18.105555828046995,42.421525770143276 +2024-09-10 02:00:00,6,18.935008627894586,21.232601157599916,60.40369098044504 +2024-09-10 03:00:00,6,22.87118127798835,17.62324609351186,51.96712424388167 +2024-09-10 04:00:00,6,13.188688409681186,30.052435029982664,56.78695510074411 +2024-09-10 05:00:00,6,27.270409403051872,26.24038183828057,49.911707903715076 +2024-09-10 06:00:00,6,4.483496445193136,23.939049043859683,58.65528943895564 +2024-09-10 07:00:00,6,38.3913359315969,20.294521588746875,52.029550303582624 +2024-09-10 08:00:00,6,9.30331202281096,16.90918542421666,55.919706139062676 +2024-09-10 09:00:00,6,31.729484333473877,26.052010498281415,47.92220223689677 +2024-09-10 10:00:00,6,34.72295509391866,22.99197392036725,65.2615145801221 +2024-09-10 11:00:00,6,35.83844049329079,29.518838177271245,51.40602037822197 +2024-09-10 12:00:00,6,19.06748553918856,27.41421203836225,52.623017847715126 +2024-09-10 13:00:00,6,31.391499461254703,22.18369669809465,56.4746719677479 +2024-09-10 14:00:00,6,42.78639128804765,29.236084721879337,59.135468177214605 +2024-09-10 15:00:00,6,27.33123381914134,23.970637204948172,53.97531156205326 +2024-09-10 16:00:00,6,33.259416688616106,19.813722281725784,60.83586200598996 +2024-09-10 17:00:00,6,20.210816654008863,26.3980680732941,46.08203056295682 +2024-09-10 18:00:00,6,30.68891206240771,25.574824935855297,69.27076343777952 +2024-09-10 19:00:00,6,34.10504282792039,19.199743523408365,53.02457093397161 +2024-09-10 20:00:00,6,23.13420191995838,23.930224740621973,48.82186901337213 +2024-09-10 21:00:00,6,34.583090695290466,22.70532835148035,59.02628267208047 +2024-09-10 22:00:00,6,33.97900240260739,18.971715461966586,50.26619231620578 +2024-09-10 23:00:00,6,36.74630864270777,24.07260609676056,36.95291495403399 +2024-09-11 00:00:00,6,26.181071403973228,23.6784923271322,55.88858740181772 +2024-09-11 01:00:00,6,32.9626511855235,24.886862282807176,54.90345094712289 +2024-09-11 02:00:00,6,26.923613151946487,27.173662366048358,51.60529500902993 +2024-09-11 03:00:00,6,23.5867185004999,21.410795376012345,68.45563353276071 +2024-09-11 04:00:00,6,5.386714628892189,29.084113828455333,32.696575770760944 +2024-09-11 05:00:00,6,31.332860895708116,15.407217776463769,50.55795216005769 +2024-09-11 06:00:00,6,28.14803918249525,24.123002265053753,56.477058016306046 +2024-09-11 07:00:00,6,19.294943549436496,23.857637921448294,49.27747739200002 +2024-09-11 08:00:00,6,22.020497686386978,24.59069403238031,50.703943534891614 +2024-09-11 09:00:00,6,26.239277963122206,15.732900915867743,67.1018829347299 +2024-09-11 10:00:00,6,28.088867854236998,22.287549236956384,72.19881931043204 +2024-09-11 11:00:00,6,22.82468282333627,26.13957949901706,40.44973511609196 +2024-09-11 12:00:00,6,44.95199335960336,23.74678854563738,57.596213415911336 +2024-09-11 13:00:00,6,40.66640668182224,17.29583015424715,55.172995495102946 +2024-09-11 14:00:00,6,31.71224304077805,20.025296145574266,57.67667490357686 +2024-09-11 15:00:00,6,36.5439583046623,24.76793475714215,53.12000573764558 +2024-09-11 16:00:00,6,31.166153446209528,16.81663604872127,65.01159791345715 +2024-09-11 17:00:00,6,17.868598703088693,20.96781956594457,59.654248363415746 +2024-09-11 18:00:00,6,20.879598040929793,17.079541702639027,56.50650154966223 +2024-09-11 19:00:00,6,30.217982918650673,20.39099434803188,54.35371480022733 +2024-09-11 20:00:00,6,26.528458586669615,25.55335676008066,54.06925794925078 +2024-09-11 21:00:00,6,28.379897305344794,26.657910098079608,50.983041126353434 +2024-09-11 22:00:00,6,28.00796728161474,26.164869246255833,43.260868449090594 +2024-09-11 23:00:00,6,14.82126593946937,21.038661658823248,30.674332065621957 +2024-09-12 00:00:00,6,26.580522198765447,16.27941397148054,52.338344090463266 +2024-09-12 01:00:00,6,11.675589388839786,19.214890611790338,53.69613979860765 +2024-09-12 02:00:00,6,19.104178148361743,15.432620254828034,55.90851581845208 +2024-09-12 03:00:00,6,25.88679050098296,26.012194866478154,52.51918837678021 +2024-09-12 04:00:00,6,20.869298798009126,12.434107423106092,61.45508673557311 +2024-09-12 05:00:00,6,11.588808030200727,20.449882358955893,65.6655471904886 +2024-09-12 06:00:00,6,23.147331028018826,29.412719280536017,53.87207295952174 +2024-09-12 07:00:00,6,14.613205482455049,27.658739643902358,48.211139271472575 +2024-09-12 08:00:00,6,11.264540247173874,29.14805613142642,54.203423432634814 +2024-09-12 09:00:00,6,33.79937574979999,19.282959534617675,45.29225743207525 +2024-09-12 10:00:00,6,32.513368055661594,25.57719580314703,60.51203977625423 +2024-09-12 11:00:00,6,32.629938342836645,21.93099541547376,47.5086807772693 +2024-09-12 12:00:00,6,22.141380460914792,27.82355745521904,49.728116145824174 +2024-09-12 13:00:00,6,36.87336102362927,25.508937369562666,65.83038811151185 +2024-09-12 14:00:00,6,28.146281617607233,22.248656268679355,54.700818213622796 +2024-09-12 15:00:00,6,30.45096914515027,25.25058466666843,51.42427923646744 +2024-09-12 16:00:00,6,31.04644093232773,26.348153195801423,53.52697328433894 +2024-09-12 17:00:00,6,23.4677143684559,17.35671442553232,53.442654977207006 +2024-09-12 18:00:00,6,47.510660912492014,29.937133379835107,56.75685892621044 +2024-09-12 19:00:00,6,32.47254624119457,20.39681420878021,50.000637130325856 +2024-09-12 20:00:00,6,15.08734377006629,25.29431544967706,50.148530101486685 +2024-09-12 21:00:00,6,31.57625804383944,23.58722183524211,61.133392052150896 +2024-09-12 22:00:00,6,19.4185111611872,17.86027854204997,53.70034650170831 +2024-09-12 23:00:00,6,32.01210367339502,18.262902849441893,53.33254014031352 +2024-09-13 00:00:00,6,7.4384698493038695,25.14893865561374,67.85382525014994 +2024-09-13 01:00:00,6,18.847834126859482,14.387895436396095,64.26269794344341 +2024-09-13 02:00:00,6,14.345069256256203,28.4000510137211,66.83154561346898 +2024-09-13 03:00:00,6,3.1842010838654247,22.364153482373293,74.77095876174988 +2024-09-13 04:00:00,6,25.904464194514226,21.71899166087721,58.257389222115805 +2024-09-13 05:00:00,6,23.70656890474883,26.20813322284892,58.72038905074955 +2024-09-13 06:00:00,6,37.85713164524684,23.664699987214554,53.333598222269075 +2024-09-13 07:00:00,6,16.633989488904994,18.544750223973892,49.73770350512538 +2024-09-13 08:00:00,6,24.02459999962342,19.764856556449104,52.89452506431214 +2024-09-13 09:00:00,6,31.249487450112508,25.103537419060928,56.33980334513657 +2024-09-13 10:00:00,6,28.42731601386282,26.732767743137405,45.64486687746014 +2024-09-13 11:00:00,6,30.416017671709916,23.47606211341128,47.25484866437465 +2024-09-13 12:00:00,6,29.10485036883296,21.62985200992671,58.55613288142593 +2024-09-13 13:00:00,6,29.110266341461884,22.157077300035944,74.58169132849473 +2024-09-13 14:00:00,6,20.311470224851934,20.759125607569697,60.82729960730121 +2024-09-13 15:00:00,6,30.793976881099137,22.446888174092518,63.92855928613363 +2024-09-13 16:00:00,6,26.994710172563288,15.03547231769066,74.62181648639503 +2024-09-13 17:00:00,6,38.1180383658085,19.234466922344996,37.92808368474641 +2024-09-13 18:00:00,6,21.159632410671886,21.57298405292624,57.858827201611135 +2024-09-13 19:00:00,6,34.19481256164793,25.83634383638495,59.15900339933146 +2024-09-13 20:00:00,6,32.70818654765133,21.93898878118022,64.09520114000364 +2024-09-13 21:00:00,6,27.215876251752672,23.864800801739218,58.4521043368126 +2024-09-13 22:00:00,6,44.42761898496097,26.33376985434068,67.89827702660061 +2024-09-13 23:00:00,6,6.109270306023202,24.84461507783779,53.07623417276937 +2024-09-14 00:00:00,6,12.853452551249568,22.56245193300675,51.56271276992053 +2024-09-14 01:00:00,6,35.68184677665986,25.23582690210939,48.926907548902044 +2024-09-14 02:00:00,6,22.311721480390137,27.304768049959414,50.259052645727195 +2024-09-14 03:00:00,6,28.348615043271124,22.877678619241365,54.38747233215605 +2024-09-14 04:00:00,6,31.30002958320559,24.033217760849908,56.47889779897439 +2024-09-14 05:00:00,6,12.41851137104403,18.20265140517312,51.146733041803515 +2024-09-14 06:00:00,6,15.324294631559846,28.233158876819243,50.78942294283997 +2024-09-14 07:00:00,6,32.59950135883708,21.90962015943327,54.81271303245686 +2024-09-14 08:00:00,6,39.24969992416368,29.966064609609568,57.42394205116211 +2024-09-14 09:00:00,6,30.053126042675014,28.394659800533788,62.72595682940885 +2024-09-14 10:00:00,6,19.218490367196118,23.38751532431427,57.981767650772504 +2024-09-14 11:00:00,6,12.751170074221255,23.995126767079572,51.62427356451402 +2024-09-14 12:00:00,6,13.05416789358076,23.106127133529657,66.71414789720772 +2024-09-14 13:00:00,6,19.418784566245108,29.918380303938992,43.518317457540434 +2024-09-14 14:00:00,6,13.496295435112481,19.49118737218104,62.521609459643585 +2024-09-14 15:00:00,6,11.552873720298415,21.536902096995203,63.20094077314387 +2024-09-14 16:00:00,6,30.472383194825746,20.069059101728307,42.46662795528492 +2024-09-14 17:00:00,6,18.308705336772768,21.683628288275653,59.58248456596907 +2024-09-14 18:00:00,6,27.296548437321988,27.67581773181769,54.601146034927496 +2024-09-14 19:00:00,6,15.059461531821452,22.643025801946315,60.53347490469511 +2024-09-14 20:00:00,6,12.242414802156858,20.318628757572583,60.58080788548806 +2024-09-14 21:00:00,6,28.4858711018766,20.06762589954743,55.729110530121865 +2024-09-14 22:00:00,6,28.28744992487375,26.33280692199461,54.77305645891791 +2024-09-14 23:00:00,6,35.82803426658443,24.08272701081275,47.175680431702396 +2024-09-15 00:00:00,6,12.265794755305306,28.84631564957207,43.18328370471265 +2024-09-15 01:00:00,6,12.456655079176004,31.740768779591157,61.09364852771638 +2024-09-15 02:00:00,6,22.871846326254726,25.46444151195768,56.145791902154 +2024-09-15 03:00:00,6,23.728076461897924,25.7208755491558,60.60394327835862 +2024-09-15 04:00:00,6,18.40578116112086,18.801326038282802,49.90090999576592 +2024-09-15 05:00:00,6,25.90692710957529,14.582878411908787,44.405800124374785 +2024-09-15 06:00:00,6,19.537494674175115,26.65462949887249,58.11682412235489 +2024-09-15 07:00:00,6,26.59896921612593,29.13426322805625,65.24352716526937 +2024-09-15 08:00:00,6,28.041651204694684,20.794255823445503,49.939515138319784 +2024-09-15 09:00:00,6,47.79928308805878,18.64671882630425,41.420206368927765 +2024-09-15 10:00:00,6,31.117606279959563,26.228120289110535,65.9989185835105 +2024-09-15 11:00:00,6,21.839193611929023,28.266457605688473,50.640306528785594 +2024-09-15 12:00:00,6,17.69098869920532,31.72841642842437,44.57278970286664 +2024-09-15 13:00:00,6,46.452803179218066,22.452461942668428,58.177300831100794 +2024-09-15 14:00:00,6,31.180428451667222,23.901811493372332,53.59121171840619 +2024-09-15 15:00:00,6,25.283366008483792,17.235047959070872,47.2718892829259 +2024-09-15 16:00:00,6,40.59621631930137,26.427149001335543,49.54533993596532 +2024-09-15 17:00:00,6,23.14385666182065,26.989806383680456,46.50444183062723 +2024-09-15 18:00:00,6,20.98017225961315,22.781233114879704,59.33253824703579 +2024-09-15 19:00:00,6,35.85740263865327,24.67124577294274,68.31311281227305 +2024-09-15 20:00:00,6,7.3136515876932435,21.476715365257924,53.93937161386997 +2024-09-15 21:00:00,6,35.00877323419848,31.157462432797765,72.79382013870121 +2024-09-15 22:00:00,6,26.61746942643537,22.80661634019098,60.1786530013284 +2024-09-15 23:00:00,6,31.668519406301336,28.46494161120937,41.938994576567254 +2024-09-16 00:00:00,6,24.486759813187362,22.562287124008165,56.54757506929438 +2024-09-16 01:00:00,6,24.118887329891514,27.922586448758864,67.0030047696004 +2024-09-16 02:00:00,6,16.31499356300134,30.56413801708939,56.50859933270809 +2024-09-16 03:00:00,6,19.294099877108607,20.82525475822734,52.660839089594106 +2024-09-16 04:00:00,6,8.077671863230911,19.566603397294063,36.221329738367174 +2024-09-16 05:00:00,6,29.678146109838128,24.078329821748518,52.15116908663104 +2024-09-16 06:00:00,6,14.078969621263456,24.659483598514612,57.479805534574695 +2024-09-16 07:00:00,6,32.67408876482057,26.36217163680002,61.70734080840129 +2024-09-16 08:00:00,6,22.221346554370466,25.350491710327418,67.33290483997916 +2024-09-16 09:00:00,6,34.75924061153614,23.41057209186473,57.660994097581586 +2024-09-16 10:00:00,6,29.41945746048969,20.48946558530953,62.18546454636587 +2024-09-16 11:00:00,6,35.26767866068274,25.27853434162798,47.84370423716922 +2024-09-16 12:00:00,6,22.242296596703202,23.581506290118856,45.288364724246236 +2024-09-16 13:00:00,6,30.901461057428918,25.43926867257279,43.18817695480176 +2024-09-16 14:00:00,6,25.081533195825028,26.37692118508733,63.563139976862416 +2024-09-16 15:00:00,6,26.877714966515114,19.061491989817377,54.52156648692158 +2024-09-16 16:00:00,6,25.68369729962196,24.759430988116513,77.60717272099183 +2024-09-16 17:00:00,6,33.60724854708658,26.69417104708386,62.31822371533595 +2024-09-16 18:00:00,6,29.87015650998848,28.833308035997955,55.34557909025964 +2024-09-16 19:00:00,6,32.93981107698054,25.132370605405693,62.47261406892858 +2024-09-16 20:00:00,6,25.630303738280787,26.75995202116846,67.50402096496477 +2024-09-16 21:00:00,6,27.31490394230341,18.313007460026842,39.46332792443981 +2024-09-16 22:00:00,6,33.38907026383684,25.83302862763021,42.173698227209414 +2024-09-16 23:00:00,6,25.45623951098283,27.877326070460946,42.95157290074761 +2024-09-17 00:00:00,6,30.506470169625665,20.165948805506773,67.69154352057271 +2024-09-17 01:00:00,6,15.789917322551904,15.892610037090115,63.80148399623496 +2024-09-17 02:00:00,6,24.805040539299412,16.78547734466294,57.25899958539862 +2024-09-17 03:00:00,6,19.576954429400907,20.34271071850127,72.06353648161398 +2024-09-17 04:00:00,6,15.081786964789277,23.951413439000103,57.21219552221455 +2024-09-17 05:00:00,6,39.43619832088615,24.20178486338531,55.067399845191375 +2024-09-17 06:00:00,6,36.26111099895959,23.645432985447677,56.06702856222449 +2024-09-17 07:00:00,6,31.460182428324057,24.331841781696404,44.84376934676069 +2024-09-17 08:00:00,6,30.02615189061872,33.30248622413146,34.95468929363528 +2024-09-17 09:00:00,6,25.660696602176284,18.83717303158909,63.33136107898462 +2024-09-17 10:00:00,6,24.082977586365867,24.45623370999772,50.94462109601798 +2024-09-17 11:00:00,6,37.788072036883214,26.345850759928044,42.527104323578186 +2024-09-17 12:00:00,6,36.21315958678365,21.123118639617022,57.03534172577786 +2024-09-17 13:00:00,6,24.31680461029064,23.055913382739952,62.074559677646256 +2024-09-17 14:00:00,6,31.22801444188805,24.561784586272836,62.33739455394096 +2024-09-17 15:00:00,6,30.5374460724055,16.3516034824483,59.4433443958178 +2024-09-17 16:00:00,6,33.30404045620552,24.88174727202372,77.66765596811464 +2024-09-17 17:00:00,6,31.432186464862156,21.252740620322395,62.12402614799016 +2024-09-17 18:00:00,6,26.350291875683325,21.48735146758085,58.428933313467084 +2024-09-17 19:00:00,6,32.326062982196405,20.99689046888192,68.449080641228 +2024-09-17 20:00:00,6,11.600314630119177,24.399367266520702,43.685310950224974 +2024-09-17 21:00:00,6,19.956127733256842,23.220706540655193,48.90193039320583 +2024-09-17 22:00:00,6,34.02546252206857,22.482042798423382,52.19251920833635 +2024-09-17 23:00:00,6,29.164914914334815,21.737847399636532,54.12396517494674 +2024-09-18 00:00:00,6,20.264042137361034,25.840696429463534,57.55498279385712 +2024-09-18 01:00:00,6,23.34129512696224,20.10274507894334,48.762166553747086 +2024-09-18 02:00:00,6,24.464967738178466,20.535205676054343,66.76987824766516 +2024-09-18 03:00:00,6,20.4830243758549,20.776419931873846,59.0960528448922 +2024-09-18 04:00:00,6,9.767980128701343,20.841025573068688,50.15587424593343 +2024-09-18 05:00:00,6,28.258290843020244,25.641613194254155,58.9574414657032 +2024-09-18 06:00:00,6,16.359062829312116,20.235130047148708,59.54876386412428 +2024-09-18 07:00:00,6,23.867642317816813,30.24026164752762,86.1523149767555 +2024-09-18 08:00:00,6,33.582073224949575,20.652186017565654,57.99789824011408 +2024-09-18 09:00:00,6,18.970118003444284,25.00373746132862,50.8440063938091 +2024-09-18 10:00:00,6,27.694298863743043,21.31597547124975,57.48103546043493 +2024-09-18 11:00:00,6,24.0315923812361,26.321484087127928,51.276571512096424 +2024-09-18 12:00:00,6,25.14328451848975,20.96202830172257,63.678942715099346 +2024-09-18 13:00:00,6,17.738751026417074,24.89846572169639,52.09101012647231 +2024-09-18 14:00:00,6,35.92165864030608,28.983907435940182,67.11986987973044 +2024-09-18 15:00:00,6,51.57248578233471,21.67341213585785,66.58403184646997 +2024-09-18 16:00:00,6,26.133577846588302,23.860211833244175,44.72762973151795 +2024-09-18 17:00:00,6,24.02757165715014,23.06786144777654,69.78120290521574 +2024-09-18 18:00:00,6,37.196927281030085,22.04596359112414,77.2474303624171 +2024-09-18 19:00:00,6,17.97021153313188,24.629947497679918,59.43563411715831 +2024-09-18 20:00:00,6,37.25080594781366,19.01662384925097,53.7235531787225 +2024-09-18 21:00:00,6,36.297778062711245,27.50411556490924,36.88108160416377 +2024-09-18 22:00:00,6,30.088608709283264,20.149431003195325,42.04455638391538 +2024-09-18 23:00:00,6,25.108421036545316,26.219986344604227,66.79050670393019 +2024-09-19 00:00:00,6,12.271570024699516,20.14444228024317,42.664030610673265 +2024-09-19 01:00:00,6,22.519373276025775,18.415665149833703,56.93833988760809 +2024-09-19 02:00:00,6,20.650227162000416,26.102240013623106,44.54631406028821 +2024-09-19 03:00:00,6,26.73348659415675,19.16447464446619,49.277948444678486 +2024-09-19 04:00:00,6,37.15485955476909,18.960231921837988,49.584321063133025 +2024-09-19 05:00:00,6,11.743131891789389,22.019634294312574,58.88742030598574 +2024-09-19 06:00:00,6,20.547356837572828,21.53008671753438,66.52224268154215 +2024-09-19 07:00:00,6,5.441343861320206,25.494693575478543,55.50959531255325 +2024-09-19 08:00:00,6,24.273248564362717,22.499583425932965,52.732243491299926 +2024-09-19 09:00:00,6,29.722726547988405,20.268996104692427,36.44274593252386 +2024-09-19 10:00:00,6,17.45618254714661,23.399425852167955,35.9269272890662 +2024-09-19 11:00:00,6,31.73821874123926,32.76244536924512,53.28149579801563 +2024-09-19 12:00:00,6,23.397686546590013,20.84826497737166,51.70376135519605 +2024-09-19 13:00:00,6,17.934840381414467,23.06815853541755,51.600394903289086 +2024-09-19 14:00:00,6,24.950236357592697,26.49664972566207,38.41038581358197 +2024-09-19 15:00:00,6,25.317122414123478,28.69189446314261,57.76499036197585 +2024-09-19 16:00:00,6,28.125415890180946,28.021558684833728,53.66436674726097 +2024-09-19 17:00:00,6,26.005301367354964,24.23208739481662,67.47699856552158 +2024-09-19 18:00:00,6,28.149522980787694,20.6282567013158,42.639027508356634 +2024-09-19 19:00:00,6,19.66512845198387,28.67355038407454,48.26988132679126 +2024-09-19 20:00:00,6,18.593956349914514,25.814730800575575,51.23084053907097 +2024-09-19 21:00:00,6,34.58557906811766,23.342900308918534,40.50688160065481 +2024-09-19 22:00:00,6,14.67594397784002,21.09752289752666,47.15697744896852 +2024-09-19 23:00:00,6,26.690218308625546,20.872019329638793,53.094051129000285 +2024-09-20 00:00:00,6,35.88838454712433,28.655428431577114,68.56065023789925 +2024-09-20 01:00:00,6,25.76360476687374,26.72772718614368,36.37635729574079 +2024-09-20 02:00:00,6,40.082977572977335,25.517489027719627,61.87390398255792 +2024-09-20 03:00:00,6,27.93572248399408,22.099289338734643,65.65039832594474 +2024-09-20 04:00:00,6,11.353327903017165,20.389841316289775,55.48967040118175 +2024-09-20 05:00:00,6,25.607790919967297,27.54791924175555,57.3119082979741 +2024-09-20 06:00:00,6,24.437043066770496,18.714463818790488,38.093025671826446 +2024-09-20 07:00:00,6,22.739720789525354,25.389592480415313,55.742953384319556 +2024-09-20 08:00:00,6,26.497437394826967,25.630609170240998,49.557229208886284 +2024-09-20 09:00:00,6,30.433116659526032,14.558544338051849,67.68907907015266 +2024-09-20 10:00:00,6,23.439848455614126,23.11083184285515,58.04656542524717 +2024-09-20 11:00:00,6,37.658900204405796,25.422412620206178,72.11177024202217 +2024-09-20 12:00:00,6,28.58328880582851,25.833556454216076,58.80008782457034 +2024-09-20 13:00:00,6,36.097531515406,25.603694361370607,58.6059404533727 +2024-09-20 14:00:00,6,32.670338090246055,21.485810695942327,54.79786515088672 +2024-09-20 15:00:00,6,17.77684487848193,26.269551834155152,52.745006348593535 +2024-09-20 16:00:00,6,37.814939116193194,25.37515258610517,49.09230538504714 +2024-09-20 17:00:00,6,27.599496634361323,29.10803704874098,52.02079372696621 +2024-09-20 18:00:00,6,31.28104341790473,26.286107891023924,70.34225083697746 +2024-09-20 19:00:00,6,31.019536813885754,26.880234344746363,63.270292974183114 +2024-09-20 20:00:00,6,40.247745120673144,23.519133713424964,63.03488261595554 +2024-09-20 21:00:00,6,26.098222891635327,28.075903463609446,49.069760042720844 +2024-09-20 22:00:00,6,22.43894897440347,25.338118438199054,52.82394589624841 +2024-09-20 23:00:00,6,37.37509352481801,21.54256396191451,46.39267770919352 +2024-09-21 00:00:00,6,30.990618962549487,27.915563873605777,75.70908255382827 +2024-09-21 01:00:00,6,29.01105257544662,12.296300445845747,46.51109310287697 +2024-09-21 02:00:00,6,17.96487810840491,17.490923099033957,53.12846253207716 +2024-09-21 03:00:00,6,10.443743716580446,27.114862647994148,43.33847046907289 +2024-09-21 04:00:00,6,30.08477597505614,26.070986358845186,55.94061509059015 +2024-09-21 05:00:00,6,17.299633237905407,21.82502432836654,53.6328338947631 +2024-09-21 06:00:00,6,22.976442571226006,22.974224966689025,51.33089785839947 +2024-09-21 07:00:00,6,20.718074052465376,29.333875719120794,50.5719183213173 +2024-09-21 08:00:00,6,27.085747481607637,30.248929470464784,56.17945337423933 +2024-09-21 09:00:00,6,27.344968075593297,22.458270775503156,53.385062298689554 +2024-09-21 10:00:00,6,23.68969531578027,26.90819035404489,73.17630436456162 +2024-09-21 11:00:00,6,31.022765530278413,21.59855491321821,54.48527143032002 +2024-09-21 12:00:00,6,15.027510202338055,23.04512236191721,70.21960602484984 +2024-09-21 13:00:00,6,14.939976901728812,21.11664854443911,54.715392765114835 +2024-09-21 14:00:00,6,23.987641723860783,23.236529458292424,57.889196651566394 +2024-09-21 15:00:00,6,28.746993973956958,22.183876585178556,65.09552456360278 +2024-09-21 16:00:00,6,27.951673313993084,23.23305796129381,56.85385425682516 +2024-09-21 17:00:00,6,23.968898014269694,25.467983184357543,60.15254316802708 +2024-09-21 18:00:00,6,36.37455504087805,21.943982383287764,47.284410519390484 +2024-09-21 19:00:00,6,27.82332958266336,26.914544010834504,47.02322351545757 +2024-09-21 20:00:00,6,32.052350051350956,23.98276573209276,48.441223835055915 +2024-09-21 21:00:00,6,27.48991163697053,20.568570804054904,44.3525895512153 +2024-09-21 22:00:00,6,38.938265132586295,23.67253048379736,58.19728708778988 +2024-09-21 23:00:00,6,31.79098171695197,27.42668206398804,37.53947362005763 +2024-09-22 00:00:00,6,17.96404920941889,24.83777325739611,64.20916871696684 +2024-09-22 01:00:00,6,23.77057971546896,23.24154825753516,72.0440159966872 +2024-09-22 02:00:00,6,21.04436167592835,24.140180665981028,54.60425408600165 +2024-09-22 03:00:00,6,27.366424104491323,27.35068072690005,65.87756863079866 +2024-09-22 04:00:00,6,18.17876543453726,24.955519241449863,59.31618990777294 +2024-09-22 05:00:00,6,0.0,24.851367099017867,61.015635674294245 +2024-09-22 06:00:00,6,43.381030159227706,23.331189921975287,48.07894661912788 +2024-09-22 07:00:00,6,18.429266507535,29.715156733195222,55.04121518839027 +2024-09-22 08:00:00,6,29.063809948682163,27.56347065118601,57.417286466503455 +2024-09-22 09:00:00,6,24.83663430907894,30.98063336250759,58.408231576194936 +2024-09-22 10:00:00,6,24.411820305411933,25.612644867267083,55.72529278179893 +2024-09-22 11:00:00,6,34.20897333426448,24.05790216141427,45.360852351870264 +2024-09-22 12:00:00,6,18.966804811044923,29.669085660219654,64.39045534085977 +2024-09-22 13:00:00,6,30.080443800166357,26.944151466388984,68.85212887872784 +2024-09-22 14:00:00,6,16.003332156102132,19.537060554766086,57.6678010732213 +2024-09-22 15:00:00,6,25.649965629296588,24.72780201448262,66.64229967661983 +2024-09-22 16:00:00,6,39.4540992976266,28.385017639640516,63.37493700983723 +2024-09-22 17:00:00,6,24.828691945828105,23.758572338079897,71.24160887994334 +2024-09-22 18:00:00,6,30.12601335163161,21.082552675091854,40.0814995355815 +2024-09-22 19:00:00,6,25.501212810121363,22.051080702726534,56.92907433250061 +2024-09-22 20:00:00,6,7.661506930650397,20.506340700316358,50.672749642278816 +2024-09-22 21:00:00,6,36.989041556372506,23.4763480409229,58.57971827508243 +2024-09-22 22:00:00,6,27.76264996662478,25.45950653578907,59.78767645617047 +2024-09-22 23:00:00,6,24.385149293969498,23.32489868036267,64.81058830168391 +2024-09-23 00:00:00,6,19.340113341772454,23.974288743413364,41.98798131995578 +2024-09-23 01:00:00,6,24.1791547424899,25.188601561110893,43.612369675155364 +2024-09-23 02:00:00,6,8.10020262449035,21.044626930685318,53.30185853979153 +2024-09-23 03:00:00,6,20.304722741671853,19.146780919139676,53.78017226752372 +2024-09-23 04:00:00,6,27.454369942443833,20.900632364885105,48.82011998113069 +2024-09-23 05:00:00,6,25.582200645537963,26.947773892352554,35.650541141680776 +2024-09-23 06:00:00,6,17.01205556391094,22.086284647736385,62.875439987387765 +2024-09-23 07:00:00,6,32.429937911817596,20.47799382126321,55.868208203655286 +2024-09-23 08:00:00,6,22.81873974194424,17.70441697265631,52.62167213019807 +2024-09-23 09:00:00,6,20.868875144722544,25.517159103817846,54.3674485099958 +2024-09-23 10:00:00,6,25.279900964367037,21.532914894382515,47.48002229542862 +2024-09-23 11:00:00,6,32.31700041728351,25.579718265407205,66.76528774396687 +2024-09-23 12:00:00,6,15.219798273675291,29.684372675674165,45.41032175365383 +2024-09-23 13:00:00,6,32.56347113165548,22.807422552206305,57.383808458136926 +2024-09-23 14:00:00,6,30.38881597124311,21.019274108910096,53.029861010569185 +2024-09-23 15:00:00,6,14.616798099528443,23.732842256359067,63.27486209773194 +2024-09-23 16:00:00,6,24.69763250731394,21.560871133438457,38.78785404549555 +2024-09-23 17:00:00,6,24.928991963909095,23.60706158545532,38.9595129708251 +2024-09-23 18:00:00,6,14.57169758526375,25.62357263285113,40.12301647198601 +2024-09-23 19:00:00,6,30.29577486294717,21.003495565858174,50.21441801067086 +2024-09-23 20:00:00,6,31.59867802117457,23.580936210522374,46.68426923923995 +2024-09-23 21:00:00,6,16.713003700683764,22.42153314249028,41.575341348794566 +2024-09-23 22:00:00,6,18.703937191221144,15.923062847048561,63.62230642698778 +2024-09-23 23:00:00,6,32.24637315929512,23.276478882643612,52.25339271404064 +2024-09-24 00:00:00,6,23.605767202107568,25.30699766975141,51.113706500252235 +2024-09-24 01:00:00,6,13.494904627238256,26.911399981054767,61.73282659379788 +2024-09-24 02:00:00,6,30.46360259607513,18.819582987429612,57.190115536043365 +2024-09-24 03:00:00,6,27.118072235766814,23.677180727111338,46.03265548817085 +2024-09-24 04:00:00,6,29.255302368996688,19.537728228752737,64.02004604933389 +2024-09-24 05:00:00,6,44.22408636975737,20.622413020555204,58.372734302007245 +2024-09-24 06:00:00,6,16.582870902386183,27.204060940580785,63.810625595736674 +2024-09-24 07:00:00,6,24.01547769727034,19.87942931801629,52.100301204259665 +2024-09-24 08:00:00,6,40.614261246173264,22.699718172529277,85.82725543956043 +2024-09-24 09:00:00,6,25.66765961523054,19.33690459861697,53.593426189016284 +2024-09-24 10:00:00,6,24.617364792792575,24.8157356839737,60.12796227435066 +2024-09-24 11:00:00,6,42.368109211334605,17.77389138631934,50.40128391668816 +2024-09-24 12:00:00,6,11.508365707147455,25.17038652854754,63.16411307822419 +2024-09-24 13:00:00,6,27.96673068636057,19.50868156502938,39.23693454945283 +2024-09-24 14:00:00,6,28.217544455024463,24.439089277097214,71.6734407016039 +2024-09-24 15:00:00,6,28.370875440172874,27.550187765729753,45.814559896597494 +2024-09-24 16:00:00,6,26.495508115364988,23.86436439243384,61.87216944678454 +2024-09-24 17:00:00,6,27.35795503142951,25.805713274442866,40.741922506231106 +2024-09-24 18:00:00,6,21.721296469012216,18.70378426500883,58.02735488331375 +2024-09-24 19:00:00,6,20.78889820106993,23.190513882677525,50.84846982226536 +2024-09-24 20:00:00,6,14.929921793704343,22.76820437704931,60.392687438543554 +2024-09-24 21:00:00,6,37.97543124842767,28.186843049149978,52.458221395477075 +2024-09-24 22:00:00,6,14.346415630079544,23.951455947298395,62.70890042430204 +2024-09-24 23:00:00,6,25.812110852786837,18.922804342431448,61.11367811944437 +2024-09-25 00:00:00,6,19.356064896048736,21.269598952472375,61.04819187539199 +2024-09-25 01:00:00,6,19.568667944585496,29.374698366016446,61.49614909982091 +2024-09-25 02:00:00,6,14.124602306280575,19.91398724128599,52.7204265350245 +2024-09-25 03:00:00,6,19.911437331252237,25.56654635675877,56.959063605814784 +2024-09-25 04:00:00,6,30.313759286160867,23.821862177755232,41.74003095570225 +2024-09-25 05:00:00,6,20.708686885885946,23.785881786830043,65.77286434341953 +2024-09-25 06:00:00,6,18.70248521358316,24.196124547487642,50.3606771852404 +2024-09-25 07:00:00,6,16.248925913896525,19.596143714253007,56.8114674662041 +2024-09-25 08:00:00,6,48.660878265542934,25.23985855723057,57.42518852965124 +2024-09-25 09:00:00,6,19.55403981569507,18.684390071638028,66.93651311287877 +2024-09-25 10:00:00,6,27.611599218431525,17.438811680061036,54.54688391909523 +2024-09-25 11:00:00,6,23.366759800548966,23.52515334462825,66.42668333205921 +2024-09-25 12:00:00,6,9.5820089689275,24.47015373682794,77.91033749881626 +2024-09-25 13:00:00,6,31.22803448338025,28.056266180129587,54.45137650075435 +2024-09-25 14:00:00,6,20.157239776736144,24.119495523105513,52.415726329924134 +2024-09-25 15:00:00,6,30.75718910368747,21.157616855304255,64.16097841508036 +2024-09-25 16:00:00,6,50.7086851259546,24.477037630314296,56.32082429459024 +2024-09-25 17:00:00,6,21.015922230217363,24.570090422030017,45.64954674721767 +2024-09-25 18:00:00,6,23.59701438750145,24.060054053139126,60.34438869381478 +2024-09-25 19:00:00,6,45.346069440761816,24.576688192219045,42.731212920683106 +2024-09-25 20:00:00,6,30.205658621522463,18.685273211778803,53.994097276784395 +2024-09-25 21:00:00,6,20.186094553956494,23.825262024579757,52.7452614218689 +2024-09-25 22:00:00,6,38.18835825372865,22.887295730704114,60.05646919413231 +2024-09-25 23:00:00,6,36.93916325045586,19.658268027717405,50.73111058211458 +2024-09-26 00:00:00,6,31.51971338838265,21.248923214866924,46.54520916745748 +2024-09-26 01:00:00,6,22.832003876654333,22.852888508689787,79.4447183875699 +2024-09-26 02:00:00,6,23.392527543156685,23.064759909723765,60.61544000341337 +2024-09-26 03:00:00,6,15.84660906859855,22.050789846169792,52.319880868666985 +2024-09-26 04:00:00,6,24.83646442910121,24.04135634440089,55.987033109471014 +2024-09-26 05:00:00,6,23.442962450975763,26.426431161484643,53.481579254014484 +2024-09-26 06:00:00,6,37.82145391143358,29.9255267907248,58.51484486387284 +2024-09-26 07:00:00,6,21.92520849699594,19.99848101627324,57.563880143205616 +2024-09-26 08:00:00,6,26.850590296114042,27.090581848365197,60.94026543403813 +2024-09-26 09:00:00,6,32.19445188586701,23.962632789438434,42.42833863737136 +2024-09-26 10:00:00,6,34.97157230581446,25.698637356676066,46.649742879054294 +2024-09-26 11:00:00,6,40.55527184102116,17.44442793485214,58.48930094408864 +2024-09-26 12:00:00,6,21.62476621187161,23.13584775748468,67.81623876309943 +2024-09-26 13:00:00,6,29.166349768958238,27.460213262807024,43.77714567296201 +2024-09-26 14:00:00,6,13.129394496684172,27.79975349011559,58.02482679063914 +2024-09-26 15:00:00,6,17.701655048207606,30.46073440294478,46.53252807414162 +2024-09-26 16:00:00,6,19.742845508173065,21.77424504175292,44.89761653288139 +2024-09-26 17:00:00,6,19.82994861618833,26.316133367949092,52.945637700333556 +2024-09-26 18:00:00,6,14.017502457219527,25.03038562191644,64.77646460171054 +2024-09-26 19:00:00,6,13.283940980053602,22.726353177069733,47.88157643201249 +2024-09-26 20:00:00,6,33.768767944957354,23.08082356255208,49.35540654027803 +2024-09-26 21:00:00,6,34.68360008607055,23.880370165033746,66.20800606165056 +2024-09-26 22:00:00,6,20.19721705481837,23.157219632642217,63.46673307999821 +2024-09-26 23:00:00,6,29.347831760356016,25.759470432584497,44.30806786919309 +2024-09-27 00:00:00,6,22.253345039723357,26.939837968514652,52.155964906305364 +2024-09-27 01:00:00,6,13.001470656151385,32.643363585221415,66.8861406862812 +2024-09-27 02:00:00,6,21.477429043019463,17.90226803461946,47.47848832627571 +2024-09-27 03:00:00,6,25.080223187724165,21.094073602823595,34.93630985902004 +2024-09-27 04:00:00,6,16.32492390610486,18.600383345282495,50.59374984627715 +2024-09-27 05:00:00,6,18.15944745304324,23.639460419980495,49.807249708005784 +2024-09-27 06:00:00,6,34.79104100961731,25.741170240573613,69.37437812566966 +2024-09-27 07:00:00,6,8.279321625026164,24.53875692962737,65.8630356520417 +2024-09-27 08:00:00,6,25.108517294960386,23.353850504553986,57.51846145391758 +2024-09-27 09:00:00,6,23.571424787622398,23.84557713420892,67.72330875637805 +2024-09-27 10:00:00,6,29.705521465188674,25.985092800136186,63.32115411978436 +2024-09-27 11:00:00,6,26.43749250200867,25.046783758374737,65.43528602588572 +2024-09-27 12:00:00,6,20.2412071441331,27.842605792041695,59.37283250339035 +2024-09-27 13:00:00,6,16.78506409592479,22.914999598242215,61.088563321838826 +2024-09-27 14:00:00,6,19.103991956201824,29.635103498663014,54.58616631044036 +2024-09-27 15:00:00,6,24.194777379074075,22.509344235796885,43.36927452883756 +2024-09-27 16:00:00,6,26.47196049286051,25.260330123143593,46.58271234681786 +2024-09-27 17:00:00,6,20.015878566622078,19.54590450549013,43.500306587052535 +2024-09-27 18:00:00,6,35.25375886776847,26.182332881126662,67.9708609711076 +2024-09-27 19:00:00,6,30.688641234563185,17.618242456437702,46.17436326503983 +2024-09-27 20:00:00,6,23.308658354949163,27.132574344581368,59.10742560678499 +2024-09-27 21:00:00,6,33.72741271186992,21.23337965121917,70.44338942081504 +2024-09-27 22:00:00,6,22.872805876235237,22.6120996420774,52.28435682069867 +2024-09-27 23:00:00,6,37.166529784289885,26.614949833234817,52.837169753445174 +2024-09-28 00:00:00,6,17.188675230476985,27.732469385515948,37.767692839985415 +2024-09-28 01:00:00,6,13.689248077166958,18.731958753653952,49.24986908588629 +2024-09-28 02:00:00,6,16.152986084976458,22.636401346111427,47.48690318979393 +2024-09-28 03:00:00,6,19.652691208580876,24.87256945480298,54.47656718101155 +2024-09-28 04:00:00,6,13.560227225362695,23.28513766278885,59.35813215918336 +2024-09-28 05:00:00,6,9.056935013090836,26.066346777338616,51.578322305345694 +2024-09-28 06:00:00,6,8.5844705468134,23.875702660851175,73.00798746128692 +2024-09-28 07:00:00,6,28.34256888727339,18.340353914797255,26.435204114472597 +2024-09-28 08:00:00,6,38.638457928445746,20.88261599931193,68.31036243182032 +2024-09-28 09:00:00,6,24.616258168129235,19.619282104504144,50.023466836882235 +2024-09-28 10:00:00,6,35.31359504855593,24.309501408087712,60.0917011663789 +2024-09-28 11:00:00,6,30.50785816022204,26.46412436777073,52.892560471045726 +2024-09-28 12:00:00,6,22.11306378880814,23.807713202724223,52.32751864437861 +2024-09-28 13:00:00,6,32.22641051341169,17.593482589630057,41.56130996822013 +2024-09-28 14:00:00,6,35.58180260431408,25.771865314728338,61.01001415797999 +2024-09-28 15:00:00,6,29.967801695242066,25.223819068326648,54.38613601051983 +2024-09-28 16:00:00,6,21.788513635607938,12.882957609578751,57.03953041916511 +2024-09-28 17:00:00,6,23.4054483856174,29.17251155589992,41.916058793064686 +2024-09-28 18:00:00,6,24.12347813549052,19.99596896643325,50.47909977690603 +2024-09-28 19:00:00,6,37.15536762457801,21.227251090188094,57.76629402554911 +2024-09-28 20:00:00,6,20.9609879902341,26.344667133168336,59.856340070228185 +2024-09-28 21:00:00,6,29.618032149840943,25.840637619047538,52.658976019555595 +2024-09-28 22:00:00,6,22.221572453694286,21.99722872677049,52.79983470583777 +2024-09-28 23:00:00,6,29.532395509809735,27.125068791705715,52.3727908921674 +2024-09-29 00:00:00,6,23.877256913365805,12.86353995986593,48.56181888984607 +2024-09-29 01:00:00,6,16.650844581747357,20.87300092430642,59.003100890671504 +2024-09-29 02:00:00,6,26.556930620808743,28.833505693424208,45.19533189552217 +2024-09-29 03:00:00,6,15.523601603021458,18.437682325351226,60.87933708916137 +2024-09-29 04:00:00,6,22.494761993906067,20.198220885277923,45.93922651207404 +2024-09-29 05:00:00,6,16.966282796094113,31.36018282640562,59.65627575735008 +2024-09-29 06:00:00,6,15.017142211849402,19.47288608534923,71.31939229806855 +2024-09-29 07:00:00,6,14.034884845317583,28.535286053596373,59.31633375727002 +2024-09-29 08:00:00,6,32.38209600757133,22.79985700636503,53.69588392224148 +2024-09-29 09:00:00,6,37.00627872682226,23.33588416224872,61.23574870013802 +2024-09-29 10:00:00,6,40.623129116790565,22.836467345467337,53.912634602142674 +2024-09-29 11:00:00,6,25.454125471594036,23.156120587371397,35.37279010933431 +2024-09-29 12:00:00,6,35.549522851046774,26.993517248785608,48.30144558302816 +2024-09-29 13:00:00,6,36.76782308307973,21.62577237765756,50.17901025501121 +2024-09-29 14:00:00,6,23.172490174573383,19.655670959876495,51.001382694507825 +2024-09-29 15:00:00,6,5.384084279119662,27.783607764551135,54.18619196545503 +2024-09-29 16:00:00,6,39.16873346565307,19.68677324764291,55.23841685903349 +2024-09-29 17:00:00,6,37.43084556659194,22.19979169376919,56.76735620901107 +2024-09-29 18:00:00,6,38.11763525849425,21.230408617122347,50.90922038764013 +2024-09-29 19:00:00,6,23.855355230065946,20.766190113626603,51.63793054342636 +2024-09-29 20:00:00,6,33.432305324878016,22.268595002596584,54.3307942657926 +2024-09-29 21:00:00,6,36.432417974864,15.726579496046567,45.893375277776 +2024-09-29 22:00:00,6,32.43408865954599,23.876984918554836,59.02304674293105 +2024-09-29 23:00:00,6,36.39691892948848,27.81823845691246,55.129619875164146 +2024-09-30 00:00:00,6,16.845504577190518,16.829036401310262,67.19650177577559 +2024-09-30 01:00:00,6,34.66599128577502,23.19838575567404,39.90517845655897 +2024-09-30 02:00:00,6,13.911179651943566,20.80407863712191,63.43100284489447 +2024-09-30 03:00:00,6,3.042712475381432,26.208812529652427,53.92693428823397 +2024-09-30 04:00:00,6,40.43326190171533,26.531389159883616,45.29349673200011 +2024-09-30 05:00:00,6,25.32251949735136,16.25229115841038,61.209029604552136 +2024-09-30 06:00:00,6,29.679632121110046,17.421784980837455,53.765986816644876 +2024-09-30 07:00:00,6,23.29105135989752,26.942456574214166,49.5784372931183 +2024-09-30 08:00:00,6,32.41480041982467,24.821457713928297,45.68404803957492 +2024-09-30 09:00:00,6,23.787007754294784,24.402635075598166,47.12946559591874 +2024-09-30 10:00:00,6,27.745981056693825,20.374859100330372,40.475314558304575 +2024-09-30 11:00:00,6,6.079643519995329,26.3867014200913,52.313207314626666 +2024-09-30 12:00:00,6,16.008286922764825,23.304683544917072,56.7024935482095 +2024-09-30 13:00:00,6,24.950923968723924,28.317216197489138,65.09286083553792 +2024-09-30 14:00:00,6,32.22493197186157,27.03665856585447,56.76187372637307 +2024-09-30 15:00:00,6,24.40195032696659,21.962525418067408,64.11158904937025 +2024-09-30 16:00:00,6,13.959791021804774,27.245238121251678,55.411341159034706 +2024-09-30 17:00:00,6,33.60846498753654,31.70685361287375,49.82833915115994 +2024-09-30 18:00:00,6,23.079392552651264,19.18246200350811,49.01983545321083 +2024-09-30 19:00:00,6,22.384143654153164,22.186472241845085,38.15789465580728 +2024-09-30 20:00:00,6,16.52962068500478,25.031431243327862,52.26750264956153 +2024-09-30 21:00:00,6,32.27553142382297,19.009995200780814,57.60428386307802 +2024-09-30 22:00:00,6,33.811618124166856,24.602668576244284,54.53140919866225 +2024-09-30 23:00:00,6,6.506990805761287,28.872821432107333,40.21282724839374 +2024-10-01 00:00:00,6,36.137238227678075,22.429745409432613,62.82157146738513 +2024-10-01 01:00:00,6,33.63719552942349,19.52488720501034,66.88799988120772 +2024-10-01 02:00:00,6,27.40069705592991,21.958970700684144,57.09531296976086 +2024-10-01 03:00:00,6,16.77405208247636,24.435285435479802,75.26579566463619 +2024-10-01 04:00:00,6,22.801014493595602,26.49867217517958,42.67791254681589 +2024-10-01 05:00:00,6,31.225072266334085,23.22064567688255,49.02386328575212 +2024-10-01 06:00:00,6,29.92080516943344,23.832614654576002,65.69907513168779 +2024-10-01 07:00:00,6,43.097279379188265,20.343429332844256,64.76330255395006 +2024-10-01 08:00:00,6,28.792248309392228,28.24188041424701,50.490571006823046 +2024-10-01 09:00:00,6,49.18657023530895,25.489296951407027,61.56943922671421 +2024-10-01 10:00:00,6,21.75467304905769,31.75963046123301,50.28447617204149 +2024-10-01 11:00:00,6,34.93665264683104,17.50737941393743,63.258522159755344 +2024-10-01 12:00:00,6,26.686242287363537,27.345732850325696,77.74582734814722 +2024-10-01 13:00:00,6,17.342940285586767,25.278500747315018,59.14729997416221 +2024-10-01 14:00:00,6,24.750005217641508,18.73082580310423,52.771931959258616 +2024-10-01 15:00:00,6,27.81836403177171,21.43298473537751,42.72355732829419 +2024-10-01 16:00:00,6,16.392548792305597,22.65230558639038,69.50748743297277 +2024-10-01 17:00:00,6,38.37775551919616,18.63334583689629,56.86201214717777 +2024-10-01 18:00:00,6,33.60494484108958,28.274006073722454,51.23136464692312 +2024-10-01 19:00:00,6,28.267066916681856,23.959973170831365,41.227391573992925 +2024-10-01 20:00:00,6,27.80252310251255,21.055842039726862,55.137952130453385 +2024-10-01 21:00:00,6,33.42296190490653,32.056914968645245,54.90204291192256 +2024-10-01 22:00:00,6,22.055041283247906,22.014677137021632,48.18200436737751 +2024-10-01 23:00:00,6,25.291433505575274,23.80837073214269,45.8452001048009 +2024-10-02 00:00:00,6,16.704731307135724,24.785276222928996,64.28539386605313 +2024-10-02 01:00:00,6,20.693259963096434,24.27570113604518,63.80129703900682 +2024-10-02 02:00:00,6,5.6986959044790915,21.982508851655357,48.68576025304196 +2024-10-02 03:00:00,6,19.347064292784545,24.268378366200032,60.44769608010063 +2024-10-02 04:00:00,6,11.821430551897642,20.651501192141847,48.648454452100374 +2024-10-02 05:00:00,6,24.306290859374403,21.38726601524718,39.79721593374185 +2024-10-02 06:00:00,6,26.50680148958926,26.509354209513823,67.25169281186298 +2024-10-02 07:00:00,6,22.626154117490064,27.58496603110746,61.95613858612882 +2024-10-02 08:00:00,6,17.868181025136657,16.86733021806737,57.57100582817006 +2024-10-02 09:00:00,6,31.46751418605093,22.43026868086165,59.570161621821974 +2024-10-02 10:00:00,6,39.74185469341882,24.15252734964705,57.54330391417487 +2024-10-02 11:00:00,6,39.74777258411363,24.100314488599615,50.49244598558027 +2024-10-02 12:00:00,6,22.467840109222713,24.835890008792344,52.94982384096687 +2024-10-02 13:00:00,6,16.76132235317976,26.365927319260614,64.54520059463808 +2024-10-02 14:00:00,6,9.630183829886477,24.02792394093683,48.02538838995399 +2024-10-02 15:00:00,6,34.43635969899353,27.65609055744783,77.42143964211974 +2024-10-02 16:00:00,6,37.345601454755275,23.813587802501946,65.88001976899152 +2024-10-02 17:00:00,6,14.607725890319934,17.171867345666875,46.70268218937067 +2024-10-02 18:00:00,6,30.072951598246433,22.253555338697605,53.83444473030773 +2024-10-02 19:00:00,6,31.295012608746923,21.532473412110626,57.96605546099782 +2024-10-02 20:00:00,6,28.537967330448964,26.591839245435008,57.22005198866392 +2024-10-02 21:00:00,6,27.0281965121566,30.4246412261016,35.84769136248515 +2024-10-02 22:00:00,6,39.19520853752285,29.110128025051665,39.417572206413936 +2024-10-02 23:00:00,6,19.173736347197703,27.48706650878498,51.412720416516414 +2024-10-03 00:00:00,6,24.287250398229773,26.5753483713503,46.19558215127722 +2024-10-03 01:00:00,6,16.483991692757527,18.959387450207313,29.758104897563992 +2024-10-03 02:00:00,6,18.263517190068736,22.910161640486816,48.165617791623234 +2024-10-03 03:00:00,6,14.4072389506025,19.919943771302087,63.75681245007296 +2024-10-03 04:00:00,6,26.169735302857017,21.875807117231474,64.72999582390365 +2024-10-03 05:00:00,6,21.23342244990773,24.053882463101335,51.34220178082837 +2024-10-03 06:00:00,6,9.422330943405377,22.303117541282727,52.131072689004704 +2024-10-03 07:00:00,6,19.290667614273232,22.245685585252655,52.0910167935016 +2024-10-03 08:00:00,6,29.79885376703446,22.398079795312597,67.87107653765437 +2024-10-03 09:00:00,6,18.630257861241837,27.200119206909118,58.873325213172556 +2024-10-03 10:00:00,6,36.77167582465019,22.108533438424516,64.57376542974356 +2024-10-03 11:00:00,6,25.459306781985564,21.080292184679863,58.90800438794052 +2024-10-03 12:00:00,6,21.7768236681244,22.003969291714025,45.717947238825275 +2024-10-03 13:00:00,6,16.954580490179133,27.27207209579838,63.15335583523014 +2024-10-03 14:00:00,6,27.408910536857768,21.888336049528665,46.215088315606806 +2024-10-03 15:00:00,6,36.44776448088106,24.722998357598524,57.05814214278507 +2024-10-03 16:00:00,6,26.593058135658712,28.266344448833163,52.94835525098232 +2024-10-03 17:00:00,6,21.83813763894291,24.110481873364055,40.17425971173257 +2024-10-03 18:00:00,6,27.79224068463384,24.066286772325114,35.05868973010594 +2024-10-03 19:00:00,6,14.196537225608381,24.459091260739836,57.87777505646861 +2024-10-03 20:00:00,6,24.77698531214272,22.165574699142358,49.592899068665766 +2024-10-03 21:00:00,6,30.724343141915455,29.516784516328187,56.289816181025145 +2024-10-03 22:00:00,6,24.72788041851951,21.654669727440584,64.90782543539228 +2024-10-03 23:00:00,6,14.285839272467628,20.69052465969722,65.26125558079215 +2024-10-04 00:00:00,6,24.36013560430015,27.82822254953614,43.198030474952596 +2024-10-04 01:00:00,6,20.195443817453953,21.473564359121095,50.23974853013847 +2024-10-04 02:00:00,6,13.036585712178173,21.097198012516927,63.125378278924465 +2024-10-04 03:00:00,6,13.133372514536669,20.59862448315929,43.27592503042884 +2024-10-04 04:00:00,6,19.017734148779322,23.68023754734176,61.46501466104913 +2024-10-04 05:00:00,6,27.790273235090524,18.480694533090382,65.94447984530791 +2024-10-04 06:00:00,6,16.875536224588373,16.989641647028243,35.71883167474198 +2024-10-04 07:00:00,6,42.16736883303615,27.011325791469773,48.973214227322785 +2024-10-04 08:00:00,6,14.819936817586603,33.259571251190565,53.10996566034329 +2024-10-04 09:00:00,6,7.1604030351799715,26.922510928182064,60.42134996487429 +2024-10-04 10:00:00,6,33.62898035234435,20.370997688852313,48.3296311707908 +2024-10-04 11:00:00,6,26.78949131986399,21.69604625017271,76.53327109441118 +2024-10-04 12:00:00,6,31.130151297484993,23.813781723125405,57.23642605765199 +2024-10-04 13:00:00,6,20.55162834046077,23.197860445573998,48.45471399759347 +2024-10-04 14:00:00,6,31.399072211587395,28.300147173343397,59.29930921173636 +2024-10-04 15:00:00,6,29.197533186518747,21.632500243602646,60.44520325669004 +2024-10-04 16:00:00,6,26.10541805364731,23.690435384855455,42.7041164299556 +2024-10-04 17:00:00,6,22.264149190611697,24.832710764227066,41.396245715288245 +2024-10-04 18:00:00,6,24.979813074217912,24.444309903734368,62.34523440723164 +2024-10-04 19:00:00,6,29.878536453084042,20.46285250011144,51.311532765536064 +2024-10-04 20:00:00,6,14.449595729772556,19.611294460848473,66.60766390169525 +2024-10-04 21:00:00,6,13.235906594442051,23.6458782671737,54.168911922859124 +2024-10-04 22:00:00,6,19.2946127090549,21.76599539334951,57.65493594209119 +2024-10-04 23:00:00,6,27.468805741009273,24.602487172830596,63.90006531935348 +2024-10-05 00:00:00,6,26.437513086688732,15.212001008886713,42.30669917332495 +2024-10-05 01:00:00,6,21.735713894605496,22.801634349189065,55.85419600584179 +2024-10-05 02:00:00,6,19.861225685398562,25.43386401377835,60.82724133911004 +2024-10-05 03:00:00,6,20.6199009472407,29.227171074424522,56.62808692659796 +2024-10-05 04:00:00,6,7.385551223371515,25.289846591284373,68.69486386599466 +2024-10-05 05:00:00,6,26.422067413436064,21.02652733755422,45.94855817051054 +2024-10-05 06:00:00,6,27.609100095895588,22.402671180525505,55.398508510021735 +2024-10-05 07:00:00,6,36.593609954800755,17.86058490795868,41.72395400311433 +2024-10-05 08:00:00,6,36.77286770821806,29.330345554926197,42.21881272050205 +2024-10-05 09:00:00,6,5.786665027917007,21.51938585420102,54.50674837640651 +2024-10-05 10:00:00,6,14.033068439546309,19.966240853942722,40.82452523293246 +2024-10-05 11:00:00,6,39.880896257747054,22.566621991759924,55.974446436858 +2024-10-05 12:00:00,6,35.55620395038112,24.531463814914446,74.96515753410307 +2024-10-05 13:00:00,6,8.668745466571053,26.25198391162458,63.4920807793484 +2024-10-05 14:00:00,6,20.44367375315344,18.228040641377223,46.89843031336331 +2024-10-05 15:00:00,6,22.236143265997214,19.711644793647707,52.87250420542232 +2024-10-05 16:00:00,6,35.094203202059184,22.26095635607463,60.32390777834097 +2024-10-05 17:00:00,6,16.449264181923915,22.30574520839549,48.05265357565761 +2024-10-05 18:00:00,6,23.35323422838714,21.017262845784163,51.91167074048793 +2024-10-05 19:00:00,6,37.23998036257391,24.001913043048596,52.45828475264307 +2024-10-05 20:00:00,6,28.02065667755118,21.760837719083227,66.42833652152973 +2024-10-05 21:00:00,6,45.80768841702927,26.603896750435798,40.407928758823374 +2024-10-05 22:00:00,6,18.981196393553287,19.43152106106435,54.18445787067125 +2024-10-05 23:00:00,6,39.89470056820567,20.331890885580865,46.09014243136896 +2024-10-06 00:00:00,6,17.604888053652182,17.775219189880897,53.84682311327779 +2024-10-06 01:00:00,6,17.577548785104412,22.442523701851144,59.71069904003597 +2024-10-06 02:00:00,6,16.212292397983173,26.787292911291594,60.733727393187976 +2024-10-06 03:00:00,6,25.08910089199086,21.548754031640527,54.19319988404822 +2024-10-06 04:00:00,6,22.927718583755794,20.091348174958426,59.01808205263555 +2024-10-06 05:00:00,6,28.886471398500166,24.121005755590502,72.10336559104212 +2024-10-06 06:00:00,6,24.026697448616122,28.824511974463473,44.009195488599715 +2024-10-06 07:00:00,6,35.604923687092636,18.49674333127652,54.26883269428216 +2024-10-06 08:00:00,6,30.884058379108012,14.830160877287488,53.2344954480961 +2024-10-06 09:00:00,6,41.448415270718364,23.854825010118702,53.743916597398524 +2024-10-06 10:00:00,6,33.15252429330109,25.835885182483487,50.34851658773335 +2024-10-06 11:00:00,6,28.992810903871014,27.32681452711016,50.887591398876104 +2024-10-06 12:00:00,6,26.18394637999053,23.055892205067014,54.90173076879008 +2024-10-06 13:00:00,6,15.84595265402611,22.929338910348882,64.32000862859591 +2024-10-06 14:00:00,6,39.682158393970155,26.68827217880334,54.66046145591222 +2024-10-06 15:00:00,6,31.875959448226208,22.640550533841616,69.9356439196621 +2024-10-06 16:00:00,6,16.243403560892947,19.069097395408537,51.92683031911981 +2024-10-06 17:00:00,6,37.322490280541075,27.117844994239032,60.43439325068524 +2024-10-06 18:00:00,6,33.45568587568094,22.224468493354344,56.27382156088012 +2024-10-06 19:00:00,6,26.743176952397185,19.76155828560002,37.995148265724765 +2024-10-06 20:00:00,6,19.2242891505469,20.667051349809896,43.69881960448255 +2024-10-06 21:00:00,6,29.587518575357308,21.73096669857302,31.81876457065047 +2024-10-06 22:00:00,6,27.270520944097523,26.879859374698732,52.02174362062407 +2024-10-06 23:00:00,6,28.26136653956126,22.111009999848363,42.24170143326248 +2024-10-07 00:00:00,6,18.856492854343344,23.88912874502828,65.14852372328905 +2024-10-07 01:00:00,6,15.342514807527053,25.80888301413721,69.05017919745694 +2024-10-07 02:00:00,6,14.216356769150206,23.674834396266938,62.88674215427021 +2024-10-07 03:00:00,6,17.399523062768587,20.990503952129657,57.070391593550774 +2024-10-07 04:00:00,6,4.666166929499219,18.771609258087754,70.44606769370846 +2024-10-07 05:00:00,6,24.725092341164782,19.968876910908875,55.12016765326603 +2024-10-07 06:00:00,6,18.205220293341974,21.34717547568997,52.139569525040265 +2024-10-07 07:00:00,6,29.5109755827911,25.2156520268261,31.23988487782556 +2024-10-07 08:00:00,6,18.73459338250207,25.44275089655591,46.14260321736127 +2024-10-07 09:00:00,6,41.47619170608244,24.548308500912352,55.62602081853687 +2024-10-07 10:00:00,6,28.206684014844665,20.046613680598817,68.36845361437614 +2024-10-07 11:00:00,6,36.25234418226577,31.628941103164404,71.5090599946088 +2024-10-07 12:00:00,6,37.53430074151666,18.403008148968564,63.096247406155726 +2024-10-07 13:00:00,6,26.111998622144185,21.82403088900448,56.466246206703374 +2024-10-07 14:00:00,6,27.734575662890244,21.39442760078509,57.57210175988445 +2024-10-07 15:00:00,6,29.521426800577466,19.630329106501502,58.22781617231023 +2024-10-07 16:00:00,6,33.82521265714408,23.48438103349449,63.28093353969493 +2024-10-07 17:00:00,6,23.25947944509283,21.64703106394702,45.51044062530319 +2024-10-07 18:00:00,6,32.80050528839817,27.563543282005476,67.64878715353915 +2024-10-07 19:00:00,6,27.170798388378202,27.551978553785396,66.86197424888672 +2024-10-07 20:00:00,6,34.6307716929841,25.429944541957084,57.60779482682413 +2024-10-07 21:00:00,6,31.9200431954387,23.385570182346807,54.96811158122418 +2024-10-07 22:00:00,6,36.360434740672844,21.978493364335435,46.70182311635295 +2024-10-07 23:00:00,6,16.07172970040298,29.39087647201431,49.59975884330391 +2024-10-08 00:00:00,6,16.707610407157837,22.416201903492855,62.075904696176266 +2024-10-08 01:00:00,6,22.3125497783189,23.99536712994076,48.745810780970224 +2024-10-08 02:00:00,6,32.077451265828685,23.058963164153507,57.60329396970522 +2024-10-08 03:00:00,6,33.21514660182288,19.745386300358362,57.98582812132868 +2024-10-08 04:00:00,6,28.276783037822888,20.99274848462554,60.38453673021368 +2024-10-08 05:00:00,6,27.16969273208381,15.494871546728108,52.00046835063181 +2024-10-08 06:00:00,6,25.722586970940856,18.381936472579735,72.96752506014607 +2024-10-08 07:00:00,6,24.2189021018629,25.618966212689962,55.99301958363576 +2024-10-08 08:00:00,6,19.385129461711564,16.671210646185095,80.22251014511622 +2024-10-08 09:00:00,6,24.946874680701832,25.22206763864265,63.54433966343193 +2024-10-08 10:00:00,6,40.42415999664284,27.714755302089475,35.978986282532475 +2024-10-08 11:00:00,6,11.742230232556764,27.099073224622718,60.25677051198049 +2024-10-08 12:00:00,6,24.560422511033806,22.90095029495543,40.54533746337051 +2024-10-08 13:00:00,6,13.744632397509251,18.950170610801326,65.55400280024081 +2024-10-08 14:00:00,6,29.226503531919608,23.312530226288718,77.57207160580876 +2024-10-08 15:00:00,6,35.29948413820833,21.673783304254652,48.65986154124593 +2024-10-08 16:00:00,6,12.35553032523625,21.134728572876742,44.02207965431711 +2024-10-08 17:00:00,6,34.78504307627559,21.276771717305405,64.22904016874121 +2024-10-08 18:00:00,6,22.65694133707397,23.4147679336448,59.62113255471169 +2024-10-08 19:00:00,6,29.613484994050477,18.67570114464716,51.06161890895767 +2024-10-08 20:00:00,6,13.372438548772982,27.629250010655813,62.816768100922026 +2024-10-08 21:00:00,6,38.985348874369826,27.011487452895285,37.91488392187197 +2024-10-08 22:00:00,6,29.653109298340798,20.694059557340708,40.879508736718265 +2024-10-08 23:00:00,6,41.6960134248183,26.037635781482646,47.52459955287488 +2024-10-09 00:00:00,6,15.33098554618347,19.017430019611343,64.67580789555953 +2024-10-09 01:00:00,6,22.369035179150792,19.77867822374903,41.7359494383968 +2024-10-09 02:00:00,6,19.011282094688106,26.173214118389012,43.07794410991962 +2024-10-09 03:00:00,6,18.535592628195293,23.79462687207151,48.95502386611503 +2024-10-09 04:00:00,6,19.20630660713964,24.989696439312272,43.747627562372855 +2024-10-09 05:00:00,6,13.562394291507117,22.821378024724922,60.8113143166063 +2024-10-09 06:00:00,6,24.071534682795058,25.73943441910402,53.09393293693664 +2024-10-09 07:00:00,6,31.182194878200487,24.874377409322097,62.58956971761935 +2024-10-09 08:00:00,6,30.044071741154358,19.657463107907795,57.64875411014839 +2024-10-09 09:00:00,6,35.62050061972782,23.55766807498219,65.58653178291331 +2024-10-09 10:00:00,6,50.60009113938984,27.708841483297256,62.33183355087824 +2024-10-09 11:00:00,6,32.647062611332764,22.5538751584903,74.7545894933501 +2024-10-09 12:00:00,6,15.150138961739792,19.19459885484867,45.021759896732405 +2024-10-09 13:00:00,6,34.70706966551995,27.503535843111226,63.591321345877304 +2024-10-09 14:00:00,6,21.08012594542142,30.51755546518296,53.79053449797375 +2024-10-09 15:00:00,6,38.544362825578965,22.234576501519072,45.54802913343326 +2024-10-09 16:00:00,6,23.333141586814254,25.569109645403557,40.89046392812013 +2024-10-09 17:00:00,6,22.012342687496236,24.261285926873043,68.9323975974967 +2024-10-09 18:00:00,6,21.93490597950945,23.333830045327783,37.935341024522856 +2024-10-09 19:00:00,6,23.934612405181813,23.12515649564762,56.95954153122343 +2024-10-09 20:00:00,6,6.144361399793958,26.398820375310578,48.56318948278964 +2024-10-09 21:00:00,6,39.552832089263305,31.066247463589818,42.88031168573712 +2024-10-09 22:00:00,6,25.78483885546838,23.172756447589737,46.90666472584185 +2024-10-09 23:00:00,6,27.6006384306947,26.34457130538436,57.919482454032895 +2024-10-10 00:00:00,6,11.694770603282588,23.609525491514518,47.841074588554946 +2024-10-10 01:00:00,6,33.10947052887505,17.741221333541304,56.80871321785231 +2024-10-10 02:00:00,6,23.425093457169048,21.54981436252847,41.9232192275164 +2024-10-10 03:00:00,6,13.762323384317433,23.602907644830605,52.4198026015273 +2024-10-10 04:00:00,6,12.45908494809488,24.68012283857331,63.3481797300703 +2024-10-10 05:00:00,6,17.45239298294787,29.53294908141139,71.52824758246753 +2024-10-10 06:00:00,6,17.00311915835048,23.104900521175907,60.527237979554016 +2024-10-10 07:00:00,6,19.20930007916414,19.373002192431663,57.07563479118455 +2024-10-10 08:00:00,6,19.70190711710656,20.097179019032332,57.735258403374104 +2024-10-10 09:00:00,6,26.78733473955226,25.60028770467999,59.0708766321851 +2024-10-10 10:00:00,6,21.01795164058353,21.726426418824516,62.69935456711489 +2024-10-10 11:00:00,6,13.826475533175122,26.925886835256776,46.73752027304714 +2024-10-10 12:00:00,6,41.669053569584605,18.65792434059171,55.43927065449658 +2024-10-10 13:00:00,6,23.930040780134735,24.57560627911845,58.41660067252991 +2024-10-10 14:00:00,6,20.173299718902584,18.932437411705486,58.635235120850126 +2024-10-10 15:00:00,6,28.049783068578837,26.268709586836597,33.23328972597757 +2024-10-10 16:00:00,6,17.49929502494617,20.721527264620097,52.402498869965605 +2024-10-10 17:00:00,6,13.85481513050607,21.109040208876152,59.92100416084376 +2024-10-10 18:00:00,6,25.302078406625498,22.195291600922463,54.593520908944136 +2024-10-10 19:00:00,6,25.387714598586705,17.760639713638568,54.50014014041292 +2024-10-10 20:00:00,6,40.88162294314535,24.51967176473402,67.40399856174815 +2024-10-10 21:00:00,6,24.904928924166352,17.058254496524977,54.909528101211755 +2024-10-10 22:00:00,6,18.955194151443855,30.709276664138628,66.8016430393597 +2024-10-10 23:00:00,6,19.656859670471928,25.104462691893495,39.43890652262861 +2024-10-11 00:00:00,6,17.03974384861776,25.034958651423633,56.06582229085112 +2024-10-11 01:00:00,6,29.718343651183943,19.10970636875545,46.86791332413785 +2024-10-11 02:00:00,6,21.575343598076934,20.39774018890682,48.480783394290825 +2024-10-11 03:00:00,6,13.447455964495761,21.621971927211398,51.69233351399754 +2024-10-11 04:00:00,6,22.908594002393468,26.963029395780275,56.73064206066443 +2024-10-11 05:00:00,6,26.740840167409342,22.043007608155236,42.69223417210621 +2024-10-11 06:00:00,6,27.881311467682796,23.747009132983735,64.74891760605819 +2024-10-11 07:00:00,6,26.117666498983894,28.89900359031784,63.04294149979465 +2024-10-11 08:00:00,6,28.24687973538474,15.727495848264727,46.00428581765287 +2024-10-11 09:00:00,6,32.365725286881606,22.660580252747813,50.58503806925739 +2024-10-11 10:00:00,6,26.761620927666744,23.495750988690478,40.456520999421976 +2024-10-11 11:00:00,6,18.817876350755576,22.421086876214623,60.618071622833966 +2024-10-11 12:00:00,6,17.416848397823212,27.251905308106412,54.083125997053905 +2024-10-11 13:00:00,6,24.66411466397306,28.057708910306722,57.88309958818068 +2024-10-11 14:00:00,6,27.37029863229905,25.47292153436266,53.48602489662567 +2024-10-11 15:00:00,6,13.230844072838615,27.674350372759783,46.45797980776444 +2024-10-11 16:00:00,6,22.1258106860165,27.217070259231903,54.29129358117634 +2024-10-11 17:00:00,6,25.06072305267898,24.597515335739004,42.52880767098427 +2024-10-11 18:00:00,6,25.409112984728573,27.22842835281293,56.687095260935045 +2024-10-11 19:00:00,6,20.64735415843618,25.09789595591114,62.28671034873331 +2024-10-11 20:00:00,6,27.950883721239872,21.808661764174747,65.45934726082109 +2024-10-11 21:00:00,6,25.4897655150227,26.644302863693074,49.77926070487582 +2024-10-11 22:00:00,6,10.992297240345525,21.726837671754673,48.61727574326268 +2024-10-11 23:00:00,6,39.33337724777671,21.73643164843497,44.74890875681645 +2024-10-12 00:00:00,6,26.08190557316632,20.551085283369318,54.69061054149341 +2024-10-12 01:00:00,6,23.058978148483014,24.86560797135447,45.44250010237494 +2024-10-12 02:00:00,6,26.71685660888288,23.110544543240007,57.43745048027469 +2024-10-12 03:00:00,6,19.87473988039669,25.55691092375637,48.82582333022927 +2024-10-12 04:00:00,6,0.13612453685869852,14.920294607960752,56.34047109760725 +2024-10-12 05:00:00,6,18.63145913491396,25.195415990609074,46.93700348728908 +2024-10-12 06:00:00,6,22.197910255427992,20.301002414565666,56.18731767393357 +2024-10-12 07:00:00,6,40.32716310909723,21.294947223982522,46.35786123453622 +2024-10-12 08:00:00,6,16.010659215291525,23.719800228330048,54.587095745885975 +2024-10-12 09:00:00,6,29.062260487007336,26.998593651861686,44.95605806780959 +2024-10-12 10:00:00,6,34.84632960801011,18.907486422471397,53.95392316865663 +2024-10-12 11:00:00,6,23.51594854707558,26.113500057602828,57.29060653056853 +2024-10-12 12:00:00,6,27.038879255230416,24.42571046740902,55.609770701155085 +2024-10-12 13:00:00,6,9.683751028029398,19.617689405600817,57.45884424580804 +2024-10-12 14:00:00,6,26.853579791582057,20.51631015840942,44.630848795901144 +2024-10-12 15:00:00,6,16.654306904284443,17.19748723666862,49.83835785984801 +2024-10-12 16:00:00,6,36.709279519005136,19.34118430265523,51.207338719431064 +2024-10-12 17:00:00,6,20.703939829586993,19.333769008692155,43.122327786683634 +2024-10-12 18:00:00,6,32.438922217227436,28.035259471879424,46.57671239993592 +2024-10-12 19:00:00,6,24.590333407461102,26.221480216571145,66.34348342566425 +2024-10-12 20:00:00,6,22.129998808598728,23.747412609522748,57.90866490253942 +2024-10-12 21:00:00,6,35.61601879657853,24.849057226399477,38.968066695239415 +2024-10-12 22:00:00,6,33.5040991489432,22.365562650822163,49.29070359038525 +2024-10-12 23:00:00,6,22.405992295616613,21.470364426919613,66.73372459169501 +2024-10-13 00:00:00,6,24.09935979716342,21.35749085238001,51.20262016374635 +2024-10-13 01:00:00,6,24.39535070244643,21.0411067963368,44.35039151438447 +2024-10-13 02:00:00,6,24.033883693350088,21.9244326074558,53.6933678427798 +2024-10-13 03:00:00,6,17.541343177838726,24.04760852217167,53.18215486339148 +2024-10-13 04:00:00,6,29.018016843313987,19.660993724324314,61.947221862735546 +2024-10-13 05:00:00,6,23.726302185461364,22.983489226775127,55.34283337828397 +2024-10-13 06:00:00,6,20.273778350141534,20.717496011846432,52.40831281206497 +2024-10-13 07:00:00,6,20.62517304167066,21.954218242897273,63.21972263561646 +2024-10-13 08:00:00,6,29.440568353585657,20.26503465576942,63.74668614344783 +2024-10-13 09:00:00,6,24.87813707218845,30.605325149790488,54.713336895453494 +2024-10-13 10:00:00,6,37.459939427379844,27.206970035547755,48.31995299553398 +2024-10-13 11:00:00,6,29.90244207788185,17.361064500518363,61.92590155921717 +2024-10-13 12:00:00,6,35.80812345242339,22.872781407441806,54.28014361552117 +2024-10-13 13:00:00,6,26.094267979932155,21.81967317005641,50.50407247975095 +2024-10-13 14:00:00,6,34.508007851874346,22.28020711280046,59.49258042006379 +2024-10-13 15:00:00,6,23.21813973145942,21.29821061898747,58.128603638849576 +2024-10-13 16:00:00,6,29.699254720336906,24.557712051568192,55.835339267016046 +2024-10-13 17:00:00,6,27.754734116168745,23.340693808716203,79.74955241965445 +2024-10-13 18:00:00,6,33.784366413802296,24.533472178049923,44.145049116144676 +2024-10-13 19:00:00,6,40.14142694727881,23.481506954308426,61.578973970646345 +2024-10-13 20:00:00,6,2.43356858846629,23.27380833797263,68.9142100498772 +2024-10-13 21:00:00,6,28.20316242725308,23.86767335100323,73.6531730155721 +2024-10-13 22:00:00,6,25.30548282764396,21.475533631789418,50.036969142001716 +2024-10-13 23:00:00,6,15.958864266863918,22.56301598485081,56.4539336002231 +2024-10-14 00:00:00,6,8.048112729675394,17.41728768582206,49.039132379620156 +2024-10-14 01:00:00,6,16.316065410079208,24.536012513446394,42.059934784214946 +2024-10-14 02:00:00,6,13.206450822568502,24.281740375468907,38.84501029777819 +2024-10-14 03:00:00,6,10.73378780174765,23.248868638766336,33.19010255797795 +2024-10-14 04:00:00,6,17.668085007302377,24.82228598994448,64.45240704574633 +2024-10-14 05:00:00,6,28.58155500268952,22.41336124798519,46.1449199251154 +2024-10-14 06:00:00,6,32.52911184287416,22.922248420677324,64.39012146933896 +2024-10-14 07:00:00,6,11.481299528981255,25.287897292962093,71.81668294463157 +2024-10-14 08:00:00,6,35.76034532807226,21.483937554366616,72.69726230096191 +2024-10-14 09:00:00,6,28.496719828812566,27.245314187774017,63.27681772121519 +2024-10-14 10:00:00,6,3.17179876552607,30.752365240350866,57.50586776516074 +2024-10-14 11:00:00,6,21.955514772692144,23.143935437198518,54.35278624706073 +2024-10-14 12:00:00,6,26.596540316907948,22.01058715754813,52.37969728744815 +2024-10-14 13:00:00,6,22.39364647134187,18.096394742088762,46.86566045095348 +2024-10-14 14:00:00,6,25.388455730189055,21.520076067703027,51.4527444997112 +2024-10-14 15:00:00,6,31.303469165931247,27.3956454633458,62.65532637542934 +2024-10-14 16:00:00,6,36.30141226654217,27.358941290898272,56.474833760066936 +2024-10-14 17:00:00,6,39.84733390977982,28.30358906165357,59.3966670381863 +2024-10-14 18:00:00,6,29.978379872359568,17.940271783392028,68.35951599616975 +2024-10-14 19:00:00,6,31.076212972606044,22.642678853799122,48.368418284239894 +2024-10-14 20:00:00,6,27.97491306051138,27.536820331650272,47.34101624137299 +2024-10-14 21:00:00,6,25.140005539747442,19.856034708437097,63.445747450698306 +2024-10-14 22:00:00,6,30.774103671295457,22.51899485089822,60.12302155244994 +2024-10-14 23:00:00,6,27.633338750137746,25.72658276949956,56.43328318428732 +2024-10-15 00:00:00,6,17.679221718848577,21.832480730853092,67.15319694502462 +2024-10-15 01:00:00,6,20.308634536923165,18.948784172802082,64.52291332680673 +2024-10-15 02:00:00,6,8.284906815560007,19.969703726853943,50.66072914658523 +2024-10-15 03:00:00,6,13.351361256113663,17.931164641624292,52.49731116033162 +2024-10-15 04:00:00,6,21.070259339463366,26.26051866555968,29.55330939606368 +2024-10-15 05:00:00,6,5.506068701400476,23.601878850276776,64.58868933175715 +2024-10-15 06:00:00,6,21.946853581831903,20.29460455098696,43.64046412891119 +2024-10-15 07:00:00,6,27.384499596181406,26.762653430360746,46.051815081239596 +2024-10-15 08:00:00,6,29.38706926551391,18.71593551171359,73.62632068777963 +2024-10-15 09:00:00,6,25.335321250670564,24.775598712206804,59.19247848696108 +2024-10-15 10:00:00,6,31.041327128119214,25.948284789055243,56.12122764619916 +2024-10-15 11:00:00,6,24.307235799971973,23.23725604166944,71.8088077380043 +2024-10-15 12:00:00,6,47.520628989286664,22.67329270468492,70.4128498655504 +2024-10-15 13:00:00,6,41.858136464135576,16.47930051400507,52.01996131180559 +2024-10-15 14:00:00,6,31.22969912287121,21.561529682051113,55.34500132921784 +2024-10-15 15:00:00,6,12.012939776960685,18.9406748766208,50.4613930235512 +2024-10-15 16:00:00,6,29.399853501055773,31.15836451525141,64.15360811175714 +2024-10-15 17:00:00,6,32.10500948452937,21.86838308165278,63.3220663999359 +2024-10-15 18:00:00,6,31.944400890395492,24.432177085106936,60.565515700427135 +2024-10-15 19:00:00,6,22.12408082949822,20.153974685429503,41.811065609007684 +2024-10-15 20:00:00,6,50.28758884851709,18.46048828471907,52.53754751894402 +2024-10-15 21:00:00,6,26.18625248450936,21.56913302134564,51.877313182420835 +2024-10-15 22:00:00,6,19.001425243143004,21.58222274309102,60.433651893102365 +2024-10-15 23:00:00,6,36.64184217212913,27.33294433137054,41.33241692884244 +2024-10-16 00:00:00,6,13.662782153582711,24.538611944231747,41.245672584247544 +2024-10-16 01:00:00,6,22.694354673939575,30.289762654665044,54.073480401012894 +2024-10-16 02:00:00,6,31.096279173482156,24.99915041731806,64.26963339148581 +2024-10-16 03:00:00,6,31.325199066429455,28.546773250008187,49.709231294223024 +2024-10-16 04:00:00,6,3.8580319218931756,25.072105463005183,64.38612176368366 +2024-10-16 05:00:00,6,29.141303086429353,23.037906512664563,53.81781423807228 +2024-10-16 06:00:00,6,26.345339425683658,23.240130312954886,42.57216293747364 +2024-10-16 07:00:00,6,33.82369827522354,23.19054096226012,58.89258432091051 +2024-10-16 08:00:00,6,22.419959592882382,25.30832614497641,51.103862758335886 +2024-10-16 09:00:00,6,47.03758841889792,22.801652012033344,41.23629551203748 +2024-10-16 10:00:00,6,45.37077161037868,26.049748444663585,64.71994027671344 +2024-10-16 11:00:00,6,16.93452332663292,19.46874563227103,64.70512508128769 +2024-10-16 12:00:00,6,41.33165802759819,24.54746103546151,40.30247405449724 +2024-10-16 13:00:00,6,24.682339266532637,23.835759748928645,40.64148650078101 +2024-10-16 14:00:00,6,23.560895488561066,28.116091227168475,67.1598672686333 +2024-10-16 15:00:00,6,25.98620391603629,17.855403247297847,43.87804717473562 +2024-10-16 16:00:00,6,23.711341984527028,21.234723173104122,55.783897313321255 +2024-10-16 17:00:00,6,22.946276964951405,21.63813852757769,70.2427021629785 +2024-10-16 18:00:00,6,40.47676545917395,23.239725679465188,68.99985705841316 +2024-10-16 19:00:00,6,20.658466716349658,25.654317907791867,46.0360152576941 +2024-10-16 20:00:00,6,21.248273547813323,25.562449246214104,49.789104627186276 +2024-10-16 21:00:00,6,11.655039664538837,23.2489337307331,47.02886652612427 +2024-10-16 22:00:00,6,20.82027590737998,22.39491787666816,50.88507122891917 +2024-10-16 23:00:00,6,35.48406705797655,16.99420476888799,64.04433727266014 +2024-10-17 00:00:00,6,8.506343948959824,20.08093741356958,66.0207141138473 +2024-10-17 01:00:00,6,34.59579871564748,19.833919605875863,52.329991921792555 +2024-10-17 02:00:00,6,0.6010755748278989,18.475193212850286,76.03068810522909 +2024-10-17 03:00:00,6,22.339331337334436,17.243692796985147,64.69038298658515 +2024-10-17 04:00:00,6,10.653684464604813,26.0034155835895,51.797731811805484 +2024-10-17 05:00:00,6,27.705409547975965,21.695350028992937,46.1792317230683 +2024-10-17 06:00:00,6,12.937149149976177,21.186660220613255,66.25202570516215 +2024-10-17 07:00:00,6,22.751405057540246,21.191626448064532,63.09632092136354 +2024-10-17 08:00:00,6,35.87112535895741,23.828359077320467,61.364689797970584 +2024-10-17 09:00:00,6,27.09602557209678,25.279974940043427,63.03773044184053 +2024-10-17 10:00:00,6,50.20238472923151,22.65500859213028,66.09829659210189 +2024-10-17 11:00:00,6,26.116384131047482,23.24793705209531,69.11591078036393 +2024-10-17 12:00:00,6,34.50797753245481,26.138685100977764,48.3470376297661 +2024-10-17 13:00:00,6,24.734652085445955,21.370672959254442,50.95951501069378 +2024-10-17 14:00:00,6,41.3430631324215,21.507300442738007,51.60873109045967 +2024-10-17 15:00:00,6,26.715968200105007,23.87940247270441,61.1980473172866 +2024-10-17 16:00:00,6,27.831490911427842,22.055743419000294,47.008416174233716 +2024-10-17 17:00:00,6,34.948029201569256,22.40314938910545,59.27752556375972 +2024-10-17 18:00:00,6,33.479483957457994,21.51087646482899,40.59637135996799 +2024-10-17 19:00:00,6,15.184834231228152,19.22625016353012,53.55368943188728 +2024-10-17 20:00:00,6,15.726509625029497,21.379596203073042,67.33542408798652 +2024-10-17 21:00:00,6,28.224938477880205,24.825570345432137,61.88129875166755 +2024-10-17 22:00:00,6,17.205725835735002,22.405422555437656,59.41114250701834 +2024-10-17 23:00:00,6,28.55465541598841,26.25493149166449,36.50362486635322 +2024-10-18 00:00:00,6,22.114287964445847,19.71420693673136,56.87896644140335 +2024-10-18 01:00:00,6,26.088153401500307,18.900983079144304,60.87020552913319 +2024-10-18 02:00:00,6,18.847551652909317,23.124298632259062,57.2804873586472 +2024-10-18 03:00:00,6,19.19504871987562,25.16575770845982,50.47074846480413 +2024-10-18 04:00:00,6,15.318273171105595,24.59960011801395,41.11474766574841 +2024-10-18 05:00:00,6,18.985316258259964,21.030177409790827,56.21361599789452 +2024-10-18 06:00:00,6,18.372767828698198,25.378956841903232,33.86109121738491 +2024-10-18 07:00:00,6,15.744998919914796,20.797954803794717,59.562180223189245 +2024-10-18 08:00:00,6,36.14015651017292,21.703350561180738,60.992871410208174 +2024-10-18 09:00:00,6,19.76174814899387,25.562153273867107,42.156223785020686 +2024-10-18 10:00:00,6,42.91933938295177,26.827867489805985,53.30625810890885 +2024-10-18 11:00:00,6,38.27021808410377,26.02939843667896,64.50412239233653 +2024-10-18 12:00:00,6,12.3346685877534,25.174428302057912,38.104182191741735 +2024-10-18 13:00:00,6,22.015683401668934,20.475720108431748,40.32741393912411 +2024-10-18 14:00:00,6,25.946620860229608,24.22811498824066,64.54084593348111 +2024-10-18 15:00:00,6,40.336160153591514,25.414318020429903,59.51104783763936 +2024-10-18 16:00:00,6,38.07155705938719,25.885693837535698,69.66330405635064 +2024-10-18 17:00:00,6,24.9999800550943,18.507885132479288,71.5236420455546 +2024-10-18 18:00:00,6,33.090880831237,15.082948849403415,54.79853861208085 +2024-10-18 19:00:00,6,18.570141317794004,20.389340735553446,57.028188607660994 +2024-10-18 20:00:00,6,20.354756565540956,22.247490880332133,61.897276602099986 +2024-10-18 21:00:00,6,23.838489724093723,20.46504533292852,47.44451555358007 +2024-10-18 22:00:00,6,28.89816584565483,25.50036271701654,62.516631904517716 +2024-10-18 23:00:00,6,27.016061239604024,20.917736092209122,45.66334569016611 +2024-10-19 00:00:00,6,19.458103541274827,25.18570680934711,60.58835513016133 +2024-10-19 01:00:00,6,43.43979839060968,23.047067555663084,59.22619804176158 +2024-10-19 02:00:00,6,27.738635907616302,24.540984952643132,70.81597960306961 +2024-10-19 03:00:00,6,23.068906393476553,25.684776797229055,69.83711993527179 +2024-10-19 04:00:00,6,24.668099191233313,26.0801067297113,47.15067821843704 +2024-10-19 05:00:00,6,15.223195461223503,21.248309652659902,60.53785433439516 +2024-10-19 06:00:00,6,19.33200053590423,13.726041786872738,57.40109526947107 +2024-10-19 07:00:00,6,8.325414903135457,22.22444696525931,62.15147616252301 +2024-10-19 08:00:00,6,33.41165096776018,28.12477934550337,73.11126773123175 +2024-10-19 09:00:00,6,43.47115613131288,23.479182461570797,61.67351718984043 +2024-10-19 10:00:00,6,23.29383836557917,25.50736562541953,67.6071208703599 +2024-10-19 11:00:00,6,26.142258447681424,23.76353197920088,62.517035331094284 +2024-10-19 12:00:00,6,20.832360665081154,20.149011131471074,38.852444452377625 +2024-10-19 13:00:00,6,28.777400507475928,22.18253109552554,60.25005755295942 +2024-10-19 14:00:00,6,40.05741643327629,25.700489567660064,57.96691653400107 +2024-10-19 15:00:00,6,33.0399819856122,20.507207421351882,46.12396810095082 +2024-10-19 16:00:00,6,26.430112938060397,24.714239281054716,62.202076963617095 +2024-10-19 17:00:00,6,36.211611815401284,23.438449005964056,54.506122736396094 +2024-10-19 18:00:00,6,15.391802203471162,24.295886839859442,66.7718438717151 +2024-10-19 19:00:00,6,23.80826028992648,22.091617489342394,74.8062427394363 +2024-10-19 20:00:00,6,22.2445183122751,22.621135902066047,51.17863180041902 +2024-10-19 21:00:00,6,9.034406626571347,25.68715978182392,59.95402982218914 +2024-10-19 22:00:00,6,13.4485236687707,21.43864698370924,50.94650599192315 +2024-10-19 23:00:00,6,13.261870236404194,24.610617175792317,52.90145787540809 +2024-10-20 00:00:00,6,24.33482481967958,23.01027196142652,45.782046689092724 +2024-10-20 01:00:00,6,14.12085942021567,29.874812753643642,63.8642060189795 +2024-10-20 02:00:00,6,7.017475872524534,22.356433147146117,61.18909855788715 +2024-10-20 03:00:00,6,36.204212332678125,30.075060569405178,67.78217314290808 +2024-10-20 04:00:00,6,19.039046300115675,35.531870038982866,51.91004346434556 +2024-10-20 05:00:00,6,10.415370765935059,22.318469680262893,63.8570147671372 +2024-10-20 06:00:00,6,24.955047350715923,19.23986554262489,50.376430599601015 +2024-10-20 07:00:00,6,24.114088262134235,24.994100904374932,69.31272060674146 +2024-10-20 08:00:00,6,27.677218136951566,20.054241720747832,53.114311231429696 +2024-10-20 09:00:00,6,25.47092863407355,21.142795762970028,61.69600965014435 +2024-10-20 10:00:00,6,21.21807115124563,24.289681384415253,53.383890770646474 +2024-10-20 11:00:00,6,15.749995951960868,25.119611835740773,39.31019973493901 +2024-10-20 12:00:00,6,20.90091986287962,23.390846935161207,47.88327667646359 +2024-10-20 13:00:00,6,14.353223485554674,18.842965150084567,66.04869820895759 +2024-10-20 14:00:00,6,11.016953068964167,20.04773526743078,46.43510604808097 +2024-10-20 15:00:00,6,36.585559230742206,25.79625732300176,49.7263742867668 +2024-10-20 16:00:00,6,14.0378231080213,22.876003470568733,55.22068020711656 +2024-10-20 17:00:00,6,27.10220291951228,26.060839443815436,57.65507382114841 +2024-10-20 18:00:00,6,36.691158467445064,26.11036659172379,58.60355895489209 +2024-10-20 19:00:00,6,26.727518444985964,25.64705912757001,62.81345966673458 +2024-10-20 20:00:00,6,15.846263435108902,22.16576647741342,56.594448210783106 +2024-10-20 21:00:00,6,25.49109544166187,24.89340472309638,57.8929822333871 +2024-10-20 22:00:00,6,26.307705440074457,24.613912386308872,64.18445109868294 +2024-10-20 23:00:00,6,28.900518736943148,26.684697389108358,57.29112831038208 +2024-10-21 00:00:00,6,13.298903082010469,19.04498599643806,58.346170824266686 +2024-10-21 01:00:00,6,16.41062153808331,19.290953353223465,64.20621594219466 +2024-10-21 02:00:00,6,22.305048324188405,27.56672682778979,59.36221067730033 +2024-10-21 03:00:00,6,17.438144944452173,17.058551140817393,68.59605718214733 +2024-10-21 04:00:00,6,8.143193872516314,16.976646347899255,42.618862489838264 +2024-10-21 05:00:00,6,27.878148200194175,20.394631468481272,54.13853109487323 +2024-10-21 06:00:00,6,27.542504787211627,30.503859925665605,52.49244877373028 +2024-10-21 07:00:00,6,5.893135015317661,29.050980642130902,64.91483443241317 +2024-10-21 08:00:00,6,32.16302967977537,29.048638380112685,75.13407223420836 +2024-10-21 09:00:00,6,37.27238828956832,26.327820312239083,45.39049189814964 +2024-10-21 10:00:00,6,30.20603611164035,23.112445017158525,39.0582164641377 +2024-10-21 11:00:00,6,33.189353474026646,27.210034368840443,55.71003530940925 +2024-10-21 12:00:00,6,45.45649354109034,24.637722760212935,64.8965984861876 +2024-10-21 13:00:00,6,27.054508013347114,24.11104176502896,57.09853144883837 +2024-10-21 14:00:00,6,22.431346718935885,21.912946201938304,57.32023802664772 +2024-10-21 15:00:00,6,31.80027880383316,17.370461978449153,48.27317186308145 +2024-10-21 16:00:00,6,37.32754675010982,22.234600670591057,51.327678734322184 +2024-10-21 17:00:00,6,15.606983595845552,22.649576035804763,55.15747936292147 +2024-10-21 18:00:00,6,26.543146575572663,27.574751513313846,58.45072015550909 +2024-10-21 19:00:00,6,24.599191435971136,20.753538891107425,72.09646796251766 +2024-10-21 20:00:00,6,19.20492276082725,22.943342088035738,64.68379436616993 +2024-10-21 21:00:00,6,22.46488666166511,28.9490851129971,57.43427506398131 +2024-10-21 22:00:00,6,16.136576335057264,21.696886702138507,66.70014679957394 +2024-10-21 23:00:00,6,25.612409609330484,21.04527817971483,52.983747560435006 +2024-10-22 00:00:00,6,30.683029218339733,15.431585287780695,25.224768178511376 +2024-10-22 01:00:00,6,34.784281273552594,24.065929216824337,57.84341196466924 +2024-10-22 02:00:00,6,6.0604569220808795,22.262189396409497,56.66296474629401 +2024-10-22 03:00:00,6,18.235014435865406,19.143933382504986,51.54805487393998 +2024-10-22 04:00:00,6,13.346777354120078,20.274347413011142,62.24636977570995 +2024-10-22 05:00:00,6,23.039300330102623,30.917646469860347,36.541715105391 +2024-10-22 06:00:00,6,14.003313384803807,22.50861551421508,52.96405167963281 +2024-10-22 07:00:00,6,18.562641354556387,29.470635289728488,60.01561038855217 +2024-10-22 08:00:00,6,40.343563216790514,22.228647723213165,54.89128216028349 +2024-10-22 09:00:00,6,35.485368618142964,22.69288929582485,43.185723592888635 +2024-10-22 10:00:00,6,18.376008155530688,29.790692506564863,61.02500706273589 +2024-10-22 11:00:00,6,8.022436086355949,20.21416183667514,44.65519090173596 +2024-10-22 12:00:00,6,26.0821244574498,24.56761999449152,63.12791071673594 +2024-10-22 13:00:00,6,31.408112644183962,22.49978774504512,59.502369509591254 +2024-10-22 14:00:00,6,23.560613780949062,17.445129299971793,60.458737168353444 +2024-10-22 15:00:00,6,28.11268914877452,20.929675332353693,48.62030454984381 +2024-10-22 16:00:00,6,49.41434310431753,21.337074275527602,69.63737673299484 +2024-10-22 17:00:00,6,18.237861884312657,22.61662771852993,65.75882008575951 +2024-10-22 18:00:00,6,30.114829108953572,22.594704528184728,39.54678524693121 +2024-10-22 19:00:00,6,35.95458138946793,21.613364848716998,58.149958606911326 +2024-10-22 20:00:00,6,37.78940532606521,26.62920196869657,60.307327257403074 +2024-10-22 21:00:00,6,28.116590495251856,25.925342937754177,40.66806109425464 +2024-10-22 22:00:00,6,50.22432118601753,24.456353974299184,64.4483097374973 +2024-10-22 23:00:00,6,31.077105117164862,23.67396250876617,59.75947365996619 +2024-10-23 00:00:00,6,17.996254184263844,21.515341575774766,61.05004089561869 +2024-10-23 01:00:00,6,8.162719585666066,23.19422049170323,42.17626463460461 +2024-10-23 02:00:00,6,18.096202328956558,19.89108259697122,46.01411192947663 +2024-10-23 03:00:00,6,16.668639275878004,21.01988621213804,40.511020603304495 +2024-10-23 04:00:00,6,10.499993727224624,22.48347195929537,67.20587873112093 +2024-10-23 05:00:00,6,13.358837117029626,26.84617800474568,55.265998759179965 +2024-10-23 06:00:00,6,13.853087962569136,22.924384573459207,47.00050714895711 +2024-10-23 07:00:00,6,41.49445795347029,24.511138329570326,74.85367624298814 +2024-10-23 08:00:00,6,26.00412345885973,22.45021727608201,56.8054857156107 +2024-10-23 09:00:00,6,32.85769432354377,20.869728422793152,53.02291588344741 +2024-10-23 10:00:00,6,15.812635891032873,19.150385227073073,56.76321044402407 +2024-10-23 11:00:00,6,27.475688159747882,19.589866293222244,65.22210225532446 +2024-10-23 12:00:00,6,28.220646189574232,15.31002605327919,53.909659062662264 +2024-10-23 13:00:00,6,31.50595833749469,29.233284516474868,62.3106035834303 +2024-10-23 14:00:00,6,27.831326937364484,25.5330698308704,51.879520192314885 +2024-10-23 15:00:00,6,24.27325107683154,20.80621991694475,43.47876633481083 +2024-10-23 16:00:00,6,24.22806156534076,21.737299642241744,64.89747405012852 +2024-10-23 17:00:00,6,16.0633833100525,23.649124334867704,35.98834882548324 +2024-10-23 18:00:00,6,29.782241196751773,22.516707323981862,55.21424429002006 +2024-10-23 19:00:00,6,22.956668422268475,29.91769438231424,44.86729392338621 +2024-10-23 20:00:00,6,25.75964189665179,17.936824589050826,55.95835094298829 +2024-10-23 21:00:00,6,16.46477338244921,20.320249761723282,59.68354931614245 +2024-10-23 22:00:00,6,23.821833879881247,26.542699536511915,48.161154377604845 +2024-10-23 23:00:00,6,26.18095175499294,18.90391096639658,65.22992985721721 +2024-10-24 00:00:00,6,34.006440310748665,25.864131368057368,57.38503881556015 +2024-10-24 01:00:00,6,14.957170537552125,11.239868094589978,55.30487293716639 +2024-10-24 02:00:00,6,28.920063037651865,22.20373897329681,46.554650644604386 +2024-10-24 03:00:00,6,22.939333427277926,21.36190818007731,57.370863842613744 +2024-10-24 04:00:00,6,35.40561303162727,22.949544941056793,57.9680810552615 +2024-10-24 05:00:00,6,27.931350860508395,25.056028723155002,57.43283078290617 +2024-10-24 06:00:00,6,30.055437289428884,26.69879618986952,52.47342345603204 +2024-10-24 07:00:00,6,24.370884217270177,20.742707745406108,74.24156991398942 +2024-10-24 08:00:00,6,8.190937045650116,26.61296405267916,70.7087458442796 +2024-10-24 09:00:00,6,29.899674594142574,22.29652077930216,52.49143493854752 +2024-10-24 10:00:00,6,24.311121310959425,22.803174289901744,53.453242712620565 +2024-10-24 11:00:00,6,40.05128457623979,18.53786688617407,46.88362806498134 +2024-10-24 12:00:00,6,30.277579653756426,28.292866772838863,42.73893751772294 +2024-10-24 13:00:00,6,29.565257304134377,20.089557957118316,65.16542045175319 +2024-10-24 14:00:00,6,32.7190996089026,18.267655268391927,41.83484668680691 +2024-10-24 15:00:00,6,31.117982482612167,20.663467913560034,40.60522447424788 +2024-10-24 16:00:00,6,28.271913844224688,18.96500552598043,57.8373490580211 +2024-10-24 17:00:00,6,20.717663450864027,21.68754120348779,42.480895439666334 +2024-10-24 18:00:00,6,27.990792789333348,22.702838329790282,58.24119300002876 +2024-10-24 19:00:00,6,14.363427504881138,25.800486819012065,52.41424832074808 +2024-10-24 20:00:00,6,14.707813493311653,22.66851045262498,55.012375828372285 +2024-10-24 21:00:00,6,8.607032390316427,23.75839226562354,56.457760285358475 +2024-10-24 22:00:00,6,27.53286076858146,22.88137879950985,64.58661644437954 +2024-10-24 23:00:00,6,31.48716404318846,27.684496990726693,67.21172111625238 +2024-10-25 00:00:00,6,20.558048026290418,20.14237515292925,60.362043753633145 +2024-10-25 01:00:00,6,16.697082587939917,30.261093524562966,58.28877317452086 +2024-10-25 02:00:00,6,8.242016173059923,25.345333464332754,40.772321801130964 +2024-10-25 03:00:00,6,6.75376665027051,24.699698769838708,66.72017036525526 +2024-10-25 04:00:00,6,25.237840723044933,20.43384468560733,51.83229432783623 +2024-10-25 05:00:00,6,15.237681034113052,23.56267175906307,60.88863661691883 +2024-10-25 06:00:00,6,27.20959088189999,24.295564028234935,55.36948662464102 +2024-10-25 07:00:00,6,43.714881907357,23.403720962150903,58.16180784549336 +2024-10-25 08:00:00,6,29.0726741513134,22.711310789782818,49.77354842579609 +2024-10-25 09:00:00,6,23.96253206107741,22.837719800944623,55.993936666545494 +2024-10-25 10:00:00,6,20.887010807265124,25.486229062769493,57.18439473348829 +2024-10-25 11:00:00,6,15.865419761108216,23.1198431173439,59.21951202159886 +2024-10-25 12:00:00,6,30.754033584855144,25.18550940177402,31.99022354371892 +2024-10-25 13:00:00,6,30.598478293538225,23.055791272831044,50.64301128677173 +2024-10-25 14:00:00,6,29.417752016703858,24.133359273651156,55.09611427928476 +2024-10-25 15:00:00,6,24.5380713145833,24.559218796637566,56.17419693168364 +2024-10-25 16:00:00,6,21.529743602951175,24.77756398795535,45.70157492584815 +2024-10-25 17:00:00,6,21.99147059654622,24.053085572243724,41.493771858159754 +2024-10-25 18:00:00,6,29.766998990058067,24.560288366545063,49.72524825329539 +2024-10-25 19:00:00,6,32.68915202252336,22.652333890407796,60.24807024053202 +2024-10-25 20:00:00,6,35.09923929896411,16.184283620055638,48.623470763823974 +2024-10-25 21:00:00,6,16.448668521599636,17.97140431189202,75.49443289666402 +2024-10-25 22:00:00,6,12.676462068513345,32.6154856829448,57.41769224976597 +2024-10-25 23:00:00,6,25.026521160236204,23.42784996004201,43.975576636340904 +2024-10-26 00:00:00,6,13.596852269869961,21.3057061553772,50.339341370664336 +2024-10-26 01:00:00,6,25.95146474448952,27.5099057838037,49.99458065639873 +2024-10-26 02:00:00,6,27.822386514001572,18.37205841294273,52.946554745718934 +2024-10-26 03:00:00,6,32.76919174783136,19.709419013645153,49.620572843165995 +2024-10-26 04:00:00,6,34.70626916568512,22.55768541213258,46.32741634771168 +2024-10-26 05:00:00,6,17.45121275783462,23.919055508370167,67.11503927161905 +2024-10-26 06:00:00,6,20.066834480670572,28.25989350622261,55.367429675992746 +2024-10-26 07:00:00,6,10.861463448876403,21.339601817758858,68.23947202357641 +2024-10-26 08:00:00,6,22.705175234686656,20.466704560566882,58.81229248306709 +2024-10-26 09:00:00,6,34.171757616227,27.290642588057178,53.580537556329986 +2024-10-26 10:00:00,6,17.387231300358334,24.932858270668333,64.39635751581265 +2024-10-26 11:00:00,6,31.3721940807984,24.124670380088645,61.82192562789516 +2024-10-26 12:00:00,6,44.385434939188855,24.82103019158126,56.15302497146374 +2024-10-26 13:00:00,6,44.4343127731467,24.717264206239168,44.448130904146964 +2024-10-26 14:00:00,6,20.079327472921232,18.43654621036801,50.10219570079707 +2024-10-26 15:00:00,6,21.834061282839492,26.664058915151813,52.19484801695762 +2024-10-26 16:00:00,6,17.18073359010681,20.47334734555139,57.478446533205414 +2024-10-26 17:00:00,6,21.23604834933176,23.704686508965917,46.40274189978301 +2024-10-26 18:00:00,6,26.224743656039948,22.659320516698045,51.06159370371402 +2024-10-26 19:00:00,6,10.101508623580617,21.55715778268829,61.63784944168213 +2024-10-26 20:00:00,6,43.21186263547594,26.700195267818792,49.4278137700195 +2024-10-26 21:00:00,6,31.079610600932973,24.889070942555392,62.30135281701085 +2024-10-26 22:00:00,6,28.82428945828235,26.89138910601708,43.227280416304424 +2024-10-26 23:00:00,6,26.37243254382097,22.083115481157957,39.06583672599555 +2024-10-27 00:00:00,6,21.739865094077384,25.484450348726323,61.27851778409472 +2024-10-27 01:00:00,6,11.31069012755098,19.117257216628293,46.46094070408597 +2024-10-27 02:00:00,6,34.113324098464766,20.613268845460357,50.851686466456165 +2024-10-27 03:00:00,6,26.909142225473133,20.7761321688648,50.84296942299246 +2024-10-27 04:00:00,6,31.091188307700364,15.259843375019532,74.99239419945903 +2024-10-27 05:00:00,6,22.639304674664206,25.564695392020003,55.6055105075355 +2024-10-27 06:00:00,6,23.37582854541299,25.10007259759071,49.43991742242086 +2024-10-27 07:00:00,6,26.041684430218808,23.424749725612884,51.73915852478378 +2024-10-27 08:00:00,6,27.861150385099556,25.942990053507007,55.059833227919235 +2024-10-27 09:00:00,6,47.14513033019033,30.486397807710247,49.1101725864128 +2024-10-27 10:00:00,6,40.327398657149644,31.005806037130274,43.1682039257562 +2024-10-27 11:00:00,6,28.224743595080877,24.943452317701443,42.456391121194855 +2024-10-27 12:00:00,6,22.36178834467541,29.154287149091957,58.82048084473841 +2024-10-27 13:00:00,6,30.269031247070703,25.321373199631832,33.23887594588386 +2024-10-27 14:00:00,6,39.129229689825365,18.02672403051379,57.60317025045621 +2024-10-27 15:00:00,6,23.891019214433506,17.757919742158773,44.900646966260986 +2024-10-27 16:00:00,6,26.95266546096786,23.18461663422572,46.71261965821165 +2024-10-27 17:00:00,6,24.841207742189575,27.845300549689767,51.62798647782056 +2024-10-27 18:00:00,6,26.0811423701614,20.84144550252539,47.58497165261195 +2024-10-27 19:00:00,6,32.242487593844345,22.989268814563044,62.49428641603236 +2024-10-27 20:00:00,6,30.878514062824994,18.001746187298377,75.07689470136708 +2024-10-27 21:00:00,6,18.610496224893616,17.75630565288378,41.75012163836576 +2024-10-27 22:00:00,6,18.758175529786897,24.14326819596895,49.420082621107234 +2024-10-27 23:00:00,6,33.59428815161565,27.10099251621911,55.87235341288713 +2024-10-28 00:00:00,6,10.734803914414902,15.542302496138774,64.58850080257174 +2024-10-28 01:00:00,6,21.134972216928734,18.29326647226943,57.77679279949739 +2024-10-28 02:00:00,6,8.11939256538463,17.98163135754666,58.37256404035206 +2024-10-28 03:00:00,6,5.1326574859945815,18.371890576107855,54.49744918017655 +2024-10-28 04:00:00,6,29.315756164883588,22.961653026273748,47.915110248438026 +2024-10-28 05:00:00,6,20.87726267106182,30.834159160925115,36.354872349356114 +2024-10-28 06:00:00,6,25.877826488904617,24.528794563489704,52.92001771241703 +2024-10-28 07:00:00,6,27.927260469395815,23.455842409107905,56.03666131948767 +2024-10-28 08:00:00,6,32.38147765984773,18.237650213374845,52.31415626873729 +2024-10-28 09:00:00,6,36.61918530246845,26.254731057418716,49.99344429919336 +2024-10-28 10:00:00,6,33.77710965127518,25.26659936413205,63.07406192323676 +2024-10-28 11:00:00,6,25.978253666846957,20.08467796626746,60.58101887184473 +2024-10-28 12:00:00,6,4.266380383649825,22.049890826869042,48.428351242153624 +2024-10-28 13:00:00,6,17.75900880399534,24.7761075431439,46.54682792579411 +2024-10-28 14:00:00,6,36.809544289025986,29.131464841809894,52.558831044447004 +2024-10-28 15:00:00,6,22.278465197728192,24.070121538924038,46.0481520114573 +2024-10-28 16:00:00,6,37.11143937606904,23.912503066059138,48.37618416688136 +2024-10-28 17:00:00,6,29.785378514326272,24.818052029542624,49.93768950013213 +2024-10-28 18:00:00,6,31.14064444355896,22.016722298187137,67.60368977986745 +2024-10-28 19:00:00,6,25.80937960182969,26.32045706776839,66.46241661334986 +2024-10-28 20:00:00,6,36.6588144593894,25.07637686362754,67.81727129408729 +2024-10-28 21:00:00,6,33.597155504129944,25.221013999468468,52.57631158805223 +2024-10-28 22:00:00,6,44.29962171190101,20.792696010777895,66.42044321179337 +2024-10-28 23:00:00,6,21.12709102966029,25.16086031763472,58.211785439221316 +2024-10-29 00:00:00,6,16.722149618977767,20.582641024831645,50.60540540071223 +2024-10-29 01:00:00,6,33.99217481194389,18.21948688542529,47.47559240518146 +2024-10-29 02:00:00,6,15.667571846021328,18.472556141046148,53.27352700809002 +2024-10-29 03:00:00,6,10.05660840602,16.85555951047347,47.45106169294678 +2024-10-29 04:00:00,6,31.50430242543081,26.783026451937445,57.902072476605845 +2024-10-29 05:00:00,6,25.064901002105884,21.230444014168704,53.119072697583924 +2024-10-29 06:00:00,6,36.084169232568144,27.319691594047704,51.66658471655663 +2024-10-29 07:00:00,6,14.38349732827279,24.22765033747929,49.45259428292187 +2024-10-29 08:00:00,6,26.657923897551342,27.767097664450638,54.61241453245832 +2024-10-29 09:00:00,6,21.494209402918298,25.94924222473812,47.5314965548983 +2024-10-29 10:00:00,6,38.819436459833554,24.845797272843786,67.27232996241744 +2024-10-29 11:00:00,6,46.42600589271709,22.974755463134485,65.06233933866109 +2024-10-29 12:00:00,6,25.706165827105348,24.61740405450027,50.18558327216833 +2024-10-29 13:00:00,6,16.751535699371004,23.225026747852695,70.41812089203538 +2024-10-29 14:00:00,6,18.316372584871413,25.24312114920148,68.86849489988394 +2024-10-29 15:00:00,6,27.135905492581635,22.748289039590478,63.05620290728186 +2024-10-29 16:00:00,6,16.12202418884018,28.470222502571307,68.12634091537508 +2024-10-29 17:00:00,6,36.244347312084784,28.552011417324266,57.30010413315607 +2024-10-29 18:00:00,6,19.955969500992026,20.185465570615452,33.90622330721812 +2024-10-29 19:00:00,6,16.34003859738897,23.850402942449815,56.365565445514335 +2024-10-29 20:00:00,6,33.61318756663802,24.278264986624546,61.41147660725116 +2024-10-29 21:00:00,6,20.490519255336018,22.22524498350625,70.25998492572066 +2024-10-29 22:00:00,6,26.043355111794412,24.918330267280204,49.02698788701528 +2024-10-29 23:00:00,6,25.064815538873066,20.29799874483534,61.99003018581328 +2024-10-30 00:00:00,6,33.469724343694345,23.351083371098383,38.027234351044356 +2024-10-30 01:00:00,6,19.69724631341065,20.118548221635706,27.328491310643404 +2024-10-30 02:00:00,6,10.317994824129723,25.229463172247,51.3912025536319 +2024-10-30 03:00:00,6,22.157397189122484,21.863441070157634,36.05680054970425 +2024-10-30 04:00:00,6,18.509970120760567,25.332883020492474,62.880368838501425 +2024-10-30 05:00:00,6,10.734182933033841,23.759591053302515,72.39035710611614 +2024-10-30 06:00:00,6,30.28454437046876,24.663273334402124,46.66733241693772 +2024-10-30 07:00:00,6,27.413447143953167,23.603516532139423,61.262014694936994 +2024-10-30 08:00:00,6,34.49804948564224,21.33048113870996,39.33966930887773 +2024-10-30 09:00:00,6,39.62570360089464,26.12928456434333,50.4630173125588 +2024-10-30 10:00:00,6,39.51149422040582,22.892407941667578,42.65488647179442 +2024-10-30 11:00:00,6,31.27188643555674,24.309814496742124,47.0349521368901 +2024-10-30 12:00:00,6,30.10374304413236,22.055814254178156,54.7464810960731 +2024-10-30 13:00:00,6,18.162202953233674,20.78731502102413,61.45591441818605 +2024-10-30 14:00:00,6,35.83776491471913,24.919592819359003,60.32283452272268 +2024-10-30 15:00:00,6,20.880453526220037,21.50551990827114,43.91747501943384 +2024-10-30 16:00:00,6,24.182595062914803,27.556365608128466,53.64095363023459 +2024-10-30 17:00:00,6,21.764606493075902,21.429856034058545,59.730760881516346 +2024-10-30 18:00:00,6,39.97423487134736,22.358525539722486,63.411030786058454 +2024-10-30 19:00:00,6,20.24772789761328,23.84683612350567,59.551242072531515 +2024-10-30 20:00:00,6,22.99107189984086,27.924812412515042,48.701274696853304 +2024-10-30 21:00:00,6,27.1477178044542,23.124424426244836,45.85135777585875 +2024-10-30 22:00:00,6,41.19485846442782,22.94088596464058,67.57894928874624 +2024-10-30 23:00:00,6,21.456158716279262,24.399787357596455,55.28517378229995 +2024-10-31 00:00:00,6,19.93692936564455,25.661299668422387,55.75057636804819 +2024-10-31 01:00:00,6,32.699646042802925,21.441915678738404,36.20807845743033 +2024-10-31 02:00:00,6,25.03383768674768,21.36977827164591,40.74957330253056 +2024-10-31 03:00:00,6,27.6251699045329,23.92013638553419,61.08378146445122 +2024-10-31 04:00:00,6,24.550981251652992,25.166499496964803,56.265941811657875 +2024-10-31 05:00:00,6,15.17618428403755,27.30610236822489,41.882361988207755 +2024-10-31 06:00:00,6,28.449720050990017,21.28669687627123,60.30165889158829 +2024-10-31 07:00:00,6,28.357290943706612,20.763382222928055,51.86029869634335 +2024-10-31 08:00:00,6,27.647998884441478,19.92713126319998,56.66764882180264 +2024-10-31 09:00:00,6,30.26954513822493,32.82063160917272,45.44033791512778 +2024-10-31 10:00:00,6,37.45637868556703,20.950634210789946,49.222001694153306 +2024-10-31 11:00:00,6,13.545405605793741,23.44889173074875,52.579979655981724 +2024-10-31 12:00:00,6,31.40334818512946,25.25008298048511,59.48123936283248 +2024-10-31 13:00:00,6,27.83563816869544,21.984204070423644,59.62834786658574 +2024-10-31 14:00:00,6,14.585479128031391,16.15940311323236,60.72483581970956 +2024-10-31 15:00:00,6,21.239589971571185,27.665270761898615,53.39245074475431 +2024-10-31 16:00:00,6,20.313402012344532,23.94466414160344,59.814196118120535 +2024-10-31 17:00:00,6,11.546918039988538,19.434298689793486,44.18748172580551 +2024-10-31 18:00:00,6,19.64002000800569,22.48577435027337,57.60410650143545 +2024-10-31 19:00:00,6,20.070642759218217,22.660784489636065,50.548955506904335 +2024-10-31 20:00:00,6,29.77943024605364,26.33791797262521,49.37931146126121 +2024-10-31 21:00:00,6,13.90903979063855,24.29771748900257,63.028384165519284 +2024-10-31 22:00:00,6,20.212108093741804,26.688628056912048,33.217555019705856 +2024-10-31 23:00:00,6,29.109885221583852,24.541074537329482,58.86773759809965 +2024-11-01 00:00:00,6,12.014859885190418,23.977934050579904,68.26469204799687 +2024-11-01 01:00:00,6,24.78232851511011,23.05896127167045,55.1524831454156 +2024-11-01 02:00:00,6,19.427512796751444,17.857605225604445,50.73458197401534 +2024-11-01 03:00:00,6,18.77697697024346,28.135748308464233,48.20251675280927 +2024-11-01 04:00:00,6,15.861747579760625,16.912438553729068,73.1549099775173 +2024-11-01 05:00:00,6,21.75701286854315,20.422138251494914,49.77943601289327 +2024-11-01 06:00:00,6,11.247709966295083,25.62496896225791,44.05046552075005 +2024-11-01 07:00:00,6,16.857681055676053,24.80211358916626,53.259379424770025 +2024-11-01 08:00:00,6,19.11667359681296,21.569665227442098,71.59511295792873 +2024-11-01 09:00:00,6,20.117869362557457,16.81806648468999,43.04545311087324 +2024-11-01 10:00:00,6,40.91866638014564,28.549100065438445,52.2833739699651 +2024-11-01 11:00:00,6,24.628983172707695,25.460401418250903,48.61809172718645 +2024-11-01 12:00:00,6,42.289079918145504,25.57516922680828,55.87061261040806 +2024-11-01 13:00:00,6,30.82925271475354,27.528077276992384,63.67037286665856 +2024-11-01 14:00:00,6,29.611481758682075,26.419609591024575,63.686208641618066 +2024-11-01 15:00:00,6,32.97215321974014,27.338055020089126,66.7519011953684 +2024-11-01 16:00:00,6,17.511619834875404,21.750324533072536,46.729592851192734 +2024-11-01 17:00:00,6,24.645922423627482,22.55356828765689,67.92737143279459 +2024-11-01 18:00:00,6,27.049165066615714,24.456422211480117,45.24719721116701 +2024-11-01 19:00:00,6,39.85741475043953,21.855257233889336,50.16276260186889 +2024-11-01 20:00:00,6,17.273054547201927,24.46693328805933,51.40834080412719 +2024-11-01 21:00:00,6,28.538302822828832,20.839654132663462,52.12574699660992 +2024-11-01 22:00:00,6,25.96469332542695,22.068665767640944,52.56926992553179 +2024-11-01 23:00:00,6,28.94688824860079,21.80004897692517,62.05233198602333 +2024-11-02 00:00:00,6,16.870209899628506,19.7958353049062,58.747973519822736 +2024-11-02 01:00:00,6,23.669326558768983,17.972261608934307,73.06657404479903 +2024-11-02 02:00:00,6,26.092070078389032,21.7449493873324,53.87918733762606 +2024-11-02 03:00:00,6,25.11360559915623,23.037280430685094,63.23512102113881 +2024-11-02 04:00:00,6,0.0,24.899656732114234,47.871360468154656 +2024-08-04 05:00:00,7,21.177134802069112,26.09515375647168,48.016200640165316 +2024-08-04 06:00:00,7,18.409565012894372,25.818536283272035,52.40066970624463 +2024-08-04 07:00:00,7,39.288833421323304,23.719684942407643,51.08311228502518 +2024-08-04 08:00:00,7,24.947948040787313,17.14000295473705,49.964254753948865 +2024-08-04 09:00:00,7,12.515440280321345,24.84356051271747,67.06879574644515 +2024-08-04 10:00:00,7,31.96198662336757,22.528047197523332,56.51986938687717 +2024-08-04 11:00:00,7,23.315658779347032,29.59748722299676,47.55373988632642 +2024-08-04 12:00:00,7,29.67079495433792,22.584893213174894,55.18242416853511 +2024-08-04 13:00:00,7,29.768803468023986,23.34526053349851,54.69014670789432 +2024-08-04 14:00:00,7,25.428018743841193,19.86579655945709,55.27631975270262 +2024-08-04 15:00:00,7,39.7759245375526,20.675603636111664,58.56673119150112 +2024-08-04 16:00:00,7,12.314308762863549,23.098649438493243,58.233515724668166 +2024-08-04 17:00:00,7,10.843147682117092,22.733544621008157,63.44924938609349 +2024-08-04 18:00:00,7,24.022033646911943,17.9227233590052,55.0779897105688 +2024-08-04 19:00:00,7,27.951664563820955,17.043597813744142,60.28736776528101 +2024-08-04 20:00:00,7,28.34400895044164,26.932728189502708,55.712233309908235 +2024-08-04 21:00:00,7,23.578889159898086,20.634798358368965,57.91875747587547 +2024-08-04 22:00:00,7,24.41107880602718,24.291653481814457,34.077438117947274 +2024-08-04 23:00:00,7,19.288180279253737,28.33942451747575,42.189506035117425 +2024-08-05 00:00:00,7,13.075777794247596,17.948330042294863,47.89427281260317 +2024-08-05 01:00:00,7,31.22101462303661,24.367018470168475,59.781794552374286 +2024-08-05 02:00:00,7,25.916723209767873,11.336876697914091,57.52968576232953 +2024-08-05 03:00:00,7,9.729629679977299,22.14591493113945,36.65652947881966 +2024-08-05 04:00:00,7,21.902165171331756,21.650225610075207,35.75436178429244 +2024-08-05 05:00:00,7,33.93461843882196,22.725434142316043,37.949062855465925 +2024-08-05 06:00:00,7,6.107792733685972,21.757765118152996,66.29596162109405 +2024-08-05 07:00:00,7,14.57989049559388,25.03711613696579,43.691803233117675 +2024-08-05 08:00:00,7,35.73219182129724,23.994624785284678,59.97082204151041 +2024-08-05 09:00:00,7,17.507785653341784,18.271595798726814,43.56025538834905 +2024-08-05 10:00:00,7,26.90141408445646,26.128822542521558,68.16904297169337 +2024-08-05 11:00:00,7,30.83667105739283,22.458906279398086,46.72494669447005 +2024-08-05 12:00:00,7,34.26662486293942,22.95842086653184,54.76399144767678 +2024-08-05 13:00:00,7,38.890350175218984,24.856915857624497,53.215501317798285 +2024-08-05 14:00:00,7,18.514083991652374,23.434388170774156,46.63957213704624 +2024-08-05 15:00:00,7,34.64748204428844,22.003679304143308,57.0481566304378 +2024-08-05 16:00:00,7,35.9589314964658,23.864173463159887,45.13621565441264 +2024-08-05 17:00:00,7,34.8494821798918,26.401927067254086,35.11961926661951 +2024-08-05 18:00:00,7,9.846546445421934,20.75439159271768,57.30650996091209 +2024-08-05 19:00:00,7,32.09066095895188,19.66625992088687,50.53872405839083 +2024-08-05 20:00:00,7,30.387142851269353,20.571948331702554,57.041269799120485 +2024-08-05 21:00:00,7,20.717551196984452,13.6358835928662,53.2456920131616 +2024-08-05 22:00:00,7,19.45238874223871,23.464392635327222,57.521268051490814 +2024-08-05 23:00:00,7,7.339201606996438,24.673203871567498,53.15888912793807 +2024-08-06 00:00:00,7,30.224026154490595,26.541962716947847,55.80150764850256 +2024-08-06 01:00:00,7,18.880719822657298,20.006554142505546,49.559313129114194 +2024-08-06 02:00:00,7,35.57582281575746,19.576996355442535,58.15893006327152 +2024-08-06 03:00:00,7,19.72061978396684,27.6598808963929,39.19142083258797 +2024-08-06 04:00:00,7,35.21931597346226,20.9445390434054,32.38867129291775 +2024-08-06 05:00:00,7,26.527248717657613,26.60272443713172,44.46576170762306 +2024-08-06 06:00:00,7,49.84667909580417,20.230853929204912,31.257592546281543 +2024-08-06 07:00:00,7,30.66956701649258,24.672124952457292,46.504788734849846 +2024-08-06 08:00:00,7,42.01265627094132,27.522834780171497,54.930036478034985 +2024-08-06 09:00:00,7,23.573248108879575,22.77647244859623,60.274616909824445 +2024-08-06 10:00:00,7,41.13054682912319,24.697344311821293,40.22301524935675 +2024-08-06 11:00:00,7,25.91539828917083,21.602693833060204,64.7225505688181 +2024-08-06 12:00:00,7,18.704705815138947,24.617789993117235,54.01392164307901 +2024-08-06 13:00:00,7,22.98723335476953,23.984480222508292,43.91296272983165 +2024-08-06 14:00:00,7,29.960710983922525,27.13962391976825,46.1666914760784 +2024-08-06 15:00:00,7,14.338355366348704,23.91444635589857,67.44639166104228 +2024-08-06 16:00:00,7,15.589415399650969,23.61368090197751,52.74629653510527 +2024-08-06 17:00:00,7,27.166410263519506,20.580629473947145,39.141067303093024 +2024-08-06 18:00:00,7,21.470695233723237,21.561150101245115,37.5224618847951 +2024-08-06 19:00:00,7,29.350069711839012,25.386585118118777,48.988589723869914 +2024-08-06 20:00:00,7,37.84367034914166,23.232869744589905,52.00171740937595 +2024-08-06 21:00:00,7,13.46309322274514,20.660117067699836,55.76743001050764 +2024-08-06 22:00:00,7,33.41151965419135,16.051236218438316,50.632151380080565 +2024-08-06 23:00:00,7,18.399536075433964,22.399881239870076,50.37112143611693 +2024-08-07 00:00:00,7,17.096607239959546,22.99089482056918,70.80007767631956 +2024-08-07 01:00:00,7,26.85131817720239,19.225799422315884,48.00594255000765 +2024-08-07 02:00:00,7,27.416583721493367,17.60512060200379,50.68266713246935 +2024-08-07 03:00:00,7,26.776376851823308,22.856434095494475,50.827488434618324 +2024-08-07 04:00:00,7,27.63489045443568,15.691894056708872,37.36589169805742 +2024-08-07 05:00:00,7,28.451942325310583,19.84876807283407,46.956725936191425 +2024-08-07 06:00:00,7,40.23809453341565,20.440602423910832,47.07707827651648 +2024-08-07 07:00:00,7,13.943304353035174,22.888052916686974,44.78288219713476 +2024-08-07 08:00:00,7,8.073179234257179,22.637301464615977,55.100659794259265 +2024-08-07 09:00:00,7,33.243890542759296,27.326892508102063,51.78989172451582 +2024-08-07 10:00:00,7,31.05602494604478,21.557968029701286,46.603331539706176 +2024-08-07 11:00:00,7,30.60491283435593,20.50469136844336,58.89307633364653 +2024-08-07 12:00:00,7,18.543497215757995,18.78391016291206,64.05337610067068 +2024-08-07 13:00:00,7,33.56811312058811,21.157623517959646,50.944861613108564 +2024-08-07 14:00:00,7,17.59881838702622,19.605748623514565,61.72611446693279 +2024-08-07 15:00:00,7,39.07790200424096,24.41084091681297,46.53967867257414 +2024-08-07 16:00:00,7,20.67126676409071,23.577003542596756,47.63902231750972 +2024-08-07 17:00:00,7,25.760806226281993,26.41688392970085,50.107465817426004 +2024-08-07 18:00:00,7,18.135537629968912,18.681793914236355,54.58139840088938 +2024-08-07 19:00:00,7,29.504057282724013,22.261087116555437,51.36192470029886 +2024-08-07 20:00:00,7,22.825001642363908,25.16806086861446,47.54902737242117 +2024-08-07 21:00:00,7,18.742170495052342,24.262359702891168,49.889185818160264 +2024-08-07 22:00:00,7,19.93371156910232,26.43303115267722,65.63276848494709 +2024-08-07 23:00:00,7,16.360175676792913,26.664049854144373,63.0979846970524 +2024-08-08 00:00:00,7,26.209296655940026,18.72996011411014,42.010736394018096 +2024-08-08 01:00:00,7,29.53937881728764,24.598492981684508,51.56661765907729 +2024-08-08 02:00:00,7,17.225813687246387,26.753567887775638,45.386173594759676 +2024-08-08 03:00:00,7,28.903940533913207,16.096366289053375,57.872846851878094 +2024-08-08 04:00:00,7,21.366088957723473,25.610757985691162,55.702462050186085 +2024-08-08 05:00:00,7,23.062714504133805,25.060108354932318,62.908149056014665 +2024-08-08 06:00:00,7,5.909142249377226,20.887823772638367,56.93869985399345 +2024-08-08 07:00:00,7,14.758514450018582,21.47188919842946,51.779312267789685 +2024-08-08 08:00:00,7,39.46148159775112,25.48914237034415,46.1924056298375 +2024-08-08 09:00:00,7,15.449672909535224,29.31803108407857,49.5338784463558 +2024-08-08 10:00:00,7,23.457892728646204,22.36380099977181,50.728707356038726 +2024-08-08 11:00:00,7,28.472096932769897,28.84265125444051,69.91974430145493 +2024-08-08 12:00:00,7,26.98122627112267,18.870115948343404,67.47704095567013 +2024-08-08 13:00:00,7,12.278209151499924,22.71884389982007,66.01033611436932 +2024-08-08 14:00:00,7,34.79426245746558,21.11231350861928,45.580063503116456 +2024-08-08 15:00:00,7,18.186968008719855,17.029759236539352,44.00653614806877 +2024-08-08 16:00:00,7,26.118918359683718,29.925189994330566,43.87951824205114 +2024-08-08 17:00:00,7,21.669803178503,20.796102179333857,46.07098565379522 +2024-08-08 18:00:00,7,32.91221415801764,17.082315784751966,54.0463672083434 +2024-08-08 19:00:00,7,19.248420977545983,17.155225336258756,51.089509318224586 +2024-08-08 20:00:00,7,0.0,25.436728539360928,58.99189669828447 +2024-08-08 21:00:00,7,25.38091532650927,21.316528857275138,61.5149365544092 +2024-08-08 22:00:00,7,26.47586334485961,16.790876709988485,53.24467150395614 +2024-08-08 23:00:00,7,26.1424269596849,21.779965222429023,47.88106240953325 +2024-08-09 00:00:00,7,31.049515644354727,16.438473659584524,34.32221662396081 +2024-08-09 01:00:00,7,27.771373928927915,21.318424359152218,52.91170126360825 +2024-08-09 02:00:00,7,18.603000385318474,26.54519074468594,59.62066057467248 +2024-08-09 03:00:00,7,23.3605567140526,19.72404939893054,55.502829024500166 +2024-08-09 04:00:00,7,37.25166302128354,27.40718995871655,50.617276130538336 +2024-08-09 05:00:00,7,24.194805682166763,27.118959795092366,47.33968190936479 +2024-08-09 06:00:00,7,16.912826438317527,21.2553438379368,51.263101639304516 +2024-08-09 07:00:00,7,13.132632385967357,17.108489122622426,45.582260734436446 +2024-08-09 08:00:00,7,29.394793150018998,23.026637582351285,67.17175167039625 +2024-08-09 09:00:00,7,30.62763681024224,24.644832273077636,70.9622255177682 +2024-08-09 10:00:00,7,19.75776263846553,25.069923284144554,58.66467329649785 +2024-08-09 11:00:00,7,15.484936748662163,20.578731689018827,49.007395985557636 +2024-08-09 12:00:00,7,16.04881369988421,23.466470240775454,41.618654353972445 +2024-08-09 13:00:00,7,22.271297479545172,24.314209782074336,49.35022886471552 +2024-08-09 14:00:00,7,21.2642808046592,23.78135100161236,49.21824111838013 +2024-08-09 15:00:00,7,13.91841328048194,30.624123518992064,42.140419795932786 +2024-08-09 16:00:00,7,16.956970094831696,23.44658524910293,58.93828631841046 +2024-08-09 17:00:00,7,18.566345346977375,20.687362390847444,59.82587981025912 +2024-08-09 18:00:00,7,13.126693406486464,27.93057220435034,46.89091603381773 +2024-08-09 19:00:00,7,48.0269425963392,27.5827169976945,53.20135704136534 +2024-08-09 20:00:00,7,28.192455991636386,21.138710686183963,45.53575133981334 +2024-08-09 21:00:00,7,16.429304782842664,24.093470427129255,56.76354511435168 +2024-08-09 22:00:00,7,31.98593238818833,23.784410165163248,46.107916629869166 +2024-08-09 23:00:00,7,17.358987425420203,27.479762569217563,51.705510629837576 +2024-08-10 00:00:00,7,22.840620351195213,24.909867922942066,44.921366465042645 +2024-08-10 01:00:00,7,15.96495882073846,25.155375526598277,41.53405551253916 +2024-08-10 02:00:00,7,19.016631422208764,22.184296063901044,41.46143880528502 +2024-08-10 03:00:00,7,33.895613034504336,26.512503509918837,54.881999994223975 +2024-08-10 04:00:00,7,32.57583481657612,15.140602246653529,41.44322825546366 +2024-08-10 05:00:00,7,26.44831271127919,16.377306115090626,44.62770085364356 +2024-08-10 06:00:00,7,25.295653272561932,23.816270693398046,45.72108978884221 +2024-08-10 07:00:00,7,22.85147789629035,22.780499624617978,61.11261887403929 +2024-08-10 08:00:00,7,21.04934194623447,26.52049884493099,55.831611255266175 +2024-08-10 09:00:00,7,8.189098705040116,25.983349258990742,45.08221286040484 +2024-08-10 10:00:00,7,22.687546175092503,25.929405856471746,55.79355267570315 +2024-08-10 11:00:00,7,33.08215325076433,22.654031260153484,55.020469487357325 +2024-08-10 12:00:00,7,20.329385898844755,33.40168739983678,73.23457712589115 +2024-08-10 13:00:00,7,19.882101467135474,21.646577434811004,45.84479175165693 +2024-08-10 14:00:00,7,32.41779990508019,25.936257820515074,35.69907754431027 +2024-08-10 15:00:00,7,31.92278352445988,18.72964855169841,55.993425583364804 +2024-08-10 16:00:00,7,17.966715010139673,25.693572905920448,58.87127303028852 +2024-08-10 17:00:00,7,28.323682442439676,26.61874835267886,81.04095353963069 +2024-08-10 18:00:00,7,25.181125726678413,18.47065733592209,44.45879664333117 +2024-08-10 19:00:00,7,36.90976344185928,21.428933748028335,41.24367967683816 +2024-08-10 20:00:00,7,19.847060413994427,22.51806062128358,48.754581351062185 +2024-08-10 21:00:00,7,19.217172454942567,20.928302049275068,54.21171124261413 +2024-08-10 22:00:00,7,6.433212028335253,14.775066531541583,63.19200325606484 +2024-08-10 23:00:00,7,35.699336593901435,25.931484992858095,73.21444610128079 +2024-08-11 00:00:00,7,37.81192770730564,27.544327532394657,54.71923247952909 +2024-08-11 01:00:00,7,26.14900351642516,22.241019992470875,37.33526927776742 +2024-08-11 02:00:00,7,14.47789890733066,18.627030561082982,60.74042700164623 +2024-08-11 03:00:00,7,0.0,20.065363942069997,41.97720384181608 +2024-08-11 04:00:00,7,32.52621163290251,25.17983450683953,54.34656729415046 +2024-08-11 05:00:00,7,29.385345996230335,22.653914881427674,62.002287423640595 +2024-08-11 06:00:00,7,20.087674093444697,29.946366233829636,53.947546054968306 +2024-08-11 07:00:00,7,14.262262418792771,20.973914731662255,41.62319681054928 +2024-08-11 08:00:00,7,37.20917234736933,20.057390009248667,35.089827761465344 +2024-08-11 09:00:00,7,26.02469076716686,21.807931095446712,48.14212696407068 +2024-08-11 10:00:00,7,25.698185137955125,29.145487706875105,48.61564410344495 +2024-08-11 11:00:00,7,27.392205720840526,29.100572871505367,67.79463012323197 +2024-08-11 12:00:00,7,20.731490354349273,25.194764297795686,55.237961998717644 +2024-08-11 13:00:00,7,21.31889530428687,26.490992364016336,57.61982051052341 +2024-08-11 14:00:00,7,20.82442343335703,28.30176323722357,42.450200475957004 +2024-08-11 15:00:00,7,13.635807044012363,24.2964605669956,62.79474033643268 +2024-08-11 16:00:00,7,7.125978097104841,22.36001192286438,50.97374465977514 +2024-08-11 17:00:00,7,33.36460027364076,26.712390786507363,53.25098447132393 +2024-08-11 18:00:00,7,27.36870158658804,24.915239860932452,60.33530404533445 +2024-08-11 19:00:00,7,30.43999427758505,14.686691251103362,49.70626602630455 +2024-08-11 20:00:00,7,6.415611879092671,18.126966702971902,47.790042185311954 +2024-08-11 21:00:00,7,22.76154741327061,22.15811576697002,58.61209790795425 +2024-08-11 22:00:00,7,22.80824365210497,22.662940749839887,48.56480388009521 +2024-08-11 23:00:00,7,27.738191749064825,24.389983336098123,53.28972258646352 +2024-08-12 00:00:00,7,27.112586806348,20.031851096550916,66.13578125293304 +2024-08-12 01:00:00,7,27.44821012718152,20.259304392263758,38.569627760187124 +2024-08-12 02:00:00,7,28.162772995170776,22.36565563646633,46.63524067763494 +2024-08-12 03:00:00,7,44.97868285959521,19.58839595033978,41.88468044294384 +2024-08-12 04:00:00,7,23.11958643284152,20.21718409162058,61.50148886806374 +2024-08-12 05:00:00,7,42.781859229661535,25.76302142105044,55.14863928562336 +2024-08-12 06:00:00,7,31.781076833465107,28.176344789254212,58.24312593419242 +2024-08-12 07:00:00,7,41.76641582374047,22.885940602095854,48.68739854636611 +2024-08-12 08:00:00,7,9.01520548479185,27.68482475511448,51.253578321450206 +2024-08-12 09:00:00,7,37.70092834312558,27.56978937482538,60.92773557484231 +2024-08-12 10:00:00,7,32.17389867728771,22.793977496055398,51.758303445063156 +2024-08-12 11:00:00,7,15.955929484129479,20.598974573054583,48.171392842757044 +2024-08-12 12:00:00,7,25.623587548767937,21.913212096420953,54.48554792511315 +2024-08-12 13:00:00,7,35.39843550076423,26.60950922025416,67.30030782205128 +2024-08-12 14:00:00,7,18.663760232574305,18.24778733158075,54.48060564304063 +2024-08-12 15:00:00,7,23.408623570271054,26.260389785946725,56.77548496946148 +2024-08-12 16:00:00,7,17.553841594072004,22.68490213909891,50.749811719636405 +2024-08-12 17:00:00,7,13.155192671943594,21.938194844994676,62.89051008312287 +2024-08-12 18:00:00,7,19.022534253166157,21.957230220678312,50.89970474527606 +2024-08-12 19:00:00,7,18.0970359797804,22.208666263468942,38.62423873392278 +2024-08-12 20:00:00,7,15.51772569025577,27.9482078352419,53.60904744558224 +2024-08-12 21:00:00,7,21.74337268501061,23.983626389086144,62.82193048826896 +2024-08-12 22:00:00,7,24.46937707194306,21.6201743436051,42.451130841273994 +2024-08-12 23:00:00,7,23.688159711747524,24.119289509121216,51.40424401178356 +2024-08-13 00:00:00,7,33.585342442959856,24.362162535757786,46.02531100603418 +2024-08-13 01:00:00,7,45.19433447828558,17.422473828785595,45.87847793393748 +2024-08-13 02:00:00,7,6.710833881012583,17.991587267542755,34.32477214285582 +2024-08-13 03:00:00,7,22.369084348809068,20.11688171529414,41.53336225370607 +2024-08-13 04:00:00,7,35.864756881125246,23.687758992102147,63.5665961602665 +2024-08-13 05:00:00,7,17.774981185329942,30.202357776107096,50.25586529972405 +2024-08-13 06:00:00,7,25.600826716417586,19.37510707340896,59.38856553492563 +2024-08-13 07:00:00,7,16.956057084123767,21.369581605584496,45.87386229993738 +2024-08-13 08:00:00,7,26.2179134636685,26.599378373841954,58.55972314092949 +2024-08-13 09:00:00,7,20.40003992962138,24.14899151882374,46.414881001653036 +2024-08-13 10:00:00,7,24.16620002488391,25.532344531328476,48.6925626482036 +2024-08-13 11:00:00,7,7.736648589177577,22.867429437232804,51.93065532284981 +2024-08-13 12:00:00,7,26.821288397453433,19.49676476417208,46.42173133021555 +2024-08-13 13:00:00,7,36.36196098852855,19.31061571493986,45.58867857804115 +2024-08-13 14:00:00,7,22.65492370655684,19.840355022230074,60.26748407874301 +2024-08-13 15:00:00,7,28.922436829292913,20.199313817721734,46.81250541819356 +2024-08-13 16:00:00,7,17.14604515061783,21.499271048792036,62.54807853941442 +2024-08-13 17:00:00,7,5.8894421846303295,19.59789725540244,39.528594273739806 +2024-08-13 18:00:00,7,19.60980285389023,23.18938539399059,55.5550078515669 +2024-08-13 19:00:00,7,18.460446502266034,21.15074925262757,55.53191038242472 +2024-08-13 20:00:00,7,17.034242609161755,18.420541517287376,48.09394529069217 +2024-08-13 21:00:00,7,19.6944060350364,26.986817639325764,67.8059757567302 +2024-08-13 22:00:00,7,29.4441702829993,24.827962418646266,28.93738845479781 +2024-08-13 23:00:00,7,16.481189216837194,23.050007309726176,58.87209361819011 +2024-08-14 00:00:00,7,22.159234191893507,20.043588650042793,56.01943603771333 +2024-08-14 01:00:00,7,16.907039977697348,24.281975484077808,44.950905979605444 +2024-08-14 02:00:00,7,21.933319812193616,27.798505698484725,64.07498993371486 +2024-08-14 03:00:00,7,8.228179194929973,22.964188083473996,53.631121505501255 +2024-08-14 04:00:00,7,36.01289927528583,21.60419747721879,71.78420893732203 +2024-08-14 05:00:00,7,8.199194313396777,19.76817895602613,62.32279886256828 +2024-08-14 06:00:00,7,19.083612755342166,25.25614471737667,44.17093726999864 +2024-08-14 07:00:00,7,19.25496928971692,24.495917363925475,32.60617865806611 +2024-08-14 08:00:00,7,26.42825223856214,29.08520192419074,66.94291602672877 +2024-08-14 09:00:00,7,23.752111318240818,24.401725319838548,48.770619660865236 +2024-08-14 10:00:00,7,28.965957625972585,29.279391033630326,51.81634445152258 +2024-08-14 11:00:00,7,20.368075214550487,31.855423241997997,58.403487257638616 +2024-08-14 12:00:00,7,8.923463921698339,21.324747229930907,49.69767070413049 +2024-08-14 13:00:00,7,28.633266429800486,22.526302404765012,50.661741651576264 +2024-08-14 14:00:00,7,28.521521600588077,24.959816567251483,38.37546770497198 +2024-08-14 15:00:00,7,18.94950810721096,22.5657127759401,54.20662124104732 +2024-08-14 16:00:00,7,19.550570884605566,19.87686470740378,54.13904389500368 +2024-08-14 17:00:00,7,29.737253894865532,22.45454244436774,42.828125634397054 +2024-08-14 18:00:00,7,23.04139472209937,21.807264896478845,54.73113067424164 +2024-08-14 19:00:00,7,28.597103363911664,26.666828973726787,64.98311298865315 +2024-08-14 20:00:00,7,15.36934077163572,16.56599799611584,51.0012836876455 +2024-08-14 21:00:00,7,24.03118861725306,21.10917491019219,45.548672410765064 +2024-08-14 22:00:00,7,39.89164498389169,19.628974532809256,40.25309360329715 +2024-08-14 23:00:00,7,23.5076414079463,32.006195993656135,55.89351256892332 +2024-08-15 00:00:00,7,3.9740469730947403,17.939457795175656,52.63142279368263 +2024-08-15 01:00:00,7,19.599109554752715,17.625084276015716,61.24825343977122 +2024-08-15 02:00:00,7,22.860791136006412,25.085502295953233,51.75206756902645 +2024-08-15 03:00:00,7,24.237319022888595,20.331283760741407,49.54990658871642 +2024-08-15 04:00:00,7,24.264760826493323,24.90765047828391,48.77684742220509 +2024-08-15 05:00:00,7,12.883528358394052,15.575045246667361,46.82712552822034 +2024-08-15 06:00:00,7,24.216766474232266,24.45376030515844,51.38049530191157 +2024-08-15 07:00:00,7,31.384758004583308,23.458464294928763,49.4785299482273 +2024-08-15 08:00:00,7,22.400735329441776,22.626246035971622,58.35595299610709 +2024-08-15 09:00:00,7,28.95913676704425,22.191088810541938,56.200241707779796 +2024-08-15 10:00:00,7,20.158735125876422,25.226461270850677,58.016760007847445 +2024-08-15 11:00:00,7,20.187700352269687,24.561759980719255,70.89067494673196 +2024-08-15 12:00:00,7,23.679951218089947,29.82756459044286,61.92148464459647 +2024-08-15 13:00:00,7,35.909455555856525,26.262407639490707,53.1736257420867 +2024-08-15 14:00:00,7,12.136172116642497,25.02680876848074,52.96740305582939 +2024-08-15 15:00:00,7,12.052349576087888,25.511518368620703,50.453052079946126 +2024-08-15 16:00:00,7,21.11565874706416,20.247992719174867,56.67472069447625 +2024-08-15 17:00:00,7,24.636476084749134,24.153754090273672,47.24440723498405 +2024-08-15 18:00:00,7,31.38990654596835,21.45308636891155,48.096869308412195 +2024-08-15 19:00:00,7,7.268980650900664,23.517863248082513,52.679339985875394 +2024-08-15 20:00:00,7,18.497800527874357,24.118973892348304,51.48740016393873 +2024-08-15 21:00:00,7,24.583991542144183,26.53185136405265,41.48086219937206 +2024-08-15 22:00:00,7,21.559300448057584,27.368006508123056,60.82796519215674 +2024-08-15 23:00:00,7,29.950214821740104,16.702931002459547,70.58713611545221 +2024-08-16 00:00:00,7,16.44241762202319,22.339785524923055,35.3191068679924 +2024-08-16 01:00:00,7,22.81454109667621,19.38761657559717,59.51090165075844 +2024-08-16 02:00:00,7,26.68567343726042,23.9170262884883,31.503167957478404 +2024-08-16 03:00:00,7,26.64487826736049,27.850153407821725,46.84950023257621 +2024-08-16 04:00:00,7,15.29797303799415,22.746699062017985,42.931388266373716 +2024-08-16 05:00:00,7,33.77364070564533,23.559753584961264,52.06629661675672 +2024-08-16 06:00:00,7,9.88442206163176,22.13312943763946,57.59850887592165 +2024-08-16 07:00:00,7,25.06519856531947,26.56908134784884,53.32500422073743 +2024-08-16 08:00:00,7,28.95319637078208,22.192103754343083,56.68532889127114 +2024-08-16 09:00:00,7,6.7413535964537274,25.74752309818324,45.02487989825645 +2024-08-16 10:00:00,7,10.57705556833857,19.430438587211967,45.99751715285458 +2024-08-16 11:00:00,7,0.0,19.763826080746064,39.98205584030911 +2024-08-16 12:00:00,7,24.668155472373744,27.639028813827267,49.9114029697558 +2024-08-16 13:00:00,7,36.17744029327571,29.24246809833663,63.1495341867899 +2024-08-16 14:00:00,7,15.587154411376648,17.938819244390146,51.03052718643357 +2024-08-16 15:00:00,7,28.600300995984405,21.602697387940466,65.4704890518441 +2024-08-16 16:00:00,7,24.082969517926617,22.99538621877121,54.75607907888039 +2024-08-16 17:00:00,7,12.579226620237828,25.1651240129716,48.83026388325637 +2024-08-16 18:00:00,7,14.28552545902881,27.543994425592153,67.10999226356749 +2024-08-16 19:00:00,7,17.62618364234892,20.938400472800215,49.53717621362372 +2024-08-16 20:00:00,7,23.01410028102632,27.485301177780734,43.8457166293466 +2024-08-16 21:00:00,7,15.30073152352602,19.931545878260106,53.531496715457365 +2024-08-16 22:00:00,7,27.888565477664514,28.346583056979167,34.785762159915585 +2024-08-16 23:00:00,7,21.723429183235947,17.963512145776402,57.04322044983738 +2024-08-17 00:00:00,7,32.38237805567599,22.531013127674246,41.909676701805054 +2024-08-17 01:00:00,7,10.98252274767844,19.01320964647741,52.196682376092106 +2024-08-17 02:00:00,7,15.404598717108815,22.347876586420394,61.02832227191437 +2024-08-17 03:00:00,7,16.28312635328828,19.64375431759137,45.10051290253538 +2024-08-17 04:00:00,7,34.187658759639135,22.15917091039599,54.8073525562298 +2024-08-17 05:00:00,7,35.90025698841184,25.7448832464114,41.51677889058471 +2024-08-17 06:00:00,7,20.987462549155424,18.347732682251078,48.24495476306094 +2024-08-17 07:00:00,7,10.877176718211269,25.200041731870858,56.06331614274866 +2024-08-17 08:00:00,7,25.648905659126864,21.252213807501864,62.812869015226894 +2024-08-17 09:00:00,7,23.346536075988933,26.070651970168697,71.23045427357135 +2024-08-17 10:00:00,7,44.278689990725,25.411295567546833,51.20745829754601 +2024-08-17 11:00:00,7,17.352749880950203,26.23841674599675,55.136718592193404 +2024-08-17 12:00:00,7,14.998351413141794,22.022476271644855,55.344265376032794 +2024-08-17 13:00:00,7,10.785936652674323,27.453077051283017,55.72122785641256 +2024-08-17 14:00:00,7,19.022696744329398,22.482798226408406,50.18631210117724 +2024-08-17 15:00:00,7,43.36128201287486,29.072545722078267,45.03533766953508 +2024-08-17 16:00:00,7,27.28179674676204,26.88399283002994,44.239641161335555 +2024-08-17 17:00:00,7,27.07356520476123,22.68380092152382,69.83965486666114 +2024-08-17 18:00:00,7,26.607342674855197,25.684073720715066,59.69947839344172 +2024-08-17 19:00:00,7,4.693737416386028,22.190643074594394,38.003711411639436 +2024-08-17 20:00:00,7,15.819025967385919,23.720323416919666,47.64317208537645 +2024-08-17 21:00:00,7,42.5705911583198,25.33396179142977,59.58123413110249 +2024-08-17 22:00:00,7,18.986364074837613,20.693687925148065,45.84082902142679 +2024-08-17 23:00:00,7,30.37932942336203,22.14048319842096,56.496822308355824 +2024-08-18 00:00:00,7,25.89727568349589,25.345947235410275,41.88745119822152 +2024-08-18 01:00:00,7,19.64854077650015,17.12045070563046,51.34033778310748 +2024-08-18 02:00:00,7,30.971738722402762,21.404516817149066,42.266369806154856 +2024-08-18 03:00:00,7,17.643057047008025,28.172539813276586,61.55373125097646 +2024-08-18 04:00:00,7,17.11392237109589,21.547355042074706,65.66138669294678 +2024-08-18 05:00:00,7,30.883320627258094,23.997581037690036,46.22450347486528 +2024-08-18 06:00:00,7,32.61836767323039,27.84108410172338,44.975030772534815 +2024-08-18 07:00:00,7,12.368370094859786,24.256091456341185,51.498551258274745 +2024-08-18 08:00:00,7,20.983436978318903,27.552123109954778,56.34337851907321 +2024-08-18 09:00:00,7,27.291260764497462,26.680986368132544,60.1758995067241 +2024-08-18 10:00:00,7,12.634920756914918,23.18021120251322,67.31645218330026 +2024-08-18 11:00:00,7,23.094633114188735,24.288716224805075,68.36056185810708 +2024-08-18 12:00:00,7,9.85131405452191,27.56630279867055,58.76625285556994 +2024-08-18 13:00:00,7,22.429582684463085,21.918781377499403,61.87124438303968 +2024-08-18 14:00:00,7,12.774659210608053,31.704967955122488,48.41878448169817 +2024-08-18 15:00:00,7,9.412923622496134,16.250737101417094,63.52541561753548 +2024-08-18 16:00:00,7,24.587327389519196,21.86827045737577,61.13814525160169 +2024-08-18 17:00:00,7,4.0716025268484906,25.412844027300405,57.73594458599938 +2024-08-18 18:00:00,7,28.13455361323067,22.99075547155682,56.0934930409679 +2024-08-18 19:00:00,7,24.712019351335798,19.885565431448363,53.69520084753862 +2024-08-18 20:00:00,7,31.73668552172338,24.57132947125469,62.738420233735475 +2024-08-18 21:00:00,7,28.158304711762568,26.983684124231655,54.07621159204037 +2024-08-18 22:00:00,7,35.59877531825225,26.772575371627184,52.6740020978748 +2024-08-18 23:00:00,7,38.32003421554262,21.199079775728894,56.07173670976343 +2024-08-19 00:00:00,7,23.977817027333398,23.548918785825833,44.15973496956057 +2024-08-19 01:00:00,7,19.900534691479915,17.584879048132727,49.13792947470603 +2024-08-19 02:00:00,7,10.18817540841085,22.94012315422762,49.035073293727464 +2024-08-19 03:00:00,7,11.436698089566729,24.473435870101184,44.83310116648898 +2024-08-19 04:00:00,7,32.64913681159153,26.17254813520956,42.968279493018954 +2024-08-19 05:00:00,7,19.314620056009225,24.073356641082565,64.7034755373902 +2024-08-19 06:00:00,7,19.68987142602571,22.6585822567664,66.88202344515264 +2024-08-19 07:00:00,7,9.324799125461462,15.446846507251434,49.58491212588728 +2024-08-19 08:00:00,7,21.89780859356879,24.185379225892213,61.80467401176077 +2024-08-19 09:00:00,7,11.427617865743569,23.281615926669442,46.29400819688442 +2024-08-19 10:00:00,7,21.145855538662797,24.366091422837222,61.89336987848071 +2024-08-19 11:00:00,7,38.039764339009714,26.5588309078067,49.462927687992504 +2024-08-19 12:00:00,7,22.699889572140037,21.94485854215519,53.62101159541415 +2024-08-19 13:00:00,7,12.819182408402408,22.62932281904541,47.96498864946305 +2024-08-19 14:00:00,7,21.84120997124939,22.8974246420194,58.06956650958333 +2024-08-19 15:00:00,7,24.027224835009477,16.16275726290869,41.25790739689629 +2024-08-19 16:00:00,7,24.22941327619645,27.78612403231542,43.51301944889817 +2024-08-19 17:00:00,7,19.752732948127488,18.085456980531546,45.860585938899185 +2024-08-19 18:00:00,7,23.002470959847667,18.513922381975263,43.47941244494655 +2024-08-19 19:00:00,7,40.26231855302022,13.968940976499617,62.93894888089543 +2024-08-19 20:00:00,7,17.960557028556877,25.701670439938816,29.983002252546378 +2024-08-19 21:00:00,7,26.90993536911117,28.333857391898828,61.72379872410805 +2024-08-19 22:00:00,7,28.496703515325216,23.84946472348532,36.813272217125295 +2024-08-19 23:00:00,7,17.795452372236557,26.170898045125913,48.041114176004044 +2024-08-20 00:00:00,7,23.596783586082033,21.099568178344008,51.5069909091217 +2024-08-20 01:00:00,7,14.50446773762592,17.30600039295914,51.03783864113426 +2024-08-20 02:00:00,7,12.61912383100741,23.102425546994958,57.951638456708224 +2024-08-20 03:00:00,7,26.236033651670397,21.551696653284523,49.27613453374609 +2024-08-20 04:00:00,7,19.503595774820575,29.83982626425329,55.45432986628436 +2024-08-20 05:00:00,7,27.19667542334011,15.344514696111123,62.58065939717916 +2024-08-20 06:00:00,7,27.421855188023745,28.736696175099006,48.433466196625034 +2024-08-20 07:00:00,7,33.09270184054989,19.90412676020666,48.98436184085199 +2024-08-20 08:00:00,7,24.024227127199616,31.670973275636896,58.392397164777634 +2024-08-20 09:00:00,7,28.08446591544677,25.534456026020443,67.64724838107135 +2024-08-20 10:00:00,7,23.503444377761475,17.268577988421548,49.122794045865696 +2024-08-20 11:00:00,7,32.93774416408433,21.155476893699383,61.173322681028466 +2024-08-20 12:00:00,7,17.10456721113037,15.817702199967176,55.78369316557663 +2024-08-20 13:00:00,7,25.362016516201784,25.300588582073242,50.286961630068525 +2024-08-20 14:00:00,7,24.948849571022304,14.349883557680979,53.22413457028284 +2024-08-20 15:00:00,7,25.15688646017238,26.504614269061747,64.02174301452776 +2024-08-20 16:00:00,7,21.85072999324594,28.47483832737577,58.04911149316285 +2024-08-20 17:00:00,7,20.461234207884875,27.69072875876049,64.15622153193785 +2024-08-20 18:00:00,7,11.688020835669006,22.291243245227317,44.75277132301025 +2024-08-20 19:00:00,7,32.07495599761165,21.336895068798114,50.55942837836469 +2024-08-20 20:00:00,7,18.026627767672604,29.082394552614055,40.12615476279292 +2024-08-20 21:00:00,7,8.15532193297827,21.932344615140096,48.21130715132795 +2024-08-20 22:00:00,7,9.959772100638776,26.958739577914734,39.42254917495908 +2024-08-20 23:00:00,7,16.37682647350981,21.239613311974054,53.09820733474736 +2024-08-21 00:00:00,7,14.824507939478135,23.76265117503008,58.28931269742485 +2024-08-21 01:00:00,7,25.319896320192676,24.047505432592025,40.90468107497842 +2024-08-21 02:00:00,7,1.3844326013312909,19.065865075207682,52.26535377030565 +2024-08-21 03:00:00,7,25.875059511392763,23.76428008648264,40.375207041431736 +2024-08-21 04:00:00,7,27.03898164936782,20.82118255273819,44.41304738972927 +2024-08-21 05:00:00,7,33.5041873441216,24.627031207706693,46.67118583249395 +2024-08-21 06:00:00,7,20.581045276090958,20.76012735623213,62.32187928627892 +2024-08-21 07:00:00,7,25.857831048043163,27.091019876694432,42.395226853197386 +2024-08-21 08:00:00,7,33.571466704134906,21.13909541034242,68.92226928727959 +2024-08-21 09:00:00,7,33.42852528962297,20.484276844202526,58.95506423772593 +2024-08-21 10:00:00,7,6.138746529063852,22.670557093764717,58.56298450463555 +2024-08-21 11:00:00,7,26.250789472076292,26.505020752106304,50.457817067607316 +2024-08-21 12:00:00,7,22.239950861501075,24.699722324859476,55.118942879737865 +2024-08-21 13:00:00,7,24.703142196876524,21.62115401682684,57.009561765502724 +2024-08-21 14:00:00,7,34.12370589475374,23.554405470252977,47.68313675206119 +2024-08-21 15:00:00,7,28.740889938652256,19.864938261706712,53.00415053145317 +2024-08-21 16:00:00,7,31.106804115776058,26.1749799880579,52.85988722043466 +2024-08-21 17:00:00,7,16.1415790157554,20.60730848364473,63.47190199551227 +2024-08-21 18:00:00,7,30.56280749546633,23.506057533361464,64.16831243013237 +2024-08-21 19:00:00,7,35.16076169242559,24.12835673477839,60.54989214002178 +2024-08-21 20:00:00,7,25.43619756189465,23.38200854006919,54.13160326655202 +2024-08-21 21:00:00,7,17.35229979773775,17.908848875766136,35.08622714683611 +2024-08-21 22:00:00,7,29.881623936203834,25.37948560564364,42.28040096037712 +2024-08-21 23:00:00,7,18.35984047204919,9.18139901199305,55.63421316419217 +2024-08-22 00:00:00,7,5.555736918422852,22.15738842613997,40.051117684557354 +2024-08-22 01:00:00,7,30.317410807058003,23.763413385967294,49.35746892104203 +2024-08-22 02:00:00,7,19.16426126006597,13.61946776689237,66.98369434121332 +2024-08-22 03:00:00,7,28.256028806193882,24.119979820436267,30.089843032383822 +2024-08-22 04:00:00,7,13.3925836214973,21.189337787976548,45.45582613651769 +2024-08-22 05:00:00,7,22.5753553048795,25.831810215091135,47.77354680100555 +2024-08-22 06:00:00,7,21.959406109468123,24.013747561360436,41.378629858671275 +2024-08-22 07:00:00,7,28.787482037553197,24.02972224580674,49.40120843810022 +2024-08-22 08:00:00,7,35.30553903366316,23.050667781941563,60.58343255182701 +2024-08-22 09:00:00,7,21.978465534861435,25.475265779907,63.809024481648656 +2024-08-22 10:00:00,7,15.385824329170674,27.06936885082195,54.220845099683395 +2024-08-22 11:00:00,7,21.358739228893047,22.166529105285953,48.03757149015845 +2024-08-22 12:00:00,7,27.471434691811282,21.427260785777342,57.2944722681186 +2024-08-22 13:00:00,7,3.9010034438070775,27.878693436913494,57.880941925322645 +2024-08-22 14:00:00,7,26.011695205578324,22.56621019904561,33.631278833871455 +2024-08-22 15:00:00,7,18.120665469464736,16.13170898669651,66.18938139060994 +2024-08-22 16:00:00,7,26.572007893062295,20.75799376708468,52.645251323825875 +2024-08-22 17:00:00,7,7.40794171543202,28.66205602000319,52.07318293125962 +2024-08-22 18:00:00,7,2.300007815660102,13.854709465781928,62.896638549700036 +2024-08-22 19:00:00,7,20.43537803894294,24.23324831345264,47.40698370411039 +2024-08-22 20:00:00,7,8.221565975092604,26.21670849544185,51.544063921842266 +2024-08-22 21:00:00,7,18.912936312502538,20.86155040273227,58.01986287398738 +2024-08-22 22:00:00,7,24.705819798919787,18.848843965706795,48.04232549115584 +2024-08-22 23:00:00,7,27.819838301990817,23.72052623033298,54.69203158025131 +2024-08-23 00:00:00,7,10.716121527756362,25.664606345036596,55.53073332691759 +2024-08-23 01:00:00,7,19.302753817771265,20.133718045718652,49.62764247567142 +2024-08-23 02:00:00,7,18.277511490354406,22.886274908982895,60.92171794306153 +2024-08-23 03:00:00,7,21.9065404808661,22.87400272352267,57.089856211471165 +2024-08-23 04:00:00,7,7.20978709401135,19.20321380143568,62.761170960583485 +2024-08-23 05:00:00,7,35.328860803301644,26.262933299747928,44.5413172673476 +2024-08-23 06:00:00,7,21.90862792558621,21.935322457517696,48.122875871527405 +2024-08-23 07:00:00,7,29.031668042221046,22.63261769842716,50.76505359265675 +2024-08-23 08:00:00,7,34.79147202780959,27.561604813245353,52.93712893665412 +2024-08-23 09:00:00,7,28.114607120737208,25.978863313380455,48.121693738061296 +2024-08-23 10:00:00,7,24.243720772958422,24.058769206497185,54.51247651992777 +2024-08-23 11:00:00,7,7.355805880167605,30.69401366257236,52.22531600181857 +2024-08-23 12:00:00,7,13.855395462421345,25.98023799303327,55.01177962639579 +2024-08-23 13:00:00,7,27.990247216774016,26.230411457148897,48.9955993265342 +2024-08-23 14:00:00,7,4.006972638201223,27.695406739260996,51.44245066435137 +2024-08-23 15:00:00,7,19.902673972182118,16.727271661785487,48.52841939074917 +2024-08-23 16:00:00,7,31.144059544258347,24.9544762307105,44.186272408767806 +2024-08-23 17:00:00,7,20.72891785863633,19.62983835624901,40.642169622446815 +2024-08-23 18:00:00,7,27.742447467566627,17.826218940734755,55.71036262148701 +2024-08-23 19:00:00,7,17.867804241538455,22.720840684155363,62.3464867142488 +2024-08-23 20:00:00,7,31.259657101683374,23.227299908170465,46.448036295114534 +2024-08-23 21:00:00,7,30.281969368220537,25.511531907224605,56.7651203768584 +2024-08-23 22:00:00,7,33.49101841966971,26.725847395795352,66.62309711612423 +2024-08-23 23:00:00,7,12.695293216497038,17.81258714771444,61.00388999461632 +2024-08-24 00:00:00,7,32.062129923349666,15.539325556189956,53.861153107054534 +2024-08-24 01:00:00,7,19.490057833203718,23.48421729898977,52.87634662566877 +2024-08-24 02:00:00,7,19.731219611661892,19.737160802957085,51.75539425451733 +2024-08-24 03:00:00,7,12.482754200726333,27.760687289077808,44.394222672716246 +2024-08-24 04:00:00,7,22.237742661550012,20.11185713802636,60.85495056696564 +2024-08-24 05:00:00,7,24.828068823169147,26.164875569688498,51.64110285049325 +2024-08-24 06:00:00,7,4.101961256151448,23.81866993276815,53.672428337456644 +2024-08-24 07:00:00,7,15.133356247704622,23.621519447510053,49.923384032578014 +2024-08-24 08:00:00,7,9.256743230255731,19.32848250477702,42.67187078162702 +2024-08-24 09:00:00,7,25.202400571788758,23.644078933972345,51.247814289063534 +2024-08-24 10:00:00,7,15.687320765283566,28.045017103665515,64.80442869574048 +2024-08-24 11:00:00,7,39.192113123479885,22.61735547419267,69.2009215490324 +2024-08-24 12:00:00,7,13.224630072756655,17.742759981201253,65.33095688221516 +2024-08-24 13:00:00,7,26.063296955903496,24.540196191080668,61.14094521916075 +2024-08-24 14:00:00,7,15.962700421840136,21.205565450851715,54.301372088796434 +2024-08-24 15:00:00,7,42.02038986316106,19.866346196479412,41.14677514472707 +2024-08-24 16:00:00,7,20.00183522282564,20.44772365863638,53.55574941434384 +2024-08-24 17:00:00,7,16.825894738238183,12.392561992983698,43.076813204117734 +2024-08-24 18:00:00,7,22.270491267109737,16.435013831759882,45.41916256797306 +2024-08-24 19:00:00,7,26.835382653013387,24.993941247274,46.657994372536315 +2024-08-24 20:00:00,7,16.47672308247232,27.64311386843859,63.75804216135863 +2024-08-24 21:00:00,7,32.52833199826814,20.657648696163992,51.243985480988535 +2024-08-24 22:00:00,7,36.36022191431364,26.763366087902877,38.267850335774824 +2024-08-24 23:00:00,7,21.234616738481563,27.904992034774068,54.075378861058965 +2024-08-25 00:00:00,7,21.41544307589165,21.672402914771546,60.08492537123507 +2024-08-25 01:00:00,7,24.083938878062657,19.06087446268352,42.172996507955794 +2024-08-25 02:00:00,7,34.1935556857067,18.89354895340607,50.62132556979598 +2024-08-25 03:00:00,7,18.79981949860468,23.631085900140896,44.76423340054037 +2024-08-25 04:00:00,7,21.63356272311189,23.260320349692908,34.98552043901053 +2024-08-25 05:00:00,7,20.798206133328794,25.429597952862068,53.18610481592077 +2024-08-25 06:00:00,7,33.587204125894665,22.90607022269064,51.718596601955866 +2024-08-25 07:00:00,7,27.31825817075703,21.653245383216554,50.39087665699548 +2024-08-25 08:00:00,7,23.18774040505533,22.43495583653749,48.35553287695035 +2024-08-25 09:00:00,7,27.972291823464786,24.502862600430483,60.85603877613539 +2024-08-25 10:00:00,7,11.248486470584957,21.118192713133222,50.67779411223037 +2024-08-25 11:00:00,7,28.60973810257838,24.767718790247475,59.75453694912127 +2024-08-25 12:00:00,7,14.929426148329823,17.59057700309202,58.9520135246827 +2024-08-25 13:00:00,7,31.216101828511942,21.930380824793726,56.66311201622643 +2024-08-25 14:00:00,7,18.8377718534411,20.300489720655555,55.20784616275274 +2024-08-25 15:00:00,7,20.44456196356726,24.040160944120817,62.930246074967656 +2024-08-25 16:00:00,7,8.677426543021248,21.463609587125358,44.556055812554064 +2024-08-25 17:00:00,7,6.452687401529671,29.89111419612373,49.0850667517499 +2024-08-25 18:00:00,7,16.156038596172692,23.93402330980074,51.07063835841922 +2024-08-25 19:00:00,7,24.73412078980398,28.615874580001574,53.611308502779494 +2024-08-25 20:00:00,7,7.181227539984443,24.177280999200736,48.06002334441338 +2024-08-25 21:00:00,7,12.16442006914608,25.517501367864693,59.190541742297356 +2024-08-25 22:00:00,7,7.930183242819563,21.117178992259735,42.03415900507535 +2024-08-25 23:00:00,7,16.06176497976488,25.481614924591696,46.23804352234737 +2024-08-26 00:00:00,7,19.627460231240327,18.73284609183935,38.34811056842873 +2024-08-26 01:00:00,7,41.58192201857108,21.322949177496678,37.25599436868013 +2024-08-26 02:00:00,7,24.14290033092968,23.76790196727703,52.59423159311402 +2024-08-26 03:00:00,7,2.599074030834501,16.735207849997774,51.02321341436219 +2024-08-26 04:00:00,7,15.946326424462601,19.92174720617408,41.56820975844445 +2024-08-26 05:00:00,7,9.852666595813274,26.121175767762566,41.51403022472073 +2024-08-26 06:00:00,7,32.601865120284096,29.12733361603542,52.07179952477298 +2024-08-26 07:00:00,7,25.241612487747155,26.3318560202055,44.90625638941415 +2024-08-26 08:00:00,7,29.199733480998937,27.57953646003118,46.90030924074115 +2024-08-26 09:00:00,7,12.78312474742198,23.39495977838715,45.28042773999509 +2024-08-26 10:00:00,7,33.18553164326118,26.184621981681545,56.108354995377596 +2024-08-26 11:00:00,7,16.815819119815497,25.253361438127136,46.57527230818192 +2024-08-26 12:00:00,7,6.595364695943054,21.908015292880258,56.91002401211272 +2024-08-26 13:00:00,7,9.795474140174468,27.495225219386572,42.89565202683318 +2024-08-26 14:00:00,7,24.347057693386184,27.06244095559087,60.838792184158514 +2024-08-26 15:00:00,7,18.598627457524078,24.29693894328298,47.804496242489066 +2024-08-26 16:00:00,7,10.791069128143276,27.25663206854625,50.73467419225914 +2024-08-26 17:00:00,7,25.148813032477506,26.890912243753302,49.85462775035028 +2024-08-26 18:00:00,7,15.514578967257972,20.496143845682724,56.34437229789676 +2024-08-26 19:00:00,7,15.310194008868812,20.344623414116555,41.701982259267005 +2024-08-26 20:00:00,7,36.94363064341058,22.22975328048895,55.948455618760136 +2024-08-26 21:00:00,7,22.591081894173044,23.883290127583532,53.50213281691729 +2024-08-26 22:00:00,7,39.619293357386965,25.67969801631108,52.76561577740268 +2024-08-26 23:00:00,7,17.479832763971636,19.32303024781998,48.76850057889294 +2024-08-27 00:00:00,7,13.509356787241677,28.18526922512868,64.18581898461571 +2024-08-27 01:00:00,7,19.4464882968636,28.181432161795122,63.30765277349256 +2024-08-27 02:00:00,7,19.839456237798988,20.36681926113882,57.54976426842308 +2024-08-27 03:00:00,7,31.85181999177214,28.684983496649224,45.38585898592036 +2024-08-27 04:00:00,7,12.337364275425573,21.606452062900857,42.71441711584429 +2024-08-27 05:00:00,7,27.951531578157084,33.15837732183884,52.746025472378165 +2024-08-27 06:00:00,7,18.624754013738514,28.31639710936413,57.540002884875335 +2024-08-27 07:00:00,7,22.19232804139128,24.99412415849665,49.41781227895668 +2024-08-27 08:00:00,7,31.960485860544818,27.043347026771862,39.85788066616545 +2024-08-27 09:00:00,7,18.715301814833975,19.730450957740658,29.067741050605406 +2024-08-27 10:00:00,7,19.83920262163036,20.583093121700966,73.61549459491815 +2024-08-27 11:00:00,7,26.42870966504165,29.03058555933642,50.41911022106976 +2024-08-27 12:00:00,7,27.3704149008681,24.647711397714875,55.56842962554009 +2024-08-27 13:00:00,7,25.036580636036817,19.76164604211031,50.40442188623917 +2024-08-27 14:00:00,7,16.272450259194535,22.787389464929824,52.532595999028814 +2024-08-27 15:00:00,7,3.8327746774784686,16.67266463108013,53.15479657672915 +2024-08-27 16:00:00,7,28.777102835112363,21.388506990791228,47.5164997623656 +2024-08-27 17:00:00,7,23.981063645409023,19.65895071349862,47.003696950277444 +2024-08-27 18:00:00,7,14.482543140513748,22.665068133404993,52.37390130260313 +2024-08-27 19:00:00,7,10.676893620128702,16.739426060110628,63.87457003308036 +2024-08-27 20:00:00,7,6.0714924191219755,28.737652410307778,40.56093732414671 +2024-08-27 21:00:00,7,17.2268917208821,11.92449391385446,40.655081325534674 +2024-08-27 22:00:00,7,20.589604208199297,25.008623169994692,60.55650980701512 +2024-08-27 23:00:00,7,22.022613215165293,16.660145225315336,62.026417858411605 +2024-08-28 00:00:00,7,19.071532366124554,27.370690424454835,47.576269356276455 +2024-08-28 01:00:00,7,15.84976648531132,23.186272491898553,46.19088036322602 +2024-08-28 02:00:00,7,16.267560765221724,31.096894182889876,43.798701121473776 +2024-08-28 03:00:00,7,26.379154899339685,22.131630811231318,44.60535558263122 +2024-08-28 04:00:00,7,17.002633822299536,15.8119202592896,42.51552511276323 +2024-08-28 05:00:00,7,19.972024285723908,17.6683425749463,53.367013459422026 +2024-08-28 06:00:00,7,21.500636553820808,22.323213580356704,51.67201016420166 +2024-08-28 07:00:00,7,17.38044760149586,28.70145787197622,47.59912083671957 +2024-08-28 08:00:00,7,32.51870961661831,24.485950783674745,45.62394944426443 +2024-08-28 09:00:00,7,15.59059983503487,29.490139282271098,51.736885406947884 +2024-08-28 10:00:00,7,17.64120243083523,24.53901454483656,52.31035119627886 +2024-08-28 11:00:00,7,32.29295444568487,25.74537932273004,49.826008556623094 +2024-08-28 12:00:00,7,29.644929485248657,20.542823080765668,57.93697172459986 +2024-08-28 13:00:00,7,21.440295407110597,16.233011692513294,43.70278139419073 +2024-08-28 14:00:00,7,28.985911352481963,23.54398742206733,46.67565453546454 +2024-08-28 15:00:00,7,15.153505953279353,19.892501902663177,57.22060674433481 +2024-08-28 16:00:00,7,43.02595572248747,22.546266433712596,39.257232517662565 +2024-08-28 17:00:00,7,23.423758800155294,27.706929195111172,44.64943213707482 +2024-08-28 18:00:00,7,23.994399944300216,23.624605797408336,62.4210609935491 +2024-08-28 19:00:00,7,6.224413139092009,24.005886391597567,44.52585251622691 +2024-08-28 20:00:00,7,29.99038932741662,22.490317632311125,50.441498222588905 +2024-08-28 21:00:00,7,30.816105438617512,24.171137832339408,39.19858333702749 +2024-08-28 22:00:00,7,13.966464946641251,25.32234119799811,49.80820724462852 +2024-08-28 23:00:00,7,24.049598949062084,23.919026113951023,56.01042022597446 +2024-08-29 00:00:00,7,18.58313262987597,21.483828847355785,53.232715056026386 +2024-08-29 01:00:00,7,13.545427490484599,23.457922404958328,34.35256787495995 +2024-08-29 02:00:00,7,29.06734142830339,21.179703245451194,56.48531605946199 +2024-08-29 03:00:00,7,26.470936412037606,21.580581917444963,46.80816117517742 +2024-08-29 04:00:00,7,25.280070206044915,29.992003071934857,48.55828798955027 +2024-08-29 05:00:00,7,26.601025766917324,23.579474572853844,55.002317590369955 +2024-08-29 06:00:00,7,29.82701533012195,22.665958279129406,55.00741879361219 +2024-08-29 07:00:00,7,25.173099283713658,22.60218660860499,63.23571803885155 +2024-08-29 08:00:00,7,26.73282118483957,20.886632611699827,53.96476174954786 +2024-08-29 09:00:00,7,13.856289214395636,20.957534574265846,51.42463889251392 +2024-08-29 10:00:00,7,0.0,21.698168817688344,58.362447554788304 +2024-08-29 11:00:00,7,15.623239514691655,31.021549865715766,63.433912299630386 +2024-08-29 12:00:00,7,22.751914871455448,25.035169406274367,53.72048212877965 +2024-08-29 13:00:00,7,12.070783483130477,20.960986809878975,54.537489336513744 +2024-08-29 14:00:00,7,14.606934497282117,27.936896788277085,48.425121282559914 +2024-08-29 15:00:00,7,23.982985900855905,21.97712743644411,45.69281185316303 +2024-08-29 16:00:00,7,11.112824176322082,22.09170959526,55.95050388162514 +2024-08-29 17:00:00,7,30.890990684059318,24.840814355173592,60.62964569584997 +2024-08-29 18:00:00,7,11.734814838950133,21.56757378643284,46.67804348070239 +2024-08-29 19:00:00,7,22.658352804686185,19.775882977032907,65.1805378076768 +2024-08-29 20:00:00,7,3.3321887103063688,19.252809631449203,50.73455953519111 +2024-08-29 21:00:00,7,19.17073407410914,25.366935407804675,41.09599604863083 +2024-08-29 22:00:00,7,27.021597862673765,29.124700903750988,45.9849149113118 +2024-08-29 23:00:00,7,35.16405444786633,24.86748317786615,50.87528059315032 +2024-08-30 00:00:00,7,28.01897606590079,21.936704250585226,49.37898874861055 +2024-08-30 01:00:00,7,11.761374540884713,28.531284493811565,45.258573249329636 +2024-08-30 02:00:00,7,22.69294689107085,23.45811861510466,57.255678454543855 +2024-08-30 03:00:00,7,18.052899915251718,20.642801913687393,54.18085079228316 +2024-08-30 04:00:00,7,21.645083779299004,26.25997854691423,47.68589941454244 +2024-08-30 05:00:00,7,15.689810686187293,28.39393609039047,57.30355149611181 +2024-08-30 06:00:00,7,12.5441841929037,19.791076174055853,52.18916221823817 +2024-08-30 07:00:00,7,25.280481046136725,18.21565076547508,50.61747884355584 +2024-08-30 08:00:00,7,36.56408526928383,23.920921086038433,60.19554534394195 +2024-08-30 09:00:00,7,9.620153857911667,17.04564726182141,48.24931593456264 +2024-08-30 10:00:00,7,19.561772362431945,28.54143327893187,56.27933380126183 +2024-08-30 11:00:00,7,15.5595645838732,26.183226369397154,64.87204888804013 +2024-08-30 12:00:00,7,42.25656496598946,25.929305289794975,64.89020997170617 +2024-08-30 13:00:00,7,20.688189143956414,21.828202560900525,47.37657493708376 +2024-08-30 14:00:00,7,31.037098333601577,20.332035494207346,46.4732642736474 +2024-08-30 15:00:00,7,32.71459769318606,25.34978115764312,47.05533556000848 +2024-08-30 16:00:00,7,28.21814687887706,20.564507153579818,55.237476815178105 +2024-08-30 17:00:00,7,10.103765994284815,25.47384849535437,53.61879551200044 +2024-08-30 18:00:00,7,17.527785702904218,20.40506903544685,63.870742441364314 +2024-08-30 19:00:00,7,12.055249471986155,22.087526423933173,47.07920047103611 +2024-08-30 20:00:00,7,20.093268840341533,18.578520713231452,49.34814247414527 +2024-08-30 21:00:00,7,13.911250020158892,21.390524937525942,62.20643012738436 +2024-08-30 22:00:00,7,24.732213135549586,18.548616678817098,45.448998776657746 +2024-08-30 23:00:00,7,22.41941316200749,15.207915538108914,50.31184619340219 +2024-08-31 00:00:00,7,21.927443799290423,24.94036156472612,48.252748807702964 +2024-08-31 01:00:00,7,28.56737755606576,20.200316949947204,57.07239477690605 +2024-08-31 02:00:00,7,4.365703990766914,24.66164925162503,48.243854866674525 +2024-08-31 03:00:00,7,35.311940394287106,26.29163904553633,62.26334199653 +2024-08-31 04:00:00,7,23.57048527908469,22.67923111269769,46.537210046394684 +2024-08-31 05:00:00,7,22.64013715305081,18.00889250688561,61.99941819569319 +2024-08-31 06:00:00,7,22.99014810407997,17.306564481951987,50.680807756245926 +2024-08-31 07:00:00,7,8.821674702757722,27.02999836308065,54.30059541523441 +2024-08-31 08:00:00,7,24.287785408037596,29.90036021545369,54.723586642804094 +2024-08-31 09:00:00,7,24.637861799662215,31.28689843110776,42.7159293065567 +2024-08-31 10:00:00,7,15.123343219191662,25.435793392584497,62.43880940835502 +2024-08-31 11:00:00,7,22.021113036677562,22.743644075126493,48.51634112315791 +2024-08-31 12:00:00,7,23.796895161534454,26.47775193040613,60.314841522222466 +2024-08-31 13:00:00,7,38.90732437321752,18.504370283155055,62.338516911427575 +2024-08-31 14:00:00,7,13.613329294948459,21.394524493760866,52.4665555454219 +2024-08-31 15:00:00,7,18.85768337272362,24.498795778238126,53.97693850365261 +2024-08-31 16:00:00,7,12.554666399336936,24.26265378702165,53.80764255328302 +2024-08-31 17:00:00,7,5.199453510562989,23.22879209401308,53.2816355877664 +2024-08-31 18:00:00,7,35.05678430752668,25.78655640363181,35.315537624648755 +2024-08-31 19:00:00,7,12.401780042561098,19.53420548932369,59.01161057953498 +2024-08-31 20:00:00,7,25.009884295088373,20.68067898027988,41.82900354469006 +2024-08-31 21:00:00,7,17.87582458673304,22.873781616840954,55.12519454980325 +2024-08-31 22:00:00,7,32.96727694832828,23.244211607146525,49.43082420962604 +2024-08-31 23:00:00,7,11.818754645564606,27.088399200927128,49.854504666679226 +2024-09-01 00:00:00,7,20.295086735550314,23.255967898211047,57.04428364997915 +2024-09-01 01:00:00,7,21.193335601354914,30.2076842345276,45.02234693576908 +2024-09-01 02:00:00,7,21.43731823245085,18.944940575498237,46.55792945529275 +2024-09-01 03:00:00,7,17.078251527172267,23.531687729496355,71.46690286447132 +2024-09-01 04:00:00,7,20.59427016852864,27.026561552203447,51.34650753052152 +2024-09-01 05:00:00,7,26.92837468840023,24.058957865926327,44.23379343515544 +2024-09-01 06:00:00,7,42.898750687384585,20.684952860997452,56.48451570017205 +2024-09-01 07:00:00,7,21.691340727708123,24.333303509299263,43.934022699695724 +2024-09-01 08:00:00,7,32.10279668693738,22.84661412783457,39.31244747459342 +2024-09-01 09:00:00,7,11.637000101868047,26.24628719762468,50.26677727327259 +2024-09-01 10:00:00,7,22.591547488607937,23.0423478836225,54.43856146900957 +2024-09-01 11:00:00,7,19.94410159777604,24.934089958388146,49.53632309622252 +2024-09-01 12:00:00,7,39.239695529141486,17.888615360452974,50.93289932748989 +2024-09-01 13:00:00,7,32.681390918383705,21.969904463626953,53.728428279610796 +2024-09-01 14:00:00,7,22.74877744066167,24.51020773768702,64.4404084324738 +2024-09-01 15:00:00,7,24.2711420439098,24.882853052628946,41.85951013397317 +2024-09-01 16:00:00,7,11.660147039143412,26.51085550385891,43.21068317177337 +2024-09-01 17:00:00,7,8.286564945861807,23.87069071292474,60.32449951639035 +2024-09-01 18:00:00,7,12.11037561332579,20.135164300867018,52.25193649393247 +2024-09-01 19:00:00,7,30.541575536768203,21.11619765685211,43.85181352127987 +2024-09-01 20:00:00,7,25.530397069842792,24.102551140263778,58.334924280673725 +2024-09-01 21:00:00,7,27.22601690117455,18.74625265833729,48.848981452082484 +2024-09-01 22:00:00,7,19.586635757753683,25.534472898191055,54.277919321430296 +2024-09-01 23:00:00,7,21.974199337753806,22.54128137933108,45.43957238641758 +2024-09-02 00:00:00,7,23.38740319964718,20.596542639529247,43.34267405219724 +2024-09-02 01:00:00,7,33.318029282369665,24.025582190640073,34.05160185789177 +2024-09-02 02:00:00,7,0.7045002278158243,19.16128956825549,52.59217813627333 +2024-09-02 03:00:00,7,17.95604340801877,23.821720580168247,53.392998186879076 +2024-09-02 04:00:00,7,21.721212360941436,32.658627991713544,49.711974216571235 +2024-09-02 05:00:00,7,31.545465384985153,25.07489712581492,45.428219431155995 +2024-09-02 06:00:00,7,15.056265719886515,28.933984168243963,51.03409642030059 +2024-09-02 07:00:00,7,17.401306337953553,24.797720020885038,60.997125567030935 +2024-09-02 08:00:00,7,30.013525856055107,24.202051171800658,36.74388084601844 +2024-09-02 09:00:00,7,27.42473564255973,28.111341583017214,51.92983689427347 +2024-09-02 10:00:00,7,29.504319010711683,26.767741588524537,48.01602696249094 +2024-09-02 11:00:00,7,24.23358206584604,25.626904020589652,57.357212025247385 +2024-09-02 12:00:00,7,12.816108013513883,26.41608034832128,37.79474370511764 +2024-09-02 13:00:00,7,14.469686842298515,25.53102588701753,63.7155549014656 +2024-09-02 14:00:00,7,22.448104964896455,21.576266944010815,59.788254384465255 +2024-09-02 15:00:00,7,22.53990535491063,19.20577135892977,61.151458831747924 +2024-09-02 16:00:00,7,27.174098118723933,25.55271019271744,65.87000415569162 +2024-09-02 17:00:00,7,24.671875329781322,27.494251451935405,57.436533034274504 +2024-09-02 18:00:00,7,24.419878840625294,17.953935104648124,58.97667067561305 +2024-09-02 19:00:00,7,27.459696504904183,20.35526363972814,53.55474799869199 +2024-09-02 20:00:00,7,35.96137188147,29.052770322310216,49.11291325095661 +2024-09-02 21:00:00,7,0.0,22.379888893298496,37.558374883232716 +2024-09-02 22:00:00,7,9.740294568417374,23.89027796715419,54.87983857835585 +2024-09-02 23:00:00,7,19.131947814926757,21.46623517919618,54.26183856591277 +2024-09-03 00:00:00,7,13.516081556472157,19.281820973478364,41.11593105270814 +2024-09-03 01:00:00,7,28.057881127904082,20.258393018684863,52.26926294347813 +2024-09-03 02:00:00,7,27.49564207943788,16.637120753759852,50.76976665396456 +2024-09-03 03:00:00,7,20.429386578746445,20.285323230734818,49.61176762343931 +2024-09-03 04:00:00,7,32.77499717942388,24.4227860024129,48.35696637700284 +2024-09-03 05:00:00,7,28.471010253541948,22.309525921390588,50.28376240032431 +2024-09-03 06:00:00,7,10.936146238739111,19.040616925469976,39.136233370557164 +2024-09-03 07:00:00,7,11.154132527546052,30.358539009222504,44.52252719233218 +2024-09-03 08:00:00,7,23.518251123017208,22.779541948438165,48.83958407352922 +2024-09-03 09:00:00,7,29.1422332033267,26.32526155002437,67.47844356967855 +2024-09-03 10:00:00,7,24.23724092341991,21.25929446590159,61.924617466375274 +2024-09-03 11:00:00,7,7.2649808360445505,22.863157477716832,60.74065609861937 +2024-09-03 12:00:00,7,23.60653398098844,24.676418634241426,54.37049410866961 +2024-09-03 13:00:00,7,45.574464998854936,19.289737217614334,67.1403926393592 +2024-09-03 14:00:00,7,24.27851947541325,21.66641715520801,59.71230241947305 +2024-09-03 15:00:00,7,22.109746323305192,17.864999992318765,44.31141403963153 +2024-09-03 16:00:00,7,12.51361936216381,22.999348737179925,59.488058317700954 +2024-09-03 17:00:00,7,42.20653484194512,20.07840854044835,51.812332094531406 +2024-09-03 18:00:00,7,17.062882144614175,17.823142468327166,42.35365303366413 +2024-09-03 19:00:00,7,9.427642675685481,18.919961780263783,49.31221058934889 +2024-09-03 20:00:00,7,8.600036731540687,19.151090977151394,64.52926178431186 +2024-09-03 21:00:00,7,38.13794214446412,17.97347466183081,49.236167679808446 +2024-09-03 22:00:00,7,31.295443169127218,25.169192075798254,65.98927749014763 +2024-09-03 23:00:00,7,32.77358758469205,20.90210410492478,52.875015331936766 +2024-09-04 00:00:00,7,16.266861260644557,19.43174267870115,49.865675417263695 +2024-09-04 01:00:00,7,11.027742830129657,26.76316128556799,50.07717835326216 +2024-09-04 02:00:00,7,22.079787223126008,18.811839325723383,57.819621252330336 +2024-09-04 03:00:00,7,14.036538339544101,21.873202109008297,55.54724610461614 +2024-09-04 04:00:00,7,23.053224176911964,22.911079032133934,54.55747427831939 +2024-09-04 05:00:00,7,42.59269881084194,16.97955880755135,66.3701453712286 +2024-09-04 06:00:00,7,25.1588976629268,21.38877270151877,53.56139022530324 +2024-09-04 07:00:00,7,16.29847186301284,23.045759464793385,48.043001474474075 +2024-09-04 08:00:00,7,6.101993788497502,26.803393804683505,51.01083743951901 +2024-09-04 09:00:00,7,21.267275709169542,25.01530071718385,60.16725930069862 +2024-09-04 10:00:00,7,30.655084664440526,25.09251320523075,56.388664185231676 +2024-09-04 11:00:00,7,10.722484454002885,23.290401565989193,64.04560153236093 +2024-09-04 12:00:00,7,18.35453293968582,24.137107296441254,52.28829531665556 +2024-09-04 13:00:00,7,16.68628515541704,23.949593328302186,62.482089666756856 +2024-09-04 14:00:00,7,40.507587212111055,20.19588122673539,47.209826823443514 +2024-09-04 15:00:00,7,20.54600106397008,28.523194140751748,45.7556045093557 +2024-09-04 16:00:00,7,28.248535312784306,21.453385702093648,57.01822030290635 +2024-09-04 17:00:00,7,23.314270597447564,21.227501978414487,51.073773880293516 +2024-09-04 18:00:00,7,13.900409543837597,21.224011966270183,59.33578081191399 +2024-09-04 19:00:00,7,30.674145789937015,18.554572497052913,58.347532642737185 +2024-09-04 20:00:00,7,7.443604393913446,13.723972040269672,52.670870730977505 +2024-09-04 21:00:00,7,33.09183794083823,24.53902939033969,65.16955494349645 +2024-09-04 22:00:00,7,42.38008204378282,19.90039949508387,47.05144779501315 +2024-09-04 23:00:00,7,26.373637867982563,23.512570029880454,46.6269321054646 +2024-09-05 00:00:00,7,37.40409775340641,19.87466828184039,47.41913951596634 +2024-09-05 01:00:00,7,41.77664002586626,26.65359747676365,44.80003016240562 +2024-09-05 02:00:00,7,23.410441888771796,19.23129169339989,65.24006743387105 +2024-09-05 03:00:00,7,32.432418262993295,22.005348088585713,57.29131139979262 +2024-09-05 04:00:00,7,13.976890584949082,17.25848195850621,39.94809521427037 +2024-09-05 05:00:00,7,9.989613104032003,25.59752743025112,60.13070501467207 +2024-09-05 06:00:00,7,27.08446469557573,24.125701809098896,53.32109076584406 +2024-09-05 07:00:00,7,19.631973006907703,20.919252251546354,48.733884696826784 +2024-09-05 08:00:00,7,28.494279162211598,29.25286139130234,41.26398363570397 +2024-09-05 09:00:00,7,17.29726584124497,22.334303121200165,46.87376936291034 +2024-09-05 10:00:00,7,21.169970268561347,22.518974609929327,58.05062263734167 +2024-09-05 11:00:00,7,51.55368884476782,23.766698264475593,62.485370269701804 +2024-09-05 12:00:00,7,27.33781353184732,27.19849020567615,53.97034358187193 +2024-09-05 13:00:00,7,44.21374914802256,20.042736699820004,52.50501751101222 +2024-09-05 14:00:00,7,19.117247952188787,16.175602279375344,66.25822529669256 +2024-09-05 15:00:00,7,0.0,23.76804492699266,54.239028606944515 +2024-09-05 16:00:00,7,9.041164407271156,27.614835326116815,53.506123621179 +2024-09-05 17:00:00,7,23.5880030409574,22.106316674108932,54.302618970806776 +2024-09-05 18:00:00,7,24.096747205313804,21.669192084230133,66.30660911178424 +2024-09-05 19:00:00,7,30.434590702654365,19.69068969402424,46.77953131487501 +2024-09-05 20:00:00,7,36.342196514925845,26.706675082693124,47.51542867318369 +2024-09-05 21:00:00,7,34.55890201866768,15.817005945392117,59.32368774160391 +2024-09-05 22:00:00,7,30.146174791545178,22.383561310309073,58.29804010292876 +2024-09-05 23:00:00,7,18.868716276674796,22.05306236241074,55.68359218250416 +2024-09-06 00:00:00,7,17.18117904975386,24.60616044108644,56.40325182747552 +2024-09-06 01:00:00,7,16.695347435421475,14.141938330612293,43.77172197060852 +2024-09-06 02:00:00,7,22.39781553614718,21.40783711859203,61.354764201668345 +2024-09-06 03:00:00,7,28.910125868659755,23.152422568210415,47.456906919603036 +2024-09-06 04:00:00,7,27.68171129399136,28.333810340825316,49.4925520626511 +2024-09-06 05:00:00,7,20.260019261939515,25.36627708873268,49.72924979091247 +2024-09-06 06:00:00,7,20.022765967721888,23.66252304084327,47.62526082687968 +2024-09-06 07:00:00,7,20.894239206405643,20.545477756096133,57.97858862402964 +2024-09-06 08:00:00,7,13.165976813593103,26.87246031775412,50.34754648896632 +2024-09-06 09:00:00,7,23.780205972994665,24.311469914942144,48.39176801437705 +2024-09-06 10:00:00,7,29.77330849653354,21.449749606491775,54.07976569384219 +2024-09-06 11:00:00,7,37.52255933944794,23.319923115810237,63.016050747941115 +2024-09-06 12:00:00,7,13.755785791606232,21.631576664513872,47.77191443152307 +2024-09-06 13:00:00,7,15.110903884646989,24.005411114845,64.60076074594045 +2024-09-06 14:00:00,7,22.028759940175192,18.09767939170999,60.83942806146249 +2024-09-06 15:00:00,7,12.271626695642535,30.019029281457318,55.814175781375084 +2024-09-06 16:00:00,7,13.844942704403483,16.3138889310675,42.817935330461324 +2024-09-06 17:00:00,7,34.28671593145511,26.08900681261633,46.7373159888265 +2024-09-06 18:00:00,7,17.994244234646196,21.276220154691547,46.20213583712024 +2024-09-06 19:00:00,7,28.018731222230777,21.712830939529958,62.02034395493822 +2024-09-06 20:00:00,7,29.801784248297345,20.64583619392625,46.56612291168287 +2024-09-06 21:00:00,7,26.387551391140775,22.60773029080527,44.18656516424609 +2024-09-06 22:00:00,7,23.43912330817478,19.662666678191485,50.84447605052176 +2024-09-06 23:00:00,7,16.607931982172556,24.25827983912277,50.99658070671658 +2024-09-07 00:00:00,7,25.269781785076567,23.244678171057156,46.811567235813264 +2024-09-07 01:00:00,7,27.18809658015552,25.94925871518601,67.16605749297909 +2024-09-07 02:00:00,7,34.80386540581475,20.619196704139117,44.58554788739405 +2024-09-07 03:00:00,7,30.263343070201984,19.716331633729773,50.10880899511008 +2024-09-07 04:00:00,7,20.740693343717606,17.773347605092496,52.52437659209824 +2024-09-07 05:00:00,7,7.555235005006201,25.209715682290437,49.118565085333955 +2024-09-07 06:00:00,7,24.824551194218788,25.045083854585666,44.72094357006504 +2024-09-07 07:00:00,7,36.46894384703214,14.885466875157109,50.814625496286155 +2024-09-07 08:00:00,7,30.222570578681932,25.717738412655883,48.655638073742665 +2024-09-07 09:00:00,7,24.813268632898726,29.522282092964815,60.11873626778943 +2024-09-07 10:00:00,7,21.297367299539367,16.554598550423762,54.3688602378761 +2024-09-07 11:00:00,7,28.95651729019187,21.363864467032098,54.46257631173658 +2024-09-07 12:00:00,7,36.36138053030291,25.630575210187423,63.309234550503 +2024-09-07 13:00:00,7,28.130031493245617,18.584648462452723,55.09539350169483 +2024-09-07 14:00:00,7,18.976433971145646,32.805802011319805,55.88868254553996 +2024-09-07 15:00:00,7,30.081446529421935,24.778456620648214,46.39492962262884 +2024-09-07 16:00:00,7,25.30965506501377,20.168523481554633,50.82093377223732 +2024-09-07 17:00:00,7,29.927030683994303,21.94381330571448,55.09762372905136 +2024-09-07 18:00:00,7,20.712458235235367,28.46437169428479,50.476501258497194 +2024-09-07 19:00:00,7,30.90210834372231,17.68354039234493,44.34075395957224 +2024-09-07 20:00:00,7,35.74096871687982,30.49809490889029,49.26367299270907 +2024-09-07 21:00:00,7,12.923650076513663,16.485485816245543,69.73843457401092 +2024-09-07 22:00:00,7,19.586317119826443,16.960461258748172,54.238384518877304 +2024-09-07 23:00:00,7,19.83189391110102,25.153957048583326,43.30923274990883 +2024-09-08 00:00:00,7,24.374414696460974,27.602652850884482,63.53166681544295 +2024-09-08 01:00:00,7,17.16076947925277,17.492136151839052,56.23484151716123 +2024-09-08 02:00:00,7,17.028851291415833,23.186012645694515,54.1324456347343 +2024-09-08 03:00:00,7,34.77038265082239,25.756736960578657,45.43442481018094 +2024-09-08 04:00:00,7,20.981403727736556,19.627612739047525,59.170861952291276 +2024-09-08 05:00:00,7,39.46599595170377,21.602704011178197,51.81121180377036 +2024-09-08 06:00:00,7,25.124309917154083,23.994774088695966,48.69138068268526 +2024-09-08 07:00:00,7,23.799517315106243,15.938223219738546,40.23524962235086 +2024-09-08 08:00:00,7,19.92258071492225,25.679040356688887,41.17678496748697 +2024-09-08 09:00:00,7,15.149482441949838,24.40664609784966,60.93969341268625 +2024-09-08 10:00:00,7,12.050194190806327,22.451620358921314,48.286604362894295 +2024-09-08 11:00:00,7,43.48175622509908,26.657271877737255,51.23004238739129 +2024-09-08 12:00:00,7,16.802980276888132,26.299079499658376,67.77681481593574 +2024-09-08 13:00:00,7,20.33151757228922,26.055953539721422,41.336668234286215 +2024-09-08 14:00:00,7,22.59067405813446,22.003278023841833,51.95318436067587 +2024-09-08 15:00:00,7,14.446582308937135,23.235801554918982,47.98811707328156 +2024-09-08 16:00:00,7,31.426824086035037,23.83859557598425,28.03090936786564 +2024-09-08 17:00:00,7,32.58399562147789,22.82294956261432,47.620799741660264 +2024-09-08 18:00:00,7,16.10945519004977,20.37670130527657,54.202680041090716 +2024-09-08 19:00:00,7,23.51730118041633,19.62592063337899,52.76999704571116 +2024-09-08 20:00:00,7,16.464844366352967,19.76042787522248,52.9286102905049 +2024-09-08 21:00:00,7,31.680202041827577,21.195041863202945,63.23983909573061 +2024-09-08 22:00:00,7,5.513606806869976,21.762008150837044,50.57089875620257 +2024-09-08 23:00:00,7,19.362772377254494,28.501009141000026,61.303583380195334 +2024-09-09 00:00:00,7,42.31545728589284,19.152386191765444,49.77150936728625 +2024-09-09 01:00:00,7,26.66364146981184,26.31589853139371,52.02992393959993 +2024-09-09 02:00:00,7,35.274525685629676,27.406654112492603,54.13066166418154 +2024-09-09 03:00:00,7,17.73284803132069,20.266039263429057,35.76096472863347 +2024-09-09 04:00:00,7,32.52769289239879,26.12141443093017,44.63190701973842 +2024-09-09 05:00:00,7,31.069787010781553,26.087799142891832,48.84145358824724 +2024-09-09 06:00:00,7,23.117152796883612,19.366597922617103,64.69515483406961 +2024-09-09 07:00:00,7,29.75256012998704,20.76917068532579,60.48120121789273 +2024-09-09 08:00:00,7,26.281544310226103,24.838116703969852,58.73753461475711 +2024-09-09 09:00:00,7,40.92405091069092,17.522701070138957,51.09083532913577 +2024-09-09 10:00:00,7,35.45075775250042,25.504842192049626,43.994571898326825 +2024-09-09 11:00:00,7,28.371797994871123,28.17703430203798,60.3018711190402 +2024-09-09 12:00:00,7,11.736251342464424,23.305445315777263,53.436366619809554 +2024-09-09 13:00:00,7,18.332160056365943,23.051715684370446,44.75160036824447 +2024-09-09 14:00:00,7,13.405224416966702,23.474505536293016,56.38851141250379 +2024-09-09 15:00:00,7,21.60761837897913,26.552675532445946,58.74145711868078 +2024-09-09 16:00:00,7,17.033925765245925,19.970208484979903,56.24595223756683 +2024-09-09 17:00:00,7,24.81173373219176,20.92013301421349,50.62878603435423 +2024-09-09 18:00:00,7,23.11545239997009,23.741145466552254,41.09206529436031 +2024-09-09 19:00:00,7,24.989844896720744,24.032033999252608,67.01406634410554 +2024-09-09 20:00:00,7,22.20978738115127,27.36911761435723,51.85996903052005 +2024-09-09 21:00:00,7,7.729928061120951,14.48089617539339,47.37573734586562 +2024-09-09 22:00:00,7,14.940558566296243,23.841960281392993,59.290536739322626 +2024-09-09 23:00:00,7,20.639352985763093,23.0806221613135,48.60456469071231 +2024-09-10 00:00:00,7,44.96370921644848,22.516496403553663,54.68552034865611 +2024-09-10 01:00:00,7,44.186288543394525,17.733270303596267,52.73592894934006 +2024-09-10 02:00:00,7,27.559754877722362,24.888075077701984,40.58559844025969 +2024-09-10 03:00:00,7,24.654377352118416,23.050963274244662,59.53237604039539 +2024-09-10 04:00:00,7,22.64201534068926,22.2329073880262,41.179713239225904 +2024-09-10 05:00:00,7,21.712962959481825,18.766072058210693,47.0532115414989 +2024-09-10 06:00:00,7,33.25911593307623,26.920695560231838,49.43841924028676 +2024-09-10 07:00:00,7,10.246133655439255,24.546440775026745,58.9285294745349 +2024-09-10 08:00:00,7,17.185205363400456,22.87117999435061,48.72256312672257 +2024-09-10 09:00:00,7,14.316586250692577,23.917325118352714,70.03501305526204 +2024-09-10 10:00:00,7,2.5519666535283676,26.56535005622754,47.33740718227198 +2024-09-10 11:00:00,7,29.277649278578394,27.61114918750252,47.29993542953163 +2024-09-10 12:00:00,7,9.003433780699465,23.471048808430563,55.028714212721184 +2024-09-10 13:00:00,7,23.83229221000287,19.37553805346264,60.91743931953928 +2024-09-10 14:00:00,7,18.01041426754961,25.586626393296072,52.300685597556544 +2024-09-10 15:00:00,7,25.090555586499747,22.777461349106723,58.15738965499956 +2024-09-10 16:00:00,7,25.254694693078758,29.480049505388973,45.75170415856823 +2024-09-10 17:00:00,7,28.89475242831398,23.411481977412105,54.100521602179846 +2024-09-10 18:00:00,7,16.586351455968753,21.214376623705736,56.52358461496761 +2024-09-10 19:00:00,7,13.61210493040015,20.496301864856285,53.16491177611786 +2024-09-10 20:00:00,7,14.186225871780064,27.269382985776694,48.5953822894136 +2024-09-10 21:00:00,7,14.612197713168584,22.402832402322392,50.36074883993696 +2024-09-10 22:00:00,7,25.791913427261544,23.097549145522187,51.74307170547715 +2024-09-10 23:00:00,7,19.646431946200575,20.870153842656748,57.68588059838313 +2024-09-11 00:00:00,7,4.713021195066272,24.848380289765153,44.60859257963502 +2024-09-11 01:00:00,7,5.350532064449066,25.90264972689876,40.096408924641736 +2024-09-11 02:00:00,7,0.0,27.707739345564395,43.10090024014057 +2024-09-11 03:00:00,7,13.611611434702576,25.256147801909496,49.17295542234456 +2024-09-11 04:00:00,7,15.101430177663826,18.578549480873228,44.04384902853248 +2024-09-11 05:00:00,7,16.794694024094255,22.87710832060413,55.72497038173502 +2024-09-11 06:00:00,7,23.45859897700509,19.875329605314313,38.23849143646916 +2024-09-11 07:00:00,7,19.398236876682763,22.740856096455772,55.05377889203789 +2024-09-11 08:00:00,7,50.14288632556098,19.966889886946152,52.97987606369015 +2024-09-11 09:00:00,7,18.06295924510194,24.39909114871027,47.299507940502 +2024-09-11 10:00:00,7,19.066449844538415,22.47891737415436,54.21734241288672 +2024-09-11 11:00:00,7,34.02666648292107,26.26134266355291,54.25490482201177 +2024-09-11 12:00:00,7,34.47851093692636,24.333120134346487,55.966602043335264 +2024-09-11 13:00:00,7,35.89205898491513,25.717553656264542,46.371274883570095 +2024-09-11 14:00:00,7,28.04238133374953,26.084768144606183,62.16543679584397 +2024-09-11 15:00:00,7,27.514427791746133,26.87935157742526,46.78563709068402 +2024-09-11 16:00:00,7,19.008511067046587,20.839630386627196,53.635403135660816 +2024-09-11 17:00:00,7,8.588948239639452,26.84171552224748,59.52311459224451 +2024-09-11 18:00:00,7,21.877493781455644,22.411255793424864,44.155324433546355 +2024-09-11 19:00:00,7,22.13273598866079,24.27354909351167,61.33452423126876 +2024-09-11 20:00:00,7,2.6532279149521045,23.286278778363744,52.615442007125566 +2024-09-11 21:00:00,7,21.227479785785924,18.45216199784003,59.1189071470538 +2024-09-11 22:00:00,7,32.03618986439136,21.378684082721907,64.79290917903406 +2024-09-11 23:00:00,7,13.19206184459957,27.292318482927467,60.743645068961364 +2024-09-12 00:00:00,7,23.85878289969219,22.457281030773505,65.22220131198152 +2024-09-12 01:00:00,7,9.218420952669549,26.63914004417879,41.47421589435612 +2024-09-12 02:00:00,7,28.54017434502007,24.91085225067685,51.35276476408367 +2024-09-12 03:00:00,7,18.152607849134235,21.301745545659934,54.17908791972727 +2024-09-12 04:00:00,7,29.40845005585527,21.326446675800025,44.709810773741395 +2024-09-12 05:00:00,7,20.492720029948273,28.73607638254144,54.41941861975224 +2024-09-12 06:00:00,7,14.112338285178105,15.74063632932636,32.25337781398609 +2024-09-12 07:00:00,7,28.364795857157908,21.29914148401925,53.5808758375595 +2024-09-12 08:00:00,7,23.263584621730082,29.450929437801555,44.65844827572397 +2024-09-12 09:00:00,7,5.326768328030834,18.800194510992682,61.11374130495743 +2024-09-12 10:00:00,7,38.24737749731238,18.47868683708753,55.858469144984284 +2024-09-12 11:00:00,7,23.616611735069252,27.895276294490635,61.89876868886016 +2024-09-12 12:00:00,7,28.944015593335806,23.248312475671202,62.449982608239694 +2024-09-12 13:00:00,7,0.23717194480813575,27.4070783498265,52.69250416104564 +2024-09-12 14:00:00,7,19.523140593353638,22.846358772625287,61.74624727738763 +2024-09-12 15:00:00,7,40.35190428446785,23.76841527872686,52.41497004803286 +2024-09-12 16:00:00,7,10.701614477641796,21.61688843256555,64.30940778784785 +2024-09-12 17:00:00,7,20.55973311328105,20.374421183835054,58.80813957037947 +2024-09-12 18:00:00,7,25.429989395599932,19.43839997753638,61.52535180111783 +2024-09-12 19:00:00,7,28.085597409019893,25.858356302569355,49.76519001513063 +2024-09-12 20:00:00,7,19.616662518083977,26.545596666410262,53.84789429917005 +2024-09-12 21:00:00,7,30.90511234589631,23.470459345277465,55.70089847514012 +2024-09-12 22:00:00,7,13.590983839118653,21.209434580021327,53.08525995041108 +2024-09-12 23:00:00,7,24.83137084747276,25.642995989754247,45.5125941106206 +2024-09-13 00:00:00,7,23.7523413853606,20.89491220685556,46.21998725715063 +2024-09-13 01:00:00,7,10.925979258619352,24.923477963766047,45.208024256018184 +2024-09-13 02:00:00,7,30.96027560703794,20.72942038834278,42.40993868699445 +2024-09-13 03:00:00,7,20.549908103574232,23.763152545868245,60.012915455962826 +2024-09-13 04:00:00,7,8.143002107724321,21.50162268239232,57.04749722119327 +2024-09-13 05:00:00,7,17.462030706362228,24.607313754292907,39.558222078269985 +2024-09-13 06:00:00,7,31.785410223562117,22.63546060516425,44.927352976384135 +2024-09-13 07:00:00,7,58.41934328117195,25.44127774376367,60.27700612964587 +2024-09-13 08:00:00,7,20.64540845772364,29.48929563236973,63.17697048392492 +2024-09-13 09:00:00,7,54.03632761976367,22.27877909662814,59.437406588876044 +2024-09-13 10:00:00,7,19.412285266542042,23.23140270944119,58.44256267443141 +2024-09-13 11:00:00,7,29.05701321806395,21.652165606898897,67.31600359892565 +2024-09-13 12:00:00,7,27.94001806494819,20.276448809869123,47.267177401462874 +2024-09-13 13:00:00,7,8.84657691340814,24.43015626111791,57.98750903032199 +2024-09-13 14:00:00,7,30.04719466012975,17.484324211316665,31.452665691364288 +2024-09-13 15:00:00,7,18.715466263188283,31.578993581925303,55.496591191783295 +2024-09-13 16:00:00,7,6.849929626644865,18.650428453262432,34.49124577180373 +2024-09-13 17:00:00,7,16.217815527357615,27.042005532812457,54.7492533440037 +2024-09-13 18:00:00,7,17.4949342899742,24.463574880674457,56.29903568922219 +2024-09-13 19:00:00,7,21.969457840949453,22.169472276584123,48.10848112472281 +2024-09-13 20:00:00,7,5.652703231764761,23.300375352844977,40.72004906833094 +2024-09-13 21:00:00,7,14.531264725523766,30.04551875901285,52.94492209241134 +2024-09-13 22:00:00,7,15.166977649134004,21.43499376306574,62.00182758182724 +2024-09-13 23:00:00,7,32.78859289398879,20.01757021984333,49.45287457363233 +2024-09-14 00:00:00,7,10.04334208827926,26.29514321014647,49.243258041184 +2024-09-14 01:00:00,7,32.43535049562557,25.997967929623996,59.283262723971774 +2024-09-14 02:00:00,7,1.9628138587241324,22.059792327124367,49.218917717283595 +2024-09-14 03:00:00,7,7.953753534456213,24.976117255143254,53.340425910024436 +2024-09-14 04:00:00,7,6.872482240291543,20.757403731646786,43.176921401534464 +2024-09-14 05:00:00,7,0.7198390979434599,26.78056499845371,62.68146022199087 +2024-09-14 06:00:00,7,14.445637794070647,23.876882855204457,38.469300520968396 +2024-09-14 07:00:00,7,27.696762286762752,20.480955416015796,48.329222844821764 +2024-09-14 08:00:00,7,32.40727966550709,26.377940122584125,53.09899982382692 +2024-09-14 09:00:00,7,16.94811959321772,26.97101852929199,54.019083581502514 +2024-09-14 10:00:00,7,42.56642732671837,28.62382631555214,57.62042206727613 +2024-09-14 11:00:00,7,7.212131425381699,17.629466973263277,51.01102325920188 +2024-09-14 12:00:00,7,21.579959895452255,28.607027165893204,55.41412195270612 +2024-09-14 13:00:00,7,16.45238480897957,29.52949504920617,52.89330137194882 +2024-09-14 14:00:00,7,27.39462655042902,18.996845198643058,40.36265637569188 +2024-09-14 15:00:00,7,23.275556166345634,18.609155332820244,56.334999216091646 +2024-09-14 16:00:00,7,32.98324778110026,28.421573263154873,59.28357618974538 +2024-09-14 17:00:00,7,32.05506631343817,18.787292901684754,53.7280492176572 +2024-09-14 18:00:00,7,31.166849503408642,27.24374081455733,50.49293399930368 +2024-09-14 19:00:00,7,16.200752711533347,19.503917366160138,59.25117475480783 +2024-09-14 20:00:00,7,0.36268491859649643,20.51944959801276,68.03751919783976 +2024-09-14 21:00:00,7,36.039700685010345,22.52706600858335,66.55472422649015 +2024-09-14 22:00:00,7,32.02143975855416,28.77314947140793,47.59479063542803 +2024-09-14 23:00:00,7,12.972001021567987,24.406649019618435,53.377573305051506 +2024-09-15 00:00:00,7,37.03195011726587,26.922014448149383,40.08502952396148 +2024-09-15 01:00:00,7,21.784656677359795,26.459034715576006,58.09236786435932 +2024-09-15 02:00:00,7,12.970857453481699,24.37200657883531,56.11510761953859 +2024-09-15 03:00:00,7,19.26778801370497,24.50615323791636,61.18941812694896 +2024-09-15 04:00:00,7,32.392266601551356,17.737183534128974,54.84676765549193 +2024-09-15 05:00:00,7,10.85443509746148,24.800514736522977,52.568831845180625 +2024-09-15 06:00:00,7,32.05169572158404,17.75391362176233,48.92501391465933 +2024-09-15 07:00:00,7,15.216268232232139,24.251313403550153,46.91640225453211 +2024-09-15 08:00:00,7,0.2657785880972092,30.76183766943843,58.252025109103286 +2024-09-15 09:00:00,7,45.348222338652306,21.553327008279215,52.27928810596761 +2024-09-15 10:00:00,7,3.796742528652196,25.2439846494181,60.33264896631561 +2024-09-15 11:00:00,7,31.601143569065997,28.862692425693794,58.96537360676401 +2024-09-15 12:00:00,7,9.30096384123488,18.523369757088673,38.74830890058045 +2024-09-15 13:00:00,7,31.689142475355034,27.456162208931033,34.90870839633685 +2024-09-15 14:00:00,7,31.26492180763733,15.860507561616993,79.49244599877038 +2024-09-15 15:00:00,7,29.075877315549874,27.067707571177372,67.79312193523262 +2024-09-15 16:00:00,7,12.973856771069839,18.50948158497807,48.514409711189366 +2024-09-15 17:00:00,7,23.314463166054594,21.23558686934563,52.67414554209692 +2024-09-15 18:00:00,7,38.66155575015131,24.977584288205136,65.9260102225757 +2024-09-15 19:00:00,7,22.047259002597066,24.437897125449933,53.21211074599165 +2024-09-15 20:00:00,7,6.587813137245947,23.285143287846026,60.6623624168725 +2024-09-15 21:00:00,7,30.78227088309273,22.54555650727755,57.52491339631539 +2024-09-15 22:00:00,7,26.9876934414096,23.807340085789512,48.84207132218714 +2024-09-15 23:00:00,7,20.858451611979273,22.16721204942395,51.35248281003657 +2024-09-16 00:00:00,7,21.40428232829558,18.526302308664558,51.77549575169394 +2024-09-16 01:00:00,7,32.968566768049506,22.696102488888535,39.36476358274009 +2024-09-16 02:00:00,7,4.346456711284077,17.934831231667715,48.475299976204596 +2024-09-16 03:00:00,7,25.54226545118978,29.590835259005953,61.072625519204095 +2024-09-16 04:00:00,7,25.54550573153098,20.153797711068986,55.830735054405515 +2024-09-16 05:00:00,7,28.84881298076805,22.93487354635384,65.41549994509077 +2024-09-16 06:00:00,7,15.113347275439427,18.984589158610177,74.98422816389993 +2024-09-16 07:00:00,7,20.43106111677547,26.843010864875545,53.36419170256651 +2024-09-16 08:00:00,7,25.82558084751784,27.4235407171931,54.29743243122542 +2024-09-16 09:00:00,7,14.276350125019887,21.82612009576747,54.52603568199732 +2024-09-16 10:00:00,7,18.72398613356706,27.625529969082116,59.59126592432374 +2024-09-16 11:00:00,7,25.059971123977064,23.520576152477915,53.22566151663095 +2024-09-16 12:00:00,7,27.16167725344015,23.85407117629955,64.98570382015902 +2024-09-16 13:00:00,7,33.02093471969143,22.336026490742974,42.66254853099116 +2024-09-16 14:00:00,7,25.748019274488442,21.127893256906507,54.2392007602739 +2024-09-16 15:00:00,7,16.78207288569259,22.63277127969578,54.51216757607675 +2024-09-16 16:00:00,7,18.017720568447768,21.256381887666347,58.13717110857199 +2024-09-16 17:00:00,7,0.0,27.206548173509535,55.449379233945535 +2024-09-16 18:00:00,7,15.390712586031466,22.42789689934287,53.97676640955094 +2024-09-16 19:00:00,7,24.569148638494546,23.33640854532045,47.10237201646498 +2024-09-16 20:00:00,7,15.477561769730736,22.272336304724508,53.46065113097691 +2024-09-16 21:00:00,7,17.467653690029014,25.301689205740526,53.446030272159575 +2024-09-16 22:00:00,7,9.105054426918883,25.40419370747456,53.57459041256463 +2024-09-16 23:00:00,7,23.508960073975686,14.88118803501435,45.611039304629024 +2024-09-17 00:00:00,7,18.5882546585578,24.611925975517536,61.13389555682298 +2024-09-17 01:00:00,7,11.235198220910645,23.892916127152315,46.56780334050022 +2024-09-17 02:00:00,7,36.8981027691006,25.758024487625946,62.90269868960884 +2024-09-17 03:00:00,7,25.348171415359605,25.19282977206703,50.955467559040294 +2024-09-17 04:00:00,7,35.81917003784968,26.271662824822133,55.08533126237873 +2024-09-17 05:00:00,7,13.665283532605494,23.103721710934735,41.850016231512086 +2024-09-17 06:00:00,7,12.709875508679962,27.947338578015,42.52567165286935 +2024-09-17 07:00:00,7,10.611826937631047,17.835447607813553,55.5067256549969 +2024-09-17 08:00:00,7,26.876275796587144,25.440517387560615,58.21494474488089 +2024-09-17 09:00:00,7,40.72198808072278,24.9847050927862,47.113546425554894 +2024-09-17 10:00:00,7,41.57410980971555,26.199620534936386,52.724730781129075 +2024-09-17 11:00:00,7,30.64709041284912,24.77628701547889,56.06685365778486 +2024-09-17 12:00:00,7,20.600689795972528,29.100625879073693,58.252144620471284 +2024-09-17 13:00:00,7,16.759785985953954,28.84264073705839,46.39428278468734 +2024-09-17 14:00:00,7,6.408315237995939,20.215790217058668,64.88650340406073 +2024-09-17 15:00:00,7,13.242869712424516,25.30874173067344,57.58842800134037 +2024-09-17 16:00:00,7,13.165437770021548,25.23607542773167,61.39758575186654 +2024-09-17 17:00:00,7,44.331802350765926,16.84853125877306,49.013309786065314 +2024-09-17 18:00:00,7,25.462665198959233,27.436922665149968,67.85881091736532 +2024-09-17 19:00:00,7,3.382819039937356,28.379522039394125,56.77986044102111 +2024-09-17 20:00:00,7,6.140426734041718,19.614486516129595,53.37377122772296 +2024-09-17 21:00:00,7,18.073892488516258,21.335015456828977,59.611680702945996 +2024-09-17 22:00:00,7,22.62668189991735,22.829780607821785,66.74981840323738 +2024-09-17 23:00:00,7,23.44120233212103,30.976143091507303,55.589531271629035 +2024-09-18 00:00:00,7,28.266594420666557,13.25340672206797,57.909747008078895 +2024-09-18 01:00:00,7,31.80584609818292,25.969170288073958,44.6439967474031 +2024-09-18 02:00:00,7,34.15896198939822,27.57001483315556,56.61974668182776 +2024-09-18 03:00:00,7,11.102778076844318,24.184480407534885,62.92267501832437 +2024-09-18 04:00:00,7,47.38876821404591,24.466066483153455,54.71227213846499 +2024-09-18 05:00:00,7,34.42174118856379,20.99618118465075,34.0865794403813 +2024-09-18 06:00:00,7,20.112188837088098,25.970505804060064,50.13969556568135 +2024-09-18 07:00:00,7,18.098846354173368,18.56931346270618,52.08898098652708 +2024-09-18 08:00:00,7,34.11802510385767,25.507678116448552,59.413503851821964 +2024-09-18 09:00:00,7,26.25945853604389,24.25153405154395,58.50567443346158 +2024-09-18 10:00:00,7,30.98876320616195,24.969659046658432,62.84346059051188 +2024-09-18 11:00:00,7,33.128429060118556,27.615542389282012,62.497129443250344 +2024-09-18 12:00:00,7,17.254739942797116,27.991526968556187,49.43013072747647 +2024-09-18 13:00:00,7,30.786491102708133,23.638845280891694,67.90246702530533 +2024-09-18 14:00:00,7,15.207510708365005,25.920788343214138,56.849211967349994 +2024-09-18 15:00:00,7,8.864820625640318,28.25325613745872,55.62543561516186 +2024-09-18 16:00:00,7,32.8715512410799,19.64013325165209,49.06975611640576 +2024-09-18 17:00:00,7,12.79582756582994,25.68345853666626,55.984958040818036 +2024-09-18 18:00:00,7,17.751320205360802,16.03203915582484,48.867777897717275 +2024-09-18 19:00:00,7,27.866320866440596,22.51190772623074,58.03906335470955 +2024-09-18 20:00:00,7,28.648643948186777,20.045146105353425,62.76273593345827 +2024-09-18 21:00:00,7,33.12852891748229,18.5300539094274,55.654977881305435 +2024-09-18 22:00:00,7,18.00934765136222,22.4797282652744,59.258060164562664 +2024-09-18 23:00:00,7,41.57014217647165,19.92543120449541,51.570473333500395 +2024-09-19 00:00:00,7,12.26203046027941,26.12621024470039,53.47555051985056 +2024-09-19 01:00:00,7,39.05001918121455,19.85227985089416,49.21621626278547 +2024-09-19 02:00:00,7,20.344904501307703,22.216841351109494,55.94588131008132 +2024-09-19 03:00:00,7,16.649230103014922,27.817537463807348,46.919496314187455 +2024-09-19 04:00:00,7,15.481348983082043,17.569695174444742,51.78930872209653 +2024-09-19 05:00:00,7,20.711187383689904,19.38979182643688,59.130233714309774 +2024-09-19 06:00:00,7,32.82367991263001,22.160000024489943,39.233412690272196 +2024-09-19 07:00:00,7,27.702140649038586,24.939526400929154,44.066920750024906 +2024-09-19 08:00:00,7,42.56183728614881,25.25783188549484,53.84798170513966 +2024-09-19 09:00:00,7,26.923077763757444,26.624771867878145,44.384134826915925 +2024-09-19 10:00:00,7,4.565190627488445,23.38187903334754,59.994580852626974 +2024-09-19 11:00:00,7,34.527986105459135,25.70709376161243,58.14589697357943 +2024-09-19 12:00:00,7,21.465587236116235,26.205475739706724,38.28966895411266 +2024-09-19 13:00:00,7,34.91455087207049,22.082012061105168,48.23206194951901 +2024-09-19 14:00:00,7,25.474915782992845,24.133258308428957,49.688663597224 +2024-09-19 15:00:00,7,23.097247247256703,22.150446999389214,58.15033896053632 +2024-09-19 16:00:00,7,14.8517879876055,22.49330045748225,39.29701230773759 +2024-09-19 17:00:00,7,13.34849503317648,24.894432160142358,38.47412154834341 +2024-09-19 18:00:00,7,0.0,18.21298004741159,52.72147575476969 +2024-09-19 19:00:00,7,28.781732509966652,24.66797913484389,51.63665276644666 +2024-09-19 20:00:00,7,36.15680254656536,27.38633814490465,48.896093044710284 +2024-09-19 21:00:00,7,10.356905754866064,22.776887750210296,60.14278843740723 +2024-09-19 22:00:00,7,30.776910164856726,20.862481472439644,47.09721606390295 +2024-09-19 23:00:00,7,31.39318455824429,17.93391213663486,53.21178750348581 +2024-09-20 00:00:00,7,17.711050071867966,21.22612839711989,51.870546917859606 +2024-09-20 01:00:00,7,44.97718817020757,22.037747283377435,54.73582150317154 +2024-09-20 02:00:00,7,7.901311578010418,25.73550264508304,45.76420168924775 +2024-09-20 03:00:00,7,26.74698652930573,19.656469981273503,48.5184336535319 +2024-09-20 04:00:00,7,17.968729640957758,17.84716568630769,50.66357334922911 +2024-09-20 05:00:00,7,6.951455056119576,22.717319380952798,44.29616671904846 +2024-09-20 06:00:00,7,37.97371309392496,21.905670606623747,61.53453716507835 +2024-09-20 07:00:00,7,35.17579480492526,23.890822761834418,70.30892304045226 +2024-09-20 08:00:00,7,18.01845168878213,21.411134043978226,52.35747271174671 +2024-09-20 09:00:00,7,35.20649429222354,24.266552179746085,44.587457285614626 +2024-09-20 10:00:00,7,44.87850832143946,24.770640697289945,58.203245342944506 +2024-09-20 11:00:00,7,23.412647553641104,21.922899084784298,50.25769784361817 +2024-09-20 12:00:00,7,39.03514839496172,24.249579907649576,45.26752806264611 +2024-09-20 13:00:00,7,30.29231022937276,19.875223281840235,58.52054843010906 +2024-09-20 14:00:00,7,16.259665127143073,25.84239289620204,58.189163914500426 +2024-09-20 15:00:00,7,17.369052464708147,21.602448456639564,53.42353533953831 +2024-09-20 16:00:00,7,7.218275336321126,21.799643431528672,41.62047543770208 +2024-09-20 17:00:00,7,35.45935900441106,25.314207219837797,51.51337020775812 +2024-09-20 18:00:00,7,9.73996361407465,22.287512633115128,40.10996097789474 +2024-09-20 19:00:00,7,18.156077487411,24.630205350505157,60.55173848780475 +2024-09-20 20:00:00,7,32.51402351329663,23.197844823825914,42.003167471232814 +2024-09-20 21:00:00,7,30.475143019060567,16.471859757766282,57.37127716195178 +2024-09-20 22:00:00,7,18.21962102004488,19.95957004753438,38.80896853455871 +2024-09-20 23:00:00,7,26.225597257856595,21.451848020537867,52.88626543277471 +2024-09-21 00:00:00,7,23.64588720683976,19.935812029316804,36.232198024205665 +2024-09-21 01:00:00,7,18.17675748757254,18.047161563176303,68.47985297577209 +2024-09-21 02:00:00,7,26.014919413859488,27.829564475829585,58.61036205408688 +2024-09-21 03:00:00,7,18.09579828041844,21.09841865482087,47.95831605882585 +2024-09-21 04:00:00,7,11.248564470321712,22.83153202422118,68.83547844587629 +2024-09-21 05:00:00,7,21.317031905439123,23.107467526503584,52.79090911541744 +2024-09-21 06:00:00,7,21.2856171545925,28.371152864867312,60.6889402229658 +2024-09-21 07:00:00,7,20.632482266672287,24.828937268715354,51.36008003953008 +2024-09-21 08:00:00,7,23.09070769908128,22.96172981368847,49.43154839406778 +2024-09-21 09:00:00,7,12.415497308054054,20.056624998267594,68.13961936419497 +2024-09-21 10:00:00,7,28.380243022147408,27.14891092216868,50.63386353647356 +2024-09-21 11:00:00,7,33.16787713814614,30.793799384032738,50.77475700647531 +2024-09-21 12:00:00,7,22.66539721891841,23.507015360177,50.884167515857285 +2024-09-21 13:00:00,7,19.331632829901064,19.889535959690114,51.82396355589495 +2024-09-21 14:00:00,7,22.421504623059565,25.972538202295592,47.28113781995902 +2024-09-21 15:00:00,7,26.674135399764985,21.680809760376796,48.6601313752471 +2024-09-21 16:00:00,7,16.397318099828073,25.835817431737315,60.31050232210146 +2024-09-21 17:00:00,7,14.791688148291502,13.717327122601965,43.03859628427957 +2024-09-21 18:00:00,7,16.180362868733347,27.39510434610952,51.6862451865061 +2024-09-21 19:00:00,7,19.541088149796707,20.691539001607403,48.98283204822249 +2024-09-21 20:00:00,7,27.842850172760198,23.521475567917815,47.469939712566095 +2024-09-21 21:00:00,7,16.862535680002694,21.840483594378348,42.59461484402887 +2024-09-21 22:00:00,7,33.37404902082168,22.456154121474835,55.009258339741756 +2024-09-21 23:00:00,7,12.665906722367046,18.968662164272548,60.60382187551512 +2024-09-22 00:00:00,7,21.40599184618268,22.229173391309782,49.43930062167205 +2024-09-22 01:00:00,7,13.207061765398931,24.88796522934152,58.120214221650535 +2024-09-22 02:00:00,7,7.084750023143508,20.840879948298458,53.73087324491011 +2024-09-22 03:00:00,7,32.86227053013429,18.581787110885767,57.672021553526534 +2024-09-22 04:00:00,7,31.16432616082546,21.400383246703004,44.29377198397244 +2024-09-22 05:00:00,7,0.5444317673088861,19.953073450689846,53.28486934520499 +2024-09-22 06:00:00,7,31.62387875730746,26.11638792372688,55.04052151743636 +2024-09-22 07:00:00,7,18.05580194966215,26.368834822217675,57.51559460475769 +2024-09-22 08:00:00,7,33.38402238283976,23.811209103985856,47.35323207598156 +2024-09-22 09:00:00,7,19.517987314326813,18.99035600318354,50.187950860793364 +2024-09-22 10:00:00,7,16.89549212666445,28.11512982595868,57.41068369532076 +2024-09-22 11:00:00,7,21.757221194494782,20.003058317656574,44.121519155014624 +2024-09-22 12:00:00,7,31.726972000952717,26.3737575718006,53.435554061492475 +2024-09-22 13:00:00,7,17.13056281419385,26.174381315684073,53.4447079175228 +2024-09-22 14:00:00,7,15.680813673978243,22.22166619533932,53.93973864130631 +2024-09-22 15:00:00,7,21.63455454060118,19.104783363691013,67.39147246653397 +2024-09-22 16:00:00,7,20.118780943169092,15.620256731142545,60.818861316257156 +2024-09-22 17:00:00,7,16.530383538300008,23.090492757599396,51.122301831389635 +2024-09-22 18:00:00,7,22.184228358587255,16.743300354588573,54.46900537276986 +2024-09-22 19:00:00,7,13.024393278749145,27.687768271462918,54.536980471503625 +2024-09-22 20:00:00,7,6.159659053118718,22.836362434215978,42.66052390787648 +2024-09-22 21:00:00,7,13.83309311215216,25.443002834498113,38.79959487310628 +2024-09-22 22:00:00,7,30.6533389548052,19.76872154620783,51.64082444301223 +2024-09-22 23:00:00,7,21.725831196422256,22.148548047492305,58.81299370073371 +2024-09-23 00:00:00,7,21.849343536494626,17.31510546030043,51.936517240053334 +2024-09-23 01:00:00,7,20.272455501498932,22.61689243958933,63.505087902148105 +2024-09-23 02:00:00,7,16.880572244801883,25.31645982063486,67.50739637566171 +2024-09-23 03:00:00,7,23.669835437178225,20.9764808184679,60.39104441578567 +2024-09-23 04:00:00,7,27.7398809162658,19.809225688400566,55.77349221150944 +2024-09-23 05:00:00,7,0.0,21.283927898534888,45.69715795791207 +2024-09-23 06:00:00,7,32.63976253816087,27.231987191748402,44.12755596571233 +2024-09-23 07:00:00,7,22.78585358203061,25.91628586866189,38.057571759912975 +2024-09-23 08:00:00,7,30.751774296311247,17.454867640208615,47.50924698673231 +2024-09-23 09:00:00,7,9.771416490218112,20.79805910821477,52.68991756642258 +2024-09-23 10:00:00,7,18.396225275291172,28.06401695583708,59.4423089918732 +2024-09-23 11:00:00,7,33.27667822998113,25.39950634110589,73.58271608400072 +2024-09-23 12:00:00,7,38.616092102388926,20.21911733118791,46.824039795965795 +2024-09-23 13:00:00,7,31.518530815974707,20.084804486878937,48.93128927925514 +2024-09-23 14:00:00,7,26.65430188740499,20.654422764792653,62.74370319904112 +2024-09-23 15:00:00,7,19.947374605316888,30.139247129131665,65.31514100347829 +2024-09-23 16:00:00,7,29.858402674128474,24.76706102005073,55.084727640092936 +2024-09-23 17:00:00,7,37.46201701487689,20.421000399438,42.66801564678477 +2024-09-23 18:00:00,7,8.202683052649752,24.647123339569593,49.30903332245458 +2024-09-23 19:00:00,7,36.13549288260053,19.526617965338765,62.08532475131627 +2024-09-23 20:00:00,7,16.030336208401305,16.340488752889307,43.44414438813785 +2024-09-23 21:00:00,7,28.90190325065414,24.37465064542397,55.20720148561263 +2024-09-23 22:00:00,7,38.109432192382755,23.822129015922872,50.866834434316736 +2024-09-23 23:00:00,7,18.354121817067544,23.171636623210674,51.73300486883577 +2024-09-24 00:00:00,7,34.164433575783235,23.863669534261028,57.58799621074214 +2024-09-24 01:00:00,7,30.792898104500573,25.977605407907355,50.08877958588107 +2024-09-24 02:00:00,7,12.679956675460481,22.20586211751063,59.02557925737911 +2024-09-24 03:00:00,7,39.701069941340705,18.5779443379917,55.75220709118112 +2024-09-24 04:00:00,7,20.36000065566652,19.29003488223962,39.74269326879339 +2024-09-24 05:00:00,7,26.408069545073964,20.49450540805204,52.57502221429932 +2024-09-24 06:00:00,7,26.37499427884595,23.45745818618377,45.2947673259144 +2024-09-24 07:00:00,7,24.280722073141224,25.51776188805988,58.11764921519865 +2024-09-24 08:00:00,7,15.376858536312767,25.65751324047172,44.10806349386135 +2024-09-24 09:00:00,7,18.22099017416344,22.280092163962955,54.17403488231722 +2024-09-24 10:00:00,7,36.367751602800894,22.848866110240593,48.02599335074621 +2024-09-24 11:00:00,7,30.28797975704022,18.545247873678356,73.85064257513608 +2024-09-24 12:00:00,7,18.831510290336453,26.51464730085952,42.68862886856399 +2024-09-24 13:00:00,7,18.792066199106404,23.893764687779814,51.38801369517333 +2024-09-24 14:00:00,7,25.567091841885965,19.672064288508547,57.11486166876138 +2024-09-24 15:00:00,7,26.856684869742395,24.050492464127235,58.934405711484445 +2024-09-24 16:00:00,7,10.366963292977522,26.02449114398134,53.854756914046796 +2024-09-24 17:00:00,7,24.625702167493593,25.709050351308715,54.859725171684474 +2024-09-24 18:00:00,7,28.950743072409892,21.940958820410742,44.810963221053505 +2024-09-24 19:00:00,7,23.693206898075104,25.81776097790122,37.17915065072801 +2024-09-24 20:00:00,7,12.903100754474906,15.396337684030172,51.84534333780649 +2024-09-24 21:00:00,7,21.436989955064114,25.912737609761574,66.95390469346736 +2024-09-24 22:00:00,7,37.57374928136382,12.393697286717849,43.691702554031 +2024-09-24 23:00:00,7,50.25194190783327,23.173405278329493,45.721314962761326 +2024-09-25 00:00:00,7,12.60460781215219,30.772579420326878,43.00600917546103 +2024-09-25 01:00:00,7,20.416680282380483,22.11994426144402,48.03255451655165 +2024-09-25 02:00:00,7,13.334454193803037,19.97108305847726,54.20515028544766 +2024-09-25 03:00:00,7,32.72816358817494,22.30798737420752,46.29056810691874 +2024-09-25 04:00:00,7,14.671912761499833,20.58508570565059,35.55050657584696 +2024-09-25 05:00:00,7,14.207119806321687,20.44143228450646,31.968595055449455 +2024-09-25 06:00:00,7,31.725418243933166,27.707723852410567,37.700136410193345 +2024-09-25 07:00:00,7,33.501216946489784,26.992404072682422,43.902819339310504 +2024-09-25 08:00:00,7,17.78440620553922,24.4065457301334,38.31258253288837 +2024-09-25 09:00:00,7,40.42372741338884,21.330996969967494,57.29990444649087 +2024-09-25 10:00:00,7,34.05439860793751,24.614261316774034,50.841273768654474 +2024-09-25 11:00:00,7,21.021019784338378,18.197949265201093,53.57514158221065 +2024-09-25 12:00:00,7,28.278348484290174,31.880501322170122,55.08720489307564 +2024-09-25 13:00:00,7,12.046458444060924,22.270224455553503,54.73930903367192 +2024-09-25 14:00:00,7,17.38522625471036,26.96367172267399,43.097647302585585 +2024-09-25 15:00:00,7,27.93622898941779,25.87941582025509,48.11643315377061 +2024-09-25 16:00:00,7,6.909184513632852,21.334269872193804,46.841870121212324 +2024-09-25 17:00:00,7,33.52546115808671,18.420461410199632,51.921530102605736 +2024-09-25 18:00:00,7,31.599106898015528,21.968643983995925,44.91040577203165 +2024-09-25 19:00:00,7,22.6925874634582,18.735242986913484,53.12068652522617 +2024-09-25 20:00:00,7,9.866548688949106,23.581663595121665,64.20435061697444 +2024-09-25 21:00:00,7,21.12474157842113,14.079220728257097,51.09603384544954 +2024-09-25 22:00:00,7,18.293120111331696,21.93291729864515,47.754979031288926 +2024-09-25 23:00:00,7,4.198523470131196,24.788311602537597,54.34622319267094 +2024-09-26 00:00:00,7,23.67226574352115,27.89205711272082,58.8096119235754 +2024-09-26 01:00:00,7,9.995710012481956,25.925990465808514,45.692450690753176 +2024-09-26 02:00:00,7,19.066627336808896,24.94026359884185,57.9488334492134 +2024-09-26 03:00:00,7,26.488858055776422,22.17583336121568,46.09457514702442 +2024-09-26 04:00:00,7,25.446375521384745,20.37026007526556,60.9800576520823 +2024-09-26 05:00:00,7,14.646100457379966,19.865073758781424,51.1411646817561 +2024-09-26 06:00:00,7,32.69208956587727,20.59058027704465,48.968386603357416 +2024-09-26 07:00:00,7,19.75458506174715,26.644417568531704,61.64085365922752 +2024-09-26 08:00:00,7,30.609616169808003,19.056985744104246,34.86557741298131 +2024-09-26 09:00:00,7,19.47386338361431,26.220696495764944,57.28019255390622 +2024-09-26 10:00:00,7,27.886639053198497,18.449444278258017,48.73217581206411 +2024-09-26 11:00:00,7,25.60978106667421,23.634231898228474,46.46711512400995 +2024-09-26 12:00:00,7,32.05719686249075,24.182444926224086,49.109684776203274 +2024-09-26 13:00:00,7,30.040431119332485,24.085066864433823,52.34325356423811 +2024-09-26 14:00:00,7,32.48090773267613,25.658714884665216,57.76267714250226 +2024-09-26 15:00:00,7,32.0968297171128,23.217893051931167,43.046472128331004 +2024-09-26 16:00:00,7,34.99434683353991,28.178786778021653,63.448949048201875 +2024-09-26 17:00:00,7,40.060858963058365,18.35621926159182,51.546849859683164 +2024-09-26 18:00:00,7,6.351525504510441,23.94874260970527,55.261235707777615 +2024-09-26 19:00:00,7,16.491337341814784,25.821293903806733,37.290243311399294 +2024-09-26 20:00:00,7,22.858718838254518,23.268779742218065,48.017602592758756 +2024-09-26 21:00:00,7,33.44273703884737,20.63240140987648,62.53347669965588 +2024-09-26 22:00:00,7,23.29410576506962,16.027613748961393,60.55890465157644 +2024-09-26 23:00:00,7,37.1600581341709,21.053305208543787,57.79862882077833 +2024-09-27 00:00:00,7,24.578934074497564,23.558019693004983,53.29504580504642 +2024-09-27 01:00:00,7,28.690591772769395,19.27379416638819,38.30154462688812 +2024-09-27 02:00:00,7,30.92857267298207,15.846847229760197,34.57374640685696 +2024-09-27 03:00:00,7,42.47615941032258,32.45792770223021,57.58964965901821 +2024-09-27 04:00:00,7,33.561785161005076,23.470334679065264,53.302167588411166 +2024-09-27 05:00:00,7,21.41964539150755,19.855129017447474,54.54587732678144 +2024-09-27 06:00:00,7,37.09541260329316,23.122124820647137,43.13056993417513 +2024-09-27 07:00:00,7,14.495279268010144,20.17129875061836,60.691140529308015 +2024-09-27 08:00:00,7,23.031902501957624,26.002180689650103,59.063367056409206 +2024-09-27 09:00:00,7,29.246092209823882,26.425515904383328,50.84223599352759 +2024-09-27 10:00:00,7,33.0509088724364,20.731612577183707,42.37520479094047 +2024-09-27 11:00:00,7,34.13190928847177,23.59898380461107,46.68120286246238 +2024-09-27 12:00:00,7,15.137570866508709,21.743537433677183,49.864578494239026 +2024-09-27 13:00:00,7,24.037924023480844,22.470510290704475,70.60621136992536 +2024-09-27 14:00:00,7,26.748146589285298,23.460115030090567,67.22744977762997 +2024-09-27 15:00:00,7,29.087360887050274,20.71387238782704,44.444766915985554 +2024-09-27 16:00:00,7,11.849569911642659,18.966873719477196,59.39753429463153 +2024-09-27 17:00:00,7,20.40423723917369,23.80072896919303,52.359381658042956 +2024-09-27 18:00:00,7,37.91843210961294,18.796063586250366,59.252170226757684 +2024-09-27 19:00:00,7,7.2903218102878515,21.706692329502925,49.46740429930557 +2024-09-27 20:00:00,7,29.04799347134444,24.722618167711396,54.4585457635876 +2024-09-27 21:00:00,7,9.592846778626544,24.531811714839705,68.60244060137072 +2024-09-27 22:00:00,7,22.427458140640397,22.70112625984528,54.735839945499244 +2024-09-27 23:00:00,7,18.210900695337603,19.768147065967156,60.40511394412252 +2024-09-28 00:00:00,7,23.473316319486223,16.242485229025494,45.13327580264899 +2024-09-28 01:00:00,7,10.769308143833843,26.395388734782692,49.47950660607418 +2024-09-28 02:00:00,7,20.97199360866471,20.24173666755705,68.43870202362771 +2024-09-28 03:00:00,7,31.903822120455814,20.853593895450768,40.7864940446318 +2024-09-28 04:00:00,7,20.988563138680835,29.583096068867384,50.187848496851885 +2024-09-28 05:00:00,7,24.96079994170292,22.959449735791374,41.27222576211519 +2024-09-28 06:00:00,7,35.984706190223775,29.201994012205596,62.52029933155385 +2024-09-28 07:00:00,7,18.960296031074755,24.639350054260927,66.576857138435 +2024-09-28 08:00:00,7,36.45356984090769,20.493075195140126,57.18102279817711 +2024-09-28 09:00:00,7,23.79113122966838,25.57116576792921,73.06574439450574 +2024-09-28 10:00:00,7,41.224865895218386,22.948497215860325,57.81760966903493 +2024-09-28 11:00:00,7,37.79441348268302,21.05742832284741,53.84252894498278 +2024-09-28 12:00:00,7,28.44593042977506,26.84308357948373,52.270285335541324 +2024-09-28 13:00:00,7,17.108114110929087,25.409099756899433,56.00594920485824 +2024-09-28 14:00:00,7,8.715019122348489,26.886882843627088,47.31579342001551 +2024-09-28 15:00:00,7,14.61809744052156,25.713663656469286,51.785745585659036 +2024-09-28 16:00:00,7,15.890893453808514,21.21073493601446,53.30297467240493 +2024-09-28 17:00:00,7,37.02396538662305,21.51771656102944,51.13334098197138 +2024-09-28 18:00:00,7,15.197682654189883,26.263002921013417,48.98621363747246 +2024-09-28 19:00:00,7,14.318519516330806,27.072173531762807,52.03071009092539 +2024-09-28 20:00:00,7,11.626744912118568,20.87667957480243,42.471371259503286 +2024-09-28 21:00:00,7,22.715846201080627,21.439737767222734,36.61300382770873 +2024-09-28 22:00:00,7,31.12191979771884,14.984413784906351,56.98837371016005 +2024-09-28 23:00:00,7,6.688769115671285,20.24889966331384,39.84318738532522 +2024-09-29 00:00:00,7,25.161630286007906,18.281471137898162,36.497044692807236 +2024-09-29 01:00:00,7,23.009669261138495,23.614683475728725,49.230443621315 +2024-09-29 02:00:00,7,27.78255936360113,24.729651966815137,64.59403046614028 +2024-09-29 03:00:00,7,8.789817143669096,25.878757407390502,64.17725153291887 +2024-09-29 04:00:00,7,24.151957280175953,21.696105555174775,49.373081445139455 +2024-09-29 05:00:00,7,32.32663454351808,25.023294002009468,50.1559990350839 +2024-09-29 06:00:00,7,2.754761482753988,18.723353561202373,58.61786813136471 +2024-09-29 07:00:00,7,17.312982354856693,23.995734896590612,51.267995006925005 +2024-09-29 08:00:00,7,11.490891567523729,20.015766147912277,63.71337245739941 +2024-09-29 09:00:00,7,30.149026851007896,27.266208230595538,61.20093961934021 +2024-09-29 10:00:00,7,31.408011621588898,18.662253440314736,53.201087559956484 +2024-09-29 11:00:00,7,26.535992146038346,24.721226593620596,50.049031192526144 +2024-09-29 12:00:00,7,13.038986159363205,25.775355142183656,45.990045990152794 +2024-09-29 13:00:00,7,29.277586129618726,27.616272905490312,60.08950876080467 +2024-09-29 14:00:00,7,6.58983883806212,24.790192564937378,54.118993343957406 +2024-09-29 15:00:00,7,18.98944502090263,21.621270361004484,60.76514581438604 +2024-09-29 16:00:00,7,37.46665152640912,19.908339726256656,69.00776407670516 +2024-09-29 17:00:00,7,13.86240032584557,27.44194354701215,55.08466142316575 +2024-09-29 18:00:00,7,6.008555994883366,21.555790187443513,46.140838660743974 +2024-09-29 19:00:00,7,30.68886264784162,19.72118012395423,42.49520966995275 +2024-09-29 20:00:00,7,25.47268458751397,20.08388187548162,56.027950451557494 +2024-09-29 21:00:00,7,17.006674380250427,20.548626388085985,58.56437375257066 +2024-09-29 22:00:00,7,13.521953951460983,18.428978611444354,48.0034701292807 +2024-09-29 23:00:00,7,18.022081030238798,20.672598386967536,61.57313764717789 +2024-09-30 00:00:00,7,18.625029102895315,27.567232411204277,55.844274894493154 +2024-09-30 01:00:00,7,0.0,23.64011307538839,52.207557469267634 +2024-09-30 02:00:00,7,24.963990756299207,26.756763663921394,67.67117520990655 +2024-09-30 03:00:00,7,23.340985361696678,25.950225041163996,52.56922679488599 +2024-09-30 04:00:00,7,11.192504491562833,22.639095869932046,41.58462803738997 +2024-09-30 05:00:00,7,16.513970455711515,19.129787360739915,60.32501754209805 +2024-09-30 06:00:00,7,25.11837093614694,22.918939004117497,50.36558790736549 +2024-09-30 07:00:00,7,35.50724470995541,21.61528102337145,31.08909276836929 +2024-09-30 08:00:00,7,24.411905008052944,25.10906594001282,68.54972743589632 +2024-09-30 09:00:00,7,20.44974307928178,27.17254430421798,50.38126513827811 +2024-09-30 10:00:00,7,18.914788548834366,24.825852038294933,50.78897456840025 +2024-09-30 11:00:00,7,33.692452945422104,23.61733259364019,61.04740279094684 +2024-09-30 12:00:00,7,22.347205482229906,26.920774697823187,50.827815173303975 +2024-09-30 13:00:00,7,13.45426281788643,16.031602605375983,57.23424538472793 +2024-09-30 14:00:00,7,11.69976904548679,15.48693204385712,52.77544369257186 +2024-09-30 15:00:00,7,33.94210491106521,20.6962266967965,51.105029763880744 +2024-09-30 16:00:00,7,9.950712365467819,14.084143608930273,60.770528282614094 +2024-09-30 17:00:00,7,12.241182896397989,21.218924548291,59.89593161672398 +2024-09-30 18:00:00,7,1.1780083942325383,16.523233015957313,57.19330021903769 +2024-09-30 19:00:00,7,7.5020411820724995,20.627289694074285,59.34290437815811 +2024-09-30 20:00:00,7,4.566497526297304,19.630277566044203,67.40352728637345 +2024-09-30 21:00:00,7,19.068552849980275,22.382063381295815,51.95386314389788 +2024-09-30 22:00:00,7,32.18219560017591,21.484358717055805,50.092564306131074 +2024-09-30 23:00:00,7,24.369258287462593,23.35461993134297,51.41848787958825 +2024-10-01 00:00:00,7,10.26859623150791,24.409480754839553,38.23413665137263 +2024-10-01 01:00:00,7,11.913382427177657,16.98737473941459,54.75920396497602 +2024-10-01 02:00:00,7,22.518409667698748,23.06280169357068,56.573495493108965 +2024-10-01 03:00:00,7,48.28763859235904,22.37762532016894,48.775278658673564 +2024-10-01 04:00:00,7,7.8532700454754885,23.13712438274577,54.279954614970215 +2024-10-01 05:00:00,7,26.90613199180394,20.084905198361618,67.91328548032936 +2024-10-01 06:00:00,7,13.11954659987634,26.350077988158695,61.37638498895405 +2024-10-01 07:00:00,7,22.259274394952335,27.165231581971856,53.86140491539083 +2024-10-01 08:00:00,7,51.31473264520612,28.79017504458157,49.303706686917636 +2024-10-01 09:00:00,7,7.505193549261705,25.49258815636197,54.55108011848421 +2024-10-01 10:00:00,7,26.45090911744383,22.624265167954825,65.14544867380143 +2024-10-01 11:00:00,7,21.860595671233394,23.886459211955625,55.069393917906254 +2024-10-01 12:00:00,7,22.306977492783062,23.580105356026674,61.021523288700855 +2024-10-01 13:00:00,7,23.89948453491324,21.91879528902361,64.94151394393555 +2024-10-01 14:00:00,7,14.400166773294327,21.833229715128297,46.55412592052738 +2024-10-01 15:00:00,7,12.693176862933345,23.682042353549043,50.03886976105265 +2024-10-01 16:00:00,7,13.661277385499961,22.010033973828325,43.77229114659882 +2024-10-01 17:00:00,7,27.92027347236582,20.521010844082827,57.82897642919936 +2024-10-01 18:00:00,7,27.031935778732063,23.253836580454852,76.73567119653774 +2024-10-01 19:00:00,7,24.22115739058844,27.167542251051096,40.82874250691832 +2024-10-01 20:00:00,7,20.413912238346704,19.61820649623947,55.106395632033255 +2024-10-01 21:00:00,7,21.605392351832194,21.584713322643154,50.54356608875202 +2024-10-01 22:00:00,7,23.945791806428428,25.890919638083805,55.75701517583765 +2024-10-01 23:00:00,7,32.740232169137684,19.264012823376437,52.344662331440375 +2024-10-02 00:00:00,7,20.967488833146042,22.772013273145358,37.52565763451422 +2024-10-02 01:00:00,7,15.032279223049485,19.590518779503647,57.62316392947636 +2024-10-02 02:00:00,7,36.451082131033836,24.93726357715603,60.16959644086681 +2024-10-02 03:00:00,7,17.348363623453295,26.82011067823537,44.673705611080834 +2024-10-02 04:00:00,7,34.75979224753904,23.051055072569937,50.46939805443257 +2024-10-02 05:00:00,7,24.22027381479243,25.02094069918075,36.722174591256426 +2024-10-02 06:00:00,7,19.03767998786691,27.22559051130277,46.246470265257315 +2024-10-02 07:00:00,7,35.022921699649615,22.52710544499409,36.25963403779963 +2024-10-02 08:00:00,7,22.980069811752255,25.022392287195515,51.57626490172948 +2024-10-02 09:00:00,7,27.829630617272446,25.45877645761637,65.39677631602093 +2024-10-02 10:00:00,7,42.16622965662331,23.241889727852136,60.3380048579201 +2024-10-02 11:00:00,7,11.247642350109901,19.96041919483247,60.27901111962835 +2024-10-02 12:00:00,7,25.56825943823859,24.959616625199743,54.35116308003668 +2024-10-02 13:00:00,7,27.161177909811787,27.044139314072915,43.104228770012924 +2024-10-02 14:00:00,7,3.093853654082377,22.481988753301508,63.92087708188656 +2024-10-02 15:00:00,7,24.563639694656622,22.48931791076878,58.55470847718629 +2024-10-02 16:00:00,7,19.630962253453383,24.01612049758274,51.57757751884628 +2024-10-02 17:00:00,7,6.145548705699815,26.693653011929495,55.61266607084675 +2024-10-02 18:00:00,7,30.589321139506037,21.597508256056894,50.358049054674474 +2024-10-02 19:00:00,7,28.31242091799627,18.140364068645702,59.42696275079478 +2024-10-02 20:00:00,7,11.685932416427004,15.572294242988953,56.20844882413217 +2024-10-02 21:00:00,7,21.14061299430626,17.789586212494843,52.906117506117226 +2024-10-02 22:00:00,7,33.88621235534717,22.063164187863112,68.51204414116134 +2024-10-02 23:00:00,7,38.4634406456255,23.24671062343981,59.56862553528195 +2024-10-03 00:00:00,7,28.792816124611427,22.91245664392935,50.678394038220404 +2024-10-03 01:00:00,7,22.183781956373675,20.995468037135435,67.06461619061294 +2024-10-03 02:00:00,7,21.493043632552165,22.037117913288586,48.0750373557399 +2024-10-03 03:00:00,7,12.33669117853359,19.517480197294685,41.00366841286086 +2024-10-03 04:00:00,7,36.82700641151008,20.487827349580847,44.08428683309312 +2024-10-03 05:00:00,7,15.424716509249645,19.942982919126127,42.29923184374916 +2024-10-03 06:00:00,7,31.532954898519513,23.28166992879455,60.951933962954406 +2024-10-03 07:00:00,7,11.712678051635116,22.848430447716616,49.05794731114797 +2024-10-03 08:00:00,7,9.482144985749748,34.16500415144471,52.56244654630494 +2024-10-03 09:00:00,7,40.404810492741476,20.21322637200883,64.73007289874246 +2024-10-03 10:00:00,7,23.303033497795237,27.683991793277478,57.72661568649748 +2024-10-03 11:00:00,7,32.6076431445743,21.237514113147995,46.46732644520179 +2024-10-03 12:00:00,7,24.64806193930781,20.37490419741621,61.26121803224273 +2024-10-03 13:00:00,7,29.581469417625307,24.40584833843383,63.26273349093152 +2024-10-03 14:00:00,7,27.57668601019664,20.528846371722096,54.11397806452674 +2024-10-03 15:00:00,7,12.103945959316755,19.86108038149923,57.342148042913664 +2024-10-03 16:00:00,7,23.39233097664243,23.6340758380708,59.423223637880874 +2024-10-03 17:00:00,7,36.215422254267395,23.773290195494752,51.947075117916974 +2024-10-03 18:00:00,7,25.334489488229217,19.667127511261814,62.68062907967391 +2024-10-03 19:00:00,7,24.544314308711943,26.30650887948539,39.75792787819712 +2024-10-03 20:00:00,7,12.22911140621313,23.868622511780075,52.107686568278304 +2024-10-03 21:00:00,7,28.461608140532057,23.94662319719396,60.758818279667906 +2024-10-03 22:00:00,7,28.354764681261614,19.879218080254397,58.096274641781925 +2024-10-03 23:00:00,7,27.437536640953855,22.4943836144207,51.84157133872983 +2024-10-04 00:00:00,7,27.634167009291488,25.95532325842214,48.92028759533209 +2024-10-04 01:00:00,7,16.04261521058433,18.99402743432924,52.32215907282282 +2024-10-04 02:00:00,7,21.71089754832824,23.371810150905795,61.65289485123957 +2024-10-04 03:00:00,7,17.787313007334625,21.614531199107155,40.54674927197138 +2024-10-04 04:00:00,7,15.960843922268552,29.242823620893066,59.99073854928996 +2024-10-04 05:00:00,7,29.157699680240768,21.74003735851858,64.46780137380276 +2024-10-04 06:00:00,7,16.281253705752313,22.59965041671513,53.132261358404975 +2024-10-04 07:00:00,7,15.121254997781387,24.944853678484883,60.20727434742376 +2024-10-04 08:00:00,7,50.68029831205946,17.703719774242607,52.48152542304839 +2024-10-04 09:00:00,7,12.898146932584167,27.04017472498265,47.50350185867466 +2024-10-04 10:00:00,7,41.90916502740035,22.323018577654043,55.41534491280401 +2024-10-04 11:00:00,7,25.899068683104296,23.484108337692337,53.09643329820857 +2024-10-04 12:00:00,7,16.63089408937184,14.64487321658626,46.85571732102842 +2024-10-04 13:00:00,7,14.13527703991551,23.316509786573132,56.83475799972164 +2024-10-04 14:00:00,7,14.581082248232255,22.411916176797604,57.14722809601909 +2024-10-04 15:00:00,7,22.667128268212522,21.929756715296257,47.83457302845767 +2024-10-04 16:00:00,7,22.68069253668976,20.64015990643189,53.52777357406567 +2024-10-04 17:00:00,7,44.65874377672583,24.242137667641508,50.921765991542024 +2024-10-04 18:00:00,7,36.47470128097969,20.13535771958471,42.00687121664348 +2024-10-04 19:00:00,7,15.847806634631677,23.412350763826435,48.72386194151154 +2024-10-04 20:00:00,7,29.720913022184696,21.697399151990496,51.93911715635768 +2024-10-04 21:00:00,7,24.682712553288752,23.70984500796208,61.008084497412625 +2024-10-04 22:00:00,7,28.89837634164369,25.999938249103927,68.43719550721381 +2024-10-04 23:00:00,7,25.937447028486858,19.088876047340342,62.746698207301485 +2024-10-05 00:00:00,7,32.37717929593069,26.230428749951045,43.272824463938214 +2024-10-05 01:00:00,7,13.758167792010106,21.20952572449631,51.26348384522937 +2024-10-05 02:00:00,7,25.987972350548468,23.414151899680753,48.93652606391569 +2024-10-05 03:00:00,7,20.22965516175927,18.04656018577426,42.3887579646755 +2024-10-05 04:00:00,7,22.996634526179324,22.92323786286218,32.90239130717304 +2024-10-05 05:00:00,7,21.020715886458486,24.302104489153784,47.71138505184033 +2024-10-05 06:00:00,7,29.33899209362166,21.042826887835776,48.03635384942546 +2024-10-05 07:00:00,7,30.145590627360406,24.647950451134324,41.112890404100604 +2024-10-05 08:00:00,7,2.0424447565571384,21.759159480088336,58.52574135824132 +2024-10-05 09:00:00,7,21.463435651088883,12.8352376461309,43.09573284165181 +2024-10-05 10:00:00,7,28.68552333167288,29.848196991069234,63.51904516756713 +2024-10-05 11:00:00,7,22.084123841962047,24.88051261573352,54.476372484041406 +2024-10-05 12:00:00,7,0.0,19.654253325599797,40.69259836316491 +2024-10-05 13:00:00,7,30.84190038116237,31.251153766510992,61.591364504082584 +2024-10-05 14:00:00,7,35.007933028803855,30.51611809147387,51.61645093683306 +2024-10-05 15:00:00,7,26.313573177970895,21.629469484661854,53.8089774724227 +2024-10-05 16:00:00,7,35.16583579267351,27.481421150925485,42.28791653071183 +2024-10-05 17:00:00,7,24.953277458116656,20.10393047392356,57.37445553851782 +2024-10-05 18:00:00,7,16.058804256754662,27.516763957511664,53.00346996029361 +2024-10-05 19:00:00,7,19.533655256735038,23.433800336083934,43.46371106673723 +2024-10-05 20:00:00,7,40.0088627253657,21.185181454841903,41.41263111145774 +2024-10-05 21:00:00,7,14.770284504296116,16.78132021776284,63.24868566658047 +2024-10-05 22:00:00,7,8.3489591548377,24.51263028282064,46.53564497767381 +2024-10-05 23:00:00,7,40.7403731287189,28.19389908663044,50.426653563743415 +2024-10-06 00:00:00,7,23.00567197853873,21.701290584846703,57.9285951392634 +2024-10-06 01:00:00,7,9.788771144712792,17.80540918721617,44.95524111068091 +2024-10-06 02:00:00,7,23.355883934950537,19.06393902611081,59.5130748921686 +2024-10-06 03:00:00,7,33.31253210372026,30.08165434328744,46.40526835879333 +2024-10-06 04:00:00,7,22.537375982373113,19.55888567464606,52.04596951493257 +2024-10-06 05:00:00,7,17.29143205227891,27.717118976251943,56.492264905814956 +2024-10-06 06:00:00,7,17.828894268292977,22.87468242672673,40.171450977246025 +2024-10-06 07:00:00,7,6.185664318075901,20.5100364913962,38.08372590245151 +2024-10-06 08:00:00,7,38.0681336599029,14.566686504827564,59.47937801128988 +2024-10-06 09:00:00,7,17.98662464222152,19.857005714923808,51.780563075322185 +2024-10-06 10:00:00,7,28.227749835897484,32.052841970315725,58.65461167838117 +2024-10-06 11:00:00,7,19.693689140062773,21.450360351096265,50.6246300399036 +2024-10-06 12:00:00,7,8.533577718574689,25.890966182001748,71.34920129091523 +2024-10-06 13:00:00,7,18.316702042224566,29.48622156279395,58.58204175195026 +2024-10-06 14:00:00,7,20.189130070061935,24.900049610776612,52.570895100010745 +2024-10-06 15:00:00,7,21.70793876312129,27.323704646471484,64.97836048103655 +2024-10-06 16:00:00,7,41.80077567461406,24.398690141202728,61.62069205755922 +2024-10-06 17:00:00,7,22.120110552853188,24.12260508436569,49.922024859171025 +2024-10-06 18:00:00,7,29.667219586033646,19.924638760908202,54.08834783858909 +2024-10-06 19:00:00,7,18.224715531739136,22.813880088388327,49.45711049545611 +2024-10-06 20:00:00,7,29.01410912152539,22.45600711955696,46.58603662364195 +2024-10-06 21:00:00,7,12.988580451404397,21.376311989218973,61.88822435127515 +2024-10-06 22:00:00,7,14.353247838543993,16.476485847637143,63.73678979903956 +2024-10-06 23:00:00,7,35.850640139767414,19.74180592464484,50.13456750979651 +2024-10-07 00:00:00,7,33.91792602428836,23.498035192070287,49.271547566944186 +2024-10-07 01:00:00,7,6.096853084345989,20.479404919056307,48.19056475479268 +2024-10-07 02:00:00,7,29.55660357407533,22.140848392395547,45.00762379686597 +2024-10-07 03:00:00,7,11.415561660264498,22.280339359098054,42.98081071590309 +2024-10-07 04:00:00,7,26.143181939103293,23.330117670011965,56.114502287428124 +2024-10-07 05:00:00,7,22.079386844126773,15.299033226965102,67.97582765313003 +2024-10-07 06:00:00,7,22.491594665460084,25.83592766743285,56.37334412385273 +2024-10-07 07:00:00,7,29.70087530645161,21.872208768903526,57.32393388134511 +2024-10-07 08:00:00,7,20.91445243892373,31.134691029588886,34.471154400111374 +2024-10-07 09:00:00,7,19.357422113255296,18.221821664165606,48.6433958148366 +2024-10-07 10:00:00,7,23.48748411195884,18.160062313208197,69.03474262690054 +2024-10-07 11:00:00,7,25.003805301359364,23.937474734727864,59.79125345340121 +2024-10-07 12:00:00,7,19.536364495641536,27.77729007825227,44.934320311872995 +2024-10-07 13:00:00,7,6.850572244029376,18.62708008722172,61.602327806789916 +2024-10-07 14:00:00,7,46.72627298845333,24.479601955209247,51.93611430662024 +2024-10-07 15:00:00,7,20.280543083581026,26.60022771854105,50.9907038057145 +2024-10-07 16:00:00,7,17.027265003902954,22.751994690834778,52.686647152244575 +2024-10-07 17:00:00,7,25.372027673069454,24.34373138243359,49.41045494326568 +2024-10-07 18:00:00,7,13.753921617859199,15.661550065262363,46.00548706632136 +2024-10-07 19:00:00,7,35.59331028192986,24.160691447715045,51.22307754673002 +2024-10-07 20:00:00,7,4.18894922958517,26.376279310520502,49.01940340519116 +2024-10-07 21:00:00,7,28.50668097088223,22.536530723921388,36.47028468176713 +2024-10-07 22:00:00,7,10.94450969673835,26.60292337863728,60.69082809340001 +2024-10-07 23:00:00,7,15.909331480763765,24.65754500884344,61.67524134888219 +2024-10-08 00:00:00,7,32.27329706930513,21.169344247061797,56.15791718182841 +2024-10-08 01:00:00,7,37.555038660589716,28.446711097597408,41.45029504586047 +2024-10-08 02:00:00,7,21.21054648240447,22.19006221470281,51.73674581363849 +2024-10-08 03:00:00,7,43.47407832319884,22.089129049393254,62.974035803630635 +2024-10-08 04:00:00,7,13.71753009857295,23.711040929271554,47.90768888759447 +2024-10-08 05:00:00,7,12.276204542056957,18.608139316446074,43.715211250602515 +2024-10-08 06:00:00,7,31.372189101177273,21.34669693971281,71.62500758262496 +2024-10-08 07:00:00,7,25.046816946357076,18.14006636133376,42.582506984798684 +2024-10-08 08:00:00,7,27.665444458664556,27.526009690937293,46.45348826570054 +2024-10-08 09:00:00,7,20.17400906666351,28.980702441619393,63.94846126916489 +2024-10-08 10:00:00,7,14.82565106178928,22.748782306895727,53.596211453368376 +2024-10-08 11:00:00,7,29.070095960499664,22.02819229729353,43.39869185174107 +2024-10-08 12:00:00,7,28.48134011743852,18.230986493758962,63.963223520551864 +2024-10-08 13:00:00,7,32.818931104488314,24.73646850743234,41.07440049297853 +2024-10-08 14:00:00,7,15.344487177401964,21.421207349331194,42.17574195362616 +2024-10-08 15:00:00,7,10.292549584816559,22.43703010949593,39.07523917320342 +2024-10-08 16:00:00,7,38.31733078142494,23.472662310945196,46.46156349282626 +2024-10-08 17:00:00,7,30.507428205961617,25.616992856269842,59.32546963776619 +2024-10-08 18:00:00,7,12.121076363138839,26.928790755750832,52.190020699395546 +2024-10-08 19:00:00,7,18.662578153380675,13.787298215624054,59.374470951889705 +2024-10-08 20:00:00,7,18.82854218872602,23.441635882233733,62.24490976399465 +2024-10-08 21:00:00,7,26.07605270746549,22.100934979571903,55.07408922861643 +2024-10-08 22:00:00,7,48.81337482321562,26.954922102492915,62.897017742294594 +2024-10-08 23:00:00,7,22.191951248169666,24.009814403638835,45.46999666054912 +2024-10-09 00:00:00,7,27.93120824824925,20.043909988821902,54.19100860063321 +2024-10-09 01:00:00,7,12.484898497584723,21.476551689827392,52.02815651121062 +2024-10-09 02:00:00,7,0.0,18.160460950598342,39.174798919362196 +2024-10-09 03:00:00,7,6.751274103644679,24.384120186178485,50.70654000095082 +2024-10-09 04:00:00,7,25.84317407849268,24.585203272260298,40.174633520765155 +2024-10-09 05:00:00,7,26.44265272147987,27.434195744847052,53.31596676351366 +2024-10-09 06:00:00,7,29.404533541298818,25.328514632055906,54.265263359900146 +2024-10-09 07:00:00,7,17.858720700539685,22.041862243383136,50.905219023855 +2024-10-09 08:00:00,7,10.931401026932534,24.892916063023808,35.9664571332071 +2024-10-09 09:00:00,7,28.344320587712826,22.202392526336844,50.14280352020117 +2024-10-09 10:00:00,7,26.305355191843294,30.124869876359167,49.87923184833467 +2024-10-09 11:00:00,7,16.157839160138302,22.283091757745837,68.89771522735258 +2024-10-09 12:00:00,7,33.13478085335156,20.797834684174653,45.81589725963771 +2024-10-09 13:00:00,7,23.947882045454996,22.154935633151077,66.6073588702339 +2024-10-09 14:00:00,7,38.2231659241643,20.18001738756852,61.11010172951776 +2024-10-09 15:00:00,7,8.900184355215131,22.72350510050179,37.44723361433924 +2024-10-09 16:00:00,7,16.79164015137794,21.40080629822531,40.70167435409826 +2024-10-09 17:00:00,7,27.039987894953782,21.277404950812706,53.79766533450405 +2024-10-09 18:00:00,7,16.44987323902385,22.116875982790578,48.999545565443114 +2024-10-09 19:00:00,7,23.610951534346665,23.759157612600774,42.38797811535227 +2024-10-09 20:00:00,7,31.68965413597364,21.017715337963516,55.92002764148352 +2024-10-09 21:00:00,7,19.032654276971883,17.383956493049453,66.37823297471206 +2024-10-09 22:00:00,7,24.817890424623954,23.013924233864522,53.0176438495054 +2024-10-09 23:00:00,7,12.456313398455473,14.885457897834362,47.696607889583774 +2024-10-10 00:00:00,7,23.50752293246972,27.333819806702664,51.0073819509468 +2024-10-10 01:00:00,7,3.125374475233908,18.382758726660757,45.09965577317994 +2024-10-10 02:00:00,7,18.20468771167147,28.184889500134386,49.68179476586076 +2024-10-10 03:00:00,7,29.148169744724704,24.948130717773267,43.0065422899583 +2024-10-10 04:00:00,7,31.981229204988814,20.14314235167568,46.917785176520006 +2024-10-10 05:00:00,7,26.787018483077226,22.609372106696057,50.64584223813207 +2024-10-10 06:00:00,7,33.18068772153913,21.418425534114338,58.08576573982664 +2024-10-10 07:00:00,7,40.22844806155618,24.18278393812604,49.73547078348514 +2024-10-10 08:00:00,7,24.477763484443336,30.28790484768251,48.18834223067607 +2024-10-10 09:00:00,7,31.27233553509858,26.02995296298888,51.429440315960825 +2024-10-10 10:00:00,7,20.99834887498432,23.862987669220615,45.597478111154246 +2024-10-10 11:00:00,7,36.090014569221154,25.071579245420494,68.62216710629191 +2024-10-10 12:00:00,7,31.188283841538862,24.600091991170896,43.05935111294966 +2024-10-10 13:00:00,7,17.970510354718265,27.122772254112725,70.43617225943507 +2024-10-10 14:00:00,7,18.341288005779788,22.888435188569158,47.04759929289571 +2024-10-10 15:00:00,7,20.072628347433074,22.596656570408413,51.23993396050255 +2024-10-10 16:00:00,7,22.70645707238583,18.67990178305251,56.963698227928 +2024-10-10 17:00:00,7,19.836753633629804,24.947524649589266,59.31100853351765 +2024-10-10 18:00:00,7,20.886833165554048,20.7920689848293,53.92586519589703 +2024-10-10 19:00:00,7,27.863235788822692,17.211369409392088,62.59842517189388 +2024-10-10 20:00:00,7,32.860903414735944,17.40891639419635,51.825242232654155 +2024-10-10 21:00:00,7,14.357638821396254,19.54802154192658,41.96688022559255 +2024-10-10 22:00:00,7,12.646796686155657,19.72614133850029,43.93055465729745 +2024-10-10 23:00:00,7,20.896473059468605,22.64073644438533,60.17218319078228 +2024-10-11 00:00:00,7,25.67556473042686,26.010881101867017,66.54473846068312 +2024-10-11 01:00:00,7,27.886493489402163,19.250886877652743,51.53960281992664 +2024-10-11 02:00:00,7,8.632084985842138,26.008686409036716,63.831232412756506 +2024-10-11 03:00:00,7,29.98133245255704,20.27372648558576,57.99595913004906 +2024-10-11 04:00:00,7,40.58116753691833,24.61774856343405,48.22412503258282 +2024-10-11 05:00:00,7,30.253087874358133,23.096712962909102,41.52724501016003 +2024-10-11 06:00:00,7,22.079347257605573,24.35232057795347,50.532635209680556 +2024-10-11 07:00:00,7,25.385829053082457,23.415804789549345,61.040837386876845 +2024-10-11 08:00:00,7,18.573223907275676,19.330915137664288,58.167621655956104 +2024-10-11 09:00:00,7,13.272678676502693,21.67947938274706,55.11769849149081 +2024-10-11 10:00:00,7,27.97624353447161,21.557458975927574,65.73872470513186 +2024-10-11 11:00:00,7,11.386975815230322,24.37219435087136,43.71890304455884 +2024-10-11 12:00:00,7,13.439901075550978,24.274780370572184,67.76530077185487 +2024-10-11 13:00:00,7,23.751555598144556,22.60795894353752,46.5738526979835 +2024-10-11 14:00:00,7,6.442701513284419,24.52084467214176,54.41930993254489 +2024-10-11 15:00:00,7,11.482911210457555,23.29333388840592,42.63034675553237 +2024-10-11 16:00:00,7,46.592813165201875,24.54433552288153,56.669619878711146 +2024-10-11 17:00:00,7,30.33924598339972,26.742664783709472,61.86980508060199 +2024-10-11 18:00:00,7,28.159278873313593,25.622304298693876,51.82371390758533 +2024-10-11 19:00:00,7,24.929461281046937,26.329476313618112,54.823005829114294 +2024-10-11 20:00:00,7,20.15197350627285,19.62299032184182,64.37480542071607 +2024-10-11 21:00:00,7,25.344698970349388,26.31288315967676,48.523117360826646 +2024-10-11 22:00:00,7,28.940507713203854,20.429796364386217,58.94162276830159 +2024-10-11 23:00:00,7,31.790222993776418,21.875983026205557,60.5165026512543 +2024-10-12 00:00:00,7,17.371601508208094,24.376532651440826,39.67486038981797 +2024-10-12 01:00:00,7,28.218289478193977,16.433631166990974,57.99736083961016 +2024-10-12 02:00:00,7,18.88518637882902,21.51785747205077,43.30503561905774 +2024-10-12 03:00:00,7,44.63030163823229,22.128841970178524,47.82820507593872 +2024-10-12 04:00:00,7,41.19071177671286,21.635881001093363,40.39353779957668 +2024-10-12 05:00:00,7,29.949766692175398,20.939072412436925,38.227885822708984 +2024-10-12 06:00:00,7,32.372078853718094,27.85650424688869,39.50574324278894 +2024-10-12 07:00:00,7,25.347070959248242,24.876229926598224,50.19692152928573 +2024-10-12 08:00:00,7,14.877477321096828,22.175622194133876,60.64912361739024 +2024-10-12 09:00:00,7,22.392178872918063,24.007304753040227,56.76988891540468 +2024-10-12 10:00:00,7,4.70056349395724,30.530084589340827,61.89196305033595 +2024-10-12 11:00:00,7,8.873132355268263,17.687125948987926,56.69524064835046 +2024-10-12 12:00:00,7,23.918388479310632,28.281174303308575,44.64006100944733 +2024-10-12 13:00:00,7,13.992830497016373,22.630688825158696,60.182804644438924 +2024-10-12 14:00:00,7,30.23414207693287,21.03913068981749,53.94326337576729 +2024-10-12 15:00:00,7,31.898673554428765,19.502402026559583,55.136155846447195 +2024-10-12 16:00:00,7,29.654863128364973,20.211121330501452,64.99533833247725 +2024-10-12 17:00:00,7,11.158320180093128,24.337204022530027,49.73052973679024 +2024-10-12 18:00:00,7,13.55030149540817,17.23584537533712,49.09771884844432 +2024-10-12 19:00:00,7,16.163586400749757,22.386158559508694,28.359460020594152 +2024-10-12 20:00:00,7,21.709783668251397,22.71416008067802,45.07590820553772 +2024-10-12 21:00:00,7,17.453228018230696,24.024791789851793,60.44973404333714 +2024-10-12 22:00:00,7,43.99634662173595,29.00365336279084,55.71981260514883 +2024-10-12 23:00:00,7,25.38117281548921,20.75681817105568,44.23437404566971 +2024-10-13 00:00:00,7,25.550367556252628,19.22111853839033,47.41846961171089 +2024-10-13 01:00:00,7,18.80997442581093,24.88422527588765,64.38857045593407 +2024-10-13 02:00:00,7,29.74698157108136,19.47938491444084,57.01648253918381 +2024-10-13 03:00:00,7,20.263237984822695,19.008859926869587,48.65884867618034 +2024-10-13 04:00:00,7,25.774193342497373,23.693422555638936,44.470994849842356 +2024-10-13 05:00:00,7,11.174569606637064,22.68594763420669,44.42379316282956 +2024-10-13 06:00:00,7,28.67943669617241,25.23201738529239,42.18065689674621 +2024-10-13 07:00:00,7,17.308800542496613,24.564198654744924,52.09070105444674 +2024-10-13 08:00:00,7,20.805832625423058,30.93528596636876,48.20648023060966 +2024-10-13 09:00:00,7,36.69134334932429,33.85221773939521,56.984826585696545 +2024-10-13 10:00:00,7,32.97333028202923,26.690275958550338,46.98669140841684 +2024-10-13 11:00:00,7,38.71822576515706,20.62291465197217,57.30116458739462 +2024-10-13 12:00:00,7,18.04248000425606,24.88914773490335,63.223969594541316 +2024-10-13 13:00:00,7,37.022480287607785,28.786371999330257,68.26770828810183 +2024-10-13 14:00:00,7,21.868057254200885,20.530217884692917,51.775209019664445 +2024-10-13 15:00:00,7,21.86730858136109,21.404824906284496,50.123542931564884 +2024-10-13 16:00:00,7,32.55508580383448,20.007726546380802,44.96926752744073 +2024-10-13 17:00:00,7,19.215877231887077,23.74230699884982,59.63867383322233 +2024-10-13 18:00:00,7,19.94785028211458,23.12042524146329,56.65648330415977 +2024-10-13 19:00:00,7,24.511193019276206,24.029169090118152,38.34836816254289 +2024-10-13 20:00:00,7,2.529253912346171,24.01628559014074,49.84290442789791 +2024-10-13 21:00:00,7,36.42577650678378,21.59912999867491,60.770529772367446 +2024-10-13 22:00:00,7,9.91939380838298,20.254432306810713,56.451433241425704 +2024-10-13 23:00:00,7,22.48554080519633,25.8578425868088,60.58808260457957 +2024-10-14 00:00:00,7,17.246731288836852,21.729677778318614,59.31334622302904 +2024-10-14 01:00:00,7,24.384471400865216,25.055406853522758,58.22616364986627 +2024-10-14 02:00:00,7,26.84264999038985,25.86798315172786,45.44547432538355 +2024-10-14 03:00:00,7,10.52257451774345,23.678661582019917,55.17106971287755 +2024-10-14 04:00:00,7,30.211425458819964,20.64841439198581,68.94324361138555 +2024-10-14 05:00:00,7,14.2120260008974,19.932791694768532,60.48881412620322 +2024-10-14 06:00:00,7,19.193200729860678,24.82479888952098,54.744294608255366 +2024-10-14 07:00:00,7,16.23906377548675,22.45522122534187,30.8941651878405 +2024-10-14 08:00:00,7,19.954864686982873,22.096337796629776,65.7006263712111 +2024-10-14 09:00:00,7,7.828083199054976,15.507220373015828,53.29110960775804 +2024-10-14 10:00:00,7,36.65104297872868,21.324551889293637,53.63156212271598 +2024-10-14 11:00:00,7,4.0389714252455455,28.571904966658305,61.13208949207889 +2024-10-14 12:00:00,7,33.903805899992214,23.185886881427425,57.853152824441516 +2024-10-14 13:00:00,7,8.886036021309769,24.413269070490273,63.39042168257835 +2024-10-14 14:00:00,7,4.821141960471632,17.563956903445447,44.98617237198091 +2024-10-14 15:00:00,7,23.720335280122576,18.508938047212922,47.17080723250517 +2024-10-14 16:00:00,7,20.894182693524325,18.992211162318313,36.25848423229384 +2024-10-14 17:00:00,7,23.196246637222732,20.448861675012907,58.34893774153752 +2024-10-14 18:00:00,7,22.23285749666363,25.133736861105298,56.84592534821657 +2024-10-14 19:00:00,7,14.957630536857081,26.075358452112816,51.060164997113034 +2024-10-14 20:00:00,7,30.2746300532263,21.945197102839558,52.11244986444187 +2024-10-14 21:00:00,7,22.81602963636667,19.74991442676115,58.11679941391815 +2024-10-14 22:00:00,7,21.68197133696542,22.20343533698756,47.82105269142409 +2024-10-14 23:00:00,7,32.05492045856734,21.084654575584274,46.16307325482183 +2024-10-15 00:00:00,7,9.749128371272874,26.327086443781113,44.53189402972306 +2024-10-15 01:00:00,7,20.853378849320062,24.996756063954123,26.407150362896477 +2024-10-15 02:00:00,7,13.145255021453373,21.72561334879121,58.31818035720594 +2024-10-15 03:00:00,7,11.42342177471114,14.871002809262944,47.31028966669412 +2024-10-15 04:00:00,7,21.894908316658555,23.667548395976944,58.73298220207233 +2024-10-15 05:00:00,7,32.933375662385,25.017275787177347,61.75110459183304 +2024-10-15 06:00:00,7,23.67406072922343,21.219396950015287,49.972146665003486 +2024-10-15 07:00:00,7,21.494671285152105,19.09658566481557,58.186666013566175 +2024-10-15 08:00:00,7,19.576324491128908,23.417426601974466,47.186829457977254 +2024-10-15 09:00:00,7,9.001768787616902,28.507662645840178,62.573858732084005 +2024-10-15 10:00:00,7,15.128516435969983,18.467082031369927,37.095249025336 +2024-10-15 11:00:00,7,20.740566397224868,21.827909132270303,56.364892057321335 +2024-10-15 12:00:00,7,22.932606963344547,20.112652914061808,37.155475982535606 +2024-10-15 13:00:00,7,31.404813952984476,15.761959325344721,49.01715296865105 +2024-10-15 14:00:00,7,15.653420410987547,19.611646299115097,58.97586308465557 +2024-10-15 15:00:00,7,7.393710091463809,25.97051022649909,47.558457316810475 +2024-10-15 16:00:00,7,20.242846639024176,23.84416584820839,46.48589747635354 +2024-10-15 17:00:00,7,2.70882802497475,27.30247582263067,51.32495198553373 +2024-10-15 18:00:00,7,13.97582173350412,19.558923730027733,42.26587296478292 +2024-10-15 19:00:00,7,4.53203496049067,15.902769031625231,61.26648615876796 +2024-10-15 20:00:00,7,21.191409546870858,25.96402828907973,40.83772115289173 +2024-10-15 21:00:00,7,23.647719665890143,24.65953016615037,53.66460942627434 +2024-10-15 22:00:00,7,5.2901282545931885,23.978254815558778,52.6266514441211 +2024-10-15 23:00:00,7,11.290280178638938,22.188980571736927,48.20527422997609 +2024-10-16 00:00:00,7,33.79864611051473,21.02096492883465,60.36919979183359 +2024-10-16 01:00:00,7,1.974004626738779,23.03464864027033,53.44825845774941 +2024-10-16 02:00:00,7,23.64591703387182,24.01452493692575,57.55462362986825 +2024-10-16 03:00:00,7,7.19770091123616,25.83269334314585,58.72615977055257 +2024-10-16 04:00:00,7,12.87745856070708,20.307294179231455,34.11909883401918 +2024-10-16 05:00:00,7,34.16720845116492,20.25058775439114,59.04838847598276 +2024-10-16 06:00:00,7,19.986112764797507,21.68810405617654,53.2892037321573 +2024-10-16 07:00:00,7,15.800516433111252,26.335718618947542,56.68850173644621 +2024-10-16 08:00:00,7,13.425243217098696,24.772102608589783,57.61722274449687 +2024-10-16 09:00:00,7,26.077521887586066,21.615144355825556,48.17572721882226 +2024-10-16 10:00:00,7,17.223122943184244,23.24971453421825,56.15677983706175 +2024-10-16 11:00:00,7,22.25484743504099,26.099016857943965,63.823144253805154 +2024-10-16 12:00:00,7,33.38783697985696,22.74785993143405,62.78812919329009 +2024-10-16 13:00:00,7,20.35104975864605,19.88365647593499,61.18483659814335 +2024-10-16 14:00:00,7,11.289623790208976,20.150655520990004,55.01592026223127 +2024-10-16 15:00:00,7,18.324368822956178,21.799891405233915,46.377029798742576 +2024-10-16 16:00:00,7,18.9710481585703,23.23456724793379,67.75750622329247 +2024-10-16 17:00:00,7,36.93094132749788,22.029589809109854,47.428758630277805 +2024-10-16 18:00:00,7,17.539947111616407,23.53654063661643,53.27697455511242 +2024-10-16 19:00:00,7,16.275399856992298,23.45928437090057,55.13158557325478 +2024-10-16 20:00:00,7,27.19986170476009,24.172926843533677,52.02784500319866 +2024-10-16 21:00:00,7,19.60711347444081,21.086012301852477,50.29729694824762 +2024-10-16 22:00:00,7,21.688921404627933,22.363096480514145,62.61999094279187 +2024-10-16 23:00:00,7,10.060503587327045,25.021311050205664,54.17804469145142 +2024-10-17 00:00:00,7,12.491362662297732,21.419034013358704,41.95531265035602 +2024-10-17 01:00:00,7,25.240138267779635,26.747816977047773,45.284548990465865 +2024-10-17 02:00:00,7,36.17776935942679,20.52823419910577,67.77317776669487 +2024-10-17 03:00:00,7,39.929067919858866,25.62834958275563,43.8459432006326 +2024-10-17 04:00:00,7,22.51371898689968,20.569460034193177,35.141072297264635 +2024-10-17 05:00:00,7,22.91620225458078,17.037607324390788,52.579356728226735 +2024-10-17 06:00:00,7,22.667033070992343,18.666648363491408,56.52497389051574 +2024-10-17 07:00:00,7,37.318777541388215,21.19452465468501,68.35034696162263 +2024-10-17 08:00:00,7,21.310624119867725,19.688177867829904,47.278268167011014 +2024-10-17 09:00:00,7,9.018797221637103,23.700524766272885,65.02094090143918 +2024-10-17 10:00:00,7,17.8551048616367,21.68530052897368,43.58729201736056 +2024-10-17 11:00:00,7,18.062757492744897,25.332133169249808,55.179661063221225 +2024-10-17 12:00:00,7,40.574655050886136,19.534839119421598,56.44881170028688 +2024-10-17 13:00:00,7,34.63085447519488,24.06232599566393,56.06360612988026 +2024-10-17 14:00:00,7,11.34629074854543,24.04845432182954,48.385766545321715 +2024-10-17 15:00:00,7,20.29683064490319,23.983076577311547,57.65035016129835 +2024-10-17 16:00:00,7,24.081755180032868,21.37284335264035,54.85072182349218 +2024-10-17 17:00:00,7,20.083018971828807,22.91550771895522,53.6723884684773 +2024-10-17 18:00:00,7,6.001694676006844,24.514601694629214,53.33377191175086 +2024-10-17 19:00:00,7,29.45380257706351,17.147911405250024,57.26411780405921 +2024-10-17 20:00:00,7,20.7925660296462,20.77912028313568,56.66018970305356 +2024-10-17 21:00:00,7,39.549839445718106,29.965976627273143,43.1330234607377 +2024-10-17 22:00:00,7,26.213451749843216,21.869796775719834,34.79232859557693 +2024-10-17 23:00:00,7,21.82180779261674,12.966924942578817,61.16465611965342 +2024-10-18 00:00:00,7,7.850961650151092,20.17597069655345,62.13922691561491 +2024-10-18 01:00:00,7,26.85556012412634,21.39108179360823,52.60157272082791 +2024-10-18 02:00:00,7,20.172321670560432,18.554481967172187,70.51926400010927 +2024-10-18 03:00:00,7,18.324977441312125,26.95681904152569,56.08701911401868 +2024-10-18 04:00:00,7,18.64059037620387,20.690171732931613,60.09444996764855 +2024-10-18 05:00:00,7,21.08793599400434,24.08917753390152,49.76788151147634 +2024-10-18 06:00:00,7,27.69014497989258,24.799004943964615,71.90368545100095 +2024-10-18 07:00:00,7,33.14827846436127,26.363378611380373,39.17366451610376 +2024-10-18 08:00:00,7,30.74215841528826,21.012004777954118,55.703929163714896 +2024-10-18 09:00:00,7,14.790494612144395,22.475344389701032,69.0978167733384 +2024-10-18 10:00:00,7,29.30256604093094,25.218967279165643,67.0471493335663 +2024-10-18 11:00:00,7,13.449805136050337,24.18060673524263,52.92574970959314 +2024-10-18 12:00:00,7,34.10136753113287,26.708167524794582,48.104704547808126 +2024-10-18 13:00:00,7,5.141227742180334,21.333042339353355,47.33776679120079 +2024-10-18 14:00:00,7,18.659885284118758,24.4966286438813,41.94890991701559 +2024-10-18 15:00:00,7,28.785400335782523,18.30254310915284,44.679345541795044 +2024-10-18 16:00:00,7,18.505848555860563,22.490318148037016,42.565283897620326 +2024-10-18 17:00:00,7,10.621842112465188,20.531525190165464,45.389183080171385 +2024-10-18 18:00:00,7,23.734173423590352,17.9019861187375,61.71394471533272 +2024-10-18 19:00:00,7,18.31774030277183,18.553668342811495,53.93719561870809 +2024-10-18 20:00:00,7,47.122426373325325,26.192020809865376,43.719326700603396 +2024-10-18 21:00:00,7,23.71094871749244,22.82204656252785,56.684998254666816 +2024-10-18 22:00:00,7,20.262148928228722,21.691087168103724,54.27652828162594 +2024-10-18 23:00:00,7,32.45945365096287,29.298983099552885,55.53055582018477 +2024-10-19 00:00:00,7,28.446237018436978,24.61591556018957,65.73071225144508 +2024-10-19 01:00:00,7,35.22468812503735,19.643079847397708,51.58639912975244 +2024-10-19 02:00:00,7,16.758089257637025,22.581019374436625,76.86055623817754 +2024-10-19 03:00:00,7,26.645365854221275,15.998730543963411,57.97055399926975 +2024-10-19 04:00:00,7,26.126295350840095,25.718473331457677,66.79972069025915 +2024-10-19 05:00:00,7,11.090242196831333,19.914117154354823,50.06731352038505 +2024-10-19 06:00:00,7,14.661810976810193,21.770865559759883,45.59586467910887 +2024-10-19 07:00:00,7,29.23041293276883,22.149080715625566,57.54784219233682 +2024-10-19 08:00:00,7,34.83012159905388,23.162801952478727,64.28824826587532 +2024-10-19 09:00:00,7,19.428838915145242,25.14356378110854,48.62918944379131 +2024-10-19 10:00:00,7,29.679854502359834,24.523192407916103,46.72637057995608 +2024-10-19 11:00:00,7,17.804239689502683,23.853875512902725,55.12579867826736 +2024-10-19 12:00:00,7,31.347438197997818,18.283522523341844,61.47457795110379 +2024-10-19 13:00:00,7,37.471588515494176,20.125903537879644,52.18613887370768 +2024-10-19 14:00:00,7,22.45315053232919,20.725473733293686,52.737900989208086 +2024-10-19 15:00:00,7,24.65760144864921,15.771726612752198,44.69024164880216 +2024-10-19 16:00:00,7,10.90151062328345,20.581157440768276,55.594730983023084 +2024-10-19 17:00:00,7,18.233186192093136,23.79500129356918,42.64981232673438 +2024-10-19 18:00:00,7,29.384227347357744,24.893783372262632,35.42200201137793 +2024-10-19 19:00:00,7,16.77594708051634,20.743686801786804,53.09491377730584 +2024-10-19 20:00:00,7,17.301789887847274,23.993159224723325,53.92120879760979 +2024-10-19 21:00:00,7,15.638514616816098,19.40387688878005,69.89109711051297 +2024-10-19 22:00:00,7,5.74030094630362,21.148493575983373,39.98456696260574 +2024-10-19 23:00:00,7,25.833930145250836,19.748774780111304,54.3475007780029 +2024-10-20 00:00:00,7,25.31507618869965,24.91394795641615,45.56708583740478 +2024-10-20 01:00:00,7,15.694451449999345,23.022649527684905,47.55809582460169 +2024-10-20 02:00:00,7,18.880991694388758,12.600715636357087,53.122071044720855 +2024-10-20 03:00:00,7,30.93463253816936,22.906409952609735,58.57787724118992 +2024-10-20 04:00:00,7,32.57241180688233,24.102304115295112,42.68125273089778 +2024-10-20 05:00:00,7,33.4477635580388,25.94721899754061,36.17886962707478 +2024-10-20 06:00:00,7,23.39985903834419,16.7560077891573,54.96602022284967 +2024-10-20 07:00:00,7,36.40963818312094,21.15954613114124,39.10491727052554 +2024-10-20 08:00:00,7,22.176016636255813,23.5855634369074,53.072086242905385 +2024-10-20 09:00:00,7,29.055600697939184,19.4692220272313,47.602580418179414 +2024-10-20 10:00:00,7,23.794194392902934,26.00136981133908,37.862769993855316 +2024-10-20 11:00:00,7,31.220198536736113,22.25322393680777,69.5372285080339 +2024-10-20 12:00:00,7,27.416818690841378,27.17885647280673,55.79242540960817 +2024-10-20 13:00:00,7,28.44592475266809,23.102379341451915,51.922852793923845 +2024-10-20 14:00:00,7,10.279724485244754,19.735973798960277,35.48329412825916 +2024-10-20 15:00:00,7,13.927127964275583,16.098889742671957,58.30338769787049 +2024-10-20 16:00:00,7,28.59065073480174,21.146281302530642,52.47356992294958 +2024-10-20 17:00:00,7,11.507876488337672,22.177576183596273,44.57492587788939 +2024-10-20 18:00:00,7,8.21407757255512,22.578930354315187,48.29638650624319 +2024-10-20 19:00:00,7,14.959763755552094,23.013084497006034,52.279136005679 +2024-10-20 20:00:00,7,26.580313445201455,18.649152428146618,63.41936857986011 +2024-10-20 21:00:00,7,25.33119809472792,26.955434423746034,68.63751342903002 +2024-10-20 22:00:00,7,31.345215732744055,29.025000092399104,49.48135479085364 +2024-10-20 23:00:00,7,25.942758787010575,24.052780285471318,64.2542100996323 +2024-10-21 00:00:00,7,7.205784675778679,23.34075329892995,46.968455055895085 +2024-10-21 01:00:00,7,38.29292866398697,23.00044663124382,51.999595623309204 +2024-10-21 02:00:00,7,26.462883743159104,19.347972504778582,46.71415196597565 +2024-10-21 03:00:00,7,21.55394892533346,23.987680254411586,60.07055248836775 +2024-10-21 04:00:00,7,30.184405633266437,20.9371356077408,48.08431903795317 +2024-10-21 05:00:00,7,27.892337343103147,18.13114572349347,67.54668783882295 +2024-10-21 06:00:00,7,16.303804213153583,18.51942465254603,49.714393896708856 +2024-10-21 07:00:00,7,18.898187799433007,25.181974495166763,39.00519171418306 +2024-10-21 08:00:00,7,25.13944020724176,18.09856883107263,50.76661210792911 +2024-10-21 09:00:00,7,32.139757205280866,19.592470893154232,50.87033125223208 +2024-10-21 10:00:00,7,30.8669978465458,20.84746887711563,71.80536654909808 +2024-10-21 11:00:00,7,27.09363480745189,24.150046084108485,60.32567697377612 +2024-10-21 12:00:00,7,21.254312835432792,22.923025047816253,44.04786694310158 +2024-10-21 13:00:00,7,7.022156390199797,18.038481760471015,46.62579785749522 +2024-10-21 14:00:00,7,18.27567987099477,19.88011313781793,67.35787292252414 +2024-10-21 15:00:00,7,19.363532559808792,21.53578226196201,65.07206427319815 +2024-10-21 16:00:00,7,30.29931224418965,30.867872635258877,43.738888689507235 +2024-10-21 17:00:00,7,18.648307886445693,22.113402504804075,50.25657082043613 +2024-10-21 18:00:00,7,32.78380547682908,31.658556192844273,53.886806700729 +2024-10-21 19:00:00,7,1.0015857135730286,21.643814011427718,54.98981072968319 +2024-10-21 20:00:00,7,26.838424098470078,19.051543556021645,46.43373589059078 +2024-10-21 21:00:00,7,28.029652821416704,27.388814744209288,53.743329448364825 +2024-10-21 22:00:00,7,25.52865256436777,27.936128989953428,58.27553277891294 +2024-10-21 23:00:00,7,41.422460062648156,27.59168833201241,54.80288782770652 +2024-10-22 00:00:00,7,34.29230286881593,26.08139406011302,53.64723622933263 +2024-10-22 01:00:00,7,18.854721449642028,18.932260836988178,67.4725502372162 +2024-10-22 02:00:00,7,16.196156371609597,24.02629126244319,44.66514154286571 +2024-10-22 03:00:00,7,20.93125101513767,25.71611952360148,56.40939861616314 +2024-10-22 04:00:00,7,8.35729451742185,20.57444530930747,63.69100873237773 +2024-10-22 05:00:00,7,22.34907639235742,21.531056540383304,50.31153513213631 +2024-10-22 06:00:00,7,26.741991956769144,20.057446587852755,54.30967509795213 +2024-10-22 07:00:00,7,23.674685186327075,30.793568248713047,62.75275044913768 +2024-10-22 08:00:00,7,27.50280717000611,28.41381034245004,77.04425509206862 +2024-10-22 09:00:00,7,8.765450473177026,22.322962729253216,59.06537718649068 +2024-10-22 10:00:00,7,29.72825812812592,28.754462170292577,41.03811329476931 +2024-10-22 11:00:00,7,31.499910434246843,25.64999529649307,59.12264296505036 +2024-10-22 12:00:00,7,30.391105953268127,25.81194423699665,42.28984452139891 +2024-10-22 13:00:00,7,12.827526980811545,16.303438410597053,46.16554377041982 +2024-10-22 14:00:00,7,27.273853885117493,20.632430758533054,49.17897285134455 +2024-10-22 15:00:00,7,19.92541467762687,25.908962309337365,61.59377409077153 +2024-10-22 16:00:00,7,23.230000476761894,20.563567782417234,51.67636181196297 +2024-10-22 17:00:00,7,8.25499926359825,19.575947620098635,59.174883627822595 +2024-10-22 18:00:00,7,20.879102682364884,30.091731546041323,44.59539103478093 +2024-10-22 19:00:00,7,20.464336974362656,22.434229824394905,56.04786077761699 +2024-10-22 20:00:00,7,13.448005326282454,28.02027861208302,54.82516986510309 +2024-10-22 21:00:00,7,24.497116745226762,19.59286758142852,55.01755941799302 +2024-10-22 22:00:00,7,19.412086015787313,22.553792451915367,55.323626147599484 +2024-10-22 23:00:00,7,11.214207897015008,31.180398860684445,46.30139983358703 +2024-10-23 00:00:00,7,28.920298358890278,24.2402971877273,49.527695013961115 +2024-10-23 01:00:00,7,31.573087362314617,23.445584812239137,46.39523383801993 +2024-10-23 02:00:00,7,32.542155989383986,18.055158730799747,58.128020215100946 +2024-10-23 03:00:00,7,27.338231989697356,23.523219169929376,48.00229681637965 +2024-10-23 04:00:00,7,14.857271214099528,22.379027008113614,56.65434727034253 +2024-10-23 05:00:00,7,30.013167710398683,18.49616868048583,42.27636554390385 +2024-10-23 06:00:00,7,9.85533096808328,31.61739478729089,64.73707833482148 +2024-10-23 07:00:00,7,29.61060968542291,23.792645376185526,67.8403680963352 +2024-10-23 08:00:00,7,18.394004330884275,22.83794060562183,47.28302937650662 +2024-10-23 09:00:00,7,28.573494596822098,23.23995438386865,51.44633877230882 +2024-10-23 10:00:00,7,39.565829031288985,30.836840917399527,56.62252732751569 +2024-10-23 11:00:00,7,19.690181915161865,26.653930868116213,44.531630950701995 +2024-10-23 12:00:00,7,30.960332229919025,20.93575689603505,45.16833121153029 +2024-10-23 13:00:00,7,14.593415099562762,25.305924761781483,54.666017465742904 +2024-10-23 14:00:00,7,2.5815799093148755,27.438983400084272,54.69801075246747 +2024-10-23 15:00:00,7,10.358328048851774,23.740657650472464,47.85301203895128 +2024-10-23 16:00:00,7,25.239142002509475,29.538529789770386,64.3989374901509 +2024-10-23 17:00:00,7,22.14230110862035,22.30943864654681,42.340943041021916 +2024-10-23 18:00:00,7,17.937205996070805,25.184730788532228,49.11796677891466 +2024-10-23 19:00:00,7,17.797470589415227,26.22558620322654,46.8361946998212 +2024-10-23 20:00:00,7,29.794744919715,21.651138897956475,52.32867369796612 +2024-10-23 21:00:00,7,27.533033898846913,32.65320524463996,47.88331332940511 +2024-10-23 22:00:00,7,26.32401897078998,18.744225949001947,63.70801795945634 +2024-10-23 23:00:00,7,23.534978984128024,15.693820348646913,70.67777327281183 +2024-10-24 00:00:00,7,26.73694398598268,25.112561806216792,60.05000066432024 +2024-10-24 01:00:00,7,16.671546844762158,24.44620111300752,59.0617098932766 +2024-10-24 02:00:00,7,12.525025407322957,28.34332433295056,53.69792550611522 +2024-10-24 03:00:00,7,12.744985501871202,27.43953850764953,41.65960853472201 +2024-10-24 04:00:00,7,19.899838120189496,23.476223715082398,52.34416184669671 +2024-10-24 05:00:00,7,12.933978895388725,22.479967358294584,57.71206421781014 +2024-10-24 06:00:00,7,14.521995601255279,25.502813729376953,39.001822172962044 +2024-10-24 07:00:00,7,16.14488822491151,24.02781536656378,55.4111915911598 +2024-10-24 08:00:00,7,46.34082638007506,23.041792082879045,64.87470126310514 +2024-10-24 09:00:00,7,30.717286706901987,19.264195118558348,41.64796870803893 +2024-10-24 10:00:00,7,13.221390026298755,15.95129528540485,61.20214526491029 +2024-10-24 11:00:00,7,32.27708271114998,31.38884063029649,63.35690385884478 +2024-10-24 12:00:00,7,32.15007481205051,24.294046195374214,48.282096781081 +2024-10-24 13:00:00,7,21.56537254550003,24.83298921480339,55.52343714817716 +2024-10-24 14:00:00,7,27.086595016242875,24.723861358710796,38.13354867909168 +2024-10-24 15:00:00,7,28.91410592574257,27.178332068656527,58.06779066367815 +2024-10-24 16:00:00,7,29.81538142793157,24.128704940394005,60.446613226395556 +2024-10-24 17:00:00,7,11.019326530027667,27.982779901201745,55.721303369398036 +2024-10-24 18:00:00,7,21.316671726259134,21.02889796247095,56.683613214148146 +2024-10-24 19:00:00,7,22.942679413067847,13.162051747672061,54.852753369009996 +2024-10-24 20:00:00,7,17.699424861741768,22.964798492836074,57.770287360925195 +2024-10-24 21:00:00,7,26.56545010667474,20.108424939506058,44.87277015675333 +2024-10-24 22:00:00,7,21.010449579625863,14.049402578457656,43.021808627248994 +2024-10-24 23:00:00,7,33.03416057316805,30.63310205058119,41.24707206990725 +2024-10-25 00:00:00,7,17.442459341499287,25.027089727227242,49.53221355343692 +2024-10-25 01:00:00,7,22.144949945968055,23.407735909187817,42.348740813505124 +2024-10-25 02:00:00,7,22.1582497906577,16.631996659868673,49.2858782569454 +2024-10-25 03:00:00,7,20.528204470206926,21.552269171173982,53.92098275898594 +2024-10-25 04:00:00,7,33.06205987081148,24.549932805363824,58.91089289693717 +2024-10-25 05:00:00,7,17.763058837552595,24.927185919113825,53.742032218429365 +2024-10-25 06:00:00,7,6.901675972085865,20.96797163938278,42.044088723817374 +2024-10-25 07:00:00,7,12.426161601496924,21.346218106316613,57.1503522782029 +2024-10-25 08:00:00,7,8.616366066738482,20.64898483244491,52.017837051928545 +2024-10-25 09:00:00,7,10.817770724605527,29.705278633445246,50.92565584610576 +2024-10-25 10:00:00,7,25.582676876868746,21.6213412365356,60.358882772688574 +2024-10-25 11:00:00,7,18.31918233171537,23.12301004507471,77.3887344197921 +2024-10-25 12:00:00,7,18.335926290596422,24.184270036791407,47.13137293637311 +2024-10-25 13:00:00,7,16.996812201586888,21.351736792283507,51.00462465566654 +2024-10-25 14:00:00,7,23.7519242536258,22.04458151654689,55.12714570485572 +2024-10-25 15:00:00,7,14.611387942448445,24.625934900961617,72.33721323034223 +2024-10-25 16:00:00,7,40.12007949180162,20.521672289488727,44.62418666387523 +2024-10-25 17:00:00,7,19.159188131156117,26.487047651332226,53.7010705015169 +2024-10-25 18:00:00,7,20.685131536564565,21.467779681172807,49.6232957768131 +2024-10-25 19:00:00,7,31.302197414306548,25.163572634250436,59.28673038327008 +2024-10-25 20:00:00,7,21.330305041031757,26.824017725421,53.19282337798755 +2024-10-25 21:00:00,7,22.677999991221323,16.18440379290257,60.64219122571531 +2024-10-25 22:00:00,7,21.973896411939272,18.20143510484496,39.6119545492704 +2024-10-25 23:00:00,7,12.135748718643967,24.633766064488746,49.92095793068615 +2024-10-26 00:00:00,7,22.40712135319515,26.48192115580261,60.58152462311186 +2024-10-26 01:00:00,7,30.491177227347386,22.811261706690367,51.26954323830603 +2024-10-26 02:00:00,7,23.895674130245975,26.412328425720712,54.10256006524616 +2024-10-26 03:00:00,7,18.886462407988102,24.083769073458484,58.29441445491442 +2024-10-26 04:00:00,7,30.97713204320221,23.512729998629116,43.110822058731046 +2024-10-26 05:00:00,7,35.69636478619056,20.14082355412326,41.49752270321706 +2024-10-26 06:00:00,7,22.70845010197404,29.506956106162207,45.696270243572094 +2024-10-26 07:00:00,7,20.490348063119725,23.38424574344535,36.21613663974038 +2024-10-26 08:00:00,7,19.977369038661802,22.102410884923167,61.89701624738315 +2024-10-26 09:00:00,7,18.014623672321303,14.261628167052303,55.68879962207331 +2024-10-26 10:00:00,7,13.875196814809616,28.88206634678614,64.33223459813776 +2024-10-26 11:00:00,7,34.513131360166746,31.436440750098537,43.8997734406974 +2024-10-26 12:00:00,7,16.171144440175265,28.13321218327036,43.388317225210955 +2024-10-26 13:00:00,7,30.611138750968827,27.47561608916401,52.64720773624518 +2024-10-26 14:00:00,7,8.303032517216963,11.703105568813902,63.899514955834825 +2024-10-26 15:00:00,7,33.7701165518202,24.566777605385205,58.533864953400084 +2024-10-26 16:00:00,7,14.280236243313784,25.203639094887215,49.91243510743103 +2024-10-26 17:00:00,7,19.715007129768367,25.95375362077583,54.00192256033735 +2024-10-26 18:00:00,7,1.0766439035062554,18.4842090625357,58.843568703572195 +2024-10-26 19:00:00,7,16.170267129830517,22.38209145833552,60.99818843351573 +2024-10-26 20:00:00,7,29.675242824974973,16.14734437006872,57.36293439470065 +2024-10-26 21:00:00,7,11.730450705622305,22.535810566343347,43.77866903803835 +2024-10-26 22:00:00,7,9.127991496304114,19.05915388081034,41.16972890765357 +2024-10-26 23:00:00,7,40.45201076872533,28.33073335234121,48.88699905070795 +2024-10-27 00:00:00,7,28.24490265007593,24.70911547172191,63.16848695407015 +2024-10-27 01:00:00,7,14.019569599379182,27.057250762550737,74.78487977659321 +2024-10-27 02:00:00,7,21.48520971061021,19.39768065172011,30.8166158997506 +2024-10-27 03:00:00,7,16.381918697428468,22.8125632970548,63.764631836768864 +2024-10-27 04:00:00,7,30.975426167738235,18.668708352276116,48.46225709173412 +2024-10-27 05:00:00,7,17.44348620552735,23.499126175558708,52.397283661789125 +2024-10-27 06:00:00,7,9.05178087473154,24.515043346411357,64.87660145879111 +2024-10-27 07:00:00,7,22.655578512636822,27.906531192070226,50.66828364500869 +2024-10-27 08:00:00,7,24.742580145874175,22.193968916196116,65.92684365987331 +2024-10-27 09:00:00,7,31.145655608584722,21.348819459111606,49.83667167711701 +2024-10-27 10:00:00,7,31.315577320141145,29.478219397528356,64.66547325248361 +2024-10-27 11:00:00,7,15.583414305800206,24.0841983667043,53.30442782053807 +2024-10-27 12:00:00,7,17.127148494312443,30.26629036524744,64.3441410081376 +2024-10-27 13:00:00,7,22.061372841695054,22.8978380088954,40.442555578129884 +2024-10-27 14:00:00,7,23.764529264669854,28.509646554396692,60.91283636540459 +2024-10-27 15:00:00,7,26.456124870671296,25.47591302071424,47.80062859631685 +2024-10-27 16:00:00,7,19.62668183268845,20.69550611348684,52.99930468529747 +2024-10-27 17:00:00,7,0.4783320248073508,24.755890346819285,51.350936164041585 +2024-10-27 18:00:00,7,0.0,23.314745479998262,49.30596235672371 +2024-10-27 19:00:00,7,37.38638877283481,22.09132325672068,47.04801385640029 +2024-10-27 20:00:00,7,18.529731387241974,27.033571789773575,64.8802038798893 +2024-10-27 21:00:00,7,23.946683473886512,19.92506947975641,39.13605676998343 +2024-10-27 22:00:00,7,11.937453280326086,20.274431704513006,41.92601737519608 +2024-10-27 23:00:00,7,14.107885649737774,22.290643901325748,68.34771559690955 +2024-10-28 00:00:00,7,7.438211307919342,25.25188092844841,39.651851841539404 +2024-10-28 01:00:00,7,38.45511478427151,26.05529634174157,45.36576127583607 +2024-10-28 02:00:00,7,24.60434305479555,20.978075966670225,45.0179715624243 +2024-10-28 03:00:00,7,29.119806532268413,20.221588448727882,43.515875717813586 +2024-10-28 04:00:00,7,34.18304066206098,26.530766285847065,44.53823835327413 +2024-10-28 05:00:00,7,15.185179161463878,14.029298055534788,50.5903837988052 +2024-10-28 06:00:00,7,37.63458210977637,26.57797446493291,58.396834558595955 +2024-10-28 07:00:00,7,22.529009120967665,21.355140757511453,44.88607191650026 +2024-10-28 08:00:00,7,25.65362722604459,22.006416786584495,59.381580314204584 +2024-10-28 09:00:00,7,30.476081374800444,29.206120823281466,63.54609468072098 +2024-10-28 10:00:00,7,21.749122372926987,20.76529463655693,59.52516390397611 +2024-10-28 11:00:00,7,25.22825896649092,26.6288991943615,54.58554524633459 +2024-10-28 12:00:00,7,8.734138991220542,20.01737575694596,64.56123808582862 +2024-10-28 13:00:00,7,21.28378175738093,27.365975536841013,64.41484153091325 +2024-10-28 14:00:00,7,4.919929871076775,23.237939439240655,38.676350760263205 +2024-10-28 15:00:00,7,14.206160273255598,18.330206233934884,51.42416513963522 +2024-10-28 16:00:00,7,15.125183213118007,24.503655452870127,70.05947359380593 +2024-10-28 17:00:00,7,9.807626470840686,20.155083495001143,64.25830752751584 +2024-10-28 18:00:00,7,37.524958763048474,26.481348411457454,55.085302912216555 +2024-10-28 19:00:00,7,20.26307845146721,23.37025731294226,58.438641234713614 +2024-10-28 20:00:00,7,22.150987297173284,24.25993412309856,49.140444842448964 +2024-10-28 21:00:00,7,15.436263908722918,20.32798312535936,43.457357255846865 +2024-10-28 22:00:00,7,20.753451330837137,18.709103162747095,40.94433862243226 +2024-10-28 23:00:00,7,24.56886191317605,13.798384065872048,51.06169112234815 +2024-10-29 00:00:00,7,21.85475120014883,25.983166478160413,51.12440099737317 +2024-10-29 01:00:00,7,22.946666447343258,30.592206186869355,57.13566235758542 +2024-10-29 02:00:00,7,25.217577246126872,24.037400967181544,30.49082719294556 +2024-10-29 03:00:00,7,17.740440652064464,23.773762543392152,38.9129120444388 +2024-10-29 04:00:00,7,45.60177579650954,22.420050911946483,50.096825418559426 +2024-10-29 05:00:00,7,24.004276925837154,21.085868630216407,50.25819621914128 +2024-10-29 06:00:00,7,25.039830704666848,20.860811782254217,39.634962092212106 +2024-10-29 07:00:00,7,21.998372929158354,26.410535835972034,47.81339331218256 +2024-10-29 08:00:00,7,17.98865481921618,20.414427392985278,57.17928199913133 +2024-10-29 09:00:00,7,29.964575729305054,24.780631341770825,53.486422994400755 +2024-10-29 10:00:00,7,22.04091844530887,21.865409555929503,68.94689547484847 +2024-10-29 11:00:00,7,41.732374269849736,21.892897708531493,47.86599463761191 +2024-10-29 12:00:00,7,0.0,28.99534799238408,55.16001292110737 +2024-10-29 13:00:00,7,24.07285339535293,17.335790492956193,57.09258092803391 +2024-10-29 14:00:00,7,23.551255111335166,27.126094928112988,53.99965602884931 +2024-10-29 15:00:00,7,11.520876381813478,21.099592548569326,66.31788326529997 +2024-10-29 16:00:00,7,22.48156653234428,22.929336706722186,53.840731617752425 +2024-10-29 17:00:00,7,23.242824438238177,19.53113399733123,58.64230293401806 +2024-10-29 18:00:00,7,9.88430686723202,21.71206877167125,49.75474691012 +2024-10-29 19:00:00,7,21.31420578989799,20.076553294827335,48.6807179233707 +2024-10-29 20:00:00,7,29.517963039015157,26.10232615665988,49.280914115081025 +2024-10-29 21:00:00,7,14.43999751937466,27.1075449135501,64.40248953743577 +2024-10-29 22:00:00,7,35.71168852841638,20.631683299592307,47.559511682069555 +2024-10-29 23:00:00,7,18.525342121120616,20.707118096393913,47.47890525504049 +2024-10-30 00:00:00,7,19.060861224375785,19.523394467302246,55.777658962238576 +2024-10-30 01:00:00,7,15.05743967025195,20.270600853893313,47.68724831293382 +2024-10-30 02:00:00,7,26.621781944504853,21.70632247554162,69.72879505194875 +2024-10-30 03:00:00,7,22.44548715967505,24.821476449397533,43.01126751775692 +2024-10-30 04:00:00,7,15.776604375462481,22.146900608872635,44.99312499443087 +2024-10-30 05:00:00,7,17.75313741384754,18.857038355524697,49.306930901454564 +2024-10-30 06:00:00,7,24.422929088760508,22.761110493561304,60.78641533958738 +2024-10-30 07:00:00,7,23.607253244437224,27.922005394235583,54.94669391233278 +2024-10-30 08:00:00,7,39.61792857567527,19.941558722220115,29.874982368451164 +2024-10-30 09:00:00,7,17.193507744904736,22.18893271785297,55.10203323068764 +2024-10-30 10:00:00,7,22.26808661942167,21.380119575992083,57.98975359975923 +2024-10-30 11:00:00,7,16.210037841772824,25.077047870756076,59.41415964981496 +2024-10-30 12:00:00,7,36.85893503619877,30.99077890783065,61.76091796630489 +2024-10-30 13:00:00,7,11.893360375548953,25.84001689051876,58.41016151069706 +2024-10-30 14:00:00,7,47.23063592841488,25.331585677966444,58.95648747419177 +2024-10-30 15:00:00,7,26.631024490317824,28.107655683493007,48.991259899134164 +2024-10-30 16:00:00,7,8.784493143691483,24.389667502527477,46.65480291542436 +2024-10-30 17:00:00,7,8.140122257169711,24.93613415209301,54.91685540648943 +2024-10-30 18:00:00,7,6.2422948780072165,20.88839038143239,46.070506331504895 +2024-10-30 19:00:00,7,24.418034056249557,26.084552703253486,46.69739534910767 +2024-10-30 20:00:00,7,40.68149001959145,26.665898946277764,46.78838597756239 +2024-10-30 21:00:00,7,18.539716358354532,23.550167932186852,55.85463913859048 +2024-10-30 22:00:00,7,13.89920627970891,25.428578859314708,42.757115436383 +2024-10-30 23:00:00,7,27.734348499346996,18.97945678922276,40.50334934447845 +2024-10-31 00:00:00,7,15.72601383668954,21.94778571268083,40.60053086346484 +2024-10-31 01:00:00,7,10.956547106159233,19.657995797534415,44.820042632679154 +2024-10-31 02:00:00,7,7.507590659034703,26.54166294936573,54.85613668489818 +2024-10-31 03:00:00,7,37.099837008162375,25.895706460828915,50.267561202310944 +2024-10-31 04:00:00,7,23.45931363567616,22.24126283896865,53.96665507001927 +2024-10-31 05:00:00,7,25.993167235194104,21.70625557149062,44.029781258411305 +2024-10-31 06:00:00,7,20.54498083432172,19.259663872355688,57.8063321775752 +2024-10-31 07:00:00,7,9.646478718945028,21.390475365860677,55.51093624240089 +2024-10-31 08:00:00,7,24.88964577578662,26.56078565599941,70.69142372659856 +2024-10-31 09:00:00,7,22.04855324132,28.253173607002196,49.9568376235516 +2024-10-31 10:00:00,7,29.03592355646331,20.653939494319452,77.47127868209962 +2024-10-31 11:00:00,7,37.91864608244913,21.915684491396377,70.83713988837937 +2024-10-31 12:00:00,7,15.213491571148575,20.769497233962117,49.726179507445735 +2024-10-31 13:00:00,7,19.620371071837415,23.694523005174986,47.77343223907541 +2024-10-31 14:00:00,7,17.469126226547317,24.474787344130792,40.12938965538599 +2024-10-31 15:00:00,7,42.818081757398566,27.732462863770238,67.07861181217983 +2024-10-31 16:00:00,7,33.08291806302512,27.294126295513887,56.722716628152625 +2024-10-31 17:00:00,7,34.844568280364854,22.342391741130367,53.138975714407565 +2024-10-31 18:00:00,7,36.302125467294026,24.414122519756333,53.1264115349405 +2024-10-31 19:00:00,7,31.350416245059503,19.056542486677102,55.96720432425198 +2024-10-31 20:00:00,7,29.495654569222275,16.363714612460566,39.39126920047244 +2024-10-31 21:00:00,7,22.098226108092803,17.657043016360923,55.51845769384909 +2024-10-31 22:00:00,7,18.443800274492627,21.62479976462105,52.60956304048133 +2024-10-31 23:00:00,7,25.66378816000255,23.255407307361367,50.332487058462426 +2024-11-01 00:00:00,7,16.839946134602,19.407273825525422,59.644916857712815 +2024-11-01 01:00:00,7,22.244805154480165,22.205157553083662,43.52943329706483 +2024-11-01 02:00:00,7,21.753485929668617,16.915595768572363,33.76033997617296 +2024-11-01 03:00:00,7,36.839983031833924,23.30931081860145,59.766749190816796 +2024-11-01 04:00:00,7,23.770821459772122,22.185343965907485,37.96976940947508 +2024-11-01 05:00:00,7,19.1757609505212,18.247950005302073,41.73295205683224 +2024-11-01 06:00:00,7,12.383726759413774,29.12765495963707,45.369009390398446 +2024-11-01 07:00:00,7,28.731320051050975,25.879912440471788,52.49880733570692 +2024-11-01 08:00:00,7,34.44558979195037,16.641964401234773,58.858281600912875 +2024-11-01 09:00:00,7,22.310365411752194,19.78146987436865,47.46858836387402 +2024-11-01 10:00:00,7,29.656856381968844,26.19461110907416,58.056469220859704 +2024-11-01 11:00:00,7,10.499571782919876,22.02607253580468,46.2908629860943 +2024-11-01 12:00:00,7,25.076224423817642,13.7764150551562,51.15901329703927 +2024-11-01 13:00:00,7,21.93708956667326,17.858615006650698,54.40082757162137 +2024-11-01 14:00:00,7,21.42430960028422,19.348427202292807,46.59516587106258 +2024-11-01 15:00:00,7,26.785941950615474,20.749115081914976,51.93422940521942 +2024-11-01 16:00:00,7,25.96245862910554,14.301230304145587,65.72384861856929 +2024-11-01 17:00:00,7,35.55590449342537,20.9179727894531,50.384332296512156 +2024-11-01 18:00:00,7,32.15540656688914,20.86685984507228,56.83678451828162 +2024-11-01 19:00:00,7,28.64363316277565,20.81323098702071,43.11852002398385 +2024-11-01 20:00:00,7,44.22516623621661,20.64523190676896,56.86191672012713 +2024-11-01 21:00:00,7,12.113615613521137,19.418743745624596,53.073702276493414 +2024-11-01 22:00:00,7,16.278625214390374,21.580794404151288,73.29055489113617 +2024-11-01 23:00:00,7,19.625691186564378,22.034694476111234,56.55730642040414 +2024-11-02 00:00:00,7,26.08587916219352,24.611439511367124,51.31627647458716 +2024-11-02 01:00:00,7,2.6818762456567136,27.820895006353656,39.9424774192003 +2024-11-02 02:00:00,7,26.01564062622824,18.871988682201447,68.17186390467697 +2024-11-02 03:00:00,7,32.40867193160984,25.556820302072616,40.42849354613652 +2024-11-02 04:00:00,7,18.655914871742002,22.022813899024907,54.74721941926041 +2024-08-04 05:00:00,8,21.58161849717929,22.34965096861082,55.08388634714047 +2024-08-04 06:00:00,8,22.081465671937718,25.946876099946092,45.38641419339426 +2024-08-04 07:00:00,8,9.664493994035873,23.718887829867143,48.837879928672976 +2024-08-04 08:00:00,8,23.280669406143307,21.312803331093185,63.39412794436135 +2024-08-04 09:00:00,8,21.463317860929244,29.827452740255893,48.11770001016037 +2024-08-04 10:00:00,8,46.157643151333296,24.961395669304377,59.77922147199829 +2024-08-04 11:00:00,8,30.876498357490203,25.513999633060457,52.76133112064867 +2024-08-04 12:00:00,8,30.61702480129436,24.4082926802886,55.13194561918665 +2024-08-04 13:00:00,8,36.769409784824795,21.818568267022407,42.87150759869825 +2024-08-04 14:00:00,8,33.14857489909589,17.9007798792522,54.197569143400216 +2024-08-04 15:00:00,8,39.75177454077073,25.99192909375342,62.37358175301968 +2024-08-04 16:00:00,8,41.40350046374993,22.958858663328254,53.883214858830925 +2024-08-04 17:00:00,8,30.676582607766175,25.591479843100082,59.58987232012372 +2024-08-04 18:00:00,8,30.47846222246892,31.632250258652057,35.77177322675029 +2024-08-04 19:00:00,8,16.172468509736483,25.491607332075333,59.52960533755848 +2024-08-04 20:00:00,8,31.81260819000722,26.79268585760615,67.48790921672342 +2024-08-04 21:00:00,8,33.20291440176334,17.958801226672684,55.786508482208575 +2024-08-04 22:00:00,8,23.273320652159406,23.26315475114203,49.2534484733396 +2024-08-04 23:00:00,8,24.967595218008594,22.525169985393013,50.35683382669357 +2024-08-05 00:00:00,8,25.681723076270924,27.88639971752886,63.36904744500325 +2024-08-05 01:00:00,8,22.050828383084973,19.818632814004417,61.661533330907076 +2024-08-05 02:00:00,8,17.003425357290375,26.042539958564756,48.46231752329376 +2024-08-05 03:00:00,8,20.576273267592228,18.220186047429515,55.598604252571484 +2024-08-05 04:00:00,8,20.88532085802934,27.11088448729955,54.376031251918256 +2024-08-05 05:00:00,8,19.31343854410406,23.291413983380796,44.237430047618915 +2024-08-05 06:00:00,8,50.1758117576928,23.203998439104254,70.76585700593257 +2024-08-05 07:00:00,8,17.299825017709644,17.77898037392,41.30664961560187 +2024-08-05 08:00:00,8,31.947803807614772,19.246932793109295,65.24769206906493 +2024-08-05 09:00:00,8,32.950253692487756,23.93097370975746,57.66113729327615 +2024-08-05 10:00:00,8,29.61118881573714,19.263458878057662,68.53094828282305 +2024-08-05 11:00:00,8,17.416806442040887,23.260156516644326,55.46247413416469 +2024-08-05 12:00:00,8,35.998321006076615,24.828931352320353,49.955480534680035 +2024-08-05 13:00:00,8,48.35559476478498,26.821038997451065,42.62465989278883 +2024-08-05 14:00:00,8,27.310395316093928,26.963146164037113,63.33124111127961 +2024-08-05 15:00:00,8,12.319537411359459,27.817870497066245,44.34107730235624 +2024-08-05 16:00:00,8,30.335980006149974,30.550536124638487,46.24205801964625 +2024-08-05 17:00:00,8,15.277910780456319,22.344554160875784,49.74332497557639 +2024-08-05 18:00:00,8,24.041451456775654,25.53444610365169,60.27820316541495 +2024-08-05 19:00:00,8,20.604200543606886,26.466655564821043,50.7906115881573 +2024-08-05 20:00:00,8,29.543120061080835,28.814733653361806,62.64718201016687 +2024-08-05 21:00:00,8,28.80498140751255,22.351674698127265,55.42916356928633 +2024-08-05 22:00:00,8,25.751411689710938,23.996024888840754,59.261936155100685 +2024-08-05 23:00:00,8,22.235832231258627,19.67861695404654,42.87605012774439 +2024-08-06 00:00:00,8,15.260333861689201,25.678295755356725,64.02686450434555 +2024-08-06 01:00:00,8,29.35549354952151,23.00919870217524,45.59604748494459 +2024-08-06 02:00:00,8,15.80511730745591,26.74882470125152,47.964091661898145 +2024-08-06 03:00:00,8,34.828566832989374,18.37615513540519,70.02643713135035 +2024-08-06 04:00:00,8,40.34963728083773,24.89576086549086,49.05964032026723 +2024-08-06 05:00:00,8,24.370077329641457,28.87646008170472,48.1963106443229 +2024-08-06 06:00:00,8,36.00921708183058,21.85282862461877,59.1445179790884 +2024-08-06 07:00:00,8,18.43098693771475,29.002124285717723,67.43722655737128 +2024-08-06 08:00:00,8,32.778415332110065,23.316301056920516,72.91128724809624 +2024-08-06 09:00:00,8,21.088109282014035,26.758205100012127,50.367773465120266 +2024-08-06 10:00:00,8,25.956993949116328,29.01101586698428,65.75358958776764 +2024-08-06 11:00:00,8,29.84523242634645,24.036785855612386,56.50177978566455 +2024-08-06 12:00:00,8,7.882411657722148,22.197711320330463,76.76546924694082 +2024-08-06 13:00:00,8,29.811471214322356,22.07256442775532,48.163369990409606 +2024-08-06 14:00:00,8,30.63236619126425,23.72232820386563,57.338595820819236 +2024-08-06 15:00:00,8,38.807380052172576,25.445764009951528,68.22074470790292 +2024-08-06 16:00:00,8,33.05875329509684,28.268435007727504,61.023474732695135 +2024-08-06 17:00:00,8,26.189844442321878,27.069221758428785,42.662391401413046 +2024-08-06 18:00:00,8,37.172273377666016,22.478887400873386,51.29118944757894 +2024-08-06 19:00:00,8,45.94035881283271,22.13183316742899,38.55734469095795 +2024-08-06 20:00:00,8,30.998773047139778,21.669628499475305,59.380103122929604 +2024-08-06 21:00:00,8,21.69321995848874,22.703587449877524,57.937075895731056 +2024-08-06 22:00:00,8,25.12364144866649,19.729878457358534,50.90365382466729 +2024-08-06 23:00:00,8,14.366086941115373,27.051499953701587,65.78320110055093 +2024-08-07 00:00:00,8,32.159499263065726,24.62545358824589,61.595843673557475 +2024-08-07 01:00:00,8,33.45094584253632,14.540408506344816,47.89390398289512 +2024-08-07 02:00:00,8,29.696134972664748,19.816610571321718,68.43991162462797 +2024-08-07 03:00:00,8,13.69285898651024,21.40356042290977,65.00006430178946 +2024-08-07 04:00:00,8,28.81715651116608,19.315506786717826,56.086773411593605 +2024-08-07 05:00:00,8,19.571168713190076,23.46117889415966,51.11630578055954 +2024-08-07 06:00:00,8,15.310231906306358,28.560421125679884,50.926687297972485 +2024-08-07 07:00:00,8,22.032642362366026,24.349903568445935,50.47290726064795 +2024-08-07 08:00:00,8,35.587214262421654,26.263218497358622,63.19242440728997 +2024-08-07 09:00:00,8,30.27440410174811,17.479827588027078,69.36898597654911 +2024-08-07 10:00:00,8,37.520442726368955,23.25073777938816,52.41294916452404 +2024-08-07 11:00:00,8,34.16986777300656,28.695215926264062,53.430591201560304 +2024-08-07 12:00:00,8,33.522799977487196,22.75222187659608,59.57484561726626 +2024-08-07 13:00:00,8,47.84754643473019,22.1214712620333,59.49821997482063 +2024-08-07 14:00:00,8,26.13569576694796,27.12189394262664,53.27020375987586 +2024-08-07 15:00:00,8,34.35070666106182,23.745429743584246,48.87811542751263 +2024-08-07 16:00:00,8,34.81564171340677,26.43764332498014,53.63705091434852 +2024-08-07 17:00:00,8,39.15199070711602,21.281995031991297,37.27242195185629 +2024-08-07 18:00:00,8,15.353110526312692,24.336300648147947,45.73883033166031 +2024-08-07 19:00:00,8,11.294857295782633,22.867382490321425,57.87621104814512 +2024-08-07 20:00:00,8,33.06478952779257,29.453906255049716,62.85321436297905 +2024-08-07 21:00:00,8,23.123428659228836,22.283930927390365,59.75911089273423 +2024-08-07 22:00:00,8,17.981922500897248,24.903695224180336,59.86964207966268 +2024-08-07 23:00:00,8,33.39941620709379,22.510838864728846,40.43689033294113 +2024-08-08 00:00:00,8,3.0257829762051784,30.7270692539768,37.2510821963039 +2024-08-08 01:00:00,8,38.70337838822347,25.43675168265288,70.9884151915264 +2024-08-08 02:00:00,8,36.33091991432188,18.553154270350486,48.55647122256898 +2024-08-08 03:00:00,8,16.595132915219224,15.729813526166165,42.14112172337491 +2024-08-08 04:00:00,8,17.53264680814528,25.81473179817582,57.923581167447566 +2024-08-08 05:00:00,8,30.171987575746954,26.18220425829962,55.911543833703874 +2024-08-08 06:00:00,8,36.71285244635809,24.230589643776895,64.75453927814254 +2024-08-08 07:00:00,8,18.99183491799758,27.585867263947662,43.941909211989724 +2024-08-08 08:00:00,8,31.61748873573057,22.123727605168728,52.58937218429155 +2024-08-08 09:00:00,8,42.461363893095935,23.399418056790214,55.35903578809996 +2024-08-08 10:00:00,8,40.69783256086446,19.163889328274596,51.20051759127138 +2024-08-08 11:00:00,8,42.39187187240408,24.087809690733202,57.71799434045054 +2024-08-08 12:00:00,8,20.718662989931566,23.599802845594702,64.56182476124653 +2024-08-08 13:00:00,8,22.582096591169826,25.740891388614124,47.726779863975885 +2024-08-08 14:00:00,8,20.806165640035225,25.060585920678363,54.512404235956566 +2024-08-08 15:00:00,8,35.72393198168348,27.234408076431173,55.3840846270101 +2024-08-08 16:00:00,8,32.97773386300193,23.37687663424917,63.216056073210005 +2024-08-08 17:00:00,8,28.047251479482494,18.68803554453727,60.897836168564325 +2024-08-08 18:00:00,8,16.353910874183377,27.369996959106395,54.9566816072245 +2024-08-08 19:00:00,8,14.052563654206882,25.892180228347357,54.90888475055017 +2024-08-08 20:00:00,8,2.3259280221891956,22.962145528662504,54.38834537183315 +2024-08-08 21:00:00,8,28.407256292025558,19.017805254099443,69.2057470835304 +2024-08-08 22:00:00,8,28.008815564614196,24.471951312354857,56.04712231860942 +2024-08-08 23:00:00,8,33.01984719462533,19.698801049048477,60.49643939638982 +2024-08-09 00:00:00,8,18.155637415673866,18.929323721941408,54.47708638606488 +2024-08-09 01:00:00,8,24.9196856424047,25.072709123995832,50.46075078184791 +2024-08-09 02:00:00,8,21.025829594377765,26.39133362345408,39.41216931816814 +2024-08-09 03:00:00,8,32.61373550533862,19.59049988157018,56.02855186316282 +2024-08-09 04:00:00,8,16.540379400541603,23.92039967527303,45.571699752984436 +2024-08-09 05:00:00,8,27.62520504815007,21.289292410913916,52.130863302487946 +2024-08-09 06:00:00,8,31.020070027191508,22.986310053688456,37.45218318887747 +2024-08-09 07:00:00,8,43.02332628504593,22.495105593031372,40.20152362571882 +2024-08-09 08:00:00,8,35.257982597427116,20.683657996807185,51.92605292997147 +2024-08-09 09:00:00,8,38.268676436348485,22.08375791662204,61.31131025099315 +2024-08-09 10:00:00,8,30.99073217609095,18.74192364307433,64.07260894807777 +2024-08-09 11:00:00,8,37.85479508150141,25.192586868700207,58.904636146443195 +2024-08-09 12:00:00,8,29.03668835108881,21.876028663027014,57.027099856420755 +2024-08-09 13:00:00,8,27.62100311700515,20.390994925405863,57.28915662126189 +2024-08-09 14:00:00,8,35.176015393919144,22.257595786801293,44.19838529180371 +2024-08-09 15:00:00,8,29.10509359367267,27.4916875324387,55.035121061434985 +2024-08-09 16:00:00,8,32.58416119275297,25.07656274498739,44.14424456139636 +2024-08-09 17:00:00,8,33.2992689958704,26.579473933964636,57.0909055651182 +2024-08-09 18:00:00,8,39.31920776309474,25.070115040578127,46.191763784902115 +2024-08-09 19:00:00,8,46.25464037500261,22.733075391975525,69.23805375504769 +2024-08-09 20:00:00,8,24.705857069430877,29.245833307816348,46.48931911334762 +2024-08-09 21:00:00,8,33.211454539441036,27.40240381488646,68.82273887623127 +2024-08-09 22:00:00,8,35.81488501738444,24.495905415312397,54.47301729598382 +2024-08-09 23:00:00,8,18.49215496244431,22.960799431537396,53.89584965930413 +2024-08-10 00:00:00,8,24.060262821027706,22.457789750824027,62.464673407545945 +2024-08-10 01:00:00,8,21.17483748546873,21.388820735426073,53.409601737314894 +2024-08-10 02:00:00,8,28.140209481411336,22.36785559977777,49.30168239808986 +2024-08-10 03:00:00,8,26.57664489857886,26.513471126063685,62.33172474667117 +2024-08-10 04:00:00,8,20.263052819172394,23.735417719643884,39.57400810085305 +2024-08-10 05:00:00,8,31.77793904570814,18.962943498568972,62.04843192822391 +2024-08-10 06:00:00,8,13.41347922740839,19.670358130210513,71.9197058112593 +2024-08-10 07:00:00,8,24.946907067109443,28.58584625456538,40.38237342174311 +2024-08-10 08:00:00,8,34.50098917728474,26.043208046633307,59.8153136206807 +2024-08-10 09:00:00,8,40.751015175892036,22.386388759083268,60.312980751058745 +2024-08-10 10:00:00,8,17.984367474936086,25.087785226113105,66.28463669494242 +2024-08-10 11:00:00,8,14.493236921094814,23.502118975745976,55.52433194883903 +2024-08-10 12:00:00,8,19.59629178699753,22.87753006592299,61.84222959888599 +2024-08-10 13:00:00,8,21.36180058330975,21.298632133354424,57.54025138742198 +2024-08-10 14:00:00,8,12.839724001766974,20.986577798329343,61.174946697499465 +2024-08-10 15:00:00,8,30.005878276970524,26.925770402108274,38.96755324979742 +2024-08-10 16:00:00,8,28.73605609252869,19.39239292661584,48.098504280672955 +2024-08-10 17:00:00,8,31.914082644021164,26.547692334008595,55.60447408659978 +2024-08-10 18:00:00,8,36.73846997795948,26.398527689528347,51.80536301331448 +2024-08-10 19:00:00,8,14.699087810064622,28.068031475501684,50.89364516661374 +2024-08-10 20:00:00,8,26.131598457731858,19.35712209302242,54.96877413677988 +2024-08-10 21:00:00,8,18.80832257869378,25.05030144107548,52.509435565465964 +2024-08-10 22:00:00,8,26.440949690934232,23.805530504714536,49.17067525884943 +2024-08-10 23:00:00,8,42.05482646338638,20.208743058294722,41.19480455847854 +2024-08-11 00:00:00,8,29.11940884921596,30.281078964933872,38.75864048535879 +2024-08-11 01:00:00,8,19.877611509119284,27.488836853973037,43.58360030192215 +2024-08-11 02:00:00,8,22.853452670519843,20.73264114215169,46.94511500600623 +2024-08-11 03:00:00,8,8.262766390140243,24.50429501886001,49.012219354138836 +2024-08-11 04:00:00,8,22.813988585360768,17.001696667932894,46.09060160084724 +2024-08-11 05:00:00,8,14.416581000247435,17.378767230960694,55.56771639029404 +2024-08-11 06:00:00,8,24.90699038007766,24.373732634591214,41.55054105600948 +2024-08-11 07:00:00,8,17.40675690850123,27.041738235006427,65.78978161248803 +2024-08-11 08:00:00,8,26.683208074885005,28.197235183475954,44.31286031845304 +2024-08-11 09:00:00,8,36.67551893063069,18.926686918705844,65.19261807928257 +2024-08-11 10:00:00,8,32.273944617739026,26.2323302683128,63.4713023833466 +2024-08-11 11:00:00,8,25.02063353130529,18.59227612344572,50.70532059626299 +2024-08-11 12:00:00,8,39.12758410292251,27.16646980851658,52.22110633603364 +2024-08-11 13:00:00,8,25.930877948121235,24.019564063432103,45.91840349333335 +2024-08-11 14:00:00,8,20.87540363251313,30.508489214822333,43.279653049865644 +2024-08-11 15:00:00,8,15.928328018252245,29.564452282224334,62.75432620747972 +2024-08-11 16:00:00,8,31.781035820098374,22.41961849137456,39.59931861901314 +2024-08-11 17:00:00,8,12.080832558313922,27.052586390229337,62.671328292866775 +2024-08-11 18:00:00,8,23.552084863875542,23.33324935656557,60.33744476173179 +2024-08-11 19:00:00,8,32.846395465054385,25.931705863628295,53.51918674475761 +2024-08-11 20:00:00,8,31.002362860920556,22.854568060138785,58.16042458099738 +2024-08-11 21:00:00,8,17.313858694207077,23.76272617375847,52.773180474636725 +2024-08-11 22:00:00,8,19.486918334969268,25.432952840591355,55.13710108832118 +2024-08-11 23:00:00,8,29.85065097242148,22.906082776575264,61.91766084446866 +2024-08-12 00:00:00,8,12.93963152441771,22.447944348771074,42.732327184080106 +2024-08-12 01:00:00,8,4.8504240703856745,22.24591516692274,53.112662128187395 +2024-08-12 02:00:00,8,23.74850293904577,31.159201919139612,63.36117129107074 +2024-08-12 03:00:00,8,26.345086449157456,27.600053600828907,64.03800671414882 +2024-08-12 04:00:00,8,15.186045366526916,22.54289191771751,57.06971830535045 +2024-08-12 05:00:00,8,41.77676822834832,19.696810264382158,40.07091914835485 +2024-08-12 06:00:00,8,25.335733710009716,24.83668042215291,55.22475639675268 +2024-08-12 07:00:00,8,23.72767875704747,26.34375815721197,57.973331003993486 +2024-08-12 08:00:00,8,41.336601648072545,23.007612962150855,63.23152760425111 +2024-08-12 09:00:00,8,26.723603377376655,17.486553795442813,62.68216075145861 +2024-08-12 10:00:00,8,42.55962740466682,29.137643854614126,52.6691649161676 +2024-08-12 11:00:00,8,32.60024473292276,26.196939510846928,52.785083948428266 +2024-08-12 12:00:00,8,32.18080795255255,23.00611877710851,53.12185837530848 +2024-08-12 13:00:00,8,20.250235247212863,21.88909362787086,40.55892892080819 +2024-08-12 14:00:00,8,21.745311395617723,22.666883532076803,55.84229555198566 +2024-08-12 15:00:00,8,20.40994109939419,23.88336684480393,64.25766646220319 +2024-08-12 16:00:00,8,23.01384642043122,26.21305271224862,64.03111817628813 +2024-08-12 17:00:00,8,43.1317092657474,26.153369758104194,42.502982805420444 +2024-08-12 18:00:00,8,31.225914432942403,26.5318333075017,56.596336609713596 +2024-08-12 19:00:00,8,20.670636997791895,27.231423588694234,61.958361459071114 +2024-08-12 20:00:00,8,20.006978530106874,21.29806652753463,67.37623609805459 +2024-08-12 21:00:00,8,28.655092907339018,21.619942926834675,58.63458391076055 +2024-08-12 22:00:00,8,39.402313241453584,19.866457203172185,69.69614877860668 +2024-08-12 23:00:00,8,15.080711349469647,20.748198190268063,36.624140716618086 +2024-08-13 00:00:00,8,37.05044886217537,22.29685059265425,53.60472317263276 +2024-08-13 01:00:00,8,28.70773776336737,23.177620723073648,46.1504476998899 +2024-08-13 02:00:00,8,23.9630348842717,21.54823101990883,60.71203579324562 +2024-08-13 03:00:00,8,27.368253487586152,24.498708986680843,52.26579149907592 +2024-08-13 04:00:00,8,22.813099230249936,18.066107724813254,49.94922716138247 +2024-08-13 05:00:00,8,30.735529100772343,21.305329342931653,47.52154961656366 +2024-08-13 06:00:00,8,23.0509576711393,22.910480731155225,57.05538206043334 +2024-08-13 07:00:00,8,30.335703120768603,27.65115961777604,61.93775696145974 +2024-08-13 08:00:00,8,31.073475779929062,25.098307736145824,54.80742214250673 +2024-08-13 09:00:00,8,13.225030154158402,25.861357373194934,66.3327316202423 +2024-08-13 10:00:00,8,26.667327189827628,23.486207236900416,59.7982630215332 +2024-08-13 11:00:00,8,32.30067913676219,27.036543477981557,50.678921105717926 +2024-08-13 12:00:00,8,22.238516104835803,23.64140672735724,53.63675180530748 +2024-08-13 13:00:00,8,21.180228724180502,24.85244826544094,62.014169214577954 +2024-08-13 14:00:00,8,33.764396337247014,26.133224900272257,49.64096898512048 +2024-08-13 15:00:00,8,35.18374806966251,32.41683985040155,55.846970233141285 +2024-08-13 16:00:00,8,28.128510316447162,21.970132238438307,62.85490793024664 +2024-08-13 17:00:00,8,22.720220693230935,25.737018643043125,60.73720250061627 +2024-08-13 18:00:00,8,27.454171257420782,28.420984392911766,54.142955920647225 +2024-08-13 19:00:00,8,31.60900035033925,23.365396957924965,48.91140203041439 +2024-08-13 20:00:00,8,26.709486693482226,20.85510792257978,54.02229418076741 +2024-08-13 21:00:00,8,30.618935577759945,21.771568797634046,59.76402730696439 +2024-08-13 22:00:00,8,3.8984135430962183,22.596424444476032,49.65247868596973 +2024-08-13 23:00:00,8,16.65213209509939,26.402277052724052,55.60665148241841 +2024-08-14 00:00:00,8,15.675895379845759,27.046897622463415,47.273381521301914 +2024-08-14 01:00:00,8,30.766814327647488,18.758151066506144,49.60474313164186 +2024-08-14 02:00:00,8,43.47688291296984,23.825989882126066,58.46221223571919 +2024-08-14 03:00:00,8,28.16532990831326,31.230516063423053,54.756850060350644 +2024-08-14 04:00:00,8,16.945105840705878,25.058282481764998,58.983753216722384 +2024-08-14 05:00:00,8,28.45430142752471,22.354256178756525,63.83850681239147 +2024-08-14 06:00:00,8,21.049068715871528,18.76371341424729,55.472923045226395 +2024-08-14 07:00:00,8,29.51512373237355,20.31262090255019,52.73671351926432 +2024-08-14 08:00:00,8,42.805723874571484,28.19125371417946,63.64431160376534 +2024-08-14 09:00:00,8,21.500213331108128,22.248028949544683,53.91842316566885 +2024-08-14 10:00:00,8,35.26258941156733,23.350905959395302,37.65358652121561 +2024-08-14 11:00:00,8,42.5124669410123,24.182859759559324,59.183446933812604 +2024-08-14 12:00:00,8,22.560043684015596,21.844597047669836,53.23810345727486 +2024-08-14 13:00:00,8,28.02027269128141,26.18548896694553,45.64479113614611 +2024-08-14 14:00:00,8,15.236693548740543,27.797752042226435,54.574285569872686 +2024-08-14 15:00:00,8,22.076236498252197,27.37268293851289,64.78145491056571 +2024-08-14 16:00:00,8,29.73110450337356,30.510094717299943,54.31375044753767 +2024-08-14 17:00:00,8,31.990825212044065,21.120988866207295,51.6102375239285 +2024-08-14 18:00:00,8,40.05890706360505,28.863502767681258,60.13739514933103 +2024-08-14 19:00:00,8,36.874924664423524,21.853273047733914,69.71236925140823 +2024-08-14 20:00:00,8,21.813192500704343,26.65263117034884,74.58215406986112 +2024-08-14 21:00:00,8,22.93354178619189,23.88332984733829,49.888108548955024 +2024-08-14 22:00:00,8,14.638046278062168,26.89688149451289,56.196643177432215 +2024-08-14 23:00:00,8,8.781012149109408,24.111324249515953,55.89837602670042 +2024-08-15 00:00:00,8,17.110703110221493,25.829512113640103,51.6657574918905 +2024-08-15 01:00:00,8,23.187404013812987,20.495378282890627,41.49092639438298 +2024-08-15 02:00:00,8,26.348208016335914,20.58058212320504,50.25209552867494 +2024-08-15 03:00:00,8,24.13600791595452,15.714414645780925,46.73349486584201 +2024-08-15 04:00:00,8,28.069309197851204,23.50029218583338,48.56117724842074 +2024-08-15 05:00:00,8,22.540966369824577,29.303754122947545,50.72415079139333 +2024-08-15 06:00:00,8,43.62239543380588,20.831843076282837,47.2144078429757 +2024-08-15 07:00:00,8,24.175993900994854,22.93710920993209,53.411539488920646 +2024-08-15 08:00:00,8,35.27982764403195,21.539336892108466,41.68833386786599 +2024-08-15 09:00:00,8,23.09518671764293,18.192690212900594,56.515131648340116 +2024-08-15 10:00:00,8,22.049169127987227,27.807914069756613,33.402155976098136 +2024-08-15 11:00:00,8,37.68631868143399,26.284809117379275,58.72380044701445 +2024-08-15 12:00:00,8,35.72532762966266,23.54569846306724,54.356929218044435 +2024-08-15 13:00:00,8,24.073144223843492,21.182302370261738,43.639919066893086 +2024-08-15 14:00:00,8,21.83829422698434,26.306629532609897,56.57311795513524 +2024-08-15 15:00:00,8,43.96214022186884,25.528901386484772,46.45778316464224 +2024-08-15 16:00:00,8,18.863267834054817,26.95694198925645,46.39928817102662 +2024-08-15 17:00:00,8,41.20900747840413,25.833744591066473,54.45867605139411 +2024-08-15 18:00:00,8,34.973614344453665,29.620427026077802,63.024683695783516 +2024-08-15 19:00:00,8,14.790740891341487,28.924015943135828,54.77467115442412 +2024-08-15 20:00:00,8,42.21090517402635,20.923409211600315,39.63253480909816 +2024-08-15 21:00:00,8,31.54446167595075,27.317407715960638,51.67256612828487 +2024-08-15 22:00:00,8,24.67224339254239,22.3091079852687,54.930804562502146 +2024-08-15 23:00:00,8,41.77493316673289,25.147688207890152,54.367412131798766 +2024-08-16 00:00:00,8,23.493050815380357,21.62269978936682,47.61663045137994 +2024-08-16 01:00:00,8,4.825171798320337,23.459264710322355,38.671169460909134 +2024-08-16 02:00:00,8,22.700138948882984,25.194183088879797,52.67300306613911 +2024-08-16 03:00:00,8,19.80858873373755,20.9237692325382,52.60749864819836 +2024-08-16 04:00:00,8,26.360724892436657,19.36429638097966,60.14181299583539 +2024-08-16 05:00:00,8,22.6716888361813,21.269667969868486,40.41535078924617 +2024-08-16 06:00:00,8,27.921336774485585,22.920641935166024,53.66082151169712 +2024-08-16 07:00:00,8,33.76335804793271,25.323321375937653,49.362759560135416 +2024-08-16 08:00:00,8,18.310809936122748,23.716796462510736,52.678921652133184 +2024-08-16 09:00:00,8,41.967788246712615,25.468703978756267,53.48187362203673 +2024-08-16 10:00:00,8,45.2440773740596,29.171700121513354,42.860652155079066 +2024-08-16 11:00:00,8,22.951367725734208,26.14397479891306,50.324469164109274 +2024-08-16 12:00:00,8,21.31568475921347,23.69918394265479,45.4396730708789 +2024-08-16 13:00:00,8,44.18198909153109,26.32470996202649,62.567384677656214 +2024-08-16 14:00:00,8,22.588126834722942,25.103312458185567,49.58183419119604 +2024-08-16 15:00:00,8,37.37054711451992,28.255870195442323,42.40076172080232 +2024-08-16 16:00:00,8,30.286470417075485,23.13190577970274,50.45033179056005 +2024-08-16 17:00:00,8,27.17498324621789,23.754277600642517,56.36770053775152 +2024-08-16 18:00:00,8,9.077153956479592,23.278711288540144,48.678326073096194 +2024-08-16 19:00:00,8,33.36505864340031,20.914587146555178,44.51793225996932 +2024-08-16 20:00:00,8,27.014889996078956,28.606364081166127,61.87173121281401 +2024-08-16 21:00:00,8,33.94697110743584,26.00853855781124,50.861831639753895 +2024-08-16 22:00:00,8,20.617678003181297,20.085621621283806,62.69266489592639 +2024-08-16 23:00:00,8,34.98009547569084,17.89768996994302,49.9723116489379 +2024-08-17 00:00:00,8,15.096251834917497,27.529343160369592,52.48766928165249 +2024-08-17 01:00:00,8,36.68739192268514,22.016044853551968,62.86426080202575 +2024-08-17 02:00:00,8,12.825909461237007,20.351042371220142,50.28988398232412 +2024-08-17 03:00:00,8,21.449831040764586,25.40807279479325,49.39213126136496 +2024-08-17 04:00:00,8,27.694274447413626,25.171563633877724,60.48689057725924 +2024-08-17 05:00:00,8,39.343058455526425,20.657744189457063,59.6178504246566 +2024-08-17 06:00:00,8,32.44687312777096,22.194407415327905,41.305032253216844 +2024-08-17 07:00:00,8,37.2955859073975,24.685414746389736,53.54195320149657 +2024-08-17 08:00:00,8,20.905355861245507,22.67293257968949,54.81966516493921 +2024-08-17 09:00:00,8,36.33937232428109,23.585130106459154,52.03933510406129 +2024-08-17 10:00:00,8,14.56467389165268,21.048445780445867,56.5414257349333 +2024-08-17 11:00:00,8,23.62674516238369,25.748450105672475,46.49494521886287 +2024-08-17 12:00:00,8,23.750652321248243,23.157859514659403,50.88377040466338 +2024-08-17 13:00:00,8,26.109037287178847,20.055389527616406,65.0800636623857 +2024-08-17 14:00:00,8,29.08615443570634,30.29535456634332,45.46861072555718 +2024-08-17 15:00:00,8,33.30021643457005,30.01345398025723,49.4115258126844 +2024-08-17 16:00:00,8,12.231369991387663,20.770742360080796,55.13823480707373 +2024-08-17 17:00:00,8,32.159467474339856,22.076282353211443,61.30618192376252 +2024-08-17 18:00:00,8,36.00844822541274,21.212649204728947,62.83620777752624 +2024-08-17 19:00:00,8,6.870062939694186,22.686481479823755,67.76842488503543 +2024-08-17 20:00:00,8,20.275304736131126,21.974387839216668,68.68639734532053 +2024-08-17 21:00:00,8,34.33766855246121,25.77077356387932,73.71902264573808 +2024-08-17 22:00:00,8,23.88506826446748,19.863814162269602,52.2713601318302 +2024-08-17 23:00:00,8,35.88348474521744,21.140558390411456,54.24510121132623 +2024-08-18 00:00:00,8,26.30315890070754,26.162710922600773,36.28746745979254 +2024-08-18 01:00:00,8,20.312841268448928,22.090017949569933,52.778098870162914 +2024-08-18 02:00:00,8,22.010047611908576,23.312006546536615,55.02453204704788 +2024-08-18 03:00:00,8,30.54395759374928,25.216294621578545,70.20802156045721 +2024-08-18 04:00:00,8,31.25613029532033,19.50503658993862,57.76177259739907 +2024-08-18 05:00:00,8,37.31610482916258,23.227917989860863,81.64520189278572 +2024-08-18 06:00:00,8,25.335482859735063,25.225762608549935,35.578507463996445 +2024-08-18 07:00:00,8,25.387669434312766,18.673468824451117,52.5593816854096 +2024-08-18 08:00:00,8,29.319749057304474,18.177116668200817,57.96439684965576 +2024-08-18 09:00:00,8,28.958106935968626,21.82278381707979,66.50758295193666 +2024-08-18 10:00:00,8,26.91384844760399,25.927860838577118,62.360587138210555 +2024-08-18 11:00:00,8,14.56848642153568,20.439484890508904,50.81021828117359 +2024-08-18 12:00:00,8,39.27046367999196,23.347615365146478,30.184121717318238 +2024-08-18 13:00:00,8,33.80440365761039,16.77654579872744,56.501263416898674 +2024-08-18 14:00:00,8,19.613053912399703,25.511287604207556,48.30628591582003 +2024-08-18 15:00:00,8,14.381871657394091,25.48342048244139,50.68874396196184 +2024-08-18 16:00:00,8,22.558038567300347,25.001399768101713,76.48322982167215 +2024-08-18 17:00:00,8,22.208438400545756,24.724305033275208,41.871480536452715 +2024-08-18 18:00:00,8,29.05044743539082,23.443190507851558,52.14538353051069 +2024-08-18 19:00:00,8,21.181404082304873,26.58214482457703,42.27704662542629 +2024-08-18 20:00:00,8,42.29149273189884,21.62422265424103,61.1115448269132 +2024-08-18 21:00:00,8,14.922254984865976,20.360344553732574,50.37098462767616 +2024-08-18 22:00:00,8,41.05131105636303,18.508624275335862,56.06093962686044 +2024-08-18 23:00:00,8,22.595236954540106,23.593869473287995,64.62776911316878 +2024-08-19 00:00:00,8,13.866563458764727,22.343448453377803,50.68478883614841 +2024-08-19 01:00:00,8,16.92272853538259,22.726632081521753,43.7312435579278 +2024-08-19 02:00:00,8,36.012957880888656,20.022843484715644,53.3558624291867 +2024-08-19 03:00:00,8,10.457384005374026,24.233230415664817,47.677622275258464 +2024-08-19 04:00:00,8,37.21261999039243,24.187869734714198,48.85201634086335 +2024-08-19 05:00:00,8,29.913255378225397,22.697823892930202,53.633965053719734 +2024-08-19 06:00:00,8,18.261745767001067,28.542873838270477,38.18371574126891 +2024-08-19 07:00:00,8,23.161397577784015,29.24384970227062,42.14161754638371 +2024-08-19 08:00:00,8,21.112789471451705,24.357309804131862,57.75050142254818 +2024-08-19 09:00:00,8,31.062524533570546,17.61245868130697,47.65578928096834 +2024-08-19 10:00:00,8,1.5196405367260795,25.882928656098294,44.741937961688535 +2024-08-19 11:00:00,8,39.120683336986936,26.411508337593357,61.320530344216905 +2024-08-19 12:00:00,8,34.99309065577917,25.013451730336694,67.77867464907872 +2024-08-19 13:00:00,8,31.844221800648224,26.72793967035892,57.35111419545703 +2024-08-19 14:00:00,8,37.29782216230413,22.564672862568052,55.88492835973051 +2024-08-19 15:00:00,8,36.38098006203567,21.536944471418842,52.68139722023223 +2024-08-19 16:00:00,8,27.44822732620762,28.70544968109622,47.121250688721645 +2024-08-19 17:00:00,8,22.110931453811567,28.246655292616108,62.17213708762873 +2024-08-19 18:00:00,8,25.93007756722764,23.033282997573277,51.77996156945823 +2024-08-19 19:00:00,8,28.047965422241568,28.234493801013,42.817491160886426 +2024-08-19 20:00:00,8,25.650651855154326,25.34334160079349,49.16867475298279 +2024-08-19 21:00:00,8,20.365317817739353,23.762424671861385,52.461826396991626 +2024-08-19 22:00:00,8,20.920271240037742,18.48920740796321,60.37402147619712 +2024-08-19 23:00:00,8,20.445863297617983,21.70084332182452,52.03171909245762 +2024-08-20 00:00:00,8,19.934310234428924,24.16021411701285,39.07317263003761 +2024-08-20 01:00:00,8,27.830364324139104,17.642845873045538,47.5178337665723 +2024-08-20 02:00:00,8,15.906309643918867,28.743941681585852,55.25494095730987 +2024-08-20 03:00:00,8,28.397281222070397,24.78108126902618,52.19075508571265 +2024-08-20 04:00:00,8,28.057673930679314,21.662882496181847,39.21232283783096 +2024-08-20 05:00:00,8,33.666507442897675,20.792275184098408,33.97156589790269 +2024-08-20 06:00:00,8,20.124702430129876,22.937645423276546,57.94938738703458 +2024-08-20 07:00:00,8,28.47141657556363,25.195519855414346,63.74465601824172 +2024-08-20 08:00:00,8,31.496500074393936,31.31093315596725,48.27427706943676 +2024-08-20 09:00:00,8,32.89221909970101,21.013216778695977,59.589544448961114 +2024-08-20 10:00:00,8,33.39155715652939,29.692023315968623,63.168496504666244 +2024-08-20 11:00:00,8,34.94946577525634,24.85046799997463,62.31711826992584 +2024-08-20 12:00:00,8,24.55406007070036,30.858143271207943,51.99327633464885 +2024-08-20 13:00:00,8,39.45191287275997,27.88341589205782,55.387098339945744 +2024-08-20 14:00:00,8,18.892248925549932,24.24508518623477,53.590134127641235 +2024-08-20 15:00:00,8,14.769842612118927,25.833415152210236,49.47171115618637 +2024-08-20 16:00:00,8,30.982897261016408,20.585136315093976,61.45335645792619 +2024-08-20 17:00:00,8,38.99237476613197,23.857914034608747,61.06403890030616 +2024-08-20 18:00:00,8,23.5297534463668,22.439156691013476,41.06128589287407 +2024-08-20 19:00:00,8,3.493606376712961,24.436331314584667,54.51308068956387 +2024-08-20 20:00:00,8,36.874838511628,24.797420374780735,45.612670685489455 +2024-08-20 21:00:00,8,27.84099428161199,24.94942378322638,57.34489835549934 +2024-08-20 22:00:00,8,24.84660619881238,19.777002637599594,52.853774921546915 +2024-08-20 23:00:00,8,23.84723584147005,24.51101212332344,43.99100137696242 +2024-08-21 00:00:00,8,16.224034041991374,24.877043630982005,42.45667939842467 +2024-08-21 01:00:00,8,15.93098381858203,23.56190463590864,60.60246075689399 +2024-08-21 02:00:00,8,25.989786072319166,19.887805752906647,32.4527530673481 +2024-08-21 03:00:00,8,4.545495028931608,24.180666960280362,47.24955623113403 +2024-08-21 04:00:00,8,33.46525728050621,20.609532056626513,46.908544882099456 +2024-08-21 05:00:00,8,49.95315569374375,24.14943912465488,44.5820163477305 +2024-08-21 06:00:00,8,42.24266494810293,20.952113326362742,44.026876680146444 +2024-08-21 07:00:00,8,33.17897300517798,22.935931828430643,52.889895619954586 +2024-08-21 08:00:00,8,27.671610144088568,19.21112701113484,49.92032250337644 +2024-08-21 09:00:00,8,27.121943866425767,17.641120949451796,60.3608391223335 +2024-08-21 10:00:00,8,24.309100899102837,28.60862052155584,73.55084801005849 +2024-08-21 11:00:00,8,12.501793507155455,21.499544494781116,67.41719986817446 +2024-08-21 12:00:00,8,20.387888558378307,24.660415484608187,50.99180383252429 +2024-08-21 13:00:00,8,35.99366580513544,23.238539826301295,43.470968184289234 +2024-08-21 14:00:00,8,25.820838516655986,20.043350254712102,52.45172602488301 +2024-08-21 15:00:00,8,27.25335343100893,25.562266926407172,62.546822807095715 +2024-08-21 16:00:00,8,19.500092815488877,31.619160119877264,48.95479796603188 +2024-08-21 17:00:00,8,45.324573933712664,29.112474319382926,49.90079242042099 +2024-08-21 18:00:00,8,31.32889921821903,16.272978151571493,46.018920145706076 +2024-08-21 19:00:00,8,23.459569406040334,20.314275287011277,67.88348601682705 +2024-08-21 20:00:00,8,35.09887444377805,31.77221536765142,55.65693871387772 +2024-08-21 21:00:00,8,16.199748249258707,24.385398400634234,37.8657893185123 +2024-08-21 22:00:00,8,20.677118305741267,23.25512003932655,50.30584506146768 +2024-08-21 23:00:00,8,14.729004025549298,25.759409223461155,49.61838409104229 +2024-08-22 00:00:00,8,13.373133249757766,26.364057152405145,60.53451461964799 +2024-08-22 01:00:00,8,40.60861638319864,21.020054714899835,49.144365779075244 +2024-08-22 02:00:00,8,27.755995934326386,24.14004608869302,51.79624784068447 +2024-08-22 03:00:00,8,18.422196523250786,25.4157185060705,62.647389749469966 +2024-08-22 04:00:00,8,27.89595533148226,22.854487699724363,54.14768102633861 +2024-08-22 05:00:00,8,29.919107554439016,21.094508986610897,69.64813001372005 +2024-08-22 06:00:00,8,7.765049424163834,22.068301649527484,40.911757956500956 +2024-08-22 07:00:00,8,33.503771210352475,20.843099255868697,53.55682685717048 +2024-08-22 08:00:00,8,33.069619914938144,27.744842958036013,51.01551114256791 +2024-08-22 09:00:00,8,28.406357172739597,17.275213971256438,41.037463349873846 +2024-08-22 10:00:00,8,16.881334590116012,25.905900313248946,43.09374983432117 +2024-08-22 11:00:00,8,31.51227818216269,25.293830753512363,46.25801756090364 +2024-08-22 12:00:00,8,32.762920662743944,27.59740256899722,60.57912983316723 +2024-08-22 13:00:00,8,42.26389860738015,23.443523365490023,53.748433029660475 +2024-08-22 14:00:00,8,37.066311104834355,26.8946157108904,62.97878404684458 +2024-08-22 15:00:00,8,9.650753896450574,24.415972498293225,53.556463862124794 +2024-08-22 16:00:00,8,19.393534603694157,26.11170857609706,60.10364880511635 +2024-08-22 17:00:00,8,32.28120052590993,29.247075875526015,57.390200830763945 +2024-08-22 18:00:00,8,19.295598754865686,25.628540646600083,43.345706373741464 +2024-08-22 19:00:00,8,23.744968320107283,24.91785612280152,56.13056261599349 +2024-08-22 20:00:00,8,27.991877026969014,26.2238612979558,42.919822243405434 +2024-08-22 21:00:00,8,26.628711574424614,18.84421624650139,58.53617844699611 +2024-08-22 22:00:00,8,26.408657310402493,25.233048610827215,58.65786301034354 +2024-08-22 23:00:00,8,23.02225612243725,24.29972240831567,60.950745682315855 +2024-08-23 00:00:00,8,16.896903863282287,24.496099252749854,54.716162702014195 +2024-08-23 01:00:00,8,21.643922830295264,20.97105644087099,54.21334274091015 +2024-08-23 02:00:00,8,21.865964927497227,22.711332777581266,46.43578508515541 +2024-08-23 03:00:00,8,20.22030156457717,20.89637914500613,42.96370750181801 +2024-08-23 04:00:00,8,24.278864061267903,26.467623397808246,50.34455270157751 +2024-08-23 05:00:00,8,9.086317931051788,23.545772143231176,68.78989137915357 +2024-08-23 06:00:00,8,32.82650261867898,21.242086115019593,52.45330181988811 +2024-08-23 07:00:00,8,30.97180467844557,16.712489147118063,40.33680271761857 +2024-08-23 08:00:00,8,27.455913561856793,25.465381538628087,46.617055292719506 +2024-08-23 09:00:00,8,20.886825505425143,25.393666728774,49.718184649507826 +2024-08-23 10:00:00,8,23.98139467465601,27.727858744746722,52.06727638714176 +2024-08-23 11:00:00,8,37.286407344344916,23.19753190631483,55.43497748465526 +2024-08-23 12:00:00,8,26.439676324420287,23.16746240227569,54.46296650066256 +2024-08-23 13:00:00,8,34.24018317044175,28.635924106525568,64.98299683297407 +2024-08-23 14:00:00,8,27.66940536799073,21.15032487581684,67.6755900822184 +2024-08-23 15:00:00,8,17.46212299533172,27.193733012471434,54.467181943537625 +2024-08-23 16:00:00,8,30.390861286246306,25.674485469011152,76.48244818281339 +2024-08-23 17:00:00,8,22.962340081800374,24.389099266373897,59.235558893966584 +2024-08-23 18:00:00,8,38.486817536677826,28.688746857709656,52.06178132044948 +2024-08-23 19:00:00,8,11.856136343049918,20.7604173689551,63.06389705049554 +2024-08-23 20:00:00,8,21.982239242686227,24.57552685449055,61.77271767325039 +2024-08-23 21:00:00,8,25.031026934341543,22.84469625167926,43.83589569625963 +2024-08-23 22:00:00,8,0.0,20.121654982148492,63.330421018723996 +2024-08-23 23:00:00,8,23.25070564279786,20.859094217580026,73.79797295515908 +2024-08-24 00:00:00,8,21.589960564466637,25.588864628841442,53.40199756353764 +2024-08-24 01:00:00,8,37.8207794373174,23.908197784508157,34.26626976055796 +2024-08-24 02:00:00,8,15.186174829838397,22.821430375495268,51.81318661038039 +2024-08-24 03:00:00,8,39.57759988257474,25.391334939252207,45.88718698576738 +2024-08-24 04:00:00,8,33.501642720847116,21.644189938292328,62.37347527899355 +2024-08-24 05:00:00,8,36.6688676732901,27.407473103183666,48.584675964930064 +2024-08-24 06:00:00,8,21.75517160502884,25.36648443889776,56.19395302788736 +2024-08-24 07:00:00,8,25.21009399151824,25.836141680456866,53.71970690085554 +2024-08-24 08:00:00,8,23.589716395105352,18.248025072150334,62.777362352560395 +2024-08-24 09:00:00,8,25.678061454564002,22.87504994136947,56.75169863578631 +2024-08-24 10:00:00,8,18.291722026394435,17.3504107381534,59.68209719505509 +2024-08-24 11:00:00,8,3.1912177018541428,26.63211116520203,51.557091629485654 +2024-08-24 12:00:00,8,20.947282922616697,21.679362822671063,53.469469450017804 +2024-08-24 13:00:00,8,22.467749743534505,28.065581699895894,72.49373259723089 +2024-08-24 14:00:00,8,26.792374255410145,20.883461436079813,42.79467773295081 +2024-08-24 15:00:00,8,40.71827349784095,22.108044170272823,38.69549161461191 +2024-08-24 16:00:00,8,27.412934000185743,23.725062428570286,69.12115411346815 +2024-08-24 17:00:00,8,12.839063217123758,31.46559273682219,45.9139832909515 +2024-08-24 18:00:00,8,25.793512893159594,21.375410173927364,66.75604498274002 +2024-08-24 19:00:00,8,21.970774255963335,27.598933007964025,63.81491035116065 +2024-08-24 20:00:00,8,16.982113677761994,22.549917710066495,54.27214134385257 +2024-08-24 21:00:00,8,28.44650818571697,22.59337103278165,50.47120662869257 +2024-08-24 22:00:00,8,19.058932031280275,25.723588050205855,44.46464384662134 +2024-08-24 23:00:00,8,14.901421085595034,27.051492317868803,65.0268628907342 +2024-08-25 00:00:00,8,16.418825514113273,22.705103980118555,75.08133050233741 +2024-08-25 01:00:00,8,29.44753709433713,26.849478638374293,46.82019288392088 +2024-08-25 02:00:00,8,30.15329417393523,26.273527506685614,52.158345296863914 +2024-08-25 03:00:00,8,25.77961015229399,20.680641618129258,52.04546616560809 +2024-08-25 04:00:00,8,18.511417783017514,21.634443383526598,41.03390773019247 +2024-08-25 05:00:00,8,24.868674589533406,24.726986346933145,49.776510401058516 +2024-08-25 06:00:00,8,19.778792705843873,22.422004185928397,60.69691186508915 +2024-08-25 07:00:00,8,49.08784412402237,18.067206160443348,64.2194704781927 +2024-08-25 08:00:00,8,16.89695061034731,23.234792305458807,54.694101682745746 +2024-08-25 09:00:00,8,30.369889467473577,23.40176531630476,52.80029192097547 +2024-08-25 10:00:00,8,41.6784389620947,25.34069327227739,61.37519270061739 +2024-08-25 11:00:00,8,25.194627120611433,24.00367351675641,62.81289197057524 +2024-08-25 12:00:00,8,40.508527334436444,23.833462356464132,45.01325515243534 +2024-08-25 13:00:00,8,27.023955317362816,20.5882450565141,52.3791006295336 +2024-08-25 14:00:00,8,26.256059620898288,27.218815800241263,60.73081907344425 +2024-08-25 15:00:00,8,14.493234773627728,23.717632490309477,43.904901285420614 +2024-08-25 16:00:00,8,29.910632972101777,21.62735692141612,43.42605754576815 +2024-08-25 17:00:00,8,32.46784464475869,25.479097722567555,55.1776652296583 +2024-08-25 18:00:00,8,36.79751411228441,27.05792640246252,47.08675988140397 +2024-08-25 19:00:00,8,41.79469339045884,21.550521268719933,53.335867867571544 +2024-08-25 20:00:00,8,27.06043581205841,20.502221740460005,54.455672195781304 +2024-08-25 21:00:00,8,18.750265570467334,16.607290537768872,50.23634856055237 +2024-08-25 22:00:00,8,30.5781483485183,23.952005630863944,59.340523007232534 +2024-08-25 23:00:00,8,14.0354398345138,23.263762005111182,63.89795460455144 +2024-08-26 00:00:00,8,28.96585953127194,23.847292711885853,50.31116570817675 +2024-08-26 01:00:00,8,39.4254213019407,28.19473330513678,54.611389848419705 +2024-08-26 02:00:00,8,22.694505431878085,28.722189700230025,56.492083990130546 +2024-08-26 03:00:00,8,31.453553154773967,18.06998015862802,50.37467757808603 +2024-08-26 04:00:00,8,25.282946669832526,24.30850240359699,55.04130223668398 +2024-08-26 05:00:00,8,21.64785138149713,22.317164167414322,59.190499448303214 +2024-08-26 06:00:00,8,24.966209965468153,17.719840397456775,46.71450691524889 +2024-08-26 07:00:00,8,32.9801244363326,21.518704494751887,79.76267981683117 +2024-08-26 08:00:00,8,23.78672897818194,23.44119841450783,69.01479637931868 +2024-08-26 09:00:00,8,20.751668267021913,21.00392325304653,49.268729728712884 +2024-08-26 10:00:00,8,28.522768137855547,22.26944131657289,46.95312448469271 +2024-08-26 11:00:00,8,18.123257618218148,23.058239023377023,55.80550786678627 +2024-08-26 12:00:00,8,27.594700518035598,22.830287443130327,57.824419357596454 +2024-08-26 13:00:00,8,34.76697073530558,26.314263084665242,61.93690700971955 +2024-08-26 14:00:00,8,36.2763693137921,28.254536290521916,57.012967115966944 +2024-08-26 15:00:00,8,25.969908244041733,26.747029370009837,81.75343574641087 +2024-08-26 16:00:00,8,42.07040726263816,24.40724375415922,48.33253419649049 +2024-08-26 17:00:00,8,31.63118640186581,24.755815130931996,69.21669929369648 +2024-08-26 18:00:00,8,24.35518192532949,21.185797224252596,49.285412004005124 +2024-08-26 19:00:00,8,25.934203860387022,24.543913695855938,36.82045925512574 +2024-08-26 20:00:00,8,35.3052699596013,18.750953209573257,62.91369636408511 +2024-08-26 21:00:00,8,35.14072610514703,25.35141975873216,45.236618067756865 +2024-08-26 22:00:00,8,35.560402242640045,27.327491886917937,66.3297064007161 +2024-08-26 23:00:00,8,31.72900526577542,23.399981078776083,61.322567050944805 +2024-08-27 00:00:00,8,36.35436382853266,21.91800551892012,45.57040709013517 +2024-08-27 01:00:00,8,29.813002150468304,25.445074672995496,63.19814475817185 +2024-08-27 02:00:00,8,31.76972558544624,19.81221549679675,41.878488151867785 +2024-08-27 03:00:00,8,31.298693169086413,26.21072149025655,63.148880645468985 +2024-08-27 04:00:00,8,36.71489008307176,17.21377261845578,52.05800853295901 +2024-08-27 05:00:00,8,42.4630985633423,17.551409470887396,52.46328326344327 +2024-08-27 06:00:00,8,42.13373343121154,25.55020743236925,67.25005913099554 +2024-08-27 07:00:00,8,25.14670642777429,28.700059330452394,53.310223496631586 +2024-08-27 08:00:00,8,34.76886696925029,28.804235757195844,54.307074175055064 +2024-08-27 09:00:00,8,13.357118258268939,23.652803570221465,54.77096653882168 +2024-08-27 10:00:00,8,47.4888787761908,26.378120793943797,50.87944286443356 +2024-08-27 11:00:00,8,11.497896010769495,28.43433977016851,55.71395374492655 +2024-08-27 12:00:00,8,18.346589663763602,26.911662350835904,56.10981215162395 +2024-08-27 13:00:00,8,20.467067246633608,24.05950633230811,48.29008959687218 +2024-08-27 14:00:00,8,31.256771666533268,22.563691461071723,52.90276004819738 +2024-08-27 15:00:00,8,46.93808587175783,25.036625727785545,40.40352942730088 +2024-08-27 16:00:00,8,19.691498503077028,28.92035488400136,56.45990161103034 +2024-08-27 17:00:00,8,38.58350096222129,24.608305138332316,39.07446842618299 +2024-08-27 18:00:00,8,46.50638618054568,22.78672369181897,35.50440019391854 +2024-08-27 19:00:00,8,44.95780158713675,21.558217005457013,55.41040608992247 +2024-08-27 20:00:00,8,11.129779347310883,26.665086007807776,77.22732343561529 +2024-08-27 21:00:00,8,15.152351379381313,25.94429810876974,38.629366526499105 +2024-08-27 22:00:00,8,12.324392899554118,19.58528080438697,44.24803262655633 +2024-08-27 23:00:00,8,19.037430830988974,22.965059222382425,69.25321784279377 +2024-08-28 00:00:00,8,25.9284564268308,26.79246905887433,33.53279458498314 +2024-08-28 01:00:00,8,15.72577748900721,27.107796418974324,56.66155127740857 +2024-08-28 02:00:00,8,13.05978181029054,28.310874166424753,54.74566476367587 +2024-08-28 03:00:00,8,17.7865156927295,20.700378820102316,54.11537986121205 +2024-08-28 04:00:00,8,40.35877657314001,24.157332503687822,47.96869544335725 +2024-08-28 05:00:00,8,14.628583066745867,22.273770212719164,62.242879836842576 +2024-08-28 06:00:00,8,31.333304338719888,23.306080683486016,55.76167185249129 +2024-08-28 07:00:00,8,21.037204893370177,24.62603202326277,76.54730423169798 +2024-08-28 08:00:00,8,23.99450611265572,22.421595372302008,65.1896993510263 +2024-08-28 09:00:00,8,27.258046796352875,18.163008294561365,56.86071742349241 +2024-08-28 10:00:00,8,39.32217112765892,23.955763740956094,46.35890898974217 +2024-08-28 11:00:00,8,26.3986538019258,15.71017436281777,58.93044576706159 +2024-08-28 12:00:00,8,25.990991779335832,22.82974245000175,40.60144409836653 +2024-08-28 13:00:00,8,24.650553753712902,27.285282581453888,57.498894589404344 +2024-08-28 14:00:00,8,24.578293575056904,27.622325839504903,52.639006479332785 +2024-08-28 15:00:00,8,36.165787191842284,25.676668914323788,65.24411843669374 +2024-08-28 16:00:00,8,23.46207964108337,20.860269423021492,60.88942388256752 +2024-08-28 17:00:00,8,23.091179299307328,23.90456976949336,67.0701378030184 +2024-08-28 18:00:00,8,21.71192884469422,23.628471765985797,56.47299749399795 +2024-08-28 19:00:00,8,15.405326857049305,23.908119738701195,52.72548999938366 +2024-08-28 20:00:00,8,16.566184547265983,18.043528811241554,48.71360826491623 +2024-08-28 21:00:00,8,28.55667324957716,23.375855181037405,67.72342559408284 +2024-08-28 22:00:00,8,10.600348675391952,24.46218982710949,53.829135691984135 +2024-08-28 23:00:00,8,9.10040865020734,32.67906752569736,63.0413011565944 +2024-08-29 00:00:00,8,21.251975447816612,23.4013365332418,37.58240767794078 +2024-08-29 01:00:00,8,35.12807728869278,24.37977723368912,51.76285266190269 +2024-08-29 02:00:00,8,22.996577254452863,30.034016828405733,56.10203049068643 +2024-08-29 03:00:00,8,31.440121538641666,26.309718243692146,43.51237980268267 +2024-08-29 04:00:00,8,37.865188847627934,21.54364816084878,47.69425600127683 +2024-08-29 05:00:00,8,27.868027307570053,22.186761374290977,48.5660111175256 +2024-08-29 06:00:00,8,33.59166655736612,24.72617959186203,56.774638673193174 +2024-08-29 07:00:00,8,25.489612861333622,23.434851397907273,54.31725834002654 +2024-08-29 08:00:00,8,17.97179997902876,21.893004459284988,49.491572156133095 +2024-08-29 09:00:00,8,40.125775666506506,23.117598470585342,45.559319151631016 +2024-08-29 10:00:00,8,39.26177343963403,21.494124553482024,61.34112556667883 +2024-08-29 11:00:00,8,18.741194944674078,22.519593757113206,58.40279209009786 +2024-08-29 12:00:00,8,18.080706118989333,25.011122977952766,64.13299847744021 +2024-08-29 13:00:00,8,34.121608039917525,30.60398411074673,49.012978473784365 +2024-08-29 14:00:00,8,50.51059660086843,22.43075232203886,30.565698319434713 +2024-08-29 15:00:00,8,11.978557757905921,22.5735569296212,54.84823624199276 +2024-08-29 16:00:00,8,36.132746798352755,25.859897914562836,42.68689449442109 +2024-08-29 17:00:00,8,38.894334470382326,23.943913682098113,60.051301407840775 +2024-08-29 18:00:00,8,33.81979425254223,23.184867179567647,54.84408373130418 +2024-08-29 19:00:00,8,8.011561169816968,25.77243643755127,50.09065677731433 +2024-08-29 20:00:00,8,22.8920456921573,27.33640559411144,48.3838822710263 +2024-08-29 21:00:00,8,7.0375452103072575,30.658721918676665,61.36162791193057 +2024-08-29 22:00:00,8,19.82004348616495,26.479229201098537,60.976090949075626 +2024-08-29 23:00:00,8,38.92066301547763,27.442215482820558,40.649458500818334 +2024-08-30 00:00:00,8,18.248248053041365,22.265614599538413,52.48311318907948 +2024-08-30 01:00:00,8,43.792280992141784,25.772471389040273,59.9200475666588 +2024-08-30 02:00:00,8,15.614683299146655,22.08131469955385,49.54981984536915 +2024-08-30 03:00:00,8,7.137501821978262,23.379510604788422,59.14919783208835 +2024-08-30 04:00:00,8,1.1957808654900397,23.70856123187764,37.66777965321502 +2024-08-30 05:00:00,8,28.173898712520696,26.024721901364728,46.74157810714595 +2024-08-30 06:00:00,8,38.867533580825906,24.282947935582957,53.12689999853202 +2024-08-30 07:00:00,8,26.532008683970638,22.109934461030615,46.985864929632484 +2024-08-30 08:00:00,8,8.836351884037711,20.33345419088408,56.25971115770131 +2024-08-30 09:00:00,8,39.490537850796265,24.971778956643448,55.80506645252553 +2024-08-30 10:00:00,8,32.64450444831939,26.71419419208562,34.58288764613036 +2024-08-30 11:00:00,8,21.981866153293385,21.029496163124037,47.710184503171725 +2024-08-30 12:00:00,8,18.33434787635035,27.415319273725505,39.18555654743488 +2024-08-30 13:00:00,8,39.54704086776084,27.15957466509393,53.745574751431256 +2024-08-30 14:00:00,8,26.17907333475806,22.843029463792238,56.013126774530804 +2024-08-30 15:00:00,8,16.286994164934107,26.258096268604614,62.394074130269885 +2024-08-30 16:00:00,8,29.090358711777505,26.440486744878598,66.07990184696114 +2024-08-30 17:00:00,8,29.313983835792875,30.642974500542536,61.54927246411753 +2024-08-30 18:00:00,8,22.333797481811203,24.721796118176762,57.53265635700105 +2024-08-30 19:00:00,8,23.98368116055459,25.91924978715971,58.28129999540373 +2024-08-30 20:00:00,8,7.499924248351718,22.612076671390223,48.09247205451466 +2024-08-30 21:00:00,8,11.796446408429413,22.758701898965548,44.11677589528808 +2024-08-30 22:00:00,8,24.526487822853213,23.504621862486797,78.93985655622214 +2024-08-30 23:00:00,8,25.23612421981906,25.767065933723657,51.814326599420426 +2024-08-31 00:00:00,8,20.982116258799817,22.74147341297888,38.8225161482146 +2024-08-31 01:00:00,8,39.36686394679582,25.254527763509145,51.45019757722686 +2024-08-31 02:00:00,8,26.050177812107467,25.0401104609959,52.75943752217314 +2024-08-31 03:00:00,8,28.774520487233367,18.90961583088677,55.06174211128029 +2024-08-31 04:00:00,8,26.01306332494059,22.40993838082047,68.16895164752889 +2024-08-31 05:00:00,8,29.43419611564739,21.091987320920808,54.36890397554028 +2024-08-31 06:00:00,8,17.74232136323988,20.888910477431942,65.92033258857943 +2024-08-31 07:00:00,8,27.46894336756139,23.637357928643247,58.73895665709548 +2024-08-31 08:00:00,8,26.0094200051403,21.765835220571585,36.613538349750755 +2024-08-31 09:00:00,8,31.86408998976159,25.74855220741457,49.66770056795845 +2024-08-31 10:00:00,8,35.53197130299824,20.472985009447637,46.40779753073399 +2024-08-31 11:00:00,8,23.73989144599285,25.337329875177677,60.70826662087615 +2024-08-31 12:00:00,8,17.792676055263392,22.460083551535597,48.541773987659305 +2024-08-31 13:00:00,8,18.735126437168685,20.78086485102023,73.33648789079959 +2024-08-31 14:00:00,8,15.690307786959965,23.43391907484787,54.870338920712705 +2024-08-31 15:00:00,8,21.636505430428812,18.051413734640875,51.90486824267673 +2024-08-31 16:00:00,8,39.17649427877827,22.677498241887406,64.24059402146327 +2024-08-31 17:00:00,8,33.158293499566824,21.84751666887446,56.60135770235413 +2024-08-31 18:00:00,8,31.750442923395234,16.850067039855713,51.10084017721746 +2024-08-31 19:00:00,8,18.140423754272867,18.9790900631361,61.71565281257043 +2024-08-31 20:00:00,8,26.1870870113338,25.43510917819992,67.3305729782986 +2024-08-31 21:00:00,8,28.186879542933355,26.896479266595115,52.714374479949235 +2024-08-31 22:00:00,8,28.92112122212002,23.9679271494027,48.42568391833779 +2024-08-31 23:00:00,8,28.42215864947909,21.85945221121024,51.28298043467354 +2024-09-01 00:00:00,8,19.571598014050306,17.274850026226527,50.5119389792816 +2024-09-01 01:00:00,8,16.030404646567476,21.821877687132332,44.80752779655884 +2024-09-01 02:00:00,8,27.571910836776865,26.591875848744987,44.66353996557549 +2024-09-01 03:00:00,8,31.132552194796716,27.097150494587705,38.141560064239464 +2024-09-01 04:00:00,8,25.261473225207556,21.40072894912642,57.82278831811674 +2024-09-01 05:00:00,8,20.30712100472245,24.981064656258436,42.22577820521107 +2024-09-01 06:00:00,8,36.397261839096416,23.17152254356868,72.77934269050513 +2024-09-01 07:00:00,8,39.270009806992924,22.929851816913366,46.38659776881988 +2024-09-01 08:00:00,8,18.01994311240795,28.142118286286944,55.82228565938963 +2024-09-01 09:00:00,8,9.835257719330961,28.029828282569607,54.834911572467014 +2024-09-01 10:00:00,8,37.621108461093115,22.509932263594898,63.36848374808059 +2024-09-01 11:00:00,8,26.287227464042278,23.992063304097083,54.03010321377084 +2024-09-01 12:00:00,8,12.314110821330942,24.73954611203615,52.913495659783564 +2024-09-01 13:00:00,8,30.891734518360987,20.625727854035702,41.092093265577084 +2024-09-01 14:00:00,8,11.886268481811623,26.51227302874922,47.488527938036555 +2024-09-01 15:00:00,8,8.737540993887176,29.75994977487046,45.12001479279433 +2024-09-01 16:00:00,8,38.23514932012455,25.341908967304246,37.12625742433929 +2024-09-01 17:00:00,8,40.25045518827376,18.667971035340546,57.545453749053436 +2024-09-01 18:00:00,8,37.17128193579334,29.143441037155846,58.29578733675074 +2024-09-01 19:00:00,8,25.133917495989692,25.05353844733306,54.564797399585224 +2024-09-01 20:00:00,8,13.796898917376613,23.49762356642715,52.02636737009691 +2024-09-01 21:00:00,8,28.992667773085024,18.455211503446783,60.93987275293141 +2024-09-01 22:00:00,8,5.763298557895574,23.94269092248802,66.54049569694212 +2024-09-01 23:00:00,8,34.743056868379156,19.801422278394437,72.62629125881004 +2024-09-02 00:00:00,8,41.28233933679035,24.780148919964883,53.743113471256905 +2024-09-02 01:00:00,8,31.409182840795964,29.987477387904946,48.325420811286335 +2024-09-02 02:00:00,8,17.347866563176577,20.249090455271997,54.495641487356274 +2024-09-02 03:00:00,8,10.513389531305197,23.60094322943088,57.53697322077307 +2024-09-02 04:00:00,8,21.318499967649352,26.28798931412232,48.310420453400525 +2024-09-02 05:00:00,8,33.331098150707156,22.498620436332065,47.784318578371156 +2024-09-02 06:00:00,8,45.2926894651891,21.924980976306127,46.706635626939935 +2024-09-02 07:00:00,8,32.3070370546579,25.75411592469168,52.30267729396459 +2024-09-02 08:00:00,8,27.388154715527815,22.993756258987773,56.3446537292697 +2024-09-02 09:00:00,8,16.3069123454477,24.938682114153867,65.7600756223132 +2024-09-02 10:00:00,8,40.552888966965874,28.082044846715803,38.06047444184193 +2024-09-02 11:00:00,8,22.38527644896039,23.43141210812043,39.446124225677636 +2024-09-02 12:00:00,8,32.11712795476747,25.688347483821694,41.41112831172677 +2024-09-02 13:00:00,8,25.358365561388407,23.777671815122183,59.64986750264942 +2024-09-02 14:00:00,8,31.579814985673067,23.09139940685905,45.700923682579784 +2024-09-02 15:00:00,8,37.88023148998707,22.951958005203625,60.11316916166454 +2024-09-02 16:00:00,8,14.659415811887456,24.08514388327508,46.698625292176104 +2024-09-02 17:00:00,8,27.891450203331978,20.314940545501976,69.44399992512965 +2024-09-02 18:00:00,8,18.29075471208725,24.39437596855415,55.64228901654366 +2024-09-02 19:00:00,8,27.51696635241582,30.340166745971214,46.66883539357214 +2024-09-02 20:00:00,8,36.717021526201016,21.03330494605133,56.73894393954082 +2024-09-02 21:00:00,8,20.949326582972525,24.409890051089224,38.69421353143491 +2024-09-02 22:00:00,8,24.553889357617162,21.796697819249175,48.375659035230996 +2024-09-02 23:00:00,8,29.424720146236993,23.269638576161174,49.87710622156014 +2024-09-03 00:00:00,8,27.54309691604673,26.54213417497386,33.58116871966169 +2024-09-03 01:00:00,8,22.020114179863093,19.301144875462676,60.56608517356105 +2024-09-03 02:00:00,8,36.015443233480724,20.124063511533755,52.697714187833725 +2024-09-03 03:00:00,8,21.11587647655102,20.51674382953764,50.66177286868778 +2024-09-03 04:00:00,8,17.711081222288158,21.049377850519654,53.32831865068224 +2024-09-03 05:00:00,8,20.20529238262474,21.33025504552578,59.93752387609574 +2024-09-03 06:00:00,8,37.28808356619706,27.05057742977746,46.92653620083419 +2024-09-03 07:00:00,8,20.5086066852081,28.21218739313037,45.069895405171025 +2024-09-03 08:00:00,8,47.563797495478994,19.539197983425048,49.52463693498492 +2024-09-03 09:00:00,8,25.24238273371937,26.528422008227466,53.38915537922533 +2024-09-03 10:00:00,8,23.805276254713682,22.04421835506014,66.70605955692284 +2024-09-03 11:00:00,8,32.96591829989868,23.667233744259928,72.332933368244 +2024-09-03 12:00:00,8,30.87211933751274,16.39439400048169,59.0879072282836 +2024-09-03 13:00:00,8,20.262992203543536,21.155182633822328,54.30294790394092 +2024-09-03 14:00:00,8,41.284777082922346,25.671567752870107,62.7299656188425 +2024-09-03 15:00:00,8,36.35867940875926,28.954625681411382,63.52020365512242 +2024-09-03 16:00:00,8,31.391645756790005,24.923914507591064,51.280190338831325 +2024-09-03 17:00:00,8,13.985723168862386,26.531451535068417,46.93772846008868 +2024-09-03 18:00:00,8,27.550784400558314,25.324254894563243,52.21124947628908 +2024-09-03 19:00:00,8,18.19418752224762,27.064946200026004,53.93999510837056 +2024-09-03 20:00:00,8,24.14362508238703,13.28594423619222,60.342390141304506 +2024-09-03 21:00:00,8,21.19576714611283,17.53202225369565,46.98636117222934 +2024-09-03 22:00:00,8,25.819896382279236,26.88149595223942,42.50300962732042 +2024-09-03 23:00:00,8,32.469928725196425,23.564040792920768,58.65952678889915 +2024-09-04 00:00:00,8,33.5249213047435,29.35870994006535,45.93182668605651 +2024-09-04 01:00:00,8,31.64063429558069,24.514488673407104,51.85049142874466 +2024-09-04 02:00:00,8,33.202141349010354,21.765964746215634,51.681433315224986 +2024-09-04 03:00:00,8,21.77774864487577,23.54515888964584,44.58401701040574 +2024-09-04 04:00:00,8,28.12435211843767,25.454767327283523,35.159233354921724 +2024-09-04 05:00:00,8,21.69335184051807,21.537032909403855,55.749768694284306 +2024-09-04 06:00:00,8,35.858297397588174,24.955632213205064,63.68759347131458 +2024-09-04 07:00:00,8,40.73669642330365,26.44067496773512,49.62804228447377 +2024-09-04 08:00:00,8,13.959223144425824,25.016835447794065,44.1479900348995 +2024-09-04 09:00:00,8,30.95358264845746,21.347513074315824,48.28185562924997 +2024-09-04 10:00:00,8,42.52627007419476,26.229341461287564,56.942081528578576 +2024-09-04 11:00:00,8,30.385373323800277,25.955087264304197,70.8910379706592 +2024-09-04 12:00:00,8,33.242143649373794,23.236849917403177,54.659888486030006 +2024-09-04 13:00:00,8,41.08504632276878,25.21584406697277,72.06644745643695 +2024-09-04 14:00:00,8,31.658618212106145,24.39715698938495,66.56767075909603 +2024-09-04 15:00:00,8,25.19461538437959,24.56578850577373,60.517737919261776 +2024-09-04 16:00:00,8,35.84081474254513,28.4511049062103,48.55865986072906 +2024-09-04 17:00:00,8,29.80234154279064,25.49173192514833,31.395825528310745 +2024-09-04 18:00:00,8,33.08339299083265,27.35926537840271,56.173827307115175 +2024-09-04 19:00:00,8,37.4117234554836,20.333938517956945,28.442752892914147 +2024-09-04 20:00:00,8,23.263111146405663,27.13070341669107,52.9445463799175 +2024-09-04 21:00:00,8,35.09858495823389,20.72268019947539,61.9643367060594 +2024-09-04 22:00:00,8,31.49384527891971,28.647457567491323,54.86959614212227 +2024-09-04 23:00:00,8,30.601393645363547,26.65207905605126,52.2668914946727 +2024-09-05 00:00:00,8,27.359576478397415,23.772865273144916,58.62825945826697 +2024-09-05 01:00:00,8,14.679000930013691,27.535946554403843,55.04775593843304 +2024-09-05 02:00:00,8,32.79237212452801,24.795077878195947,52.71474147314651 +2024-09-05 03:00:00,8,15.729810053919477,18.715499826814245,61.380463236048456 +2024-09-05 04:00:00,8,11.998109755931258,23.38764461391797,32.45975101697475 +2024-09-05 05:00:00,8,35.158082073015386,23.671178851608914,45.646552496470946 +2024-09-05 06:00:00,8,32.12652422225485,23.588971963513522,49.86417669640508 +2024-09-05 07:00:00,8,29.714186818649512,27.39043084593414,53.932961559206475 +2024-09-05 08:00:00,8,24.825406684641095,22.877358610503514,50.980812583394204 +2024-09-05 09:00:00,8,44.65246120696864,18.113179897435916,42.48904438953055 +2024-09-05 10:00:00,8,17.398300660998615,18.1430990791487,64.28838063934552 +2024-09-05 11:00:00,8,22.93812655648285,24.937353140703227,43.647650021215696 +2024-09-05 12:00:00,8,35.904007101152686,25.4107783937989,54.211597456983405 +2024-09-05 13:00:00,8,24.572009352922183,22.029675478406038,59.52219820747386 +2024-09-05 14:00:00,8,36.38736081548752,23.015822458385887,64.41303012366089 +2024-09-05 15:00:00,8,30.772399267400882,25.876405679967405,46.65098178246763 +2024-09-05 16:00:00,8,32.1271233738098,27.605934989369608,69.04545620971147 +2024-09-05 17:00:00,8,36.586556162889366,25.826047019730314,43.56384957000662 +2024-09-05 18:00:00,8,38.137124992881084,24.31462405271241,56.53719193480816 +2024-09-05 19:00:00,8,26.264812207267063,28.313228568244455,51.81903568310969 +2024-09-05 20:00:00,8,37.77353815825115,23.101114213701457,68.09265238065346 +2024-09-05 21:00:00,8,24.33431075785192,26.367130641375557,56.26721685643662 +2024-09-05 22:00:00,8,44.735342776157935,27.76568290588404,61.89239833691325 +2024-09-05 23:00:00,8,30.563425527280387,18.29182602931239,66.01355926572431 +2024-09-06 00:00:00,8,26.80119885704709,20.44084794907623,60.94994492297099 +2024-09-06 01:00:00,8,42.17738661953731,18.81143730568243,49.57491162686078 +2024-09-06 02:00:00,8,24.152391091949937,26.401448926162697,44.43086804524674 +2024-09-06 03:00:00,8,25.82761264078892,21.459099449216993,42.22864956205172 +2024-09-06 04:00:00,8,26.19962322018712,22.26413349997798,51.01695639182424 +2024-09-06 05:00:00,8,22.74459706256361,18.48326917147913,61.97022997485297 +2024-09-06 06:00:00,8,32.53829548960523,26.256898969041455,65.95915671662456 +2024-09-06 07:00:00,8,30.925498823350974,30.357444550753584,53.638941091930036 +2024-09-06 08:00:00,8,33.35848835715248,28.272632940200268,41.60694337164792 +2024-09-06 09:00:00,8,35.15881854267133,23.289077098347178,54.016782783585406 +2024-09-06 10:00:00,8,20.78439618416845,23.916836522570872,63.214732744558184 +2024-09-06 11:00:00,8,44.54973309680561,23.23283228204483,53.99746521067933 +2024-09-06 12:00:00,8,5.723878548786448,20.015877942454964,45.69073318133056 +2024-09-06 13:00:00,8,34.08470204473996,22.75442070883881,53.588763135756984 +2024-09-06 14:00:00,8,31.79804501477753,20.550707884657204,60.04835359261493 +2024-09-06 15:00:00,8,35.04175747905765,23.90444300489358,64.81715651514536 +2024-09-06 16:00:00,8,35.14045862836044,20.327800416182818,44.85124939634734 +2024-09-06 17:00:00,8,16.82375328967558,23.019476513578713,56.14113536593674 +2024-09-06 18:00:00,8,38.20784988091307,24.22708517511727,49.960027355698905 +2024-09-06 19:00:00,8,43.40724346585772,24.15832301219514,41.82023603953074 +2024-09-06 20:00:00,8,21.558550052852492,21.058358366334353,58.10786532122187 +2024-09-06 21:00:00,8,20.66749461959165,24.259659600795132,54.302263761914006 +2024-09-06 22:00:00,8,28.107760551900093,24.238294934723488,58.96928545439857 +2024-09-06 23:00:00,8,33.39762146468682,25.144306437154242,51.97312555836831 +2024-09-07 00:00:00,8,19.967512561663476,19.949267664182155,49.379703923414695 +2024-09-07 01:00:00,8,20.192688969543006,19.14003381885807,46.62113629576016 +2024-09-07 02:00:00,8,26.24521314933615,29.44372867854459,46.96448001593017 +2024-09-07 03:00:00,8,30.355856347244327,26.322250452751003,44.45712407468413 +2024-09-07 04:00:00,8,15.836846480204953,27.103533585739722,65.439595056157 +2024-09-07 05:00:00,8,17.926627419265166,30.293716806536715,41.636726829816126 +2024-09-07 06:00:00,8,25.232338778850416,21.44715336066624,49.593055149151226 +2024-09-07 07:00:00,8,20.47644148843064,17.561213433151124,53.134662853085864 +2024-09-07 08:00:00,8,28.452396472448022,17.643050339533964,36.555951732617366 +2024-09-07 09:00:00,8,14.31255319634876,24.29273709883935,40.92086020771104 +2024-09-07 10:00:00,8,40.24028150008793,24.4810260172593,61.61975269290067 +2024-09-07 11:00:00,8,28.6100485231218,20.681888736832004,69.90224841395053 +2024-09-07 12:00:00,8,29.03315563056807,29.462610589811227,44.49381730721208 +2024-09-07 13:00:00,8,24.02477746203666,27.539861091697265,45.3147270227786 +2024-09-07 14:00:00,8,16.545807104842133,25.90330426762273,51.7345379235916 +2024-09-07 15:00:00,8,32.866543375887446,21.353970668757608,54.408874281490256 +2024-09-07 16:00:00,8,31.40062766290743,24.951754072301117,40.00999931055328 +2024-09-07 17:00:00,8,29.669972751332214,24.891603907499345,62.045238334000395 +2024-09-07 18:00:00,8,29.588933335036376,26.91650832694491,52.347050135392934 +2024-09-07 19:00:00,8,25.69728863540595,28.907558447860133,59.735537111697276 +2024-09-07 20:00:00,8,37.17241243423824,26.472472835008432,46.34522104888391 +2024-09-07 21:00:00,8,28.524121950991354,27.637902653191734,63.60118304014555 +2024-09-07 22:00:00,8,23.453289770068253,22.164044875295872,54.17750134799656 +2024-09-07 23:00:00,8,36.58951077849022,20.206233877367396,66.31559194199332 +2024-09-08 00:00:00,8,24.227756915701047,26.058215120439687,50.42828899804801 +2024-09-08 01:00:00,8,34.180533794087815,25.82095898963572,51.48434116308058 +2024-09-08 02:00:00,8,37.86747033498953,20.365771401028123,47.94353636240817 +2024-09-08 03:00:00,8,0.0,20.83480368153316,63.26166691531067 +2024-09-08 04:00:00,8,34.23673471516566,21.19192679205493,44.929532130706676 +2024-09-08 05:00:00,8,18.391520827680637,24.033130388866,59.595906486213465 +2024-09-08 06:00:00,8,34.24894030917201,23.848070071670342,66.68367882260088 +2024-09-08 07:00:00,8,28.490704988059782,23.435385826465883,71.19464343639828 +2024-09-08 08:00:00,8,19.83306007148754,26.63955416861412,55.69367920108949 +2024-09-08 09:00:00,8,33.50010859558793,21.864485238376766,55.93426156154992 +2024-09-08 10:00:00,8,29.23623509870559,20.983000085381263,57.27770537970195 +2024-09-08 11:00:00,8,34.72999232046374,20.659887680992053,42.84600242746757 +2024-09-08 12:00:00,8,27.424405324022818,21.098536072377915,62.670038707257405 +2024-09-08 13:00:00,8,32.54077218200187,25.084639591612042,53.02273134571467 +2024-09-08 14:00:00,8,27.718522001999684,24.692575750438923,62.26294871314969 +2024-09-08 15:00:00,8,38.967185788280915,21.906097528426937,50.08094177139765 +2024-09-08 16:00:00,8,31.7847343423075,24.797316165762496,61.20441453977721 +2024-09-08 17:00:00,8,30.417458393939892,22.401336206012402,59.455210057821674 +2024-09-08 18:00:00,8,27.640715864114757,20.712585862171494,63.45199408017608 +2024-09-08 19:00:00,8,32.151414112017484,24.52193641890518,58.39577752189575 +2024-09-08 20:00:00,8,23.505962568008012,23.709119081612474,58.112421778041245 +2024-09-08 21:00:00,8,39.276248093482735,24.94267079021764,40.81876636215476 +2024-09-08 22:00:00,8,43.068918210276436,22.51480040326506,39.5969450059128 +2024-09-08 23:00:00,8,30.47253325464505,26.490363356966597,60.594660386601845 +2024-09-09 00:00:00,8,15.52230384281826,21.736110396267627,64.09514467381781 +2024-09-09 01:00:00,8,27.593577424859618,22.450414961568743,53.03128186702037 +2024-09-09 02:00:00,8,30.263552020751177,18.02589586325942,50.40754637055565 +2024-09-09 03:00:00,8,17.81394220204607,23.424623512019483,61.6403285094445 +2024-09-09 04:00:00,8,26.585063009222875,20.813028555112393,46.48286603070197 +2024-09-09 05:00:00,8,25.371206301163813,21.828147475151123,48.153967651462736 +2024-09-09 06:00:00,8,25.792022865172942,23.81392460425858,47.60299858158912 +2024-09-09 07:00:00,8,40.36750059650979,25.463142365721588,67.71597395253328 +2024-09-09 08:00:00,8,12.590904064756277,23.063119484353685,75.639833728026 +2024-09-09 09:00:00,8,51.72762191113816,27.048926713784798,69.71433922726767 +2024-09-09 10:00:00,8,29.478516918966427,25.75304771191174,35.68957426240735 +2024-09-09 11:00:00,8,15.856555000396604,25.315997169245797,48.681837492435804 +2024-09-09 12:00:00,8,13.269665189217717,20.908856771018677,62.67950667669765 +2024-09-09 13:00:00,8,25.833959464397733,29.737827326744213,45.802043073793456 +2024-09-09 14:00:00,8,39.44963922861619,22.83381629786906,57.345462322233374 +2024-09-09 15:00:00,8,20.116125336917342,21.045913611471054,49.73133106460079 +2024-09-09 16:00:00,8,28.793854552085694,21.262558675828245,49.95601459161495 +2024-09-09 17:00:00,8,43.82798274221446,28.08405901698009,44.440402635668676 +2024-09-09 18:00:00,8,20.993553853824412,24.65137688373146,46.896172882490504 +2024-09-09 19:00:00,8,33.83830827950651,23.711785137896243,55.53369208856532 +2024-09-09 20:00:00,8,36.327035149723855,24.361991739119322,40.96126621339258 +2024-09-09 21:00:00,8,17.06654361742938,23.75940073980364,53.14227466976075 +2024-09-09 22:00:00,8,35.36422003700398,25.580863135699804,57.86267212856456 +2024-09-09 23:00:00,8,25.602040122700664,18.56364720339238,53.474565414944145 +2024-09-10 00:00:00,8,2.630492544535283,18.47812439512922,53.523387305572236 +2024-09-10 01:00:00,8,16.580657497450268,19.35064450554707,32.06633951949739 +2024-09-10 02:00:00,8,31.638169159660876,19.956935510391702,59.021313398627775 +2024-09-10 03:00:00,8,25.557428860267464,23.475749169353197,44.354963992112125 +2024-09-10 04:00:00,8,10.298362698004537,23.363884350147835,47.00984498367396 +2024-09-10 05:00:00,8,18.643794035249616,25.344766100915827,60.27450563140826 +2024-09-10 06:00:00,8,35.73389499146785,26.12800296254322,50.991458562223 +2024-09-10 07:00:00,8,38.82018827517187,17.22431835684385,54.57289544653274 +2024-09-10 08:00:00,8,34.19597271244652,22.577960413191363,49.28281359656545 +2024-09-10 09:00:00,8,24.793196387225482,22.092085314339283,60.58930285301344 +2024-09-10 10:00:00,8,24.476611433170202,22.997704609652942,49.487458463067824 +2024-09-10 11:00:00,8,36.246953665049986,21.297794170679882,50.81196246795196 +2024-09-10 12:00:00,8,49.022646380527874,26.44199109424152,41.722317391085646 +2024-09-10 13:00:00,8,19.467222764921758,24.52770783973844,65.85358296070103 +2024-09-10 14:00:00,8,30.567031218857476,28.27776995642301,54.215272344181344 +2024-09-10 15:00:00,8,26.73181430295379,26.352885339627623,63.318758057610815 +2024-09-10 16:00:00,8,19.97262927645135,20.340352718355643,62.47515464000452 +2024-09-10 17:00:00,8,31.00949476070787,25.967429192528172,72.17190456608392 +2024-09-10 18:00:00,8,16.64436100430641,25.83159321051731,45.9093301421197 +2024-09-10 19:00:00,8,21.823306961606267,26.661240936987777,39.33125707717594 +2024-09-10 20:00:00,8,31.60185114130751,23.14575938935455,50.29251193162694 +2024-09-10 21:00:00,8,25.728037232519053,21.60514004393186,49.679782595936054 +2024-09-10 22:00:00,8,31.492517657446996,21.803310927798158,63.10906256346336 +2024-09-10 23:00:00,8,32.183978302680075,25.65521998439888,36.16751143786934 +2024-09-11 00:00:00,8,25.90612225796987,24.64509091743984,55.42246491628808 +2024-09-11 01:00:00,8,32.008608360348504,25.724046800164164,44.0663262812847 +2024-09-11 02:00:00,8,18.166758087273177,28.328245695190887,47.980320032738646 +2024-09-11 03:00:00,8,12.945549025008047,24.871461841537883,41.274880940134956 +2024-09-11 04:00:00,8,20.55029776951125,23.31825714672124,53.15340959056848 +2024-09-11 05:00:00,8,13.187867453296889,25.39349166363801,41.80641902605254 +2024-09-11 06:00:00,8,19.450592730838167,31.09415972368347,62.06421700278571 +2024-09-11 07:00:00,8,28.11570534324704,17.31498858976583,61.40982029207342 +2024-09-11 08:00:00,8,47.53462365555629,22.455666584168807,51.55929860116561 +2024-09-11 09:00:00,8,16.94158708046114,19.764269937606993,54.92205037298177 +2024-09-11 10:00:00,8,29.819124844069645,23.164574398265934,57.48008416700136 +2024-09-11 11:00:00,8,26.98356068665955,25.640432501028215,60.78259132423111 +2024-09-11 12:00:00,8,35.83942408250955,26.874967829622783,53.139598949612235 +2024-09-11 13:00:00,8,34.110311396000164,24.75482175315282,51.8613218753434 +2024-09-11 14:00:00,8,31.85254500282476,22.30092398340321,42.130535557943205 +2024-09-11 15:00:00,8,27.800630396866914,28.16814534648429,42.33064131736395 +2024-09-11 16:00:00,8,27.3895834575433,25.004399327544352,60.5691627411338 +2024-09-11 17:00:00,8,31.295535348448954,24.443607979092253,32.6938793603092 +2024-09-11 18:00:00,8,19.016704743794946,22.519884825741272,55.68255918803279 +2024-09-11 19:00:00,8,28.551356236491362,28.041758441856885,56.199306174564256 +2024-09-11 20:00:00,8,10.294751203559565,23.65429646444651,62.438540236059765 +2024-09-11 21:00:00,8,38.21564034808018,24.31719060606916,56.63784957616097 +2024-09-11 22:00:00,8,23.188466365731042,18.50183229889138,57.855440707884966 +2024-09-11 23:00:00,8,21.602206081979258,22.898443794246585,45.166559777420126 +2024-09-12 00:00:00,8,11.52518134405474,20.879423087951977,56.41839989675476 +2024-09-12 01:00:00,8,23.212988504174167,20.82049424153207,37.6022796956037 +2024-09-12 02:00:00,8,20.596310752455786,22.139819890145283,50.95866164567043 +2024-09-12 03:00:00,8,32.0946938611988,29.579450325463473,40.628218660534216 +2024-09-12 04:00:00,8,32.374014030998445,22.029063527851804,59.5666894579267 +2024-09-12 05:00:00,8,35.40936759396313,24.743140539004273,45.22925639407174 +2024-09-12 06:00:00,8,24.32910792894207,26.889713399143908,62.30378043520895 +2024-09-12 07:00:00,8,35.447114615983956,23.276595532623627,49.12037691096923 +2024-09-12 08:00:00,8,31.80603239272117,23.285925914166814,69.28665537645787 +2024-09-12 09:00:00,8,33.863120658492925,20.645442698416034,51.7894102805037 +2024-09-12 10:00:00,8,44.668861147137974,26.00605725435527,45.845683334814794 +2024-09-12 11:00:00,8,16.505962789586068,18.45364624217063,56.97193896177734 +2024-09-12 12:00:00,8,25.645588517893408,19.41863066326858,54.347915159207304 +2024-09-12 13:00:00,8,30.74284087670411,26.713529188940264,48.83809375423571 +2024-09-12 14:00:00,8,31.450345102516295,23.172595246170513,57.56343315692077 +2024-09-12 15:00:00,8,45.80075473185305,28.62152447111862,45.77353381575433 +2024-09-12 16:00:00,8,35.81960759373427,27.209168061763854,52.89213715533683 +2024-09-12 17:00:00,8,19.10902393945765,31.3568192708259,53.96618783385087 +2024-09-12 18:00:00,8,16.694079238035822,19.49285593499709,50.355762504461886 +2024-09-12 19:00:00,8,13.799320849859512,23.816123444415243,57.01602360109003 +2024-09-12 20:00:00,8,21.256802172113076,20.758966260181392,66.44528681563291 +2024-09-12 21:00:00,8,23.899792836892868,22.528520533741943,61.2181116754407 +2024-09-12 22:00:00,8,41.390903839167336,16.342046911385832,52.94269928765752 +2024-09-12 23:00:00,8,17.85834477491558,25.8230906745339,47.33140861495683 +2024-09-13 00:00:00,8,29.26411406532519,20.456510821682485,39.91853020851142 +2024-09-13 01:00:00,8,24.959706155779124,28.751709818333236,51.1801370013483 +2024-09-13 02:00:00,8,38.34271485710305,16.701293488571125,65.0267793569324 +2024-09-13 03:00:00,8,28.85976617461062,24.750373390448853,57.97038041897616 +2024-09-13 04:00:00,8,27.170273592928936,21.072878874123692,41.19426739454751 +2024-09-13 05:00:00,8,17.15653024857912,21.807326638743405,62.30704616142952 +2024-09-13 06:00:00,8,27.074047460947888,25.64696586788893,69.81496224249673 +2024-09-13 07:00:00,8,30.5282799371433,24.763603950642324,49.9821285882057 +2024-09-13 08:00:00,8,14.122701881838452,24.178773568482182,49.63990087232819 +2024-09-13 09:00:00,8,33.960895764494744,24.53235783753777,40.08182050880981 +2024-09-13 10:00:00,8,22.27975099152501,19.055110536224582,45.62669524215993 +2024-09-13 11:00:00,8,37.62173011762224,20.424298807600877,52.78909785093967 +2024-09-13 12:00:00,8,32.73760301420008,28.116727134226988,52.62255787048261 +2024-09-13 13:00:00,8,49.10920769949804,22.857378736101303,58.687455827658106 +2024-09-13 14:00:00,8,29.337200559211116,23.755938500700715,55.98123039348182 +2024-09-13 15:00:00,8,44.984010292302116,28.537323379970342,56.55520911556586 +2024-09-13 16:00:00,8,12.171140449769808,21.416976238527617,43.16474167458625 +2024-09-13 17:00:00,8,29.18976429337923,29.10262702750654,65.69796279337146 +2024-09-13 18:00:00,8,37.05114768243938,22.426746832717562,49.19657770656958 +2024-09-13 19:00:00,8,21.949802537065654,29.888805317254153,47.61365799695499 +2024-09-13 20:00:00,8,0.0,16.97429908018494,61.02267512854243 +2024-09-13 21:00:00,8,32.29746081487902,22.371090618341373,50.82009985168345 +2024-09-13 22:00:00,8,7.318135592434974,21.813470260327755,53.524323172439026 +2024-09-13 23:00:00,8,23.8710066938211,23.480595027889603,31.19842863895383 +2024-09-14 00:00:00,8,27.706872488216966,24.65697347136094,42.05269711406104 +2024-09-14 01:00:00,8,24.75509682031613,25.685448255658567,61.71912388251996 +2024-09-14 02:00:00,8,40.24221923258902,22.941577936397245,54.27052344297262 +2024-09-14 03:00:00,8,33.69874943326033,22.624950890754754,54.97326470264748 +2024-09-14 04:00:00,8,18.48094707621874,19.442208321662566,49.88276254581211 +2024-09-14 05:00:00,8,14.865960863182734,26.873200888565982,48.6635584968504 +2024-09-14 06:00:00,8,24.60379673156335,17.833215160123366,40.59181794469435 +2024-09-14 07:00:00,8,25.561489538098908,22.9847865658871,61.153142766206564 +2024-09-14 08:00:00,8,23.8011521282932,19.49264675046154,46.50623329636857 +2024-09-14 09:00:00,8,36.31692583409787,20.338557187173976,57.00324505410163 +2024-09-14 10:00:00,8,16.076398592534805,22.437763739790615,47.85142213145994 +2024-09-14 11:00:00,8,39.301035483062805,26.02843744716611,72.62439169186884 +2024-09-14 12:00:00,8,23.768811464076826,22.169484619295332,45.41061073897989 +2024-09-14 13:00:00,8,39.2784938400843,22.37581196247299,64.7760131561653 +2024-09-14 14:00:00,8,26.979582860672046,23.762622617733033,46.76632680354646 +2024-09-14 15:00:00,8,37.40946292448906,16.998589222714497,57.81146285115862 +2024-09-14 16:00:00,8,26.936208693885842,24.999336572489057,66.6827769470509 +2024-09-14 17:00:00,8,23.532823039457657,25.52337869708523,49.93753024734291 +2024-09-14 18:00:00,8,13.777502963578762,25.20603331306842,51.686011700624576 +2024-09-14 19:00:00,8,32.56582952095692,25.4831027210199,55.44747957606036 +2024-09-14 20:00:00,8,42.6953180502681,25.503059216837904,52.294302285433474 +2024-09-14 21:00:00,8,39.43795609034821,24.092508986068978,48.31167508657711 +2024-09-14 22:00:00,8,27.4172725709301,21.598597651894718,56.70213364836971 +2024-09-14 23:00:00,8,18.34071804484345,18.641175412737265,45.444545980971384 +2024-09-15 00:00:00,8,17.63179236311462,26.00619630681077,45.855244188586674 +2024-09-15 01:00:00,8,27.135371187352504,25.75871221852425,42.29743149115632 +2024-09-15 02:00:00,8,30.75856076990793,25.904653081690693,52.56243745417756 +2024-09-15 03:00:00,8,17.685699059708618,22.18841625507926,52.94353345456322 +2024-09-15 04:00:00,8,20.092455404837786,21.776755331232085,69.26137104686707 +2024-09-15 05:00:00,8,13.145286930331455,24.824593481234988,54.41641140903875 +2024-09-15 06:00:00,8,30.57788488927563,20.813379709729812,48.16537729512087 +2024-09-15 07:00:00,8,15.137430212611994,25.161215667504788,39.010164115143155 +2024-09-15 08:00:00,8,24.283110286352688,24.575015107103834,62.368146665281216 +2024-09-15 09:00:00,8,40.54598681711821,27.76837623467857,57.728602062057334 +2024-09-15 10:00:00,8,33.69088829615671,21.77592699392703,59.529869886487816 +2024-09-15 11:00:00,8,36.24721259388125,27.09764597078121,36.16699407516931 +2024-09-15 12:00:00,8,35.810702652152365,18.351214648031178,53.9210633698876 +2024-09-15 13:00:00,8,38.58117563075455,25.079870466568778,57.97387953878364 +2024-09-15 14:00:00,8,21.58422851442277,28.694333423223423,60.384739718310925 +2024-09-15 15:00:00,8,12.264214874663535,23.798118025639877,64.8638571172728 +2024-09-15 16:00:00,8,24.086830640695066,22.629919643328144,50.33030706513862 +2024-09-15 17:00:00,8,25.808118187767953,23.903240085233634,56.890279111489654 +2024-09-15 18:00:00,8,7.5072420797221255,25.587665645609412,63.46557375212883 +2024-09-15 19:00:00,8,22.050706693314403,20.579210473342915,50.314164965083464 +2024-09-15 20:00:00,8,25.939743785668036,19.674534046892628,72.24469719091344 +2024-09-15 21:00:00,8,25.60980696456578,20.69566571283891,60.61521463123378 +2024-09-15 22:00:00,8,22.791366918256994,24.22466459832613,49.80808468944261 +2024-09-15 23:00:00,8,26.997517763544703,24.15704932555845,36.81679996584615 +2024-09-16 00:00:00,8,28.780490097452777,15.614691201386936,52.79460533384456 +2024-09-16 01:00:00,8,11.642322978332427,23.99606850010644,49.85596379164228 +2024-09-16 02:00:00,8,34.073555289001334,26.341337404996274,56.05534649763878 +2024-09-16 03:00:00,8,11.857019610718702,21.39424258035617,48.62034441794701 +2024-09-16 04:00:00,8,32.68263086391517,24.14253572660789,41.10552151663358 +2024-09-16 05:00:00,8,17.15146027170629,25.952945132501398,38.01326636498772 +2024-09-16 06:00:00,8,41.17704539304148,22.60161907911888,63.30163035577148 +2024-09-16 07:00:00,8,7.449104742305867,24.491200135464346,51.36703246144772 +2024-09-16 08:00:00,8,40.156958160663194,24.096991478562177,71.8508402821329 +2024-09-16 09:00:00,8,35.50126550810436,24.69973001740183,50.083062715476174 +2024-09-16 10:00:00,8,37.484088354078814,23.190980246696896,75.37299971275083 +2024-09-16 11:00:00,8,19.66380108156916,30.38016531240136,61.290931491806404 +2024-09-16 12:00:00,8,28.32596878435476,21.983793931778045,51.096857907647625 +2024-09-16 13:00:00,8,36.41325511001906,23.709635714120736,61.456936602560724 +2024-09-16 14:00:00,8,38.21601204360429,25.080834079328056,53.83498661472401 +2024-09-16 15:00:00,8,22.926793867429737,30.040631408519012,56.343381992378895 +2024-09-16 16:00:00,8,33.63988250649017,23.81733169939973,42.68516972689293 +2024-09-16 17:00:00,8,18.49514059973827,25.905932179033606,55.43007186054716 +2024-09-16 18:00:00,8,33.63696888432134,23.715166995072995,57.48203014290461 +2024-09-16 19:00:00,8,15.637664946599141,27.459202555023378,44.137202888111375 +2024-09-16 20:00:00,8,26.52118627762587,21.917587628425867,63.66083274284313 +2024-09-16 21:00:00,8,30.816511484451752,29.830411270885406,53.1583184388011 +2024-09-16 22:00:00,8,40.17983861843523,25.271046113921283,50.67742006116769 +2024-09-16 23:00:00,8,36.49059522067446,20.96884877402834,50.84979940770913 +2024-09-17 00:00:00,8,18.90830334673175,24.83045478925525,51.63522720451504 +2024-09-17 01:00:00,8,9.386991122191434,23.4605468401342,49.22800712978594 +2024-09-17 02:00:00,8,31.262797302005065,28.681693542978074,59.509842490786845 +2024-09-17 03:00:00,8,23.281429423236673,32.86862770031922,47.89621902566509 +2024-09-17 04:00:00,8,27.902183376081997,23.753505358608123,58.14091056964202 +2024-09-17 05:00:00,8,27.147618275400937,22.33931367179705,58.442286027909674 +2024-09-17 06:00:00,8,38.44480403246758,21.510005533025282,57.75658185639632 +2024-09-17 07:00:00,8,28.48119784521568,22.10666165978197,68.47508609543561 +2024-09-17 08:00:00,8,32.5588875830963,24.60170351661043,64.68600700918495 +2024-09-17 09:00:00,8,32.31281662666062,23.206643746952,41.74402083971182 +2024-09-17 10:00:00,8,28.700142940429146,22.845659331931493,47.48911189440064 +2024-09-17 11:00:00,8,9.648767111672267,28.76491988574216,51.63411533327014 +2024-09-17 12:00:00,8,27.058172268984823,26.925564402547664,76.35611526207948 +2024-09-17 13:00:00,8,22.165745223292994,22.594008109969625,37.47389055755387 +2024-09-17 14:00:00,8,32.712949487542886,26.505150235491268,52.96079546035807 +2024-09-17 15:00:00,8,28.86985589632083,24.58982515765637,47.5124277394759 +2024-09-17 16:00:00,8,21.91059931690875,21.612450959216606,59.244229788381986 +2024-09-17 17:00:00,8,25.75711344653637,26.20004090904728,53.52354213146346 +2024-09-17 18:00:00,8,38.085969491221455,26.84473889447055,47.86105278338248 +2024-09-17 19:00:00,8,24.109981627775117,26.299861013962705,38.57819076375998 +2024-09-17 20:00:00,8,22.927177494790428,25.24449216350485,50.304527234919895 +2024-09-17 21:00:00,8,42.7187320269159,24.78616394667905,55.663541648142356 +2024-09-17 22:00:00,8,33.85555647368427,26.35097381902456,52.84306597738244 +2024-09-17 23:00:00,8,20.7736828219222,22.98477935725708,62.99567146925971 +2024-09-18 00:00:00,8,5.602693317335458,18.537457227473652,46.652927705177945 +2024-09-18 01:00:00,8,37.446980805950886,22.93985997221715,51.5056475110182 +2024-09-18 02:00:00,8,16.46882027003775,21.787718402498395,61.279940912965465 +2024-09-18 03:00:00,8,28.413465184195047,24.675296572588234,49.68640904717499 +2024-09-18 04:00:00,8,53.444546590042734,27.70570098945776,54.48570234479226 +2024-09-18 05:00:00,8,16.62194507994198,16.36937364676402,48.18121257618816 +2024-09-18 06:00:00,8,24.875572114740304,28.4314159792806,44.47756803764388 +2024-09-18 07:00:00,8,21.39609710450992,20.948888724572836,44.57883126696808 +2024-09-18 08:00:00,8,43.35692589333081,20.156712918678473,55.44491684965704 +2024-09-18 09:00:00,8,28.638789656655227,19.721662639263418,58.91743452568268 +2024-09-18 10:00:00,8,15.3908633801313,19.506216810066025,56.2630056075876 +2024-09-18 11:00:00,8,34.81613079957539,24.782580776737476,65.59342057724109 +2024-09-18 12:00:00,8,22.646532188743695,29.413234134993466,52.580112940691734 +2024-09-18 13:00:00,8,26.368521894268095,20.994457743312427,45.00380554612292 +2024-09-18 14:00:00,8,16.24611882539986,25.40679733013424,58.136501877832515 +2024-09-18 15:00:00,8,36.48376015562358,17.120396561094875,61.73654356504392 +2024-09-18 16:00:00,8,22.629110487559014,27.19318969179875,50.62572447125789 +2024-09-18 17:00:00,8,20.529165437605414,30.114531325911507,49.52876130312654 +2024-09-18 18:00:00,8,29.146739583662942,24.419560026013208,63.254527029942324 +2024-09-18 19:00:00,8,40.573040127853226,20.49960926518883,75.7573422383409 +2024-09-18 20:00:00,8,13.06441310834981,28.74942743488776,55.04915222874856 +2024-09-18 21:00:00,8,13.798525976217363,26.21034444523084,46.31759200219692 +2024-09-18 22:00:00,8,17.60537311752116,24.19329879872621,54.96057656125088 +2024-09-18 23:00:00,8,42.115044722228376,22.506195250295534,55.62276060972216 +2024-09-19 00:00:00,8,35.28656712423426,20.289295988212796,61.91569898030195 +2024-09-19 01:00:00,8,31.615882072999554,25.41465877674746,53.965977644496874 +2024-09-19 02:00:00,8,24.493255871075366,22.539418447632475,53.771783803781155 +2024-09-19 03:00:00,8,14.20927414733664,27.606855464724372,57.31892329247053 +2024-09-19 04:00:00,8,16.45707540849444,24.867195644977826,46.84472558156931 +2024-09-19 05:00:00,8,31.797597544789898,22.245647261037497,65.67189374746147 +2024-09-19 06:00:00,8,35.16746878912254,18.722376658781833,51.78968102614212 +2024-09-19 07:00:00,8,29.797580069688397,28.09635142830298,66.7970765197594 +2024-09-19 08:00:00,8,27.27612333079964,20.431916963875146,50.28717105668361 +2024-09-19 09:00:00,8,27.45881846547957,16.99698204104699,69.32676527897831 +2024-09-19 10:00:00,8,21.692825231682885,27.505578453242926,51.640449162621515 +2024-09-19 11:00:00,8,26.32008295596656,20.553407356307645,42.3488672653654 +2024-09-19 12:00:00,8,34.885437519295635,20.602674664436396,45.909689123309114 +2024-09-19 13:00:00,8,19.001106473614154,23.63619368301054,52.38852226604689 +2024-09-19 14:00:00,8,38.39443569226293,23.14312562359823,73.28425163541692 +2024-09-19 15:00:00,8,10.335826725608033,22.072565590357907,49.64204303457132 +2024-09-19 16:00:00,8,21.83339775359402,21.32322812065506,63.91609119575564 +2024-09-19 17:00:00,8,34.401906795573026,27.80322863089119,51.79892203645999 +2024-09-19 18:00:00,8,23.8806401604595,22.208889635755774,57.068998510384304 +2024-09-19 19:00:00,8,31.562644755259278,24.971032963846838,59.62519307164696 +2024-09-19 20:00:00,8,21.70795913148314,21.775846293879386,56.580588516572334 +2024-09-19 21:00:00,8,14.407069224278597,26.518195361849163,61.76551720736065 +2024-09-19 22:00:00,8,30.651027665750938,20.45739449892318,68.52458458502593 +2024-09-19 23:00:00,8,23.9805937711375,20.384693792616286,57.71218985214398 +2024-09-20 00:00:00,8,8.099547340465513,23.25177504529664,58.20987450002659 +2024-09-20 01:00:00,8,27.50851820697509,22.316259622815778,56.69147132126737 +2024-09-20 02:00:00,8,39.30090828320624,23.28867671403956,45.923805500545036 +2024-09-20 03:00:00,8,33.47118796288428,20.43010564740497,57.57228608930015 +2024-09-20 04:00:00,8,25.284910158404003,22.007146191630333,52.37946727894149 +2024-09-20 05:00:00,8,29.997905023945105,23.124134788233015,48.50117143694519 +2024-09-20 06:00:00,8,33.240202455624434,22.84307786818404,48.536164879699136 +2024-09-20 07:00:00,8,23.217768120673405,27.702699917870802,39.06211966279555 +2024-09-20 08:00:00,8,17.907153507991133,25.484623788800604,58.425796383576724 +2024-09-20 09:00:00,8,45.211446967124914,20.04600648402816,58.84127964400284 +2024-09-20 10:00:00,8,28.01464194108901,24.468465591264817,59.55202807970875 +2024-09-20 11:00:00,8,28.348045042166405,26.0714768563977,49.0943057707129 +2024-09-20 12:00:00,8,25.94149949814285,22.080376061181703,69.7007941302468 +2024-09-20 13:00:00,8,21.904032022901504,25.557761000956372,65.71717094087668 +2024-09-20 14:00:00,8,35.01172996621566,19.47847952771906,58.80334902137372 +2024-09-20 15:00:00,8,39.25081887760453,23.968019334155457,47.30782930813217 +2024-09-20 16:00:00,8,36.78209043151794,23.12859677914948,51.21126780327384 +2024-09-20 17:00:00,8,18.784353460389283,26.434411494566195,38.54808749583016 +2024-09-20 18:00:00,8,17.866019968505377,28.600914720004447,61.96610728681199 +2024-09-20 19:00:00,8,21.231441488176024,26.36231411020488,52.36278359884557 +2024-09-20 20:00:00,8,22.007137266611053,23.856240611161674,50.21684976852492 +2024-09-20 21:00:00,8,52.53090378493775,28.300608385825278,56.527942275901836 +2024-09-20 22:00:00,8,29.879404203022595,27.438833317872092,54.45279279109039 +2024-09-20 23:00:00,8,19.132006099371576,24.36611185370637,40.81913739790478 +2024-09-21 00:00:00,8,23.471101059650643,22.30969259034478,39.63729291215442 +2024-09-21 01:00:00,8,15.253417070046522,20.030241464115832,54.988837353339186 +2024-09-21 02:00:00,8,26.41138741507195,24.90036507359499,59.33183170367575 +2024-09-21 03:00:00,8,0.0,23.62737934407391,53.42479420781119 +2024-09-21 04:00:00,8,4.160640016496565,26.022713875877365,45.02878113970193 +2024-09-21 05:00:00,8,17.660821286690098,23.621394937936568,45.134810793803325 +2024-09-21 06:00:00,8,25.70406928058469,22.57600204370933,40.92149503451769 +2024-09-21 07:00:00,8,14.417185003843782,26.576496356386983,53.71073731493901 +2024-09-21 08:00:00,8,24.7072197646526,20.552135105623154,47.54339521430191 +2024-09-21 09:00:00,8,26.65205528596895,22.60786216002892,62.262728126254046 +2024-09-21 10:00:00,8,16.40084253667304,22.936573807939435,57.32446388070941 +2024-09-21 11:00:00,8,19.78679818585956,25.746876168964338,50.35234686771669 +2024-09-21 12:00:00,8,21.698614294483463,27.944828258216873,49.094160335372536 +2024-09-21 13:00:00,8,26.081837504713153,20.11818863514658,73.34571735950209 +2024-09-21 14:00:00,8,20.017887682188906,24.879975951119313,67.53553504024448 +2024-09-21 15:00:00,8,25.073845401846768,27.896607931100988,60.17489691929848 +2024-09-21 16:00:00,8,34.0236339496135,28.1032356397394,56.94688854529721 +2024-09-21 17:00:00,8,23.74675255457441,28.995073102071093,78.0619308022459 +2024-09-21 18:00:00,8,19.56755495736641,27.57861119986577,73.23683006771412 +2024-09-21 19:00:00,8,18.118923080913536,22.969735775767795,56.244585049820856 +2024-09-21 20:00:00,8,27.225534539559703,23.629971864034868,64.96899795960117 +2024-09-21 21:00:00,8,31.278361328482713,22.473820671343308,58.730895281344615 +2024-09-21 22:00:00,8,25.47959173232742,22.450473664466916,55.572664716327694 +2024-09-21 23:00:00,8,28.481323710076257,28.45204139526116,62.77627297495095 +2024-09-22 00:00:00,8,11.959915744674536,22.911244127538158,53.00251185938272 +2024-09-22 01:00:00,8,37.73146820419788,26.98173305038535,47.39428671347917 +2024-09-22 02:00:00,8,28.812868991602272,25.451186942953846,48.005605078372014 +2024-09-22 03:00:00,8,21.797223625190007,21.03555362727255,49.93151092521863 +2024-09-22 04:00:00,8,31.270644106670023,23.73634028219963,49.85720864307436 +2024-09-22 05:00:00,8,33.94191987765731,19.61637152630461,49.01843332788249 +2024-09-22 06:00:00,8,14.649826959156986,23.30460698511692,40.0026312468545 +2024-09-22 07:00:00,8,38.449746763857334,21.998562496231713,47.73190378819895 +2024-09-22 08:00:00,8,29.817673272913545,28.003118532643906,46.89253122844987 +2024-09-22 09:00:00,8,22.791885856630294,18.456824172870437,64.72120079194436 +2024-09-22 10:00:00,8,29.770611011594497,17.49499423192063,44.50531973716077 +2024-09-22 11:00:00,8,17.30805763756779,23.200294101517777,53.30791063035846 +2024-09-22 12:00:00,8,35.62108313887829,23.265826921158514,36.14617251482728 +2024-09-22 13:00:00,8,22.877250049148763,22.83124879187007,46.41495196673005 +2024-09-22 14:00:00,8,24.899243575211795,21.958903482829875,59.693121280854896 +2024-09-22 15:00:00,8,17.79916754752154,23.99428396143512,40.20666342263368 +2024-09-22 16:00:00,8,15.683581564024808,21.658059531902545,51.85946710219245 +2024-09-22 17:00:00,8,24.111339270968124,21.57282828318075,49.76707433182257 +2024-09-22 18:00:00,8,18.730637505640367,27.298201887625044,54.96410618359444 +2024-09-22 19:00:00,8,36.93527982810462,25.0955663316143,64.92050624732003 +2024-09-22 20:00:00,8,23.550956850291072,23.515464001916246,51.76751087459503 +2024-09-22 21:00:00,8,28.929624035640842,24.403681792969188,61.28390256961902 +2024-09-22 22:00:00,8,16.466469964532465,32.83347170407227,49.62633031143505 +2024-09-22 23:00:00,8,27.812662020971004,21.418301738113623,48.46727281896843 +2024-09-23 00:00:00,8,34.94483274105786,27.114758131390214,48.70293280657908 +2024-09-23 01:00:00,8,7.877469939548298,27.164253654229192,78.30763219091948 +2024-09-23 02:00:00,8,26.691481317057576,25.42646140883837,60.46794282219452 +2024-09-23 03:00:00,8,27.20465588638602,26.487515554528635,50.71830717796145 +2024-09-23 04:00:00,8,21.22233789753899,16.59595411363563,60.49599962538006 +2024-09-23 05:00:00,8,18.790488693460603,27.518725163770128,42.03868531235325 +2024-09-23 06:00:00,8,18.760812346771985,19.423129268789275,48.40562521525537 +2024-09-23 07:00:00,8,29.90986514022043,23.647702015652047,58.86557552365644 +2024-09-23 08:00:00,8,23.509529182247277,24.222808589027597,46.081218328053296 +2024-09-23 09:00:00,8,35.753229377267374,24.46712088951276,58.3712977687115 +2024-09-23 10:00:00,8,27.36362639555945,19.178728208545053,69.96543704059943 +2024-09-23 11:00:00,8,23.45598921596631,22.86330575145041,46.36937724214692 +2024-09-23 12:00:00,8,41.07932982174591,20.63406533398697,46.89622464148786 +2024-09-23 13:00:00,8,33.21827015498219,24.96177968916725,43.23110505553795 +2024-09-23 14:00:00,8,8.435394937787493,27.753703314949323,50.81479334590208 +2024-09-23 15:00:00,8,15.956029800024645,29.009051748812333,54.13402898318598 +2024-09-23 16:00:00,8,22.23378482595388,25.85473360821636,61.073520391033696 +2024-09-23 17:00:00,8,41.812089164634955,27.53255698652314,39.94196623156826 +2024-09-23 18:00:00,8,17.157112168438548,20.36672344633172,54.26726632703914 +2024-09-23 19:00:00,8,34.17512949499763,25.40658312665041,61.38578928640347 +2024-09-23 20:00:00,8,13.301797413505534,31.12074763412333,43.45505065484198 +2024-09-23 21:00:00,8,26.569790381441546,27.515074345413034,57.18510514764819 +2024-09-23 22:00:00,8,24.253699283835928,27.36095682939642,49.97911612750868 +2024-09-23 23:00:00,8,34.250005931727465,26.416177004533232,53.70324066272448 +2024-09-24 00:00:00,8,31.60665918881346,23.005069588842304,63.547532975979465 +2024-09-24 01:00:00,8,22.052869761272827,26.73472394852417,46.53145549039351 +2024-09-24 02:00:00,8,13.07494026552836,22.877196846825207,45.16579945595643 +2024-09-24 03:00:00,8,19.794332642567554,17.395476772364447,57.23368154463742 +2024-09-24 04:00:00,8,13.277577814788453,24.20675084873761,52.81068944921846 +2024-09-24 05:00:00,8,30.20515404478698,23.629482764062026,58.34223802394511 +2024-09-24 06:00:00,8,26.864767447740423,18.28153403003523,70.34368974849211 +2024-09-24 07:00:00,8,31.366492424419754,27.350145767414418,53.00009982429911 +2024-09-24 08:00:00,8,16.868839060575358,18.869096945621074,65.33876571417707 +2024-09-24 09:00:00,8,21.00711386096571,25.440034468684626,41.311990686678115 +2024-09-24 10:00:00,8,31.82326652300022,27.64625631534767,39.94160973861429 +2024-09-24 11:00:00,8,23.770212699620938,17.772020586201325,57.30117423047479 +2024-09-24 12:00:00,8,29.561227767436463,27.52549383761125,37.603043920575935 +2024-09-24 13:00:00,8,22.443426450058762,25.28054005854116,63.54947273694643 +2024-09-24 14:00:00,8,30.25658538809946,21.479078524553675,67.82329080734992 +2024-09-24 15:00:00,8,44.974483090980144,27.842066852442816,60.17077350124174 +2024-09-24 16:00:00,8,32.894624615980085,21.2650033347764,57.972381570711775 +2024-09-24 17:00:00,8,24.154549936147927,23.905312074629766,40.93029848089538 +2024-09-24 18:00:00,8,34.27495139089744,26.5819203231734,53.22137394742916 +2024-09-24 19:00:00,8,33.07434151527613,18.745613680758286,40.89954712230943 +2024-09-24 20:00:00,8,16.400995391716005,30.732472999847104,30.95921111502585 +2024-09-24 21:00:00,8,4.099979853225673,17.75857222049611,66.77647246963554 +2024-09-24 22:00:00,8,31.400629256519906,21.427522759619656,56.38563366283196 +2024-09-24 23:00:00,8,27.488810358169836,22.869994331816677,66.76023530082054 +2024-09-25 00:00:00,8,20.28511714052446,22.725906868796415,43.69149596454214 +2024-09-25 01:00:00,8,15.742581616093545,27.18946970501476,70.47255997827001 +2024-09-25 02:00:00,8,17.929927152387556,25.237722026108738,50.0981181499903 +2024-09-25 03:00:00,8,32.641927825525315,24.129561161973395,63.61457888501012 +2024-09-25 04:00:00,8,9.075897308718062,26.749165954322898,54.34224297792889 +2024-09-25 05:00:00,8,41.15510936662787,23.402376865602292,41.41911428844843 +2024-09-25 06:00:00,8,31.724649002217056,22.726589600528804,58.26301931279231 +2024-09-25 07:00:00,8,22.298529542693977,19.922585784753323,51.57647783796009 +2024-09-25 08:00:00,8,56.066734595619934,21.89159068293524,61.23246302514757 +2024-09-25 09:00:00,8,17.628469983372405,25.224052764123627,63.94909928947823 +2024-09-25 10:00:00,8,31.266904366302434,24.208389124573277,72.73079422992222 +2024-09-25 11:00:00,8,45.81387778895216,24.828956072542102,56.07054421793159 +2024-09-25 12:00:00,8,29.491825908436386,20.262333517491047,51.13546912441095 +2024-09-25 13:00:00,8,36.10693980953016,27.51255052670823,54.525108697206015 +2024-09-25 14:00:00,8,18.447129949720704,28.561425202094725,33.23072512841882 +2024-09-25 15:00:00,8,49.58163141115521,25.209004407476282,52.26016511159252 +2024-09-25 16:00:00,8,33.61743938442755,24.022940265616164,44.16299781398267 +2024-09-25 17:00:00,8,31.304463931845515,24.267862403527012,54.209813558149136 +2024-09-25 18:00:00,8,33.53532673185718,27.121952947239254,59.414890492766524 +2024-09-25 19:00:00,8,33.91605982928056,27.56156372664854,44.12693884704544 +2024-09-25 20:00:00,8,34.22730360102536,24.235007573427527,52.08683368088251 +2024-09-25 21:00:00,8,27.095087846702565,25.62068611433587,51.78088789839133 +2024-09-25 22:00:00,8,27.120186139237607,26.82628767358381,48.460760003634995 +2024-09-25 23:00:00,8,32.72264640790251,19.409104136211397,76.56049660019318 +2024-09-26 00:00:00,8,16.837234270609276,24.40483769068275,52.50150298860887 +2024-09-26 01:00:00,8,36.89509846041834,22.8862143843114,46.777395011409304 +2024-09-26 02:00:00,8,20.757446619538406,27.12428756632467,45.900415201325195 +2024-09-26 03:00:00,8,23.041779650254533,25.130776670707004,45.35689308022096 +2024-09-26 04:00:00,8,22.107474555984947,23.18628923050793,51.355466978950695 +2024-09-26 05:00:00,8,12.11059586893627,20.329379434984155,43.892855095229535 +2024-09-26 06:00:00,8,36.46805615390586,24.641814544404546,65.55824061781337 +2024-09-26 07:00:00,8,24.23392629275499,20.80633646879917,55.923026169434294 +2024-09-26 08:00:00,8,22.406364631993394,18.739427755605394,48.12923177033349 +2024-09-26 09:00:00,8,20.438991480647104,20.20319154478954,68.14289316393553 +2024-09-26 10:00:00,8,25.507453558090333,21.229931339315964,64.51379642002678 +2024-09-26 11:00:00,8,26.392187100777175,20.637424332063205,37.25127544592663 +2024-09-26 12:00:00,8,17.648835533732488,29.306203943434188,63.334933256297006 +2024-09-26 13:00:00,8,24.59100284761995,24.587434003779034,61.93316874894252 +2024-09-26 14:00:00,8,16.622942179700583,22.605273967268328,43.21098024658335 +2024-09-26 15:00:00,8,35.07084743650671,26.289775500547197,47.87779291599521 +2024-09-26 16:00:00,8,4.348250637016598,22.97065079539528,60.956217162821495 +2024-09-26 17:00:00,8,26.4839895787117,20.54237527045623,56.99757671997579 +2024-09-26 18:00:00,8,23.264752645926,20.364089684379767,47.56654292403761 +2024-09-26 19:00:00,8,29.971740640249546,20.54726333005076,51.07506343241335 +2024-09-26 20:00:00,8,22.269582891183767,27.297892066725446,49.14153902617521 +2024-09-26 21:00:00,8,10.788910843079286,23.070090131070558,53.26219322861703 +2024-09-26 22:00:00,8,25.711187471822196,25.18108539094119,49.661680466040515 +2024-09-26 23:00:00,8,27.583308483060613,22.87628524479213,52.199874526304015 +2024-09-27 00:00:00,8,27.11856677198441,20.624308951247112,61.89429218175272 +2024-09-27 01:00:00,8,22.458523908096133,19.277738149956996,58.59515979914849 +2024-09-27 02:00:00,8,23.258730407654227,23.27265332855692,58.27181919569717 +2024-09-27 03:00:00,8,45.13426422712145,23.349554093664953,55.88050916370539 +2024-09-27 04:00:00,8,24.8103618103776,23.81010992320298,64.58293568732691 +2024-09-27 05:00:00,8,31.705613348801727,24.36352630000655,61.95974032601767 +2024-09-27 06:00:00,8,14.766544961010148,19.361591449965147,40.97125591712819 +2024-09-27 07:00:00,8,29.649457998984857,22.560389895869754,54.00864073983449 +2024-09-27 08:00:00,8,17.39354254336037,24.597623702217774,55.72097126185875 +2024-09-27 09:00:00,8,32.60191459840591,21.617266845914365,56.27591232585944 +2024-09-27 10:00:00,8,41.10816796710963,19.449041625212885,41.02777878693792 +2024-09-27 11:00:00,8,18.475111156476434,24.399636209754533,65.58815808619568 +2024-09-27 12:00:00,8,28.099899753898562,19.071020564673315,46.41854899251074 +2024-09-27 13:00:00,8,37.11670166201513,29.087422691947147,41.965257023137006 +2024-09-27 14:00:00,8,32.15106023432541,21.244632894640844,54.590929426106065 +2024-09-27 15:00:00,8,39.068801010976415,27.753014550483574,60.02920120043786 +2024-09-27 16:00:00,8,29.188031956493408,22.51658199010801,55.650324039710576 +2024-09-27 17:00:00,8,32.95479328342105,28.93538962767117,39.2311495777146 +2024-09-27 18:00:00,8,29.732369758863225,25.56538305883566,48.9239854401966 +2024-09-27 19:00:00,8,37.692631694617404,22.299221270673836,52.27360159681791 +2024-09-27 20:00:00,8,28.672197941026663,29.07993818710863,56.53957127095129 +2024-09-27 21:00:00,8,28.444877073608495,23.80899874558331,48.41199749828942 +2024-09-27 22:00:00,8,25.322982380408114,21.630503348130283,61.24532659978761 +2024-09-27 23:00:00,8,41.38394677751035,23.843813308313955,66.90666665290513 +2024-09-28 00:00:00,8,32.63431020448556,21.941335895685782,53.146361712249046 +2024-09-28 01:00:00,8,36.319539906321694,23.25457499038189,61.04132019119294 +2024-09-28 02:00:00,8,27.789670758569056,29.56792263685803,48.256197526096884 +2024-09-28 03:00:00,8,35.101243882910055,22.09152493884044,47.44161653153702 +2024-09-28 04:00:00,8,31.631696358744556,20.531645372047326,60.076493311339846 +2024-09-28 05:00:00,8,30.869005988212308,26.020313690048656,43.41394799505235 +2024-09-28 06:00:00,8,17.134834371749978,28.932312435276206,62.14105140830663 +2024-09-28 07:00:00,8,9.727941003874072,22.648947850171268,52.37952985422729 +2024-09-28 08:00:00,8,24.125298107693872,21.10742643052863,49.29245533811146 +2024-09-28 09:00:00,8,38.24908955650986,22.247924515888915,55.34479810407841 +2024-09-28 10:00:00,8,26.781767646594687,24.258562074029225,45.43127265832296 +2024-09-28 11:00:00,8,29.931674749548367,22.728394140317167,59.694217188100616 +2024-09-28 12:00:00,8,31.13114560197332,21.15440868836749,42.864048623842244 +2024-09-28 13:00:00,8,35.3337601862666,21.74564454814406,61.45542209482952 +2024-09-28 14:00:00,8,38.9106491225198,28.361670523041212,54.86364239826532 +2024-09-28 15:00:00,8,27.53692998748692,22.740812849199852,62.819011888295904 +2024-09-28 16:00:00,8,28.70510403097492,25.393555401811977,57.64582808038973 +2024-09-28 17:00:00,8,27.28733178174462,28.61056444692635,52.68375554752245 +2024-09-28 18:00:00,8,26.984761983756826,25.98818391320915,52.21735844373171 +2024-09-28 19:00:00,8,25.878112530819738,26.421002038557816,52.78253415301564 +2024-09-28 20:00:00,8,37.87175154160514,22.617082388726857,47.97058292322439 +2024-09-28 21:00:00,8,17.067254984764283,27.226007456614244,64.54224541063759 +2024-09-28 22:00:00,8,18.733831425812223,25.562663299280487,66.30545543353264 +2024-09-28 23:00:00,8,24.19942964040917,23.494832191803912,39.00636501429611 +2024-09-29 00:00:00,8,27.08845628089272,19.479269433783436,43.572204165173574 +2024-09-29 01:00:00,8,27.644000898619133,22.163114750346573,53.04957366332486 +2024-09-29 02:00:00,8,24.626346865704836,15.342563468859613,47.75796711583518 +2024-09-29 03:00:00,8,20.14532540540948,22.812253675011366,42.22606208822684 +2024-09-29 04:00:00,8,24.38522165732661,20.058882956169548,49.373325613868765 +2024-09-29 05:00:00,8,41.95204764497724,22.323653160301088,60.07193625242921 +2024-09-29 06:00:00,8,24.10391271517587,24.403782328284386,49.65130813320565 +2024-09-29 07:00:00,8,34.189082056802285,26.827189457934914,39.768006016521284 +2024-09-29 08:00:00,8,34.21018323422941,24.58273186946685,52.80048139527512 +2024-09-29 09:00:00,8,32.258127928941406,23.02753869115583,46.52170310277072 +2024-09-29 10:00:00,8,30.74754350554451,21.576925104834622,50.03512882268091 +2024-09-29 11:00:00,8,3.6610119371528036,26.675758581415824,53.60220687976989 +2024-09-29 12:00:00,8,29.09746168556399,22.765699288895807,52.64388501922553 +2024-09-29 13:00:00,8,35.879814475607674,27.574433442684608,44.53840463125568 +2024-09-29 14:00:00,8,42.46638489496864,21.397585338270915,65.59101630420456 +2024-09-29 15:00:00,8,33.86578866638544,29.581797378795166,54.85116678625194 +2024-09-29 16:00:00,8,17.853073894098618,24.483255268181406,67.57825084261609 +2024-09-29 17:00:00,8,27.26401169396645,26.470481813617653,52.50192460681863 +2024-09-29 18:00:00,8,28.133155108568694,31.26227902045406,49.235381547300875 +2024-09-29 19:00:00,8,35.8220204049949,25.157177293731053,70.03538149475119 +2024-09-29 20:00:00,8,41.701133256293254,26.729979464410814,45.997497559497376 +2024-09-29 21:00:00,8,23.07316730463696,28.330228720853057,62.80729369515251 +2024-09-29 22:00:00,8,23.063994999032726,25.417580042768215,54.60641712130326 +2024-09-29 23:00:00,8,36.545324855221715,29.626093538056807,62.875140070272444 +2024-09-30 00:00:00,8,21.8519656633944,21.693640581833648,43.907946152344174 +2024-09-30 01:00:00,8,23.521823337695228,26.946851970898024,72.97079362037545 +2024-09-30 02:00:00,8,25.130165568727495,23.427109223063482,44.27512586256148 +2024-09-30 03:00:00,8,40.265356523691665,21.387107425145075,72.13052447567145 +2024-09-30 04:00:00,8,19.30737291929495,28.85376396560097,56.157774507713874 +2024-09-30 05:00:00,8,30.769029841147407,24.336971993286532,69.09898975429759 +2024-09-30 06:00:00,8,23.726777097383472,23.539832370789714,50.357807519047526 +2024-09-30 07:00:00,8,29.386695470147902,27.871103647578067,54.33205907033911 +2024-09-30 08:00:00,8,23.00611083503473,19.891730614010662,53.366355168293275 +2024-09-30 09:00:00,8,19.924332683504396,24.031633334647857,55.02597879158709 +2024-09-30 10:00:00,8,7.922446477194242,25.588004039381737,54.03061754084023 +2024-09-30 11:00:00,8,25.629054457037466,23.47198418731044,46.59278950495693 +2024-09-30 12:00:00,8,39.198117976635146,25.583623644355583,55.76925075680565 +2024-09-30 13:00:00,8,25.11201245662343,24.992639221800996,50.4225358547462 +2024-09-30 14:00:00,8,21.98854509031419,23.799975385147356,54.862059841257725 +2024-09-30 15:00:00,8,39.324573635357225,28.951629753616658,46.369757960242154 +2024-09-30 16:00:00,8,16.11263626930483,20.38239008323982,63.73791240340233 +2024-09-30 17:00:00,8,25.98738790740637,29.23036658877374,57.8808592635734 +2024-09-30 18:00:00,8,27.961593879159146,19.489623661688462,59.78727002713427 +2024-09-30 19:00:00,8,21.94847377397576,20.531482931368433,57.43416057971387 +2024-09-30 20:00:00,8,6.168784835715524,26.96577813959436,55.228370270274894 +2024-09-30 21:00:00,8,25.91087239909893,18.984621697785173,52.407679283810474 +2024-09-30 22:00:00,8,26.009579633993766,20.491727872453982,41.979110221799516 +2024-09-30 23:00:00,8,26.947366529824425,23.95278310907891,70.67736565252322 +2024-10-01 00:00:00,8,31.55098934497422,32.33013388135896,45.06004278692673 +2024-10-01 01:00:00,8,14.318718437001056,23.88028277132386,45.146085349852854 +2024-10-01 02:00:00,8,22.360716224538827,23.98659860054616,61.50565900665241 +2024-10-01 03:00:00,8,22.311447569044635,24.94056994047914,62.599272462381705 +2024-10-01 04:00:00,8,29.198482154641287,24.567318448606024,45.99709354053719 +2024-10-01 05:00:00,8,24.46007867497873,21.187299496262575,63.30356282478223 +2024-10-01 06:00:00,8,34.84699495366758,21.243136196589166,44.05840964898782 +2024-10-01 07:00:00,8,22.270505642089688,21.67697511287554,60.11364840686656 +2024-10-01 08:00:00,8,24.627692791958292,26.215527111747793,46.14244052289392 +2024-10-01 09:00:00,8,39.89062298832819,19.419119730062793,60.8273186732054 +2024-10-01 10:00:00,8,33.02793303533361,20.468075125361,67.60336154458848 +2024-10-01 11:00:00,8,33.135057232058365,26.01169158347396,57.51961423136546 +2024-10-01 12:00:00,8,25.53396436134462,24.957266040928214,40.446379284774295 +2024-10-01 13:00:00,8,34.99382453664692,23.235123413154987,61.07278816782109 +2024-10-01 14:00:00,8,41.76007274401418,29.442948552696077,51.23505842958759 +2024-10-01 15:00:00,8,22.483497098510114,26.451611795719316,67.12073000245057 +2024-10-01 16:00:00,8,14.896494933513942,26.039292734203073,55.76617743905272 +2024-10-01 17:00:00,8,17.49685105685567,21.943258528759888,52.656152666768094 +2024-10-01 18:00:00,8,29.932450195408915,29.63070795288295,76.356282776207 +2024-10-01 19:00:00,8,33.984066129293105,22.022508487211624,51.754503219054655 +2024-10-01 20:00:00,8,26.74151548256012,21.61642198389053,42.771585695262694 +2024-10-01 21:00:00,8,12.038659757356534,23.222812650372813,48.75041490220351 +2024-10-01 22:00:00,8,35.13542283936442,21.251258730546585,44.320433651278684 +2024-10-01 23:00:00,8,25.16818028170107,30.327568772002856,56.741022357719686 +2024-10-02 00:00:00,8,13.555640872143107,19.730676386905056,59.33285387261609 +2024-10-02 01:00:00,8,27.25358562304613,24.06828271909,52.65351961956487 +2024-10-02 02:00:00,8,34.50004080777237,28.95780160585104,42.87562136777084 +2024-10-02 03:00:00,8,15.405075688668902,23.65526394935072,40.656049548793135 +2024-10-02 04:00:00,8,22.456857058957848,23.393918350661053,50.425504290635565 +2024-10-02 05:00:00,8,22.280768540880217,23.862916007926895,53.7301985809774 +2024-10-02 06:00:00,8,26.094275470062996,30.2944301132829,58.53336849177521 +2024-10-02 07:00:00,8,24.656562868620885,26.8853888739574,37.02315004245104 +2024-10-02 08:00:00,8,14.645949992523082,21.553492545548817,45.121808488496455 +2024-10-02 09:00:00,8,36.52874526744991,28.16351850952607,43.84983964160061 +2024-10-02 10:00:00,8,13.109892140010594,27.60057453583871,68.62722915588202 +2024-10-02 11:00:00,8,45.43888743125876,25.429813321779346,59.03643107590459 +2024-10-02 12:00:00,8,26.787734795253222,24.273749535695732,62.706949346326276 +2024-10-02 13:00:00,8,24.028379888659963,25.145955903826607,55.72575941342303 +2024-10-02 14:00:00,8,23.6851808759073,25.73693277600309,56.435040939190124 +2024-10-02 15:00:00,8,24.639661122228347,30.353944351183845,43.79006151740134 +2024-10-02 16:00:00,8,35.72911886417607,19.942433798086565,57.09451969078253 +2024-10-02 17:00:00,8,29.628987428863056,23.836837611437655,59.62570981654847 +2024-10-02 18:00:00,8,24.788316899626853,25.16168777026428,47.80617659219298 +2024-10-02 19:00:00,8,20.921158764583787,29.05195227299736,45.27952009891683 +2024-10-02 20:00:00,8,17.05579339231841,21.859043352727593,51.46071130876093 +2024-10-02 21:00:00,8,18.349924798550866,24.099415461472372,59.4529306080314 +2024-10-02 22:00:00,8,11.31050566993715,24.760051655905407,62.259034437022244 +2024-10-02 23:00:00,8,33.89752708774781,25.016624039685208,63.458529057945796 +2024-10-03 00:00:00,8,28.240057189482513,27.516585708062326,55.760301917598426 +2024-10-03 01:00:00,8,10.338038246311115,19.729700846696584,74.91155134136417 +2024-10-03 02:00:00,8,3.56193069684986,19.52971777029407,50.04498898309447 +2024-10-03 03:00:00,8,21.401497265522277,28.08096703461053,45.83495298681213 +2024-10-03 04:00:00,8,31.491148368564872,26.13090970177879,43.39968158852745 +2024-10-03 05:00:00,8,17.34756399203786,24.130463702855504,50.204403093735564 +2024-10-03 06:00:00,8,27.9339351542509,27.703638720300003,47.30493872813841 +2024-10-03 07:00:00,8,34.51001940307998,21.034969400139797,48.6495229720492 +2024-10-03 08:00:00,8,44.604244658954244,24.00956055683644,59.7440635791773 +2024-10-03 09:00:00,8,20.181960693195713,26.369208519934553,63.844183928327546 +2024-10-03 10:00:00,8,15.39021488574135,22.846446285172455,63.20817614404055 +2024-10-03 11:00:00,8,17.494702475271648,27.209513319537674,61.44885174558867 +2024-10-03 12:00:00,8,34.924681209135315,24.022820387089993,59.63380985057021 +2024-10-03 13:00:00,8,30.456643107729935,21.438778666755486,58.159255670924004 +2024-10-03 14:00:00,8,29.763160474713096,18.4103896780682,61.05900039500948 +2024-10-03 15:00:00,8,39.20998049384385,29.678225983296333,44.340598924237185 +2024-10-03 16:00:00,8,4.295888202372513,20.841586136081727,53.748827525246895 +2024-10-03 17:00:00,8,35.310724527664206,25.42904682908044,48.52751973026278 +2024-10-03 18:00:00,8,22.782519332026265,25.034613970119423,49.61893980003188 +2024-10-03 19:00:00,8,21.84183344239186,24.58541915690986,53.06174271235175 +2024-10-03 20:00:00,8,18.243233229973903,24.867708004701043,46.23690704451494 +2024-10-03 21:00:00,8,41.16810565614051,24.900434170857114,59.191727300696314 +2024-10-03 22:00:00,8,16.086330081055962,21.109042352107288,46.4723006806623 +2024-10-03 23:00:00,8,21.13843186492293,18.23910473178,50.45300474044713 +2024-10-04 00:00:00,8,16.34170030226209,16.902768833368278,53.97945703967226 +2024-10-04 01:00:00,8,4.112041007261624,23.7916839953974,48.14725645283018 +2024-10-04 02:00:00,8,24.696601853386593,18.984294402838508,60.77945168883768 +2024-10-04 03:00:00,8,34.43133339872277,13.052273028374135,63.831000282996975 +2024-10-04 04:00:00,8,30.036409863970075,22.30041828055385,49.4183398589708 +2024-10-04 05:00:00,8,25.942210810888888,28.156005047220788,37.99530147926799 +2024-10-04 06:00:00,8,44.1284686222539,20.553491202422684,45.62916171868015 +2024-10-04 07:00:00,8,23.67783613069583,23.950620691305687,50.36543656491295 +2024-10-04 08:00:00,8,40.16583868530667,21.164745375519114,58.827862520650264 +2024-10-04 09:00:00,8,23.536950063140115,22.56330986496286,52.88454747654258 +2024-10-04 10:00:00,8,27.37680210449153,19.880745751344953,72.8930822203896 +2024-10-04 11:00:00,8,25.016191049095482,22.43781856879769,59.75464301175945 +2024-10-04 12:00:00,8,28.794520396817255,17.567701520425956,39.79872366543704 +2024-10-04 13:00:00,8,20.0922180757342,25.82019294189643,50.479564498162986 +2024-10-04 14:00:00,8,29.865702828859817,20.300034335919463,56.6391032115131 +2024-10-04 15:00:00,8,39.96615536419418,23.66451020086025,51.462421013549594 +2024-10-04 16:00:00,8,36.184370905779915,17.45927290097998,42.94189810353901 +2024-10-04 17:00:00,8,27.16222713483838,26.07896333138648,63.50691661548561 +2024-10-04 18:00:00,8,17.91161554597403,24.986342965411403,43.93287487333865 +2024-10-04 19:00:00,8,34.892582436028476,23.99767704653406,67.70482882209278 +2024-10-04 20:00:00,8,23.22615378243826,25.159398714595124,62.07310909790183 +2024-10-04 21:00:00,8,44.54838853374254,24.077750860727626,32.476016224549745 +2024-10-04 22:00:00,8,31.022752476661964,22.988835110332975,71.60020050715178 +2024-10-04 23:00:00,8,24.052304260745245,25.50460369140072,67.64828221823052 +2024-10-05 00:00:00,8,18.763475352660247,19.228126232148533,57.24479866844329 +2024-10-05 01:00:00,8,37.08425883223041,23.638115267269704,58.59062028942326 +2024-10-05 02:00:00,8,21.09967649152739,25.215172099592497,63.334466094937525 +2024-10-05 03:00:00,8,36.384245841071596,29.951100246612636,40.852058488312906 +2024-10-05 04:00:00,8,21.697956942535946,24.047622841599278,53.04782634388998 +2024-10-05 05:00:00,8,22.163731729867827,27.393290656465183,36.04361276008409 +2024-10-05 06:00:00,8,25.84650341647268,16.292440232243237,45.569631136737144 +2024-10-05 07:00:00,8,35.161948765399416,18.330158447762297,53.74895010634104 +2024-10-05 08:00:00,8,22.09637209561654,25.974152790213882,62.76631231641667 +2024-10-05 09:00:00,8,44.08363578540941,21.251484697089474,48.82656750321793 +2024-10-05 10:00:00,8,32.87677863076786,25.433959948275824,52.07993996399058 +2024-10-05 11:00:00,8,31.55692982966518,23.732024114312,64.72989474157298 +2024-10-05 12:00:00,8,26.016387192105555,23.910061473717686,45.04353433501193 +2024-10-05 13:00:00,8,33.11294462380171,24.830967491331336,47.46264123761577 +2024-10-05 14:00:00,8,56.11621862248701,26.235226725934258,54.663016102923514 +2024-10-05 15:00:00,8,25.563168048110583,19.571916788707444,73.45022950586387 +2024-10-05 16:00:00,8,26.94287447987409,20.448615628572636,65.68840958077281 +2024-10-05 17:00:00,8,22.512619125751115,23.507720116643476,63.52962523621117 +2024-10-05 18:00:00,8,42.86860413524672,19.78495961190061,53.261136014079035 +2024-10-05 19:00:00,8,16.246324836453013,21.529569750872753,53.492839452351646 +2024-10-05 20:00:00,8,7.759411906812133,23.536022406288918,32.65533912040409 +2024-10-05 21:00:00,8,16.01806730835755,25.578207632846436,34.64454626332593 +2024-10-05 22:00:00,8,36.91855015252102,33.29386375794208,37.91794724236051 +2024-10-05 23:00:00,8,22.072841301386003,25.05324192167277,37.874923510115515 +2024-10-06 00:00:00,8,21.64034493243894,20.992725197272744,57.25944595864309 +2024-10-06 01:00:00,8,34.04549842209519,18.9685204536204,75.87358855087558 +2024-10-06 02:00:00,8,32.68345109636847,15.343437733375424,52.042493213963546 +2024-10-06 03:00:00,8,21.330525431749745,16.404749179911022,62.728917611589374 +2024-10-06 04:00:00,8,25.068812519806272,28.154527798319823,55.8681395648432 +2024-10-06 05:00:00,8,28.59926941217443,24.73213615877959,46.023269665335874 +2024-10-06 06:00:00,8,34.55432169729747,19.1462365948677,48.89425452541471 +2024-10-06 07:00:00,8,26.314518782689756,22.680652201626696,45.83461488403434 +2024-10-06 08:00:00,8,42.38048844029641,26.843652282444815,74.1529142149224 +2024-10-06 09:00:00,8,48.633503798933916,17.23732236437703,62.32579337384873 +2024-10-06 10:00:00,8,20.69388646834802,22.26754134422018,59.67089056837894 +2024-10-06 11:00:00,8,20.914781257047977,21.340541030761287,80.1822538283659 +2024-10-06 12:00:00,8,24.329726722402967,21.59627684792553,79.72981618837845 +2024-10-06 13:00:00,8,40.490334175149144,24.034388828094595,58.228136881779015 +2024-10-06 14:00:00,8,36.3569589352523,18.90987074048253,36.97878046572626 +2024-10-06 15:00:00,8,29.927144544439777,27.240010328426184,40.468391581477704 +2024-10-06 16:00:00,8,17.085124243606998,26.39271861616781,41.112781496850396 +2024-10-06 17:00:00,8,30.322314623820112,16.346533556593208,60.78270280809627 +2024-10-06 18:00:00,8,41.5441596981108,22.08100033373726,45.586501290332905 +2024-10-06 19:00:00,8,27.958066175517672,23.694074827489125,45.3966330876839 +2024-10-06 20:00:00,8,18.6661299777697,20.42336516980037,58.50835808438738 +2024-10-06 21:00:00,8,6.699118279287735,28.690981992721124,59.76463218507993 +2024-10-06 22:00:00,8,30.735173856250704,20.975275092278757,56.87238275644876 +2024-10-06 23:00:00,8,2.122016239113858,27.161614757155903,65.31212897505107 +2024-10-07 00:00:00,8,43.07964206011205,30.26934537931394,57.37258530111744 +2024-10-07 01:00:00,8,29.98329230072232,20.53008022991945,51.82673217723184 +2024-10-07 02:00:00,8,26.08425491415578,24.06630177555151,66.84600379508974 +2024-10-07 03:00:00,8,19.084582067422936,20.404178937961778,53.02172424535404 +2024-10-07 04:00:00,8,25.4694166388984,26.894753550167597,48.36535250640347 +2024-10-07 05:00:00,8,18.573586076958023,20.222332066331145,52.01225127824091 +2024-10-07 06:00:00,8,19.69601422269421,19.870431050988948,53.62042471595825 +2024-10-07 07:00:00,8,27.813565216553577,20.36725977866872,78.5783717379503 +2024-10-07 08:00:00,8,20.520281190216927,20.639548658867348,59.27921706276042 +2024-10-07 09:00:00,8,22.907713422020073,22.272523892105283,47.89637245880179 +2024-10-07 10:00:00,8,43.776659252174575,27.927852943522122,68.08983529677326 +2024-10-07 11:00:00,8,28.028820158515053,19.129069423705,43.90801929543207 +2024-10-07 12:00:00,8,31.94254641850837,22.689667478875947,62.75054087176498 +2024-10-07 13:00:00,8,22.38589605530587,22.85034597650495,45.443434167139 +2024-10-07 14:00:00,8,14.182293912805894,22.80132301222883,63.63185140737825 +2024-10-07 15:00:00,8,44.66715320263914,20.520083066665677,65.75613193688409 +2024-10-07 16:00:00,8,20.588999124791762,27.21699536239405,47.02035798461038 +2024-10-07 17:00:00,8,27.77770162746227,30.704040180202526,46.9426643326792 +2024-10-07 18:00:00,8,29.07194006829731,25.29690075265437,63.8317334545456 +2024-10-07 19:00:00,8,17.22531390801435,21.29591445850228,53.258110187747704 +2024-10-07 20:00:00,8,24.90894232599752,23.511187553528078,53.99169506295325 +2024-10-07 21:00:00,8,28.769868368991528,26.434429361219408,61.501895390714324 +2024-10-07 22:00:00,8,26.639297076307567,20.81189011677881,63.18924771267484 +2024-10-07 23:00:00,8,29.87153800195054,24.28564971684959,68.06981163792818 +2024-10-08 00:00:00,8,25.076663894607524,19.102180723888367,57.342007363028486 +2024-10-08 01:00:00,8,18.48552249609829,20.522303027696246,34.657931929163 +2024-10-08 02:00:00,8,31.795893766287602,22.835840092536838,49.58196797003779 +2024-10-08 03:00:00,8,26.494613988696607,21.93415461430554,52.97647234107765 +2024-10-08 04:00:00,8,34.48349220081889,20.80230165650785,55.848198672462026 +2024-10-08 05:00:00,8,22.747041647113736,26.582370153520046,47.17521745010215 +2024-10-08 06:00:00,8,46.22578410898311,24.58978416228706,59.98468761209128 +2024-10-08 07:00:00,8,25.982291608873652,24.38232287917866,57.41626863856477 +2024-10-08 08:00:00,8,19.548862078900665,19.422586876268298,46.11914577098807 +2024-10-08 09:00:00,8,32.475453163353436,24.48228033367643,45.77954998300251 +2024-10-08 10:00:00,8,26.613765683858215,26.871472319339542,42.82800341689992 +2024-10-08 11:00:00,8,38.99038561212532,23.506714195990277,54.21361154154854 +2024-10-08 12:00:00,8,16.415310133456185,26.523125475299622,40.45767803830989 +2024-10-08 13:00:00,8,32.36925753968768,25.542460588375306,62.63334522029791 +2024-10-08 14:00:00,8,16.983812156035924,25.934976811918336,51.70736601897309 +2024-10-08 15:00:00,8,20.331622692386677,23.246768851300864,59.034530537210784 +2024-10-08 16:00:00,8,20.461474120722606,28.22448564327858,54.41064742739218 +2024-10-08 17:00:00,8,25.686909670940988,26.569536530016542,46.37577879908142 +2024-10-08 18:00:00,8,30.41902694096249,21.912049329792197,43.50723410371131 +2024-10-08 19:00:00,8,19.643252612429386,25.987214532785732,74.25397022871365 +2024-10-08 20:00:00,8,29.875256344007187,32.857219015954065,59.397356967500635 +2024-10-08 21:00:00,8,32.3309957971837,21.263428999640848,48.5442237301127 +2024-10-08 22:00:00,8,26.533651828816758,18.03125565820047,58.074970563915585 +2024-10-08 23:00:00,8,24.098582557831282,27.771187764937167,65.65475904730246 +2024-10-09 00:00:00,8,34.05732882665018,23.181599043260523,48.16512857126005 +2024-10-09 01:00:00,8,19.485133251884378,25.135287153577046,52.78853736656468 +2024-10-09 02:00:00,8,15.638421898458455,18.07267690159657,35.29295242051087 +2024-10-09 03:00:00,8,6.713331367215968,26.481403408769996,72.02407034115774 +2024-10-09 04:00:00,8,30.860209902165924,22.93531445300903,41.9696586125892 +2024-10-09 05:00:00,8,23.680040841690772,19.722800330192236,50.973143746211136 +2024-10-09 06:00:00,8,22.75446368917984,22.184543258969754,40.40238328119152 +2024-10-09 07:00:00,8,17.25761653559168,24.215610592046165,63.748683647524395 +2024-10-09 08:00:00,8,26.92093618708359,24.850237099083706,58.34214721460969 +2024-10-09 09:00:00,8,24.331849998354787,26.70140705157373,56.65876754594358 +2024-10-09 10:00:00,8,14.842792473980863,24.359599756075664,56.99139001510105 +2024-10-09 11:00:00,8,46.78566418073447,24.418880703725765,60.51311006570954 +2024-10-09 12:00:00,8,15.55147269604485,19.20831537699119,65.45652553724867 +2024-10-09 13:00:00,8,21.831421634682037,23.786340064210055,50.350497258246016 +2024-10-09 14:00:00,8,16.456365429783187,28.52608647766158,50.949741179255305 +2024-10-09 15:00:00,8,39.7272981417162,21.659490437040724,59.47726921202993 +2024-10-09 16:00:00,8,30.54954035326438,26.409250259067694,46.50704267040376 +2024-10-09 17:00:00,8,27.636299312129374,27.6429747238756,49.306800433541724 +2024-10-09 18:00:00,8,34.390041899594536,30.451022288560225,57.13396770684417 +2024-10-09 19:00:00,8,39.802794567949334,25.68106148969513,59.283582189718175 +2024-10-09 20:00:00,8,16.963247315467825,22.357723922903908,39.708902369347854 +2024-10-09 21:00:00,8,16.906308792090403,21.22038159338496,45.45112329603509 +2024-10-09 22:00:00,8,26.409839073765784,20.834127418142504,51.49591059416346 +2024-10-09 23:00:00,8,17.65411513915887,22.36016011709811,56.27753156745543 +2024-10-10 00:00:00,8,27.864474218485043,17.632984296444043,44.30516990953837 +2024-10-10 01:00:00,8,26.892004846638795,28.643079228958115,38.129438197917494 +2024-10-10 02:00:00,8,12.440740342207631,21.516688392469487,56.86179188497398 +2024-10-10 03:00:00,8,22.069493329836362,22.0195440341644,47.82447194309793 +2024-10-10 04:00:00,8,29.65161104448165,23.448530302263368,55.36878168093689 +2024-10-10 05:00:00,8,17.518728699167248,23.405179829068057,53.17652652067041 +2024-10-10 06:00:00,8,32.73240875133627,22.299752176045747,40.702940207165824 +2024-10-10 07:00:00,8,24.349653526961482,23.615445923675697,33.93244395549417 +2024-10-10 08:00:00,8,22.885048136247732,22.443914885344117,48.243262382790526 +2024-10-10 09:00:00,8,39.78420665976352,19.384237480999854,53.51528927658173 +2024-10-10 10:00:00,8,22.035885496025884,18.87371527550706,66.16294360574375 +2024-10-10 11:00:00,8,8.745779881339963,20.24893393178037,53.81925062854786 +2024-10-10 12:00:00,8,17.539004221460814,28.933855837052597,59.28621488555437 +2024-10-10 13:00:00,8,17.032624278177785,20.49861624818048,56.99202414571529 +2024-10-10 14:00:00,8,28.268002197392438,23.96509163624266,46.71602827374141 +2024-10-10 15:00:00,8,18.242482331343275,24.256582166859538,57.199432099053126 +2024-10-10 16:00:00,8,33.40867901461485,34.32045750773606,55.293443135445926 +2024-10-10 17:00:00,8,46.96721138994586,28.48222576011638,59.278638632037996 +2024-10-10 18:00:00,8,13.550725521087266,28.618358712334675,73.76868346223989 +2024-10-10 19:00:00,8,28.337117640552403,25.09515312655889,65.83134795917194 +2024-10-10 20:00:00,8,24.74919192507069,28.516164642113864,55.99345476471754 +2024-10-10 21:00:00,8,28.264016525201647,17.069961390513214,60.09235307204901 +2024-10-10 22:00:00,8,24.75061208968088,18.194211862800135,57.45867994295005 +2024-10-10 23:00:00,8,36.70367776458136,25.718735435527236,59.70707257017034 +2024-10-11 00:00:00,8,39.44813086131971,22.407041949576946,53.77691162474524 +2024-10-11 01:00:00,8,36.141601608835074,25.534793780502838,70.63762406372197 +2024-10-11 02:00:00,8,28.445785217651974,28.05141369643805,50.28766247011842 +2024-10-11 03:00:00,8,13.97143853875286,22.679425274781902,61.3183180843666 +2024-10-11 04:00:00,8,19.906224505781868,22.04554514271085,41.36301719118115 +2024-10-11 05:00:00,8,20.22997735598484,20.650891348229923,67.37742816430327 +2024-10-11 06:00:00,8,25.44492452667111,21.23279885410966,45.45958953382763 +2024-10-11 07:00:00,8,33.173893690060844,30.641107461029964,71.64842477950437 +2024-10-11 08:00:00,8,31.523372252986977,25.929596942393406,56.892046905539175 +2024-10-11 09:00:00,8,27.654500410145022,22.02963451254536,65.73641187042412 +2024-10-11 10:00:00,8,37.94055306734155,21.276164443526056,47.36538209960739 +2024-10-11 11:00:00,8,35.2042031541461,25.75514286986658,58.08987160304897 +2024-10-11 12:00:00,8,24.9184779752906,26.160029239061878,54.82075611180904 +2024-10-11 13:00:00,8,20.51824040439977,25.143396335195785,64.71063070801797 +2024-10-11 14:00:00,8,19.713995125473126,28.469533231411535,48.07262613197996 +2024-10-11 15:00:00,8,21.75753107476493,21.33635409011154,54.63234232698381 +2024-10-11 16:00:00,8,42.300674949631656,23.574770849302194,68.70094400973068 +2024-10-11 17:00:00,8,24.835760509778115,26.876840211641827,54.93642542473476 +2024-10-11 18:00:00,8,22.91661084509421,21.10609832089331,53.24075578605118 +2024-10-11 19:00:00,8,28.735641316489943,16.78052239594595,48.20304813223108 +2024-10-11 20:00:00,8,21.444357989233026,22.465506605087814,61.61686647000844 +2024-10-11 21:00:00,8,30.89148284581595,23.267632852380242,51.98513816321215 +2024-10-11 22:00:00,8,32.3790529001829,18.193686324499993,57.900353355925915 +2024-10-11 23:00:00,8,25.114939920177584,22.14807275127053,52.09407528639196 +2024-10-12 00:00:00,8,35.79936394160842,18.77268562908992,67.26879244091423 +2024-10-12 01:00:00,8,15.551366404153528,22.94434899143714,61.96666332643931 +2024-10-12 02:00:00,8,15.098571609386932,23.34631357428036,51.220862729483216 +2024-10-12 03:00:00,8,22.704066584958845,20.809736658293037,44.01242693942707 +2024-10-12 04:00:00,8,29.515472956320316,19.278519643074343,48.28157864171895 +2024-10-12 05:00:00,8,36.27936858867013,25.294390572753663,34.5473795142548 +2024-10-12 06:00:00,8,32.66247949435013,23.637529594402043,54.65588651672534 +2024-10-12 07:00:00,8,30.187452084832742,24.526084689714317,58.502291365239955 +2024-10-12 08:00:00,8,35.70544165179213,20.395011854922057,48.75598020803082 +2024-10-12 09:00:00,8,38.96910574402328,21.537705230053778,46.07394762556449 +2024-10-12 10:00:00,8,39.733677290584495,20.917277538652055,49.945318445423126 +2024-10-12 11:00:00,8,34.16646832933423,23.13280617717328,49.15232417489485 +2024-10-12 12:00:00,8,39.77394913237645,25.831332552643907,56.44760907433471 +2024-10-12 13:00:00,8,36.44845522965882,22.284560097878433,53.96291949344967 +2024-10-12 14:00:00,8,36.289263937134876,28.245746850410185,51.880392314496774 +2024-10-12 15:00:00,8,30.009564842473353,22.486713075614087,61.458655619294966 +2024-10-12 16:00:00,8,43.59478691041868,29.619058745532506,39.298449196141206 +2024-10-12 17:00:00,8,11.5034854972799,22.159196529259564,59.68766187398655 +2024-10-12 18:00:00,8,28.696606640686323,26.332928026990388,73.96136604368733 +2024-10-12 19:00:00,8,31.93258890542848,22.18128512496523,41.79134128018633 +2024-10-12 20:00:00,8,24.811661122711268,26.496585917179814,40.90878761952944 +2024-10-12 21:00:00,8,28.498149457536393,27.433586386528788,53.762744814969096 +2024-10-12 22:00:00,8,21.229298691913883,21.41504815880974,50.42426053726109 +2024-10-12 23:00:00,8,29.05293707438284,21.345949965712727,54.08732772930276 +2024-10-13 00:00:00,8,23.436600189254435,27.38654222369233,50.08272649165565 +2024-10-13 01:00:00,8,17.27423340631099,24.32405188151014,43.2413769955288 +2024-10-13 02:00:00,8,18.23742344955459,14.915317840381901,49.12854387692798 +2024-10-13 03:00:00,8,12.230684043240943,23.859042785269867,57.986410395329855 +2024-10-13 04:00:00,8,29.26088587712649,23.047918925021996,54.678980732986375 +2024-10-13 05:00:00,8,31.995442694887636,24.494718978114836,60.38378687755777 +2024-10-13 06:00:00,8,18.50760343446805,30.486789393147205,46.97427515372789 +2024-10-13 07:00:00,8,23.12579831687768,22.63879366086577,30.68709206133361 +2024-10-13 08:00:00,8,25.92135574569123,12.248762375887562,48.257463146072496 +2024-10-13 09:00:00,8,10.572905864581521,24.209954721684515,62.73148391706722 +2024-10-13 10:00:00,8,28.40966159953938,23.13581816825931,54.45985576121474 +2024-10-13 11:00:00,8,31.721791750216862,23.59569798916337,57.72754629781626 +2024-10-13 12:00:00,8,28.965290365594253,26.521241297714514,65.2311186222592 +2024-10-13 13:00:00,8,32.061682493099894,23.96703835426069,42.950334970336776 +2024-10-13 14:00:00,8,40.22732489380596,27.54568750894213,56.773672512562534 +2024-10-13 15:00:00,8,12.075046158712121,26.417785944155302,66.87231966836434 +2024-10-13 16:00:00,8,39.41863982549967,23.58942508699727,37.58571785706222 +2024-10-13 17:00:00,8,33.759669926985616,23.50728900816198,48.92749831236989 +2024-10-13 18:00:00,8,38.67840321822861,27.231645681211262,52.13465437386708 +2024-10-13 19:00:00,8,29.054667995680216,23.770724498921197,61.41506032179996 +2024-10-13 20:00:00,8,3.804142852867816,25.721724458457597,51.60169827781503 +2024-10-13 21:00:00,8,33.92643569217887,21.08840991601258,56.67863042158226 +2024-10-13 22:00:00,8,21.305977222777322,21.758697504549907,48.43177265224993 +2024-10-13 23:00:00,8,23.932737567749605,26.804633746896794,54.07571418174481 +2024-10-14 00:00:00,8,17.13417507475898,22.388277831746606,44.77614959314652 +2024-10-14 01:00:00,8,33.95821531308019,25.306227013398075,62.70960095082156 +2024-10-14 02:00:00,8,33.66939071660177,24.237299087788795,49.307650014688285 +2024-10-14 03:00:00,8,23.617313840968002,25.119227700410995,56.75326390919217 +2024-10-14 04:00:00,8,9.640699832131867,21.954572836918885,75.54487209320659 +2024-10-14 05:00:00,8,22.380426731663086,24.74952477911639,51.005017437236894 +2024-10-14 06:00:00,8,30.929532864291605,22.9975265829484,53.38118023362532 +2024-10-14 07:00:00,8,16.654428385170135,24.58181777719303,44.82395221856062 +2024-10-14 08:00:00,8,22.863442570023896,21.15406055448736,52.37518568042162 +2024-10-14 09:00:00,8,9.37673196128712,18.98475337521259,50.94281706426584 +2024-10-14 10:00:00,8,26.368175304005213,19.675420271838505,53.96807724369475 +2024-10-14 11:00:00,8,27.861808909248364,24.07480096145889,59.52382445235877 +2024-10-14 12:00:00,8,33.01000373088708,19.11121811680205,63.32989166486324 +2024-10-14 13:00:00,8,25.75460626711295,17.709393735576334,56.47716604596031 +2024-10-14 14:00:00,8,20.107080381596933,21.24285445680572,33.339077455279565 +2024-10-14 15:00:00,8,20.027971677908724,21.149930598433464,60.452210942468895 +2024-10-14 16:00:00,8,23.589164706969378,23.596154205814777,42.88718401146616 +2024-10-14 17:00:00,8,22.00281958146707,27.08285153239673,56.549041504484904 +2024-10-14 18:00:00,8,6.117671177280307,24.20012612927339,48.507424434295665 +2024-10-14 19:00:00,8,26.343722478714945,29.59233011547912,54.471696768589176 +2024-10-14 20:00:00,8,35.28452808935628,22.56113028580485,56.817141320868366 +2024-10-14 21:00:00,8,9.174446663780891,23.66970734486648,64.1650788711329 +2024-10-14 22:00:00,8,21.502368804095525,21.669464529576057,69.86245342740928 +2024-10-14 23:00:00,8,30.35674148487684,21.35789776407167,63.69641050450856 +2024-10-15 00:00:00,8,22.541018577714944,24.636064816665787,62.28497875512315 +2024-10-15 01:00:00,8,20.375393695728235,21.874921187834087,54.79002927999499 +2024-10-15 02:00:00,8,41.58531684382169,24.41363543691237,45.81469191374049 +2024-10-15 03:00:00,8,29.71139538905291,14.181384856591572,47.586570829617166 +2024-10-15 04:00:00,8,33.68539998048176,17.705080521755804,66.49224555454906 +2024-10-15 05:00:00,8,18.684352521323923,20.350740902176003,50.48617736326314 +2024-10-15 06:00:00,8,7.072649309130423,19.888407789970046,39.72654118334129 +2024-10-15 07:00:00,8,5.360823890657326,20.8908072643361,52.957776796633894 +2024-10-15 08:00:00,8,18.279921630738244,24.198888061994488,72.2442099221556 +2024-10-15 09:00:00,8,42.49880303798798,21.283432167537903,41.924201691724626 +2024-10-15 10:00:00,8,27.24196145204837,25.81540592101924,48.831798071068675 +2024-10-15 11:00:00,8,24.51816758791603,21.46401421338185,39.57765565745856 +2024-10-15 12:00:00,8,32.739244098125624,22.665223675536467,59.37198625876332 +2024-10-15 13:00:00,8,15.671591793260642,26.97726186856719,48.04135325919639 +2024-10-15 14:00:00,8,41.980254905851936,28.75177946681792,55.678986856411605 +2024-10-15 15:00:00,8,28.345687225358045,22.779321200548775,40.90188713821298 +2024-10-15 16:00:00,8,12.4315063211611,26.66348275588686,61.48484148729236 +2024-10-15 17:00:00,8,37.72478032467846,22.19859107787115,55.79815594966578 +2024-10-15 18:00:00,8,14.562569186425323,26.041625856614818,55.220985308381934 +2024-10-15 19:00:00,8,15.208189468965067,23.937547435199725,48.00295777649097 +2024-10-15 20:00:00,8,16.141897821257416,21.88998477609055,54.106854864396944 +2024-10-15 21:00:00,8,21.817669711742454,23.20946433217631,56.05936074396923 +2024-10-15 22:00:00,8,22.188039843887115,22.603336249757525,41.99065860344725 +2024-10-15 23:00:00,8,42.20070340694779,21.523399186404966,61.864096990064546 +2024-10-16 00:00:00,8,35.934447551278105,25.345863614632684,58.23143714842843 +2024-10-16 01:00:00,8,11.687483550800804,23.079817512462686,65.98938415097405 +2024-10-16 02:00:00,8,25.020201570943453,26.343917164011046,65.89318230397731 +2024-10-16 03:00:00,8,37.31817090441332,20.93080142038032,41.958306477731064 +2024-10-16 04:00:00,8,15.950599913753704,28.030524415968017,60.7829111280743 +2024-10-16 05:00:00,8,20.66097491690324,16.35825531973785,61.114990932936664 +2024-10-16 06:00:00,8,22.984364362033777,27.106019981811407,46.90698259225467 +2024-10-16 07:00:00,8,33.65297196413324,19.922343773451104,49.73497711484322 +2024-10-16 08:00:00,8,30.496390493035005,20.559948139866616,42.666151170384154 +2024-10-16 09:00:00,8,36.16826367877931,24.804325000462974,69.31781217746162 +2024-10-16 10:00:00,8,29.292545052598545,26.268993690773442,52.01381085486289 +2024-10-16 11:00:00,8,31.2621467808505,21.855546170722242,39.59584861332168 +2024-10-16 12:00:00,8,25.190058445746303,25.20046933653169,52.08298414467948 +2024-10-16 13:00:00,8,30.351592113036176,24.372493551148114,69.76931924890798 +2024-10-16 14:00:00,8,25.036277086319252,14.064217801113017,44.54460829279307 +2024-10-16 15:00:00,8,13.434683193238683,28.20098445573209,44.28033959337203 +2024-10-16 16:00:00,8,23.60472108579101,14.855287575083317,61.80059187007008 +2024-10-16 17:00:00,8,34.051766680327546,21.083743140334093,39.86476000053763 +2024-10-16 18:00:00,8,32.1621729516818,23.541949007089944,35.62462004708624 +2024-10-16 19:00:00,8,11.63905521058362,24.25323513888899,43.63553731541496 +2024-10-16 20:00:00,8,20.159317770386867,24.050921020258336,62.707998578568024 +2024-10-16 21:00:00,8,26.48245573953722,22.57036242166314,56.818079344495565 +2024-10-16 22:00:00,8,30.10494850785835,26.270895379910062,76.28868081877978 +2024-10-16 23:00:00,8,30.297050997885457,20.694957300324873,53.19772224664321 +2024-10-17 00:00:00,8,26.45211211059551,18.814446064033216,54.017082486952965 +2024-10-17 01:00:00,8,34.83487598515418,18.937223419968774,55.53766001224343 +2024-10-17 02:00:00,8,28.96004861244763,17.684379594883417,57.82833038958011 +2024-10-17 03:00:00,8,29.026742944723043,19.967690641330137,73.90415262483342 +2024-10-17 04:00:00,8,27.531491948430805,24.65556921002846,55.81937730534785 +2024-10-17 05:00:00,8,37.06385418406527,21.84640573928395,47.85976773832381 +2024-10-17 06:00:00,8,25.194932559793646,21.460349609121746,35.314617125933836 +2024-10-17 07:00:00,8,35.10210553123256,25.118615573100264,42.216976648929204 +2024-10-17 08:00:00,8,38.45246946257619,26.52542957205066,54.11172469199434 +2024-10-17 09:00:00,8,18.513649661257634,20.916352681441136,57.57531371457564 +2024-10-17 10:00:00,8,25.81437753344941,23.575676500975124,56.635077625610464 +2024-10-17 11:00:00,8,42.45153367355624,22.29339112333534,57.63057350713898 +2024-10-17 12:00:00,8,35.300603036832975,21.53274198340554,49.883354602875414 +2024-10-17 13:00:00,8,31.94792148421375,23.797954523231848,42.13078123393622 +2024-10-17 14:00:00,8,21.359909685536188,24.980239451768448,51.48560338680102 +2024-10-17 15:00:00,8,42.11461235355884,19.93225106498277,52.542636873303195 +2024-10-17 16:00:00,8,41.943877372965055,27.56122506101109,62.350963260644996 +2024-10-17 17:00:00,8,30.30967315582067,19.111136207795155,68.96193153284136 +2024-10-17 18:00:00,8,37.58780586504466,21.243480176362933,63.088710587020614 +2024-10-17 19:00:00,8,20.58880636566211,21.22142640920403,48.37248172124743 +2024-10-17 20:00:00,8,21.39146839085427,28.577147513965457,46.06784864672369 +2024-10-17 21:00:00,8,13.921661053810027,30.307866023554773,46.65067004698338 +2024-10-17 22:00:00,8,35.376745518682185,26.358803298126926,50.55087759146234 +2024-10-17 23:00:00,8,19.05815961950327,23.929126329780523,66.8151657374968 +2024-10-18 00:00:00,8,16.607181721195147,22.107482626225043,53.524941497631865 +2024-10-18 01:00:00,8,31.508512654878146,21.838395382052596,33.46646706759132 +2024-10-18 02:00:00,8,16.994135666653897,22.159883800761566,57.14674015687289 +2024-10-18 03:00:00,8,35.399873488292194,24.088905420064016,35.56712932298497 +2024-10-18 04:00:00,8,44.633105393630814,24.31262270649976,67.47737994936017 +2024-10-18 05:00:00,8,21.855523722646723,25.150449256940952,68.99823224512883 +2024-10-18 06:00:00,8,31.19238546333804,23.92713607858248,48.589456244476914 +2024-10-18 07:00:00,8,32.29549101293669,20.762062847216292,59.83881063110506 +2024-10-18 08:00:00,8,37.12865555952189,21.270042155005182,42.03743586640957 +2024-10-18 09:00:00,8,27.15204981252758,20.875390630070125,64.71753427868985 +2024-10-18 10:00:00,8,31.835732674979063,27.103212537343637,52.3394177694827 +2024-10-18 11:00:00,8,23.42913542771309,21.30922584546379,54.263519394178935 +2024-10-18 12:00:00,8,24.863020805100803,20.22744525033951,53.777378365785616 +2024-10-18 13:00:00,8,50.07162471332707,23.986382086110734,50.65088569949459 +2024-10-18 14:00:00,8,19.279377669410408,23.762257529182037,59.60394783871985 +2024-10-18 15:00:00,8,26.508288974798273,25.145124271105853,44.293397167925185 +2024-10-18 16:00:00,8,25.237714478658017,28.594054650963752,47.46634073234764 +2024-10-18 17:00:00,8,34.49158623117283,26.027103997090652,57.752270836002 +2024-10-18 18:00:00,8,29.78453875594074,22.57341432487555,66.96058727760641 +2024-10-18 19:00:00,8,18.75814059470828,28.22704474264829,55.358406316680174 +2024-10-18 20:00:00,8,20.857869411541856,28.101212495365775,50.97029527502515 +2024-10-18 21:00:00,8,45.55833489486782,27.94369952104854,50.72909315729272 +2024-10-18 22:00:00,8,22.235457699454795,21.62478872397891,57.964574724896025 +2024-10-18 23:00:00,8,25.82082665646164,20.17423325971713,43.34265986134322 +2024-10-19 00:00:00,8,17.369886792258242,21.991224748190994,63.57342615070523 +2024-10-19 01:00:00,8,40.15392350793043,23.674286440739355,49.638793106553315 +2024-10-19 02:00:00,8,23.15657773150139,22.032955631951484,47.983347597992164 +2024-10-19 03:00:00,8,22.266273358095518,22.042819209392437,27.818816852868764 +2024-10-19 04:00:00,8,26.519941323661712,22.81121106790988,69.94617537807926 +2024-10-19 05:00:00,8,24.112390497137365,29.691094994871285,49.40156669982225 +2024-10-19 06:00:00,8,26.96169181903418,23.750075376393728,65.11350929791321 +2024-10-19 07:00:00,8,30.90046776963636,25.55861487281649,57.17351310815889 +2024-10-19 08:00:00,8,41.95544836262782,20.666863713373846,59.534742119101544 +2024-10-19 09:00:00,8,24.716038433224178,23.396352757962312,70.93211224699648 +2024-10-19 10:00:00,8,43.93214380704832,17.390884305267463,59.14324909982809 +2024-10-19 11:00:00,8,26.902332349273358,27.448192927578418,71.28539542219593 +2024-10-19 12:00:00,8,15.18360698206522,26.674668922689005,51.608503972240015 +2024-10-19 13:00:00,8,22.95906571305321,16.66614449073458,43.00729696222271 +2024-10-19 14:00:00,8,30.06361416096266,24.40822898590918,58.97068196779855 +2024-10-19 15:00:00,8,29.44145643421258,26.947154660858246,43.56007610958917 +2024-10-19 16:00:00,8,27.553747499640256,28.244235833382408,42.27859187621051 +2024-10-19 17:00:00,8,34.64993856114373,26.387821195116683,43.072808889051856 +2024-10-19 18:00:00,8,24.21074235729011,25.587631100203563,63.28820060145233 +2024-10-19 19:00:00,8,27.5319339631856,23.340495336281023,50.99861851817717 +2024-10-19 20:00:00,8,24.942518601302297,25.326214047025804,74.03147338599547 +2024-10-19 21:00:00,8,20.175845173832773,25.66280817981869,56.817987222697 +2024-10-19 22:00:00,8,19.086484353769688,21.4348479520875,59.415053908750295 +2024-10-19 23:00:00,8,31.726702286118915,24.118306494916357,39.35447050843929 +2024-10-20 00:00:00,8,14.218768697049487,24.471184177629215,61.21407493709236 +2024-10-20 01:00:00,8,32.19713930695173,22.622449143548515,58.295400753715995 +2024-10-20 02:00:00,8,29.77470967654162,31.053581891931593,44.806726275852 +2024-10-20 03:00:00,8,26.82234857006268,27.007741014573618,44.79480304616726 +2024-10-20 04:00:00,8,36.58431737915428,24.856034863997532,67.3786182699514 +2024-10-20 05:00:00,8,14.725563185721247,22.71408207054924,44.21637859599993 +2024-10-20 06:00:00,8,39.873137359567785,20.593322593966388,57.02756017814064 +2024-10-20 07:00:00,8,36.022581281395375,17.333168431517645,67.82560986539383 +2024-10-20 08:00:00,8,14.625403583883418,20.057045704709168,68.90940797084534 +2024-10-20 09:00:00,8,38.43602364607415,25.41434849579361,53.47406479809605 +2024-10-20 10:00:00,8,34.12069008950526,24.090877846884688,47.63427294875297 +2024-10-20 11:00:00,8,37.69959756521549,24.179764700017056,48.95047627207412 +2024-10-20 12:00:00,8,33.85113587362364,27.828425658561613,71.78027000736547 +2024-10-20 13:00:00,8,35.61469963872357,21.700239579171598,52.34824353082757 +2024-10-20 14:00:00,8,23.293293828767563,17.110709165332555,46.02616145752218 +2024-10-20 15:00:00,8,12.848663374199479,27.5693369159012,58.89003262937332 +2024-10-20 16:00:00,8,27.055258983135325,28.617875627480636,55.82500751578541 +2024-10-20 17:00:00,8,38.770052952346134,21.585325323862392,43.59401844860402 +2024-10-20 18:00:00,8,28.415008289913622,25.710371840369717,53.57024912502535 +2024-10-20 19:00:00,8,19.70289327042356,20.899574996522215,73.57297926611776 +2024-10-20 20:00:00,8,43.64598515414082,22.503639526708696,41.06970042379561 +2024-10-20 21:00:00,8,31.316664305610146,24.247997899276754,41.5842479351625 +2024-10-20 22:00:00,8,30.977197961097833,22.720133777427474,59.06020465382783 +2024-10-20 23:00:00,8,19.05833260148491,24.97498035418488,46.36955611931429 +2024-10-21 00:00:00,8,24.756063266824427,23.819077854347302,47.1915314924625 +2024-10-21 01:00:00,8,21.47336392783081,22.69860688754291,56.56441322010969 +2024-10-21 02:00:00,8,13.40397123560942,25.239497425070656,63.64400618747602 +2024-10-21 03:00:00,8,14.427844022302004,25.526300744711534,54.21094313878697 +2024-10-21 04:00:00,8,34.21569592746189,21.583858914988856,53.801599408469514 +2024-10-21 05:00:00,8,15.416962308391925,25.055661068429306,36.7539409776014 +2024-10-21 06:00:00,8,25.338691399416632,25.265364470966198,43.61347299866695 +2024-10-21 07:00:00,8,27.064460654389336,21.091393728292626,40.41801401257372 +2024-10-21 08:00:00,8,15.833802593279916,17.686513073842306,54.23223877578546 +2024-10-21 09:00:00,8,16.723385954411334,23.739277492894853,70.46339302303377 +2024-10-21 10:00:00,8,36.42274608277566,23.61573005951845,44.47831131032692 +2024-10-21 11:00:00,8,29.515194574604834,23.022678802017243,61.908372764785135 +2024-10-21 12:00:00,8,22.348522597449367,25.425392544960733,58.903071913149354 +2024-10-21 13:00:00,8,28.95628890722984,23.315050856085687,59.51043851723705 +2024-10-21 14:00:00,8,36.87091802672218,21.211974301435383,54.858915003334964 +2024-10-21 15:00:00,8,32.257452697700465,30.3846519603105,41.14333655426704 +2024-10-21 16:00:00,8,14.463941403602709,25.64135219462807,43.08372231711879 +2024-10-21 17:00:00,8,38.21538445257522,24.236487259799205,65.21898621480932 +2024-10-21 18:00:00,8,45.75567574735781,19.03036347119335,49.290406856988945 +2024-10-21 19:00:00,8,25.200893156309323,22.359878983630654,55.956486928844065 +2024-10-21 20:00:00,8,31.51556508333607,25.25099311189025,62.68145455113801 +2024-10-21 21:00:00,8,22.8011154811414,24.692206583330908,67.18756268126889 +2024-10-21 22:00:00,8,21.59651216589655,23.506985724000888,54.717883434080754 +2024-10-21 23:00:00,8,27.13743213204544,16.8245082600923,53.2159235794168 +2024-10-22 00:00:00,8,37.3287889760795,27.291403730646685,54.26249929325072 +2024-10-22 01:00:00,8,25.963782738701205,22.725644110800772,52.50525739470416 +2024-10-22 02:00:00,8,15.040889414517663,30.382709096367684,56.33417184352936 +2024-10-22 03:00:00,8,21.819441327824013,24.13204883158663,64.01952108030085 +2024-10-22 04:00:00,8,8.591357343597412,24.33019300397646,48.134534180720735 +2024-10-22 05:00:00,8,34.68455091013915,21.07907930971988,71.06533449815512 +2024-10-22 06:00:00,8,28.041627076734894,23.838064244639376,53.55221777940329 +2024-10-22 07:00:00,8,41.47446482523312,19.226401713520083,61.64516328020383 +2024-10-22 08:00:00,8,27.914429026324488,17.62805157484101,39.9191558920333 +2024-10-22 09:00:00,8,41.755475505674816,28.12092620447247,67.23928182810769 +2024-10-22 10:00:00,8,35.49417448616275,26.16651305587188,56.570083243353146 +2024-10-22 11:00:00,8,31.234893227926033,28.832377473733928,53.490799623694116 +2024-10-22 12:00:00,8,35.10782092665468,19.52007445063396,67.96492711559748 +2024-10-22 13:00:00,8,24.101537353219914,24.536064342260456,53.57327247028069 +2024-10-22 14:00:00,8,29.722546242303153,28.51133503697083,56.457619489463795 +2024-10-22 15:00:00,8,37.06282617567469,24.4212634127469,55.17002218132666 +2024-10-22 16:00:00,8,35.65627034191387,22.771772316184098,71.75203014296193 +2024-10-22 17:00:00,8,26.38172059322856,22.951379967686744,54.30714898371706 +2024-10-22 18:00:00,8,11.785741222497995,25.43877184042986,49.93714988787623 +2024-10-22 19:00:00,8,20.60044673374372,16.49092417515436,59.37737014484527 +2024-10-22 20:00:00,8,32.72653219016957,19.842834777401166,57.787559295950764 +2024-10-22 21:00:00,8,7.978405210279039,21.0169768011333,70.27348558994096 +2024-10-22 22:00:00,8,10.629675738012395,24.449680632081538,49.760826308385795 +2024-10-22 23:00:00,8,45.10847621289001,28.44624347891748,63.236703758693984 +2024-10-23 00:00:00,8,5.661476698195198,26.57104285516961,62.3534516017714 +2024-10-23 01:00:00,8,23.29357983493061,24.820381980306525,54.89509935841902 +2024-10-23 02:00:00,8,27.15352034092407,22.421540147783745,58.01153815339129 +2024-10-23 03:00:00,8,12.88713160297853,22.639184203684408,57.1957089088911 +2024-10-23 04:00:00,8,20.825008916511393,27.34434053169299,55.476862955381634 +2024-10-23 05:00:00,8,31.775463127517288,19.174898817831014,54.81615546902383 +2024-10-23 06:00:00,8,47.66134511964804,28.684095470899642,53.47509989311186 +2024-10-23 07:00:00,8,35.122583736449315,24.798857708905746,49.30076359936449 +2024-10-23 08:00:00,8,42.84026103014698,25.26834788403109,52.217328635839536 +2024-10-23 09:00:00,8,1.241644974857529,20.1175933141126,71.98351194152687 +2024-10-23 10:00:00,8,33.21198702287371,28.5494230882007,69.37574521854543 +2024-10-23 11:00:00,8,41.59495034240193,18.38781188081859,50.03946306693704 +2024-10-23 12:00:00,8,25.634766190376343,23.28902233564503,56.18514665176781 +2024-10-23 13:00:00,8,29.473537832606574,27.26148571711917,56.106489384536474 +2024-10-23 14:00:00,8,39.7790077454441,26.842883144448255,53.307699523465935 +2024-10-23 15:00:00,8,31.618523720406664,28.7728409159961,48.8817890102919 +2024-10-23 16:00:00,8,37.93416557257337,25.75950614363166,59.38686208509584 +2024-10-23 17:00:00,8,16.14726338762334,25.848870145172764,51.80719687902759 +2024-10-23 18:00:00,8,16.88921639424592,23.957269793393916,45.51628559321307 +2024-10-23 19:00:00,8,39.518037720709344,22.72764195950887,36.52884268043245 +2024-10-23 20:00:00,8,15.399734253146873,22.246167602205084,36.99831167838305 +2024-10-23 21:00:00,8,23.575054175163203,22.23747212945105,50.526860189988184 +2024-10-23 22:00:00,8,27.644011578231137,27.290853547730304,61.582585301293676 +2024-10-23 23:00:00,8,26.048117619016843,23.06531765691993,45.673196963033575 +2024-10-24 00:00:00,8,27.69357495518057,25.35238401383072,57.57635647269849 +2024-10-24 01:00:00,8,11.558638378910619,22.11795815739586,41.660816572102696 +2024-10-24 02:00:00,8,24.529411201283946,22.708085609530283,49.93608091050268 +2024-10-24 03:00:00,8,15.708927379110943,22.925649982142886,50.76166611956326 +2024-10-24 04:00:00,8,12.95321220083084,23.79387183184444,54.284595252479924 +2024-10-24 05:00:00,8,47.83250151075062,21.312356954219283,46.75417223197311 +2024-10-24 06:00:00,8,41.57013709352375,20.18743804851065,40.27209425245845 +2024-10-24 07:00:00,8,22.136357407942192,22.122043184101432,50.473387202372315 +2024-10-24 08:00:00,8,30.522236253980992,19.238067403966365,60.090361430342114 +2024-10-24 09:00:00,8,36.04961830058372,20.577937307305,62.21972244476066 +2024-10-24 10:00:00,8,23.898708827255014,23.284886163857383,53.82241596482094 +2024-10-24 11:00:00,8,16.284086660582858,20.763397806290246,56.45812600788473 +2024-10-24 12:00:00,8,39.55737984165441,23.50096255726165,45.275523730997094 +2024-10-24 13:00:00,8,43.44975600469301,19.611558950994322,39.033841396778385 +2024-10-24 14:00:00,8,23.411523483856623,21.576313170892817,65.73429852372738 +2024-10-24 15:00:00,8,39.40506383049909,28.564296210242244,61.1304568193618 +2024-10-24 16:00:00,8,18.415661324616742,26.602893864732625,52.68197438073264 +2024-10-24 17:00:00,8,28.74928161973873,21.341140484067157,65.24089861232397 +2024-10-24 18:00:00,8,24.124757919397236,22.332794599311555,65.11014720947448 +2024-10-24 19:00:00,8,24.26734626071743,30.747099272013465,62.96381707079347 +2024-10-24 20:00:00,8,24.033692744783266,26.138211139210092,60.92845507063389 +2024-10-24 21:00:00,8,26.917789800450546,25.26745662338412,48.877780106790084 +2024-10-24 22:00:00,8,46.43704378632206,28.051547655365933,53.95130036826611 +2024-10-24 23:00:00,8,25.570660952627012,22.696997880003206,52.75647821258192 +2024-10-25 00:00:00,8,13.783230709390438,23.449831595280656,53.66672185811778 +2024-10-25 01:00:00,8,24.13726200567908,22.274674138602744,41.50277380397647 +2024-10-25 02:00:00,8,26.138734136957332,18.931218542472127,40.805787559602685 +2024-10-25 03:00:00,8,24.310031183189093,18.475495280188365,59.08565495456624 +2024-10-25 04:00:00,8,8.7427968355827,26.324702783752013,61.747004387595695 +2024-10-25 05:00:00,8,33.90195379642664,22.337191778825588,60.24761133267898 +2024-10-25 06:00:00,8,22.905673990284505,19.534864866539507,64.41011148002434 +2024-10-25 07:00:00,8,22.23614611225136,31.543175399614945,60.96596931686731 +2024-10-25 08:00:00,8,34.45402174374749,22.42537998454379,49.29402096561438 +2024-10-25 09:00:00,8,33.993130943388316,24.256043118313627,56.221394377958866 +2024-10-25 10:00:00,8,22.233286927001235,23.612000485810338,61.23759074557318 +2024-10-25 11:00:00,8,45.507039830911495,28.064611569788376,72.62048108655897 +2024-10-25 12:00:00,8,39.30548577367688,29.411710618184426,54.36822757839655 +2024-10-25 13:00:00,8,45.313315539552974,24.591398498925436,65.81586345067815 +2024-10-25 14:00:00,8,22.95323809551152,28.427599764787274,48.65616024568537 +2024-10-25 15:00:00,8,18.04503543859395,30.295084295687158,61.78660504808643 +2024-10-25 16:00:00,8,31.99447895629934,21.599526135806574,46.60123552721133 +2024-10-25 17:00:00,8,17.550509511523675,21.005142418678613,60.25073693295526 +2024-10-25 18:00:00,8,27.214480374674746,22.076061833458127,52.379838233099 +2024-10-25 19:00:00,8,24.548648141939825,21.83908014095443,62.55589324031553 +2024-10-25 20:00:00,8,10.751092330912625,24.87882711139959,52.30303940898915 +2024-10-25 21:00:00,8,20.054342458881493,24.56557020360975,55.66702092952306 +2024-10-25 22:00:00,8,42.48194106915322,27.986182243901688,67.1193097130784 +2024-10-25 23:00:00,8,29.542515931162427,31.835028208278878,46.63290369183522 +2024-10-26 00:00:00,8,9.915550025669534,22.54642303828584,52.317950618645526 +2024-10-26 01:00:00,8,33.86788198718497,32.737777015484,44.488794298152555 +2024-10-26 02:00:00,8,16.40219803569198,25.33640295979862,62.82007751132332 +2024-10-26 03:00:00,8,25.873749930237786,19.895510753282878,37.14947231341537 +2024-10-26 04:00:00,8,28.781608302327523,21.058839567666553,48.15942739684 +2024-10-26 05:00:00,8,26.247155885749784,18.508350958995273,64.18692816544112 +2024-10-26 06:00:00,8,23.8504597169909,19.524581683358267,58.876779314689735 +2024-10-26 07:00:00,8,26.552542645024058,20.635703504630886,50.442598550313804 +2024-10-26 08:00:00,8,24.06358969277776,23.23249676617634,72.51104957535043 +2024-10-26 09:00:00,8,38.84124970600072,22.85422458094172,59.636605105361376 +2024-10-26 10:00:00,8,35.15954753768241,20.190707648458186,43.96663617387175 +2024-10-26 11:00:00,8,47.11255193255779,25.929112195951504,52.147646297185325 +2024-10-26 12:00:00,8,44.409857943622384,23.921618232566157,38.157236080164694 +2024-10-26 13:00:00,8,28.35407120432718,28.23028785605121,52.38192430277941 +2024-10-26 14:00:00,8,25.29214358495642,23.235811550604087,66.4142529294737 +2024-10-26 15:00:00,8,41.74653524662803,24.717180890843892,51.135190269863585 +2024-10-26 16:00:00,8,24.62617945027878,20.763935789853697,56.64566044973057 +2024-10-26 17:00:00,8,11.727854651384552,25.084119385423076,56.144554834720275 +2024-10-26 18:00:00,8,8.85154861709346,26.622621570305757,35.57051908860979 +2024-10-26 19:00:00,8,16.514556288884506,25.788070542171877,53.047403023110235 +2024-10-26 20:00:00,8,24.745701469212047,27.514505037821017,55.318710686482646 +2024-10-26 21:00:00,8,21.012410946652043,22.45290256680739,67.14830625989568 +2024-10-26 22:00:00,8,26.854409182750167,14.218618635378794,43.09673261745809 +2024-10-26 23:00:00,8,27.373540705631296,23.049526392394807,54.15689462387374 +2024-10-27 00:00:00,8,18.492341679016263,20.589528304841977,50.56596916255912 +2024-10-27 01:00:00,8,30.55593670076349,20.665414804324257,43.57309214962193 +2024-10-27 02:00:00,8,31.483136739630552,21.730655178872535,59.00223205395109 +2024-10-27 03:00:00,8,42.28020632619305,21.693633596105876,67.52218891446388 +2024-10-27 04:00:00,8,28.628244141735557,23.42914357404842,41.52082678836411 +2024-10-27 05:00:00,8,22.523177065413304,18.47601838535875,54.09244769435397 +2024-10-27 06:00:00,8,29.7493861400188,21.042984954194303,55.69252104943082 +2024-10-27 07:00:00,8,34.09011695530852,24.235104436500173,68.96489454485167 +2024-10-27 08:00:00,8,34.43168821003643,19.95307778610713,39.26898081729469 +2024-10-27 09:00:00,8,25.34554228535403,20.24401887874653,49.07158653301138 +2024-10-27 10:00:00,8,36.25695369625676,22.026669024834597,53.40833370023279 +2024-10-27 11:00:00,8,30.11134192778063,26.494349605618257,62.55670493019896 +2024-10-27 12:00:00,8,35.05510687622497,24.70927728356542,62.41640328171247 +2024-10-27 13:00:00,8,32.78899675125889,18.23490237453023,54.11762395174316 +2024-10-27 14:00:00,8,28.682248597284616,24.43623703763602,55.67452694585366 +2024-10-27 15:00:00,8,38.274631336874066,25.537974051890743,54.70312787475876 +2024-10-27 16:00:00,8,11.717568169332187,26.4332601947373,49.52054705838127 +2024-10-27 17:00:00,8,22.996075152567037,20.552865692401046,43.85074711561734 +2024-10-27 18:00:00,8,14.51815554203808,22.61516102578657,43.1289820365069 +2024-10-27 19:00:00,8,29.201290894278475,24.67475102116048,69.67162712169569 +2024-10-27 20:00:00,8,25.488953402405464,29.357312608823932,65.74678425234939 +2024-10-27 21:00:00,8,12.917479033966421,18.601408787589637,65.7716047292149 +2024-10-27 22:00:00,8,11.278219292483174,26.37916887368523,55.532827561025925 +2024-10-27 23:00:00,8,23.860726180947097,24.619631028456542,41.6597405226086 +2024-10-28 00:00:00,8,32.54944910849882,24.639644929369705,56.33284355078264 +2024-10-28 01:00:00,8,14.607452643744244,28.47571873012582,58.502384594504456 +2024-10-28 02:00:00,8,24.32841305417488,22.570503532692076,46.36801674769205 +2024-10-28 03:00:00,8,32.13030830037191,31.645289634398246,51.485110983568866 +2024-10-28 04:00:00,8,19.724533004984618,22.26625126331399,43.733981024075014 +2024-10-28 05:00:00,8,23.389530949805504,25.61840796972636,67.21077356098534 +2024-10-28 06:00:00,8,14.260255548755199,29.035938630891955,55.47464438038551 +2024-10-28 07:00:00,8,14.575257479375288,17.98001781971675,54.961538589715815 +2024-10-28 08:00:00,8,32.0183225520836,25.08479504783312,39.19950112486578 +2024-10-28 09:00:00,8,27.163716474483056,24.1489933327697,47.33635537554422 +2024-10-28 10:00:00,8,26.120859048868837,18.187097602172436,54.333104119837145 +2024-10-28 11:00:00,8,12.551768994764377,26.44958281368723,47.05673172443737 +2024-10-28 12:00:00,8,42.119287699953354,22.21271289062948,52.90847597971588 +2024-10-28 13:00:00,8,33.38801746874935,26.12136885006303,42.510651880008865 +2024-10-28 14:00:00,8,29.51744869815455,22.690447926626234,57.478164244137375 +2024-10-28 15:00:00,8,26.419751247549232,18.812466932258264,54.64804824063361 +2024-10-28 16:00:00,8,24.462920800560536,22.94883800091705,65.4000746584839 +2024-10-28 17:00:00,8,43.01634903746108,22.43501808940277,42.891109628548925 +2024-10-28 18:00:00,8,34.831470137206445,25.445698221333465,55.775034460764104 +2024-10-28 19:00:00,8,26.661115612139767,25.772558526872754,56.71685857152895 +2024-10-28 20:00:00,8,25.37772990785298,24.142062810701308,42.412950411650996 +2024-10-28 21:00:00,8,6.256420559548673,26.039867130298873,48.131369919001216 +2024-10-28 22:00:00,8,13.411573092220944,26.3329952181098,42.05026459631716 +2024-10-28 23:00:00,8,0.0,24.690319780428702,48.590264658627135 +2024-10-29 00:00:00,8,24.551113349517372,27.29800757277615,57.51599852977335 +2024-10-29 01:00:00,8,26.769224253268625,26.81536169605435,54.00853979160675 +2024-10-29 02:00:00,8,17.934960386524086,24.967609923959827,48.41782663862676 +2024-10-29 03:00:00,8,20.415843705673733,21.722755850857126,59.55525387944928 +2024-10-29 04:00:00,8,20.28333024133495,22.29046470314049,48.71135882349756 +2024-10-29 05:00:00,8,16.80700596953642,21.833200954166735,47.195304860699096 +2024-10-29 06:00:00,8,32.57762308319472,24.359341130763507,51.40407659273619 +2024-10-29 07:00:00,8,34.678175767969066,22.781811817850585,43.20921808183011 +2024-10-29 08:00:00,8,26.19541577939984,18.88061177463711,61.47498654280687 +2024-10-29 09:00:00,8,26.111144710885167,24.62219920124921,55.33485138286455 +2024-10-29 10:00:00,8,30.675996171109436,21.149056991152516,54.252173589789564 +2024-10-29 11:00:00,8,20.777103703796996,28.599869331973693,54.208415648878926 +2024-10-29 12:00:00,8,18.294777962807146,25.75051499260058,50.64823922362004 +2024-10-29 13:00:00,8,27.39936672324364,22.647575740076032,54.99828558716644 +2024-10-29 14:00:00,8,34.809809510552,29.414340440483805,51.28957321545486 +2024-10-29 15:00:00,8,34.1454807099818,23.06465687889377,62.771141353469986 +2024-10-29 16:00:00,8,32.11425864811289,28.727311129616293,56.78072540420472 +2024-10-29 17:00:00,8,33.01073818953044,23.814801321580845,52.014114627751866 +2024-10-29 18:00:00,8,20.718666763787557,20.059682464967413,53.81779098494709 +2024-10-29 19:00:00,8,15.551273177897324,18.79953326346019,46.10724899394882 +2024-10-29 20:00:00,8,9.621284014057261,26.426129493543662,45.25416072332119 +2024-10-29 21:00:00,8,39.59624559569954,26.206690239602914,62.80999601857102 +2024-10-29 22:00:00,8,26.236262305343438,21.038840107806266,47.301064462313164 +2024-10-29 23:00:00,8,26.751598969599318,23.003764500592062,60.615821769918156 +2024-10-30 00:00:00,8,17.453689299762424,25.736807331977804,37.486328246654004 +2024-10-30 01:00:00,8,28.480067066030227,29.069967965723393,36.72798668497176 +2024-10-30 02:00:00,8,17.032210921063832,21.588108642817744,51.318039050862026 +2024-10-30 03:00:00,8,29.411827496584156,26.31545039872646,57.43248645934963 +2024-10-30 04:00:00,8,4.382672186545076,23.807214608412384,60.72795396113866 +2024-10-30 05:00:00,8,27.46584458366719,19.97768891515828,49.75712156839811 +2024-10-30 06:00:00,8,39.583273225872894,19.220140059676332,37.271542387556245 +2024-10-30 07:00:00,8,24.334238856463738,28.85951039694021,51.200461711010014 +2024-10-30 08:00:00,8,36.17454971507237,26.789622336568343,59.41594483839504 +2024-10-30 09:00:00,8,11.198323486906467,23.01546378890142,41.1721350247536 +2024-10-30 10:00:00,8,26.628420441498413,22.407123775814473,63.54009801092093 +2024-10-30 11:00:00,8,32.704504152175495,28.874374152849356,58.50737394834151 +2024-10-30 12:00:00,8,42.77216495430652,22.238438935907844,52.224063752827 +2024-10-30 13:00:00,8,28.005651714314524,16.837476235497178,52.476218987970285 +2024-10-30 14:00:00,8,36.05135242096081,22.718542510372743,61.666545459139506 +2024-10-30 15:00:00,8,21.545473574471288,25.15892808705165,49.55067296662896 +2024-10-30 16:00:00,8,28.746927870764317,24.81600706911847,62.33346268746155 +2024-10-30 17:00:00,8,9.915489704536515,26.30865542715606,48.54730553604885 +2024-10-30 18:00:00,8,35.37507350002875,19.014429470558053,44.64662842000514 +2024-10-30 19:00:00,8,18.948216905045285,23.340266460377762,50.44233412239292 +2024-10-30 20:00:00,8,15.456062051041204,29.48391285803452,46.64905188088981 +2024-10-30 21:00:00,8,33.6832532115634,21.767599970079775,45.733178852502746 +2024-10-30 22:00:00,8,13.065890507704221,18.047881834089154,52.43429372444733 +2024-10-30 23:00:00,8,36.831094908510885,28.474161105947758,56.83194325330306 +2024-10-31 00:00:00,8,23.33229964564997,22.723282599395898,47.64510196072973 +2024-10-31 01:00:00,8,24.676382265006737,23.433819546419144,54.22660662361857 +2024-10-31 02:00:00,8,39.30138728952892,19.494891982946996,58.335481855090094 +2024-10-31 03:00:00,8,34.59638785038362,26.417787727181526,43.710487990912114 +2024-10-31 04:00:00,8,34.30160109732015,25.515626866679202,59.93124634931514 +2024-10-31 05:00:00,8,20.535011106903845,26.996066759532994,42.85691404854482 +2024-10-31 06:00:00,8,32.179910457287214,21.346637704184786,62.55303535719142 +2024-10-31 07:00:00,8,29.88460038115227,24.61922599382518,63.775908012463574 +2024-10-31 08:00:00,8,22.68399293202716,20.66908142533782,44.847596843010976 +2024-10-31 09:00:00,8,21.722837017689187,24.474814056045204,52.55102524980933 +2024-10-31 10:00:00,8,34.18911779999853,21.9574180724397,56.644919886892126 +2024-10-31 11:00:00,8,31.271796182196105,23.352671395995312,50.83737569304411 +2024-10-31 12:00:00,8,24.16604306654787,23.39480150630697,46.2511798670058 +2024-10-31 13:00:00,8,10.928578556953084,26.647426983978246,63.023877241779815 +2024-10-31 14:00:00,8,28.702523800954626,27.56160986429485,45.34330736853792 +2024-10-31 15:00:00,8,24.86374839020224,23.349075298840205,63.472704816787875 +2024-10-31 16:00:00,8,43.63494158564707,22.87054418666994,60.12553339847693 +2024-10-31 17:00:00,8,38.31301923136374,25.364363627512454,49.32859027796338 +2024-10-31 18:00:00,8,18.86655501947118,23.765233944322752,58.793953129468676 +2024-10-31 19:00:00,8,28.043580085597263,25.3537078588296,59.75984018402079 +2024-10-31 20:00:00,8,30.38192950679886,21.64905852919027,60.098982070848386 +2024-10-31 21:00:00,8,9.809068122758354,19.777338783741357,52.084042307084516 +2024-10-31 22:00:00,8,30.087992781717055,27.046724083856883,42.07359609480842 +2024-10-31 23:00:00,8,24.76537577361519,22.49941403676923,63.88104218412445 +2024-11-01 00:00:00,8,17.829672440471924,23.110424431556947,51.410604305231246 +2024-11-01 01:00:00,8,17.10905604836849,27.69352594532495,62.03984009196377 +2024-11-01 02:00:00,8,17.40247125924689,21.529307145337853,43.777030789053455 +2024-11-01 03:00:00,8,24.17831634777507,24.09046960804436,72.70448423505857 +2024-11-01 04:00:00,8,22.883944426902644,22.914171255360042,52.71898938633966 +2024-11-01 05:00:00,8,21.00505835990688,24.372606228263,63.86237773548128 +2024-11-01 06:00:00,8,18.21302892180804,25.11587502059544,47.96835320208959 +2024-11-01 07:00:00,8,40.760485555338505,17.972353001795238,54.559903264188335 +2024-11-01 08:00:00,8,24.699147641934385,27.248360368184265,61.752924884072954 +2024-11-01 09:00:00,8,26.398263042415653,28.005120694558624,61.24622468444251 +2024-11-01 10:00:00,8,12.671359752734443,21.692767538978,53.92379545685294 +2024-11-01 11:00:00,8,37.960114655893776,18.533513377247598,63.59583852816833 +2024-11-01 12:00:00,8,13.260627475140785,21.87040799127639,57.504729739408546 +2024-11-01 13:00:00,8,29.44907160235091,25.452733083810806,53.843326653188335 +2024-11-01 14:00:00,8,33.18945788844857,24.749021800967494,46.731207858877916 +2024-11-01 15:00:00,8,43.835476261701004,29.651733228914246,66.32152385359896 +2024-11-01 16:00:00,8,13.260221476692013,21.395172216878734,59.7745387260102 +2024-11-01 17:00:00,8,21.473545201824965,21.55371445928972,46.85858765723533 +2024-11-01 18:00:00,8,20.39909213518682,25.608291373121094,44.179502238387116 +2024-11-01 19:00:00,8,21.043234385842634,25.969289833460266,68.84157810782865 +2024-11-01 20:00:00,8,34.74666354363342,27.474771549221625,65.06729745375168 +2024-11-01 21:00:00,8,22.197370248625383,21.817441195058954,34.39277215875179 +2024-11-01 22:00:00,8,17.964803401259005,22.27458350152106,40.30646331684495 +2024-11-01 23:00:00,8,21.45190218468464,26.074511513284524,35.17784450846435 +2024-11-02 00:00:00,8,34.03885824183031,21.52296312969362,63.64992969823223 +2024-11-02 01:00:00,8,45.16905322958813,18.307778797460102,51.76720506479587 +2024-11-02 02:00:00,8,7.125982043385253,17.95217443526172,47.70353308756779 +2024-11-02 03:00:00,8,20.488898036856742,29.576841655929158,52.86415883501211 +2024-11-02 04:00:00,8,14.13851936611901,26.90820394669992,39.85193501552706 +2024-08-04 05:00:00,9,7.069602972394993,24.179833428082922,53.874866733759646 +2024-08-04 06:00:00,9,19.941095845096932,22.226899663999653,59.2711976729793 +2024-08-04 07:00:00,9,33.67895361799609,28.233106127544467,55.111383567034764 +2024-08-04 08:00:00,9,37.966924783265966,23.339884736516332,53.37273141433131 +2024-08-04 09:00:00,9,36.459301417529815,23.35653395394421,61.65815744826957 +2024-08-04 10:00:00,9,25.454236638414564,17.192910236371866,57.45700245022851 +2024-08-04 11:00:00,9,26.173951778348336,20.30500136089567,52.50250993087381 +2024-08-04 12:00:00,9,17.260468309516256,25.89054613978477,57.47365560819501 +2024-08-04 13:00:00,9,26.185105714783138,27.13863441315275,65.99981981057422 +2024-08-04 14:00:00,9,53.10023197482897,27.424272078324574,47.31211760500551 +2024-08-04 15:00:00,9,50.909370847338636,25.561455043705593,49.702001835110636 +2024-08-04 16:00:00,9,18.965456641586332,23.07262085944864,61.074533842320285 +2024-08-04 17:00:00,9,32.424909841071475,21.04984786243623,56.61742474373404 +2024-08-04 18:00:00,9,30.27438475956859,23.129962214668797,42.7608570192947 +2024-08-04 19:00:00,9,24.537625068500883,21.30096504854872,59.37475852574885 +2024-08-04 20:00:00,9,39.237190364197076,19.045287838082242,49.94539945750017 +2024-08-04 21:00:00,9,25.41915465154079,21.55666642288859,46.945010409775584 +2024-08-04 22:00:00,9,15.815544208776503,21.11058458600721,62.22008010922672 +2024-08-04 23:00:00,9,14.9867032575031,25.793697747333027,49.60808495261662 +2024-08-05 00:00:00,9,39.09767007415391,25.643425281015887,56.199073779703994 +2024-08-05 01:00:00,9,28.577212043401495,22.191280453301324,40.11376697333846 +2024-08-05 02:00:00,9,22.485552000557917,26.35809384202058,49.09809373537 +2024-08-05 03:00:00,9,20.059545939731134,19.238170666050763,49.18072069364716 +2024-08-05 04:00:00,9,27.540252743285915,23.87909354155156,53.33484382554172 +2024-08-05 05:00:00,9,12.125810088097769,23.621738087854066,52.399058965105 +2024-08-05 06:00:00,9,36.18269504620943,21.835283533797632,61.66754497355866 +2024-08-05 07:00:00,9,5.8078887699226485,28.680734430335725,56.120523483805655 +2024-08-05 08:00:00,9,25.198374483908076,26.366761481834878,57.64139972918006 +2024-08-05 09:00:00,9,21.473132196450468,22.712611818797974,60.02946005175792 +2024-08-05 10:00:00,9,21.685398334295712,26.723822849381364,53.01890171019839 +2024-08-05 11:00:00,9,20.98857050638139,24.789590614438758,30.780993901681693 +2024-08-05 12:00:00,9,35.593956140094235,25.991073080254317,49.203985612581846 +2024-08-05 13:00:00,9,25.641202747733647,24.174419188487086,28.69755410573853 +2024-08-05 14:00:00,9,25.270625018087948,23.629476575456437,49.06923520495642 +2024-08-05 15:00:00,9,20.491007857355868,28.12200202229781,65.21400895414735 +2024-08-05 16:00:00,9,35.67405411545852,25.458943012075455,44.73725190602607 +2024-08-05 17:00:00,9,15.4554360073516,27.806032417304614,43.70853554115295 +2024-08-05 18:00:00,9,11.17261264121669,23.464326131835012,61.01068900627237 +2024-08-05 19:00:00,9,18.20431058191142,19.05264176637736,54.19110548044163 +2024-08-05 20:00:00,9,29.574720412930898,23.881651132380746,47.58527068483524 +2024-08-05 21:00:00,9,38.30503087362124,24.106646913933886,50.37977407562271 +2024-08-05 22:00:00,9,21.513098185979615,24.682140275777535,47.45836491722346 +2024-08-05 23:00:00,9,12.432770096896332,22.27799802531465,51.474871812670415 +2024-08-06 00:00:00,9,13.253366517451285,28.608414933651744,48.339734128533806 +2024-08-06 01:00:00,9,35.159112045818105,20.050995125850015,44.467928524455345 +2024-08-06 02:00:00,9,16.205999051433604,27.561746012030547,52.45244532733299 +2024-08-06 03:00:00,9,21.919226891666657,17.99165437307286,63.24973797839696 +2024-08-06 04:00:00,9,26.165922957156774,23.445686927301033,51.08061908295926 +2024-08-06 05:00:00,9,29.13517674273381,23.973746468986267,51.82817951323294 +2024-08-06 06:00:00,9,24.718450750414195,24.40136367145565,58.80717027210547 +2024-08-06 07:00:00,9,31.643374629154163,31.799142101738685,50.79903052254031 +2024-08-06 08:00:00,9,52.34734900352058,21.188071427888524,56.69837753468316 +2024-08-06 09:00:00,9,30.153934022971907,25.313780149718703,39.554951628882634 +2024-08-06 10:00:00,9,39.97108329047754,18.934956792384213,51.12886550278725 +2024-08-06 11:00:00,9,17.804282572487047,24.459946995991466,42.852304707105446 +2024-08-06 12:00:00,9,14.057989692207537,28.513329259677327,56.04970132536137 +2024-08-06 13:00:00,9,22.726423714744648,18.374060167634923,51.377336482339686 +2024-08-06 14:00:00,9,29.5227848308075,26.925799582472887,44.99499859013338 +2024-08-06 15:00:00,9,35.2218230231253,33.66835544773517,45.05946955319686 +2024-08-06 16:00:00,9,25.854189570442593,31.115830222922327,70.91192618107377 +2024-08-06 17:00:00,9,9.69504771518848,21.965212984478615,34.72176936149508 +2024-08-06 18:00:00,9,30.393154114641113,22.970336856326533,53.66479931126013 +2024-08-06 19:00:00,9,28.923163626038754,20.988212322554226,55.29184994586562 +2024-08-06 20:00:00,9,21.37218957323199,25.28447751463138,62.344434371973584 +2024-08-06 21:00:00,9,25.409687426051633,25.10172433799601,74.32077872913254 +2024-08-06 22:00:00,9,10.994773196000013,21.006217846977282,41.86004078487305 +2024-08-06 23:00:00,9,5.0846197653548515,24.814636893663984,53.23815830262151 +2024-08-07 00:00:00,9,14.968786184389144,31.700397612675538,42.405680810016236 +2024-08-07 01:00:00,9,30.07863812355864,28.257717938223806,61.44446043736055 +2024-08-07 02:00:00,9,39.89180241269632,25.962018973667796,53.5828294310264 +2024-08-07 03:00:00,9,31.669113788454617,25.288740061160578,42.54438925148466 +2024-08-07 04:00:00,9,21.110991931842978,24.617832920176994,55.85591305107136 +2024-08-07 05:00:00,9,28.358717967353435,21.868253692580602,71.7837959092618 +2024-08-07 06:00:00,9,19.008745798707913,20.3617616263883,51.84546533648128 +2024-08-07 07:00:00,9,17.08491717772492,16.89544922534831,49.94547996983444 +2024-08-07 08:00:00,9,22.98904493129307,27.66174654369718,53.28729368772559 +2024-08-07 09:00:00,9,35.047243250031876,24.034682293539785,65.49428079369756 +2024-08-07 10:00:00,9,14.181993725273232,21.10167107586111,48.05003975187992 +2024-08-07 11:00:00,9,28.89361092870246,24.02875118712627,67.00466581150894 +2024-08-07 12:00:00,9,35.132894042113534,17.784848139956548,57.09660765253728 +2024-08-07 13:00:00,9,27.294726578316915,30.113598416386385,47.27705551712663 +2024-08-07 14:00:00,9,21.369208085276274,22.783756522682577,50.631598750922 +2024-08-07 15:00:00,9,22.96224439311257,20.472606975697186,61.274367154572616 +2024-08-07 16:00:00,9,27.698078686194442,31.3085064072253,49.53575488583059 +2024-08-07 17:00:00,9,37.56989937388559,27.288531490518913,51.04412375859046 +2024-08-07 18:00:00,9,23.021116527154767,23.42398832909891,43.068262726618514 +2024-08-07 19:00:00,9,14.231221357915246,25.594387378212943,51.9101662977689 +2024-08-07 20:00:00,9,9.038550939935625,22.638161264407255,61.18819834939228 +2024-08-07 21:00:00,9,28.138519495987275,19.241757927006848,64.98723935047695 +2024-08-07 22:00:00,9,17.51322215199113,28.67210304782703,70.72147105738578 +2024-08-07 23:00:00,9,24.51062276261161,21.429824054034036,52.95643251537735 +2024-08-08 00:00:00,9,22.72890384289599,24.24941909935478,46.23387844263993 +2024-08-08 01:00:00,9,22.758028828221295,23.720984417061924,54.92845589476907 +2024-08-08 02:00:00,9,13.596761578842926,25.293186049326028,60.421364708452174 +2024-08-08 03:00:00,9,39.334882108223,19.062842176136776,56.24908181830788 +2024-08-08 04:00:00,9,31.90098352144432,26.97158328528472,63.170073744466 +2024-08-08 05:00:00,9,15.878415073453892,20.1896946938053,56.26765738713607 +2024-08-08 06:00:00,9,32.88511633924092,26.727615646285372,49.710321271919405 +2024-08-08 07:00:00,9,18.27278665670199,20.069847024019957,60.03527789587202 +2024-08-08 08:00:00,9,38.80316166914284,22.788277951171146,50.10809474559061 +2024-08-08 09:00:00,9,17.72086298120556,25.959527114684604,55.0644263251903 +2024-08-08 10:00:00,9,26.897602239869794,26.22426180941912,58.88645702852926 +2024-08-08 11:00:00,9,22.079570261169824,23.887534197580496,53.25880402193019 +2024-08-08 12:00:00,9,38.91843405194555,21.97147011220469,64.90256220974669 +2024-08-08 13:00:00,9,21.622169233300088,29.3635853260032,60.28782309516998 +2024-08-08 14:00:00,9,29.66406230450596,32.30127159099165,59.99485938319344 +2024-08-08 15:00:00,9,38.34473419361693,31.601649339519728,52.608808314593325 +2024-08-08 16:00:00,9,12.640857496595435,25.506915202769406,59.5819071394788 +2024-08-08 17:00:00,9,26.574553469830178,25.07218685216365,58.55638517097717 +2024-08-08 18:00:00,9,41.80654377678505,28.345552073584162,40.805226844736424 +2024-08-08 19:00:00,9,45.223394620362484,22.91003587214018,65.9048891046196 +2024-08-08 20:00:00,9,28.158998661509685,26.66556231930471,55.07588528480568 +2024-08-08 21:00:00,9,21.612887850868354,22.10537846915047,40.167177977560385 +2024-08-08 22:00:00,9,22.993758232129853,13.974493401896584,56.51388053726652 +2024-08-08 23:00:00,9,11.913611670704825,19.33710041324109,54.07526255969262 +2024-08-09 00:00:00,9,19.70013712899773,25.67509759388565,47.81881845078895 +2024-08-09 01:00:00,9,21.873677613728013,23.595613518897256,50.73294731880488 +2024-08-09 02:00:00,9,14.212436885592044,22.70400107372938,35.731269086366545 +2024-08-09 03:00:00,9,18.830400392847636,18.832604989534275,41.05148323649683 +2024-08-09 04:00:00,9,5.703568992182234,27.486675753225157,41.3309237588427 +2024-08-09 05:00:00,9,34.38876065446011,22.071402456146895,48.614180550171646 +2024-08-09 06:00:00,9,16.508595219366974,18.46530431838024,63.26977729053599 +2024-08-09 07:00:00,9,26.397391772047662,16.23005403831,44.6035976268992 +2024-08-09 08:00:00,9,25.706696554483056,21.977700594261492,52.40359616839997 +2024-08-09 09:00:00,9,22.254321391286084,23.86554322798671,62.65209511365187 +2024-08-09 10:00:00,9,37.297044475873754,22.06982997566691,49.187418068676806 +2024-08-09 11:00:00,9,32.446977422441584,23.692725460728465,48.23224482939239 +2024-08-09 12:00:00,9,32.822549240711766,30.938391068813104,54.23305068689839 +2024-08-09 13:00:00,9,15.281078639635535,29.03702333577599,50.96026474796376 +2024-08-09 14:00:00,9,25.493181604235208,33.21627719461896,61.9380633553671 +2024-08-09 15:00:00,9,35.30296944676594,23.295880641405482,36.53859132058514 +2024-08-09 16:00:00,9,41.967661769288114,25.757930114413657,48.33727601636216 +2024-08-09 17:00:00,9,25.622433629318554,33.35187068061764,52.566328023209245 +2024-08-09 18:00:00,9,32.456543824670014,21.509170099556005,41.74921852125004 +2024-08-09 19:00:00,9,21.534446349371795,27.244471232470907,57.35427835034372 +2024-08-09 20:00:00,9,23.206874930566247,27.579147336193465,54.32837084659858 +2024-08-09 21:00:00,9,44.39745237559753,19.46366602874188,59.94018202265122 +2024-08-09 22:00:00,9,28.405673585582576,24.386996930283768,62.87620164602066 +2024-08-09 23:00:00,9,26.533560164585154,24.716866187281727,70.06015244537768 +2024-08-10 00:00:00,9,32.88004395507427,26.836592320402072,54.772648236248564 +2024-08-10 01:00:00,9,17.413507334602315,18.971740463102847,61.24582704395449 +2024-08-10 02:00:00,9,21.805972335396945,27.63076960311667,54.148834671108276 +2024-08-10 03:00:00,9,18.309649332996393,24.655441645752074,42.34101324905242 +2024-08-10 04:00:00,9,26.18671950611214,22.858550767721393,54.39095534840761 +2024-08-10 05:00:00,9,26.9040874827852,24.41616789087986,61.00433937076768 +2024-08-10 06:00:00,9,19.827251634720263,22.92896702352336,45.09457010346725 +2024-08-10 07:00:00,9,28.697846974230334,23.408455879979375,53.17928993860819 +2024-08-10 08:00:00,9,40.12563553310567,27.795207848547793,64.12528938103482 +2024-08-10 09:00:00,9,17.95968634417116,19.555180337673853,36.347281973118825 +2024-08-10 10:00:00,9,29.958438688054034,26.74830456827058,46.44068320380593 +2024-08-10 11:00:00,9,21.927705936216746,23.52866285026654,41.18479051771401 +2024-08-10 12:00:00,9,8.020230667751491,20.148644822681582,46.70744110257062 +2024-08-10 13:00:00,9,25.00979514775461,26.157810688945464,62.702404087507325 +2024-08-10 14:00:00,9,33.66682124477481,21.059297390404815,52.171465113235286 +2024-08-10 15:00:00,9,24.567769655973073,29.52143713992386,48.49578372792369 +2024-08-10 16:00:00,9,21.176524848042583,24.262702457027046,50.28507250399696 +2024-08-10 17:00:00,9,41.389926892191724,21.061552470888618,58.93896018585647 +2024-08-10 18:00:00,9,19.326916109109586,26.042430973944967,58.00519863815523 +2024-08-10 19:00:00,9,36.01262184328974,26.538450315107912,51.35422282653487 +2024-08-10 20:00:00,9,23.8875045547535,20.83624234012502,66.34441131939275 +2024-08-10 21:00:00,9,27.629633950597974,21.635982423242712,53.358296527251134 +2024-08-10 22:00:00,9,28.23804708949332,22.779099832546407,59.864623419682076 +2024-08-10 23:00:00,9,27.596636834431905,19.075569092759146,56.14827057249332 +2024-08-11 00:00:00,9,32.469723594521014,24.282227416451633,40.5032216274533 +2024-08-11 01:00:00,9,22.68518171942595,22.07589493488436,51.78624894399356 +2024-08-11 02:00:00,9,32.626572492821694,19.568864782235238,50.91514128961708 +2024-08-11 03:00:00,9,27.0044666310742,26.190078131619277,58.97420415362913 +2024-08-11 04:00:00,9,24.44682200050635,28.898358475681633,66.68246830963696 +2024-08-11 05:00:00,9,32.9969348719808,30.251561557520155,56.71502098725802 +2024-08-11 06:00:00,9,28.351521039531605,22.61753936302159,48.24703008313725 +2024-08-11 07:00:00,9,23.253398819837606,21.88133319108188,48.30643897269181 +2024-08-11 08:00:00,9,17.509198075040498,29.11117562350889,58.39439056301444 +2024-08-11 09:00:00,9,6.336628604538774,26.908823257629415,59.587905651916905 +2024-08-11 10:00:00,9,31.459953360281663,24.031271479839386,63.3692281659709 +2024-08-11 11:00:00,9,37.22476636745141,21.435976498952623,71.58192336221555 +2024-08-11 12:00:00,9,21.15042666198757,26.94515956346313,54.80447898252295 +2024-08-11 13:00:00,9,33.39259110561068,24.111265315387122,66.01789340153198 +2024-08-11 14:00:00,9,8.171501301421145,31.649215001366052,36.850872619282605 +2024-08-11 15:00:00,9,12.402641886177463,23.213903157054737,42.426735388093725 +2024-08-11 16:00:00,9,20.344780413517505,21.131371945791003,36.33783169064271 +2024-08-11 17:00:00,9,15.601971472367909,23.2424302643122,58.491909159081345 +2024-08-11 18:00:00,9,24.27503564402709,24.33955892735306,49.79369147395093 +2024-08-11 19:00:00,9,27.07721320378914,17.702530165474073,59.893352309611394 +2024-08-11 20:00:00,9,22.842013132556065,24.46468823372291,49.50089587309039 +2024-08-11 21:00:00,9,25.34000458887587,23.65426135638705,48.2053152708518 +2024-08-11 22:00:00,9,26.8227184032675,19.724455093736612,58.60162701992979 +2024-08-11 23:00:00,9,35.65096539155245,17.429045580161556,64.5373462187096 +2024-08-12 00:00:00,9,23.403504011060352,29.231456624636873,38.365132566017785 +2024-08-12 01:00:00,9,31.356438139858746,24.597646573314538,45.19078899951034 +2024-08-12 02:00:00,9,28.27036293080858,21.55528025384414,51.426147295614186 +2024-08-12 03:00:00,9,28.112829266994613,16.005114141945405,46.78292567335078 +2024-08-12 04:00:00,9,28.613712547744832,22.64076077230134,57.22302750661416 +2024-08-12 05:00:00,9,26.671475826181467,26.728755835601735,44.72685399794088 +2024-08-12 06:00:00,9,20.149288827256015,26.73120216746664,61.87757786779971 +2024-08-12 07:00:00,9,11.617038064380361,29.485025518261036,57.669931448567056 +2024-08-12 08:00:00,9,25.19402895770526,24.866321240099477,50.41404995273478 +2024-08-12 09:00:00,9,29.900079295862998,19.688087914144536,61.57334862518913 +2024-08-12 10:00:00,9,26.823303795100735,23.034470091987956,51.176618036930506 +2024-08-12 11:00:00,9,19.683108395244204,19.3143974518373,53.08549379656062 +2024-08-12 12:00:00,9,22.76228710132131,27.251306484412517,58.24818196660726 +2024-08-12 13:00:00,9,44.09603823260726,21.134289590710566,53.58154944534814 +2024-08-12 14:00:00,9,29.12227667268509,27.871255797585594,52.08697990986618 +2024-08-12 15:00:00,9,19.888551260853117,29.911354869386923,51.47502417889282 +2024-08-12 16:00:00,9,33.929788098070745,24.74734832947131,65.6298198406196 +2024-08-12 17:00:00,9,17.785609554104816,22.583515682073568,50.48865739178939 +2024-08-12 18:00:00,9,16.985187504434617,24.75343014505821,70.00641741080085 +2024-08-12 19:00:00,9,24.43327874696551,24.049851045110916,64.93455600524436 +2024-08-12 20:00:00,9,30.996572915876676,21.22286424326703,46.60686653289561 +2024-08-12 21:00:00,9,27.178559571232235,21.582997376011495,43.82285188174446 +2024-08-12 22:00:00,9,21.75440714091696,16.829704404469794,54.50117667717448 +2024-08-12 23:00:00,9,11.698258317660336,15.262058437363823,55.73122956879126 +2024-08-13 00:00:00,9,30.01785854506174,19.399239944913866,55.689451783107266 +2024-08-13 01:00:00,9,19.244526978927592,16.82676788902651,39.93839960592312 +2024-08-13 02:00:00,9,27.613138018019704,19.57574774121176,38.636991408632746 +2024-08-13 03:00:00,9,54.654802798917125,26.276873801856013,66.43734588210589 +2024-08-13 04:00:00,9,23.143179727276756,25.14181044900365,51.56226776376714 +2024-08-13 05:00:00,9,23.88489890173377,21.299829143379885,66.8035806067553 +2024-08-13 06:00:00,9,14.185822282198759,26.977371289959976,54.26890871454507 +2024-08-13 07:00:00,9,26.017787684002723,21.961960608616803,44.529334114699466 +2024-08-13 08:00:00,9,24.029283366476907,26.410776203613654,52.65025128729945 +2024-08-13 09:00:00,9,28.972650931947374,21.400294958351715,72.11556479619257 +2024-08-13 10:00:00,9,29.17703647225702,25.030709061361968,48.10430891378307 +2024-08-13 11:00:00,9,22.778706747801916,22.465327253328375,58.439330987633205 +2024-08-13 12:00:00,9,33.4316819269738,26.589403973895394,70.30376258704896 +2024-08-13 13:00:00,9,18.187856972802578,25.499946062563883,54.02813017509626 +2024-08-13 14:00:00,9,28.356983933114286,23.607706151427653,56.68602442586754 +2024-08-13 15:00:00,9,38.11156447835522,21.14484235897043,37.937958104186045 +2024-08-13 16:00:00,9,11.871249436421174,24.138006080833893,51.31010123906088 +2024-08-13 17:00:00,9,30.17636495179127,25.645804859700032,52.311695257328275 +2024-08-13 18:00:00,9,23.82118087770816,26.36979653734985,44.28685885549901 +2024-08-13 19:00:00,9,39.638759487307915,26.08976209663644,61.375803164765124 +2024-08-13 20:00:00,9,25.238086134943646,21.77656492038147,54.40169134551916 +2024-08-13 21:00:00,9,7.726666323503192,12.343580910044262,38.18649244117651 +2024-08-13 22:00:00,9,34.13471741583293,26.20901177181637,53.407582301925494 +2024-08-13 23:00:00,9,42.29259915635547,30.47029208145399,57.4160478671448 +2024-08-14 00:00:00,9,39.82934675267768,19.155957805521627,41.188634553614804 +2024-08-14 01:00:00,9,28.391768086550233,25.003059611770283,53.68884658234343 +2024-08-14 02:00:00,9,22.363624109516458,19.520923781812897,50.86593030130456 +2024-08-14 03:00:00,9,38.891054859696744,17.552854671422708,53.205879430215845 +2024-08-14 04:00:00,9,18.139859008653602,23.8634596838895,54.916498669878365 +2024-08-14 05:00:00,9,35.25975805842788,30.116642869243343,51.04023144942989 +2024-08-14 06:00:00,9,21.346021285490018,29.21581599742551,70.36606758269696 +2024-08-14 07:00:00,9,24.641903220308976,29.83924535042394,45.427841373094566 +2024-08-14 08:00:00,9,30.626715944780603,27.007907281060515,67.95183944017819 +2024-08-14 09:00:00,9,23.95831804180229,23.685127283127436,53.40684963961328 +2024-08-14 10:00:00,9,26.563126578829454,26.046954069030328,55.17035237258841 +2024-08-14 11:00:00,9,33.71015695280194,24.862142503834512,54.3573043457258 +2024-08-14 12:00:00,9,34.494564537232485,28.692126399254008,44.17980719524473 +2024-08-14 13:00:00,9,33.334846939385244,21.450054767568474,44.64221244881428 +2024-08-14 14:00:00,9,32.6408017622055,21.63889099490105,48.770076719154346 +2024-08-14 15:00:00,9,25.822362690924436,24.75597543190104,63.84473892230085 +2024-08-14 16:00:00,9,20.80426390906896,23.087454993603306,36.300222550388725 +2024-08-14 17:00:00,9,28.861274124207103,22.531875350464897,61.896963709055285 +2024-08-14 18:00:00,9,29.78088292579408,28.92567942974215,47.5249754447845 +2024-08-14 19:00:00,9,31.887750744903038,18.06844223224258,46.61658116874071 +2024-08-14 20:00:00,9,15.885843707485876,27.154740897811443,44.49680999250901 +2024-08-14 21:00:00,9,21.34576907231438,22.640795641085877,48.168497332525845 +2024-08-14 22:00:00,9,39.58316917078207,21.544245516051266,62.14639305447904 +2024-08-14 23:00:00,9,17.324755012753762,23.984561634139265,78.34567648288406 +2024-08-15 00:00:00,9,31.371828151589554,29.309089846991057,46.77211743066452 +2024-08-15 01:00:00,9,43.61052372493454,13.964263620547976,36.38904629594974 +2024-08-15 02:00:00,9,23.095833795228298,21.845141950723534,50.527920429313035 +2024-08-15 03:00:00,9,18.48083277609705,25.713586474418022,61.25330168356861 +2024-08-15 04:00:00,9,29.97945717482365,26.033953668315533,54.40856567968955 +2024-08-15 05:00:00,9,24.505637019367278,24.00927137741485,52.01656533048516 +2024-08-15 06:00:00,9,23.896933134029684,19.507991501371606,52.19627914550179 +2024-08-15 07:00:00,9,27.148479257621783,21.040258569150286,60.585284615278724 +2024-08-15 08:00:00,9,28.94166054033035,23.432145589071755,56.74741124652663 +2024-08-15 09:00:00,9,25.647770173729068,19.28906977649701,41.514615484978535 +2024-08-15 10:00:00,9,17.225677565403718,23.230811908730377,46.02104587897313 +2024-08-15 11:00:00,9,38.45999198579153,21.56408729526862,65.24003909060889 +2024-08-15 12:00:00,9,17.076807973327156,25.155910121482364,63.508839269020854 +2024-08-15 13:00:00,9,22.427921120996228,29.034189363095077,66.00671663735659 +2024-08-15 14:00:00,9,19.33677143867322,26.18906709480403,58.670442091792765 +2024-08-15 15:00:00,9,39.661411373496925,25.160733333252548,55.3116924583203 +2024-08-15 16:00:00,9,32.552735976480285,19.60036252802843,47.3252137768771 +2024-08-15 17:00:00,9,24.036094476981276,20.00572376228383,63.044860041644284 +2024-08-15 18:00:00,9,26.436158099425516,24.43048583031321,46.929104980139826 +2024-08-15 19:00:00,9,35.77801873211844,22.445534189307363,72.63902714584788 +2024-08-15 20:00:00,9,23.511807895938457,22.82095378341883,61.2339938306575 +2024-08-15 21:00:00,9,22.713746446857396,21.333953807118395,60.844838536030856 +2024-08-15 22:00:00,9,30.98457802488875,22.83302862217639,58.902176866637966 +2024-08-15 23:00:00,9,30.439987653436294,23.89713108765097,55.01374867405704 +2024-08-16 00:00:00,9,22.138909950234897,21.704295840569273,49.47865330193804 +2024-08-16 01:00:00,9,23.63294199086827,22.970666508858375,42.234510251915225 +2024-08-16 02:00:00,9,38.405240315449475,22.70544468001743,25.050121242985885 +2024-08-16 03:00:00,9,28.20317972983647,25.607315299191868,51.53351617012111 +2024-08-16 04:00:00,9,28.215277115992187,25.50096568256314,49.48965722180624 +2024-08-16 05:00:00,9,24.307675639683655,22.808052979942087,58.428433638211565 +2024-08-16 06:00:00,9,29.73495495427787,19.75944338678906,36.58128957933624 +2024-08-16 07:00:00,9,37.766717084319126,25.307712845694397,59.97766368526463 +2024-08-16 08:00:00,9,31.975580930869555,23.652145232071064,56.48643591038558 +2024-08-16 09:00:00,9,16.346811376462664,21.951293768994077,58.61437703272043 +2024-08-16 10:00:00,9,11.414393974821254,28.094816412920476,52.31099202544375 +2024-08-16 11:00:00,9,23.895582803619003,26.58348984094302,45.13175222597317 +2024-08-16 12:00:00,9,33.54942785656112,25.365979676612227,60.71950161837313 +2024-08-16 13:00:00,9,19.193624802439373,24.1286787389229,47.54345203087599 +2024-08-16 14:00:00,9,34.994882729417796,28.668945494142076,53.48559797531727 +2024-08-16 15:00:00,9,16.138529564438528,21.226205563518274,49.380019942090364 +2024-08-16 16:00:00,9,22.385922192115096,27.05693063334303,48.738164854564566 +2024-08-16 17:00:00,9,12.060176588513903,32.72339859571954,45.87039663282196 +2024-08-16 18:00:00,9,29.474267520461954,30.913666244905404,61.94191409446684 +2024-08-16 19:00:00,9,26.463493699635848,21.034523519871023,34.7493919006472 +2024-08-16 20:00:00,9,25.534748616456913,16.991471091645625,40.02881159899284 +2024-08-16 21:00:00,9,12.71684949718313,25.300966748059864,63.99150465901366 +2024-08-16 22:00:00,9,20.070756421881345,24.508850476500996,62.72308210059459 +2024-08-16 23:00:00,9,6.517099800250783,18.955404296387325,58.04681564767829 +2024-08-17 00:00:00,9,38.186645004744776,20.413378247174457,56.425654906397504 +2024-08-17 01:00:00,9,36.617958084754335,26.285151902336757,45.338298253329015 +2024-08-17 02:00:00,9,20.961461458559736,26.001458523695142,59.68130428573888 +2024-08-17 03:00:00,9,22.60346456569924,19.63149496799471,66.23607399793516 +2024-08-17 04:00:00,9,14.61457886850747,25.920522675537516,42.9380246790027 +2024-08-17 05:00:00,9,25.371244120969482,31.02563238101537,51.74824394742645 +2024-08-17 06:00:00,9,10.067059331994383,22.92002900220353,32.32269954920501 +2024-08-17 07:00:00,9,40.00970099913734,26.021875508428042,62.669775823210834 +2024-08-17 08:00:00,9,20.921726886983194,22.341114632500172,50.56024425047103 +2024-08-17 09:00:00,9,24.298556132180895,26.579336013786897,69.02916744028524 +2024-08-17 10:00:00,9,12.148120294548614,23.93837861798562,63.235885938438436 +2024-08-17 11:00:00,9,15.36327281008512,23.35358977032788,52.92308960406594 +2024-08-17 12:00:00,9,21.82127785810555,24.022522490186027,56.73030861973674 +2024-08-17 13:00:00,9,26.648449466234734,22.057354500810447,63.41552375851609 +2024-08-17 14:00:00,9,21.58555231394518,21.259557923660953,42.50741600485708 +2024-08-17 15:00:00,9,28.958119786007625,21.106457552080265,55.77957045045544 +2024-08-17 16:00:00,9,15.925757381297931,21.180623835716325,51.973284663789435 +2024-08-17 17:00:00,9,24.252145263533546,16.92586528431432,54.30088988818739 +2024-08-17 18:00:00,9,22.836836830960173,23.269254218081,48.63460103024121 +2024-08-17 19:00:00,9,25.91700215991907,25.452251705701627,57.700278456416875 +2024-08-17 20:00:00,9,30.99910061160593,16.148863948670943,53.162560486429385 +2024-08-17 21:00:00,9,22.395077109214483,28.16787689659903,60.25137875799643 +2024-08-17 22:00:00,9,37.768856249902235,17.511505674704267,62.025789600064115 +2024-08-17 23:00:00,9,21.936837648539473,24.984814359236463,57.829618701228696 +2024-08-18 00:00:00,9,22.967837274663975,18.160850217863473,61.65379923631081 +2024-08-18 01:00:00,9,22.67677538205954,27.276677987005513,42.4217154629724 +2024-08-18 02:00:00,9,20.891876562104926,29.738327585728477,52.52949851967755 +2024-08-18 03:00:00,9,17.147496635939504,17.90415769103688,45.23983939327028 +2024-08-18 04:00:00,9,31.636118585962723,27.092759795840855,51.65539559217099 +2024-08-18 05:00:00,9,42.91805662672193,24.185154041551108,42.58500657475239 +2024-08-18 06:00:00,9,11.58827997465999,21.871424099293925,48.32565385333191 +2024-08-18 07:00:00,9,22.545529824068844,19.049195012706058,52.72639935835362 +2024-08-18 08:00:00,9,24.40723092325306,25.00072262462059,61.116152721241875 +2024-08-18 09:00:00,9,25.977187831771992,20.711814975929652,41.82674571786204 +2024-08-18 10:00:00,9,38.78368525350287,19.43568074511197,56.07946340703012 +2024-08-18 11:00:00,9,21.267716546758194,27.025597444041114,52.678107075964526 +2024-08-18 12:00:00,9,43.29293736889553,27.92635128021582,58.16886580081855 +2024-08-18 13:00:00,9,29.13462698484377,31.862434539877913,55.2595822225552 +2024-08-18 14:00:00,9,17.751645275060202,27.65801871853095,46.34678986557281 +2024-08-18 15:00:00,9,30.231633377684066,22.501066592640445,57.22133124450594 +2024-08-18 16:00:00,9,28.833451161034443,29.115833603567683,37.47276102518037 +2024-08-18 17:00:00,9,6.735615359365269,26.560411333313453,29.649148723000106 +2024-08-18 18:00:00,9,21.778803040987825,24.610515435696914,55.66385389757668 +2024-08-18 19:00:00,9,30.40887020020903,22.957198349424154,71.58745072217525 +2024-08-18 20:00:00,9,22.968181581537,31.134397803976128,62.323590646282035 +2024-08-18 21:00:00,9,22.3766402026205,25.766709177716645,57.665082263035835 +2024-08-18 22:00:00,9,15.324704817940125,17.520456616614467,62.477679599812795 +2024-08-18 23:00:00,9,17.5659387251454,22.912078056809364,68.2970521425399 +2024-08-19 00:00:00,9,34.46330173275107,31.27609484559752,47.790994998380576 +2024-08-19 01:00:00,9,21.03899355791841,29.506190531739996,66.14828941298957 +2024-08-19 02:00:00,9,18.922593521990922,22.69150270341713,48.82666036101169 +2024-08-19 03:00:00,9,24.706513522180398,26.139125829559408,57.46969366081633 +2024-08-19 04:00:00,9,28.353438825267084,25.55872050270937,35.70627746101571 +2024-08-19 05:00:00,9,20.558483489171955,22.309618685119464,47.71094157200586 +2024-08-19 06:00:00,9,27.568049359220613,30.487323684352624,50.29105478856389 +2024-08-19 07:00:00,9,21.844291495864216,24.378001651211274,71.28399142019282 +2024-08-19 08:00:00,9,34.07563439250131,20.46291560987825,76.88837142917232 +2024-08-19 09:00:00,9,19.651809309458507,21.80072702337822,57.363084703122986 +2024-08-19 10:00:00,9,13.511881562138782,20.824274333232292,37.40122571384555 +2024-08-19 11:00:00,9,39.71687235846542,24.527312302780565,44.99816473217197 +2024-08-19 12:00:00,9,27.308970443071924,26.320275455507797,49.428371497802274 +2024-08-19 13:00:00,9,17.843289856209836,27.35005783015242,37.918772919190175 +2024-08-19 14:00:00,9,26.57693916770677,30.340130620951083,48.948606207286005 +2024-08-19 15:00:00,9,46.63818482038902,17.779637023910084,38.79636318674166 +2024-08-19 16:00:00,9,39.682177350251756,21.833380595480623,39.622085679669865 +2024-08-19 17:00:00,9,23.700295019750396,24.658729387786636,60.17795206940511 +2024-08-19 18:00:00,9,31.653280202225456,28.6814844791749,53.67802025563177 +2024-08-19 19:00:00,9,8.966015073394129,27.521393217208463,47.89646616305643 +2024-08-19 20:00:00,9,36.6260646035335,26.385191424274392,52.814486494896535 +2024-08-19 21:00:00,9,30.842789282106068,24.642197356426575,54.89571049718158 +2024-08-19 22:00:00,9,3.065371811673149,18.080701886734396,65.97771781175011 +2024-08-19 23:00:00,9,32.68598542474283,19.138260628899616,62.397417662225 +2024-08-20 00:00:00,9,14.04033140609611,26.131571302174663,53.45305997974314 +2024-08-20 01:00:00,9,23.162352926989005,23.745847173739445,48.33900470193444 +2024-08-20 02:00:00,9,20.848641702448173,21.811652805762623,60.64146765339274 +2024-08-20 03:00:00,9,28.208217126213146,27.876074098038707,60.089565401155646 +2024-08-20 04:00:00,9,32.98119851040309,26.573486693657028,68.38253341998792 +2024-08-20 05:00:00,9,23.83381038540774,25.038909829107578,49.44877527937881 +2024-08-20 06:00:00,9,11.009599924157454,26.857053532369658,63.35649583951387 +2024-08-20 07:00:00,9,34.333789186512604,19.840292123202484,57.424688283070076 +2024-08-20 08:00:00,9,32.93885847543764,29.910279164745987,57.96542445266423 +2024-08-20 09:00:00,9,13.829437042231005,28.115238720681408,57.33249169261717 +2024-08-20 10:00:00,9,15.21402392730621,19.37400610095553,51.186053309488116 +2024-08-20 11:00:00,9,31.136907059375353,26.585922742366606,58.96713986228571 +2024-08-20 12:00:00,9,35.264882707091246,24.17674251002755,56.3859368104752 +2024-08-20 13:00:00,9,19.530899374968527,26.40906285601202,56.801693986377536 +2024-08-20 14:00:00,9,32.97345584846642,27.688792127389945,67.95182429529075 +2024-08-20 15:00:00,9,24.203346268981598,29.652962266730242,62.42354508781223 +2024-08-20 16:00:00,9,46.26846050079713,25.705997534991674,63.25613969786961 +2024-08-20 17:00:00,9,27.359375932003605,23.988931335758735,43.76695696535718 +2024-08-20 18:00:00,9,38.43129005515405,28.34460538224789,45.58585822548558 +2024-08-20 19:00:00,9,23.46254807747431,25.49325102575863,53.57434026878616 +2024-08-20 20:00:00,9,28.904304565360473,25.404100440097842,61.107848516010904 +2024-08-20 21:00:00,9,23.585154324321053,28.329464219302704,61.90969165620938 +2024-08-20 22:00:00,9,19.669637229204888,18.76160933576513,66.40991361400818 +2024-08-20 23:00:00,9,15.068475602588858,25.237119881295403,49.720598366617224 +2024-08-21 00:00:00,9,45.76293262537092,23.159919810383656,55.29834458460375 +2024-08-21 01:00:00,9,45.25556424903778,20.90203079564871,41.48434789680961 +2024-08-21 02:00:00,9,40.19052402486027,25.124438284689273,54.61402603372369 +2024-08-21 03:00:00,9,20.971199055881016,20.75192633147922,58.05269344640606 +2024-08-21 04:00:00,9,15.077518726913146,24.404505301033915,45.983367247228294 +2024-08-21 05:00:00,9,38.368780960511785,25.95319063865959,57.96598376150548 +2024-08-21 06:00:00,9,30.439121137090666,20.360439470882085,46.51626073588348 +2024-08-21 07:00:00,9,26.403478299331848,28.22859756704094,65.74203278288805 +2024-08-21 08:00:00,9,36.18135074001726,21.72453042641888,56.70727239758309 +2024-08-21 09:00:00,9,11.769450654309889,24.08881533393781,51.889317512446155 +2024-08-21 10:00:00,9,24.40600987810975,21.616960329614873,48.97320603501347 +2024-08-21 11:00:00,9,28.361510991118923,26.545715074294883,60.924844279868964 +2024-08-21 12:00:00,9,18.184539332566082,28.011162084419034,56.89627757852046 +2024-08-21 13:00:00,9,23.251779367389137,26.86849693446379,68.50416798163644 +2024-08-21 14:00:00,9,23.798675366074274,22.899837192638387,71.91596953412763 +2024-08-21 15:00:00,9,15.777695688756932,22.879505961689535,45.391356011021486 +2024-08-21 16:00:00,9,27.641363066459494,26.553555917309808,44.773191984182006 +2024-08-21 17:00:00,9,30.61214914797894,29.509097102553064,51.86192154506671 +2024-08-21 18:00:00,9,28.250123766129057,18.271801712922525,61.75681713841577 +2024-08-21 19:00:00,9,37.38697401579213,22.769253388355526,55.78812034902796 +2024-08-21 20:00:00,9,28.960512504027133,26.13099550580822,54.73660842733073 +2024-08-21 21:00:00,9,32.68494943169132,27.124927695321603,46.038746079684714 +2024-08-21 22:00:00,9,23.20124740743395,22.29882051464337,61.327806337787905 +2024-08-21 23:00:00,9,27.903925001606215,23.51411566700457,54.28419129246283 +2024-08-22 00:00:00,9,40.669672152129294,24.28182915895021,73.77156114953411 +2024-08-22 01:00:00,9,26.521485809302963,28.459392723293774,58.08688685409889 +2024-08-22 02:00:00,9,17.614759327633863,24.878175536956586,60.85922054560286 +2024-08-22 03:00:00,9,32.843027864342844,22.84793530160441,47.82547496002123 +2024-08-22 04:00:00,9,24.40806353621611,22.90394183106123,45.70018149851969 +2024-08-22 05:00:00,9,38.515716040270966,29.262710955114258,51.88921301115377 +2024-08-22 06:00:00,9,33.855702153683595,30.492764602934628,60.2555611842668 +2024-08-22 07:00:00,9,16.88131025043947,20.859252316641737,62.578582759816605 +2024-08-22 08:00:00,9,33.3842714828747,17.353898587216,53.205166936421534 +2024-08-22 09:00:00,9,27.74811420243315,20.30128427595529,43.51482402178397 +2024-08-22 10:00:00,9,38.81511982127668,21.375827746478073,58.93404519457454 +2024-08-22 11:00:00,9,22.754470847452044,21.364772892030242,48.69045091165083 +2024-08-22 12:00:00,9,19.933735234480668,26.363669551118125,51.87594501096924 +2024-08-22 13:00:00,9,22.87657053110061,21.886610224583865,52.93506037537624 +2024-08-22 14:00:00,9,27.563705008606703,26.767470264422894,64.27021601169656 +2024-08-22 15:00:00,9,32.04142985765742,28.007098880151126,47.333984725936276 +2024-08-22 16:00:00,9,12.797669307684309,33.09565717674538,48.49873921279332 +2024-08-22 17:00:00,9,19.49460744142946,24.758108580254337,44.36539952617674 +2024-08-22 18:00:00,9,45.547800341346225,17.28497246212745,45.08879723945711 +2024-08-22 19:00:00,9,34.45844398372377,29.19137662443597,60.65330190180487 +2024-08-22 20:00:00,9,21.211150233319596,28.97140193957655,47.94056141932313 +2024-08-22 21:00:00,9,30.7297134423185,18.733004505283493,57.11374361075573 +2024-08-22 22:00:00,9,21.328091875215204,13.481226430656818,59.95220409355198 +2024-08-22 23:00:00,9,9.151464179616868,20.08406657085878,58.51595691894132 +2024-08-23 00:00:00,9,30.753689934527557,20.24965195742057,50.89857487678316 +2024-08-23 01:00:00,9,38.21801594534152,25.957436605170525,40.61387528346084 +2024-08-23 02:00:00,9,41.78211518174844,24.51724347841586,64.21148902981614 +2024-08-23 03:00:00,9,30.279007256393,20.455123479521056,53.02382673990282 +2024-08-23 04:00:00,9,11.33544747480412,26.294610354917012,62.299431538527124 +2024-08-23 05:00:00,9,23.867236443452192,28.392391020906384,58.448791773498925 +2024-08-23 06:00:00,9,21.063645058452146,33.20230852019958,51.262737979463424 +2024-08-23 07:00:00,9,27.314099922755727,20.928592789495788,55.67898848585156 +2024-08-23 08:00:00,9,26.55932633622786,19.4900473250575,62.78807638039336 +2024-08-23 09:00:00,9,29.50477953775981,25.610505707702952,38.2495817138082 +2024-08-23 10:00:00,9,26.64672925351014,19.043126396356644,51.77616023672325 +2024-08-23 11:00:00,9,29.48851130032862,26.61328230281414,56.20776154887364 +2024-08-23 12:00:00,9,17.055709843369744,20.965967149363415,51.164058094716715 +2024-08-23 13:00:00,9,29.915504520603832,17.634897735289968,64.00936736079272 +2024-08-23 14:00:00,9,44.81820825662043,26.918868848414213,43.8321146208645 +2024-08-23 15:00:00,9,30.287793238285644,20.648514372696575,69.85494789638823 +2024-08-23 16:00:00,9,21.19888726149884,30.508327433609022,47.532843699390774 +2024-08-23 17:00:00,9,32.99788968642823,27.079887659685955,47.76739774359366 +2024-08-23 18:00:00,9,27.469381617159012,26.180324231029616,68.37113299950082 +2024-08-23 19:00:00,9,30.871646047133424,27.157465249575075,70.59984388526985 +2024-08-23 20:00:00,9,23.74509639570707,24.02236721947377,66.06672053270451 +2024-08-23 21:00:00,9,28.04888693686729,23.173481978940462,54.89127677630966 +2024-08-23 22:00:00,9,34.34170062252422,21.745204234226563,59.35152878627328 +2024-08-23 23:00:00,9,25.51614943301314,28.03108222707777,62.43807225290813 +2024-08-24 00:00:00,9,18.677243529231653,27.32526852602892,48.20862133240078 +2024-08-24 01:00:00,9,33.10344727245717,23.33181514656814,52.45688480025917 +2024-08-24 02:00:00,9,28.596547664443595,27.193121072373522,48.91554139551827 +2024-08-24 03:00:00,9,24.566093760160538,26.575965271766492,55.53037209602207 +2024-08-24 04:00:00,9,33.54413972645845,23.403309405402723,47.49655087560986 +2024-08-24 05:00:00,9,31.160793447706805,21.33616270144995,68.77286273661971 +2024-08-24 06:00:00,9,34.974825481164174,24.110157729067772,56.46761801804074 +2024-08-24 07:00:00,9,23.751721809721886,26.381616136069756,60.113628496263175 +2024-08-24 08:00:00,9,22.368527698036445,21.645204897120482,58.00646577618768 +2024-08-24 09:00:00,9,26.080410045291686,24.40973783363058,69.69401750403352 +2024-08-24 10:00:00,9,18.75169351695884,21.87580512536521,50.88152091445979 +2024-08-24 11:00:00,9,24.071278267996686,27.293579815328883,57.593366796517344 +2024-08-24 12:00:00,9,26.887493064577306,25.302946009245023,70.1207232351172 +2024-08-24 13:00:00,9,34.41453610490224,27.41466593463505,62.89248070381483 +2024-08-24 14:00:00,9,13.387024487619176,24.447741231128106,55.267491121531584 +2024-08-24 15:00:00,9,14.993600821322746,25.668459863806763,46.664887346524544 +2024-08-24 16:00:00,9,14.634989627199971,28.574811906859892,46.56350311194835 +2024-08-24 17:00:00,9,40.5690500983481,26.63581791871151,49.85067202386503 +2024-08-24 18:00:00,9,38.235725785978154,29.699938847071714,53.731913585402225 +2024-08-24 19:00:00,9,36.242798230474676,26.360377656292286,57.64007666196247 +2024-08-24 20:00:00,9,29.327912799374435,21.925517859148968,63.78181301785659 +2024-08-24 21:00:00,9,23.611613761144447,30.058522901266095,62.46826149795211 +2024-08-24 22:00:00,9,22.839999639637533,26.785436449294483,67.79671918498943 +2024-08-24 23:00:00,9,21.198212786978516,15.72612784569176,48.57763236589618 +2024-08-25 00:00:00,9,9.651372136165826,27.83554380606884,51.565735342925414 +2024-08-25 01:00:00,9,20.486617051250164,25.0100401846546,54.11772821964456 +2024-08-25 02:00:00,9,40.91948921388706,23.756854941085944,56.23858943456086 +2024-08-25 03:00:00,9,32.32562627148794,20.27977390470412,33.82594563346805 +2024-08-25 04:00:00,9,21.60218596104751,23.426500007529036,53.606442694761675 +2024-08-25 05:00:00,9,28.702460507892404,18.64888747114521,52.62232102799741 +2024-08-25 06:00:00,9,33.46028571331675,34.15662433871133,57.46431026045477 +2024-08-25 07:00:00,9,30.435853873915306,27.6330495320216,55.0951353271441 +2024-08-25 08:00:00,9,26.901393165183542,25.503574560911375,53.59000546544883 +2024-08-25 09:00:00,9,0.0,21.49051406237003,41.79168544926321 +2024-08-25 10:00:00,9,25.096887448759837,28.70283394881326,52.935944586651104 +2024-08-25 11:00:00,9,29.991235177233918,18.29716074355364,50.89559902336432 +2024-08-25 12:00:00,9,21.873482337007747,26.58679982576998,34.979061563282926 +2024-08-25 13:00:00,9,21.774008773427596,27.00704423341856,61.22521365049561 +2024-08-25 14:00:00,9,33.1439936422391,33.60313187696009,57.38930335012758 +2024-08-25 15:00:00,9,17.115377556990946,22.059381898958502,58.04215092694987 +2024-08-25 16:00:00,9,21.891443008511736,20.401851631458364,62.146325147705824 +2024-08-25 17:00:00,9,34.78922853836884,22.77771416931522,54.14574800194222 +2024-08-25 18:00:00,9,29.329407955650655,31.113051278009934,37.35450064017488 +2024-08-25 19:00:00,9,20.27417904855991,26.677371670381454,59.67064192761806 +2024-08-25 20:00:00,9,11.939514979975332,22.581377808351228,51.69351419497387 +2024-08-25 21:00:00,9,17.136657875087387,14.785287105997295,67.7842767822195 +2024-08-25 22:00:00,9,16.550319453385782,24.111146243043233,65.93786951302863 +2024-08-25 23:00:00,9,38.76790380869362,27.981817187667822,53.962001558415324 +2024-08-26 00:00:00,9,43.071193691698866,23.598509552801076,67.43661844068484 +2024-08-26 01:00:00,9,31.402920445552805,23.791495782413328,59.30051525172632 +2024-08-26 02:00:00,9,37.261433856820965,28.620811946527404,44.66495722890113 +2024-08-26 03:00:00,9,31.034362221648482,22.92629597974391,61.903279663841374 +2024-08-26 04:00:00,9,31.11061283556781,29.791028288438028,51.145947439566086 +2024-08-26 05:00:00,9,37.25919898573285,17.835582754274714,57.62665092668428 +2024-08-26 06:00:00,9,29.006177522738714,23.550450207134567,55.34949558209777 +2024-08-26 07:00:00,9,26.04170805167166,29.335103339253127,72.4206959032295 +2024-08-26 08:00:00,9,31.796529421684287,22.89337544455079,66.21328513403282 +2024-08-26 09:00:00,9,32.87346424398073,21.178871099103457,64.74581234256085 +2024-08-26 10:00:00,9,23.324917619238025,22.63514605308198,51.979713091701136 +2024-08-26 11:00:00,9,15.80338618953885,22.34207029846522,48.97036845177913 +2024-08-26 12:00:00,9,16.608614420424566,25.50594008867673,47.48128094892043 +2024-08-26 13:00:00,9,33.87404221790445,30.679365448930696,46.94030264998197 +2024-08-26 14:00:00,9,16.52599854474417,25.930194352924882,69.8108424833737 +2024-08-26 15:00:00,9,22.942779689775993,31.555937209772765,38.029552651677875 +2024-08-26 16:00:00,9,17.74616016003742,20.494977801844758,54.93574151002277 +2024-08-26 17:00:00,9,37.6383838233875,22.083891330647255,48.97442638947843 +2024-08-26 18:00:00,9,31.071556016699162,19.328272769575477,56.36600611631106 +2024-08-26 19:00:00,9,18.072341942669276,23.33080641064233,68.78010846851271 +2024-08-26 20:00:00,9,32.89341428002205,19.483829337044327,59.168693936479286 +2024-08-26 21:00:00,9,12.987051204524786,20.177954698928644,38.66004744453504 +2024-08-26 22:00:00,9,38.1635695780251,22.670739005399923,74.84318500880178 +2024-08-26 23:00:00,9,4.007638455507752,21.674118290617525,61.86290583968964 +2024-08-27 00:00:00,9,20.773409257309957,26.308821683517337,53.549612951399 +2024-08-27 01:00:00,9,34.37737663025323,26.255375002281497,70.48775310064312 +2024-08-27 02:00:00,9,27.74443963252485,24.1490985473037,74.91793725679648 +2024-08-27 03:00:00,9,27.35799740628337,29.923596160660907,66.3431338547852 +2024-08-27 04:00:00,9,33.6172563157251,26.283541855929087,39.89949555637311 +2024-08-27 05:00:00,9,21.016050498917693,22.673321393504956,57.842247644451646 +2024-08-27 06:00:00,9,31.60138156652803,15.581868842314705,56.50513956068872 +2024-08-27 07:00:00,9,16.53984653156477,23.324553377929746,63.64738582221419 +2024-08-27 08:00:00,9,30.837562940023524,22.914976894214327,60.07923951113125 +2024-08-27 09:00:00,9,23.2644468766147,31.163914307294256,50.43076720191602 +2024-08-27 10:00:00,9,39.467926821392346,24.642140647102458,47.04790105653997 +2024-08-27 11:00:00,9,33.571307071319254,27.750187746937854,53.11655302974075 +2024-08-27 12:00:00,9,19.609947206009426,23.271325552958903,57.24621796771098 +2024-08-27 13:00:00,9,28.376723634799625,26.607092169036715,53.9328418439543 +2024-08-27 14:00:00,9,46.04732576308778,24.05829234432243,40.16573163136789 +2024-08-27 15:00:00,9,22.58859729693153,19.575666311496125,40.577132571339675 +2024-08-27 16:00:00,9,20.691706517829232,34.243946545998064,40.43890147253184 +2024-08-27 17:00:00,9,32.83966774933582,22.050104650217264,62.75989553661463 +2024-08-27 18:00:00,9,29.22700428851116,21.87310830718363,58.94999127214693 +2024-08-27 19:00:00,9,17.786523800361337,20.29378614656835,51.820051565865974 +2024-08-27 20:00:00,9,31.73890492628052,23.13248865088894,55.72772819789102 +2024-08-27 21:00:00,9,33.28758001007735,16.094260719852898,65.56207689889624 +2024-08-27 22:00:00,9,18.31800370990238,17.372253813530268,75.37696014863644 +2024-08-27 23:00:00,9,23.180271381450225,22.614603476179774,56.07474219811813 +2024-08-28 00:00:00,9,24.252193426622547,22.019414090330088,47.17344724313888 +2024-08-28 01:00:00,9,42.774762533571746,19.169269851729037,45.25174589926609 +2024-08-28 02:00:00,9,23.23812065093361,21.00167708522262,54.99397466229799 +2024-08-28 03:00:00,9,19.129083777175897,19.682074845372128,57.96901860346907 +2024-08-28 04:00:00,9,32.09479448106724,20.994243473370464,41.36045372035434 +2024-08-28 05:00:00,9,16.75625241963288,27.86858523518176,60.86764031119121 +2024-08-28 06:00:00,9,30.618029930253975,24.759123702792152,48.92053596651213 +2024-08-28 07:00:00,9,21.962968458229454,15.574619669896833,47.0288269919256 +2024-08-28 08:00:00,9,28.089751768606124,21.837219489563235,53.76329144817119 +2024-08-28 09:00:00,9,20.613948803636173,29.631979117896506,63.61542194639083 +2024-08-28 10:00:00,9,32.093757826364076,19.918942352534646,44.111558550002435 +2024-08-28 11:00:00,9,13.733519628360346,23.659778676144057,57.740323998595 +2024-08-28 12:00:00,9,47.959810026570295,21.274472430204245,51.653470269135866 +2024-08-28 13:00:00,9,46.79575631944972,19.894569607612844,57.7474956293622 +2024-08-28 14:00:00,9,12.130665075826094,27.40618221517003,39.5333369045155 +2024-08-28 15:00:00,9,23.429475239174366,26.01727560212035,53.84524405209511 +2024-08-28 16:00:00,9,12.822884365993927,23.169726652082065,57.72251968520714 +2024-08-28 17:00:00,9,31.246685004991136,27.037649419261093,52.787803117605826 +2024-08-28 18:00:00,9,15.593779218465857,19.694119701668917,52.71713463461335 +2024-08-28 19:00:00,9,25.45132664392918,19.48769200941591,65.64466382324704 +2024-08-28 20:00:00,9,22.870019084019557,19.854275125969874,40.4275063460059 +2024-08-28 21:00:00,9,10.802264814403015,17.798735498262108,43.803524173767194 +2024-08-28 22:00:00,9,35.12821270903454,26.857612729286213,60.26607872548646 +2024-08-28 23:00:00,9,22.433357960891648,24.323574128108287,65.64706089803255 +2024-08-29 00:00:00,9,32.05486506200465,25.36595565442277,49.89274425626172 +2024-08-29 01:00:00,9,24.366751163879968,24.11560112554374,66.77053663527539 +2024-08-29 02:00:00,9,28.79980324821175,13.670391693370268,74.78329367805681 +2024-08-29 03:00:00,9,29.28971128480852,20.32262686265999,59.111693437199655 +2024-08-29 04:00:00,9,30.70964915752368,21.263410128912604,58.16709688106105 +2024-08-29 05:00:00,9,25.64148468951827,23.3559084716202,44.89477577523074 +2024-08-29 06:00:00,9,15.971252123014388,23.465105550445006,61.65274118362827 +2024-08-29 07:00:00,9,36.32783941302859,26.659818911138053,66.65135004221429 +2024-08-29 08:00:00,9,25.581623707874513,28.48835863254306,64.7650240359703 +2024-08-29 09:00:00,9,17.473736928549254,22.333510890772782,52.82277913752678 +2024-08-29 10:00:00,9,23.997059732685653,14.978147997458933,66.76310363694114 +2024-08-29 11:00:00,9,10.0845376549484,20.914479663774625,45.14425904897884 +2024-08-29 12:00:00,9,12.77930821232702,26.09799174684937,60.3654337984195 +2024-08-29 13:00:00,9,25.655118001522204,23.854382957566468,43.07177906299075 +2024-08-29 14:00:00,9,28.830701867980856,25.62398258122791,55.490020422939764 +2024-08-29 15:00:00,9,20.754375809973318,29.994966292837375,64.07841720199495 +2024-08-29 16:00:00,9,18.56934299451618,22.527667417140705,62.637883320455245 +2024-08-29 17:00:00,9,32.05154900447066,20.37355700751081,47.526308937791825 +2024-08-29 18:00:00,9,20.999744608588976,24.735366524408015,32.16656850638499 +2024-08-29 19:00:00,9,21.319099042701225,19.806971720841226,62.64058570863103 +2024-08-29 20:00:00,9,24.8879993743498,22.959329932709423,47.74003931418769 +2024-08-29 21:00:00,9,25.882335824445413,20.331827197721616,41.925718275273184 +2024-08-29 22:00:00,9,25.111598064347447,14.907519242300065,50.913592513818344 +2024-08-29 23:00:00,9,21.768774812320043,15.982966635463548,61.136673732085896 +2024-08-30 00:00:00,9,24.473864126455005,22.82754819701161,41.36611284803257 +2024-08-30 01:00:00,9,33.58136627274102,24.962168080383478,52.92484646832684 +2024-08-30 02:00:00,9,22.838181772340963,29.87768513373782,51.28177624657973 +2024-08-30 03:00:00,9,14.692543930775788,20.782120379300746,62.510333736420165 +2024-08-30 04:00:00,9,15.561812443436555,30.409816617217665,51.635520629445736 +2024-08-30 05:00:00,9,25.649463117945118,27.94745780957693,59.48317383003065 +2024-08-30 06:00:00,9,33.771471469561206,28.00759334223735,45.112345560517866 +2024-08-30 07:00:00,9,20.988113507852372,31.033033273183317,66.31459491785876 +2024-08-30 08:00:00,9,17.882034244680323,26.780476006588536,69.52665127622264 +2024-08-30 09:00:00,9,25.904222739878467,23.25936968762292,37.823496546422284 +2024-08-30 10:00:00,9,34.74073732046102,25.75220536829831,58.078088736752896 +2024-08-30 11:00:00,9,23.807923647524156,24.16527798087688,53.7006624605335 +2024-08-30 12:00:00,9,28.647964309486312,24.066202054970983,43.23187288010704 +2024-08-30 13:00:00,9,14.261066261082265,22.481269641930112,52.20367867367048 +2024-08-30 14:00:00,9,38.99299198951752,25.573424084220715,40.94223400893952 +2024-08-30 15:00:00,9,18.643089980072542,22.756402777616458,72.47158922830761 +2024-08-30 16:00:00,9,22.941475652190043,28.545039643882344,64.98969241308365 +2024-08-30 17:00:00,9,27.851174308379708,20.453558000818383,61.613973615009726 +2024-08-30 18:00:00,9,20.43602479191543,29.707521338242703,54.98732924207302 +2024-08-30 19:00:00,9,8.298335030067015,26.391512747686765,54.53799715214608 +2024-08-30 20:00:00,9,23.623478858985383,28.329083269728383,59.58277305458645 +2024-08-30 21:00:00,9,27.091188435651855,19.943502542725202,51.950731525422384 +2024-08-30 22:00:00,9,18.139045019705712,27.025154134829744,71.34807463853242 +2024-08-30 23:00:00,9,26.894727289501173,22.494771846669927,63.07410864860369 +2024-08-31 00:00:00,9,37.2854154260239,19.9692806083158,71.45269800661367 +2024-08-31 01:00:00,9,17.607349948415127,24.70812255505793,47.27219070023175 +2024-08-31 02:00:00,9,46.92064034786151,22.365395205123775,47.59796416696419 +2024-08-31 03:00:00,9,28.74820166312247,23.877654477255746,69.41431744493367 +2024-08-31 04:00:00,9,25.189846199377985,22.772401853728088,52.34678458719767 +2024-08-31 05:00:00,9,11.757143435781686,17.374077916768435,63.54599029165708 +2024-08-31 06:00:00,9,20.409075008014796,23.177439493019563,50.000007650290144 +2024-08-31 07:00:00,9,10.378820806245546,20.24751001117414,57.97414151012174 +2024-08-31 08:00:00,9,30.937455928942263,17.464812293714765,50.91518012815806 +2024-08-31 09:00:00,9,27.507705557452457,35.81033272223038,50.859885161417616 +2024-08-31 10:00:00,9,10.701464066110312,29.42119888005586,74.81744258622481 +2024-08-31 11:00:00,9,24.994208106418725,26.34535547910769,51.269398300763335 +2024-08-31 12:00:00,9,21.6425944549798,14.525648326086955,55.238848840786595 +2024-08-31 13:00:00,9,27.791137393742886,27.37789771539282,46.943888182645004 +2024-08-31 14:00:00,9,24.376976823482135,27.538481009550708,54.625539259139764 +2024-08-31 15:00:00,9,27.921644993788007,28.710154381222715,53.62880667140512 +2024-08-31 16:00:00,9,25.475434778618297,28.055732085966195,54.19480224523652 +2024-08-31 17:00:00,9,19.77317017187757,30.503018089321415,55.84839053000881 +2024-08-31 18:00:00,9,9.25058373497938,24.016748318751898,47.302432898040315 +2024-08-31 19:00:00,9,16.31248938940184,24.72037537830674,45.46243400835211 +2024-08-31 20:00:00,9,20.39185315299728,21.92629299636106,46.10845822335477 +2024-08-31 21:00:00,9,19.013369961014973,21.984299208195072,62.16346176885777 +2024-08-31 22:00:00,9,20.261802109992907,31.056069934347704,55.1269846711164 +2024-08-31 23:00:00,9,32.983076135263815,18.115467633164656,65.19371627890138 +2024-09-01 00:00:00,9,43.15191666424532,16.298963886941703,51.07151989155249 +2024-09-01 01:00:00,9,20.92670110907057,26.433742138921318,39.851271601436615 +2024-09-01 02:00:00,9,49.371992925008925,22.9063705725402,53.5207130042332 +2024-09-01 03:00:00,9,30.466137562357464,24.678884127914362,61.03632238691272 +2024-09-01 04:00:00,9,24.715542263078603,24.456029105617386,63.47346185775057 +2024-09-01 05:00:00,9,34.05155817864396,30.373664195729198,35.89555698431549 +2024-09-01 06:00:00,9,40.425554108925446,22.431618753160834,51.887200175650236 +2024-09-01 07:00:00,9,18.69483058145419,20.29970323596379,54.843338496710885 +2024-09-01 08:00:00,9,22.961825106465902,24.965192057961293,45.9732691388609 +2024-09-01 09:00:00,9,31.264884323030717,27.834212525737577,48.03397698852532 +2024-09-01 10:00:00,9,30.686727058091673,25.322087010033314,64.21001533492161 +2024-09-01 11:00:00,9,27.747421548695925,27.896542061823705,46.98174533780631 +2024-09-01 12:00:00,9,17.950258490715363,27.778322041289236,36.074483823908736 +2024-09-01 13:00:00,9,28.527038497987366,18.080317722352746,47.4825608914616 +2024-09-01 14:00:00,9,30.533609455640494,24.345919676811686,53.67278523554164 +2024-09-01 15:00:00,9,13.356761056106523,24.06572080781762,64.11563227803555 +2024-09-01 16:00:00,9,38.021619231266904,27.22025521969304,65.07359691849724 +2024-09-01 17:00:00,9,7.005929068045127,27.288384929962866,41.21751224244724 +2024-09-01 18:00:00,9,15.47179462857526,17.41748338633937,43.27593317243826 +2024-09-01 19:00:00,9,18.532032428761696,23.10991622432115,49.84331887569845 +2024-09-01 20:00:00,9,21.467702556782225,28.641918148148026,50.353340475955804 +2024-09-01 21:00:00,9,11.28747173883824,24.034317090445615,63.23665802873045 +2024-09-01 22:00:00,9,23.407498370504698,26.570738992140807,52.025321386782096 +2024-09-01 23:00:00,9,21.435200605243672,28.869809068737343,48.23206221691546 +2024-09-02 00:00:00,9,35.34770241530897,22.61576501004336,46.2799352592558 +2024-09-02 01:00:00,9,24.296343841195664,17.844713536818514,53.4300826104927 +2024-09-02 02:00:00,9,29.285558968893163,26.457822419427426,59.00630414203384 +2024-09-02 03:00:00,9,32.73718959814259,22.63028145221554,40.34094164128062 +2024-09-02 04:00:00,9,37.68607435344799,25.86808262399701,59.15940969494034 +2024-09-02 05:00:00,9,22.984650686799835,27.059333418714353,61.34782491308776 +2024-09-02 06:00:00,9,26.783201990478375,25.115997638981682,54.36181134752716 +2024-09-02 07:00:00,9,25.7707039851847,27.208827500633948,70.59482074752513 +2024-09-02 08:00:00,9,11.779698289667808,26.599142237364937,51.64186103396693 +2024-09-02 09:00:00,9,17.596552956405592,23.870063348304818,43.927406669874784 +2024-09-02 10:00:00,9,21.482563733326486,26.430324548023037,57.111291901059914 +2024-09-02 11:00:00,9,38.138225258065646,25.844164643542914,48.73948717308772 +2024-09-02 12:00:00,9,28.285207121722987,27.589244810144784,70.98549806820178 +2024-09-02 13:00:00,9,30.173488348737962,28.13309338678479,69.22551874001962 +2024-09-02 14:00:00,9,36.99439461299778,21.935133855521492,45.606296315142615 +2024-09-02 15:00:00,9,23.84223944649444,29.056462454862412,34.99055844714073 +2024-09-02 16:00:00,9,46.75761625265776,25.091876545022593,39.25248468062849 +2024-09-02 17:00:00,9,43.66529278771887,27.963636910408674,64.52341933196875 +2024-09-02 18:00:00,9,24.737337414799807,25.606323896246735,50.82774053210192 +2024-09-02 19:00:00,9,9.82877988495212,19.696906622913076,59.765745111806275 +2024-09-02 20:00:00,9,20.234223419956038,21.844097529558965,55.31378275577247 +2024-09-02 21:00:00,9,28.867314881405647,23.448030717782835,69.30991397356772 +2024-09-02 22:00:00,9,29.360565696333428,21.839769217106106,47.800192908920465 +2024-09-02 23:00:00,9,7.561798263789271,18.401817806750735,67.99768851074988 +2024-09-03 00:00:00,9,24.257352535122642,24.121548899246484,52.089748479139956 +2024-09-03 01:00:00,9,22.348098294914415,23.910694803107887,30.921220434871277 +2024-09-03 02:00:00,9,32.30846629767241,19.550736431326396,53.30564223446148 +2024-09-03 03:00:00,9,23.25130672309108,22.377155908528007,66.33875635138037 +2024-09-03 04:00:00,9,33.95446095362922,22.96638171735352,58.41192578275112 +2024-09-03 05:00:00,9,34.098896399192235,20.749163442415988,59.61621285938692 +2024-09-03 06:00:00,9,23.718288112436632,26.040200043967797,43.8541599676127 +2024-09-03 07:00:00,9,13.45367228095643,22.107566707667356,62.90147849847477 +2024-09-03 08:00:00,9,28.170953331734154,20.83990536019153,60.066813966654514 +2024-09-03 09:00:00,9,15.913036507917116,22.429924549667863,57.31805906182407 +2024-09-03 10:00:00,9,14.423383827379224,22.732725149454566,61.86822692122923 +2024-09-03 11:00:00,9,41.58669539120433,22.295066778385483,53.950605245103226 +2024-09-03 12:00:00,9,27.641880157867902,26.735600746610427,44.03600227029897 +2024-09-03 13:00:00,9,32.23678414503355,26.569759798116493,52.98672098997022 +2024-09-03 14:00:00,9,30.273211634783873,26.73820264192586,57.80863218376212 +2024-09-03 15:00:00,9,25.38366231448814,29.134594268116743,45.08732208211479 +2024-09-03 16:00:00,9,39.79133274951777,29.492017282333695,49.076054071418916 +2024-09-03 17:00:00,9,17.43495134241105,27.562737459592434,77.12173489771666 +2024-09-03 18:00:00,9,17.405146017499156,22.14697579857028,61.236651971236526 +2024-09-03 19:00:00,9,30.10499105949226,21.44723259970377,57.68995108463745 +2024-09-03 20:00:00,9,21.542884311572628,20.333575693447816,42.869256512863004 +2024-09-03 21:00:00,9,26.94194606088491,28.6565274170869,48.73568010163303 +2024-09-03 22:00:00,9,15.032984971688904,22.538166613403597,44.229118732942666 +2024-09-03 23:00:00,9,12.049473350135626,20.56204934595677,54.40825647580723 +2024-09-04 00:00:00,9,29.230547219289654,23.655442853496485,46.856249195375426 +2024-09-04 01:00:00,9,19.739417141806978,22.631791123827323,57.318748946584954 +2024-09-04 02:00:00,9,33.3304227106682,21.357117737413724,79.8211813994237 +2024-09-04 03:00:00,9,20.885222621920835,22.845782408715486,42.537203348123946 +2024-09-04 04:00:00,9,35.966188464239444,30.168545446158426,51.53914194609076 +2024-09-04 05:00:00,9,6.605172818812992,17.687147162607175,61.18574129287624 +2024-09-04 06:00:00,9,41.28724725906424,26.556466603668365,46.849182582913016 +2024-09-04 07:00:00,9,20.74906494361901,20.443725864100195,53.216734124722336 +2024-09-04 08:00:00,9,28.740935277507006,23.940790078084408,54.90684044995803 +2024-09-04 09:00:00,9,43.680862904453136,24.137868926655614,46.045676429274074 +2024-09-04 10:00:00,9,19.60018972734447,23.96709491687685,55.684408262694284 +2024-09-04 11:00:00,9,23.814814777727847,23.246521081445227,52.41915552313853 +2024-09-04 12:00:00,9,33.94408596502874,20.96682973022552,40.82338772292768 +2024-09-04 13:00:00,9,65.74068562984897,20.4451820156056,55.26904596977876 +2024-09-04 14:00:00,9,11.989665922781784,16.339637218990916,49.66969128644085 +2024-09-04 15:00:00,9,23.327098083711974,16.366618601375123,52.92492161303165 +2024-09-04 16:00:00,9,41.82285927717144,22.48384995252896,39.5629490428977 +2024-09-04 17:00:00,9,30.353170124379314,26.128700672793375,60.120784713469 +2024-09-04 18:00:00,9,26.11317450045717,33.61306675414316,47.109657819448536 +2024-09-04 19:00:00,9,10.504568195133915,26.130396942886634,68.17839918209657 +2024-09-04 20:00:00,9,9.975212350263545,20.225904306626347,50.75387469603142 +2024-09-04 21:00:00,9,32.44393824638148,20.610493258450514,66.3342630155718 +2024-09-04 22:00:00,9,22.10602712492797,24.00098656073764,67.88174796246211 +2024-09-04 23:00:00,9,25.12458679507115,23.376290565135296,55.0074063182296 +2024-09-05 00:00:00,9,30.211861567043186,22.8752946890489,52.22633005746699 +2024-09-05 01:00:00,9,21.616745163495967,18.748290785013648,48.693119457354676 +2024-09-05 02:00:00,9,42.7485705742035,21.51723838575753,32.498748392199275 +2024-09-05 03:00:00,9,29.322776269941457,23.863752536033328,58.57873007923476 +2024-09-05 04:00:00,9,33.12227380014704,20.313806394140695,56.54460320616924 +2024-09-05 05:00:00,9,32.83595285142949,26.563209286707007,69.35271436951508 +2024-09-05 06:00:00,9,20.98024854050466,25.03122980473712,56.13690863875562 +2024-09-05 07:00:00,9,24.484146564325165,27.81529011775123,57.69463645292941 +2024-09-05 08:00:00,9,39.62740046549128,24.845890277266317,53.095858271788565 +2024-09-05 09:00:00,9,13.730056492725602,19.44181244094181,63.42685398451537 +2024-09-05 10:00:00,9,19.554798959472453,21.622126916312272,44.129666195477405 +2024-09-05 11:00:00,9,21.00109585427733,21.794815279581066,70.68324594882463 +2024-09-05 12:00:00,9,33.956461639081056,28.15686716707068,57.51066832155483 +2024-09-05 13:00:00,9,33.275224205138294,21.334302581596265,53.59045840394406 +2024-09-05 14:00:00,9,29.742618547608572,26.272937050726057,65.88018700360078 +2024-09-05 15:00:00,9,22.846727734135115,22.66985051353564,60.328154320612626 +2024-09-05 16:00:00,9,34.881056572754574,33.60110862012094,62.400657342923395 +2024-09-05 17:00:00,9,43.93222896817106,26.90936977014016,50.967641585717224 +2024-09-05 18:00:00,9,11.912750179363421,21.916155974918947,42.87595157791158 +2024-09-05 19:00:00,9,33.17393054619719,27.59675699699826,60.52948016170828 +2024-09-05 20:00:00,9,30.365868498019502,26.239197659793277,59.85303832157805 +2024-09-05 21:00:00,9,35.64997576662441,26.07366180880313,51.59806687000603 +2024-09-05 22:00:00,9,31.70259093317931,23.74642934586174,55.10965430009057 +2024-09-05 23:00:00,9,29.36552904407764,30.848878763633365,42.55122930308255 +2024-09-06 00:00:00,9,22.258441087950573,28.87317979391795,39.02762183167716 +2024-09-06 01:00:00,9,26.384994217002287,32.0709413274465,68.4778388221143 +2024-09-06 02:00:00,9,40.11730967201277,24.48252318611228,63.4556486614172 +2024-09-06 03:00:00,9,20.061970824649265,22.73209940210077,54.06281962232874 +2024-09-06 04:00:00,9,14.348520940101299,26.140835963976816,80.01297962222935 +2024-09-06 05:00:00,9,18.72183958769252,26.613448503355972,49.05905570890096 +2024-09-06 06:00:00,9,34.5530084886678,19.2343449836385,60.725752798835146 +2024-09-06 07:00:00,9,20.355379001801108,26.176430595902282,53.27600153104963 +2024-09-06 08:00:00,9,40.30398468847697,28.597598754443204,58.8241160676961 +2024-09-06 09:00:00,9,22.20873662801273,26.68630768390943,50.78626049634936 +2024-09-06 10:00:00,9,9.459829909633688,28.90476094237882,41.08793062938213 +2024-09-06 11:00:00,9,45.944127934666426,23.618522302164216,59.65702908750927 +2024-09-06 12:00:00,9,32.94142277003379,25.963260702025515,42.09776747390548 +2024-09-06 13:00:00,9,27.0433450001087,27.121747750466515,46.408507856376886 +2024-09-06 14:00:00,9,28.81049061514408,28.715459954611674,48.83934303264269 +2024-09-06 15:00:00,9,30.966337888202126,34.086444046074384,65.30528914506475 +2024-09-06 16:00:00,9,16.32667871055462,23.475545921208116,54.35954507783623 +2024-09-06 17:00:00,9,12.507355840328866,29.712989509736342,61.23431650773567 +2024-09-06 18:00:00,9,4.986940741499119,20.09587540165947,54.46493587432954 +2024-09-06 19:00:00,9,36.5204844169602,23.0872446041497,61.383003051047794 +2024-09-06 20:00:00,9,25.9144914216377,23.989135336072952,53.00396352889056 +2024-09-06 21:00:00,9,12.764008272157055,18.359849539576913,54.1237593385337 +2024-09-06 22:00:00,9,24.760310790940107,25.639265721695093,63.10834499170748 +2024-09-06 23:00:00,9,21.777554558401565,16.553396393471832,46.77050980029251 +2024-09-07 00:00:00,9,30.354658455179624,22.893915656456297,43.11399304403728 +2024-09-07 01:00:00,9,30.190974499163666,26.10788885402291,46.37139604268823 +2024-09-07 02:00:00,9,28.572503039043944,20.744862754938996,50.41052098551426 +2024-09-07 03:00:00,9,29.727253572887996,19.151456571775633,52.21687587790741 +2024-09-07 04:00:00,9,16.238775135119475,22.35624718935053,48.821575712496305 +2024-09-07 05:00:00,9,27.433417803330308,21.552298271529555,61.22178365215754 +2024-09-07 06:00:00,9,34.1416076744694,25.689292584277133,42.007890240934735 +2024-09-07 07:00:00,9,31.46046745785774,27.10795539224151,48.92862017660925 +2024-09-07 08:00:00,9,22.347122575716355,25.67332483511469,64.72745933229271 +2024-09-07 09:00:00,9,33.504409349469604,24.19051541892404,60.45103639788925 +2024-09-07 10:00:00,9,19.125910964538953,22.497065295350083,48.10950985551222 +2024-09-07 11:00:00,9,30.760379021576313,17.603221745092785,43.47197412551998 +2024-09-07 12:00:00,9,16.6037040864153,29.713042223492124,55.62094249936404 +2024-09-07 13:00:00,9,22.214506143747276,29.78609988769154,69.43638283080077 +2024-09-07 14:00:00,9,18.07313030541645,21.43455516718734,76.09835738745582 +2024-09-07 15:00:00,9,26.050150228902858,28.207418057258018,56.918256987549746 +2024-09-07 16:00:00,9,19.15200203887438,33.242003628969655,44.23497661261822 +2024-09-07 17:00:00,9,36.558891581819196,30.409823419118986,55.59385890049663 +2024-09-07 18:00:00,9,23.041788192778434,22.17394930979099,40.5818598158164 +2024-09-07 19:00:00,9,29.356296809492196,24.217031317301522,48.26825567538347 +2024-09-07 20:00:00,9,33.419420107995755,28.09769143635713,68.29047658358331 +2024-09-07 21:00:00,9,26.223902751318168,24.07759871493544,53.43451136044974 +2024-09-07 22:00:00,9,19.04886191114498,31.647104801468963,52.761942848114344 +2024-09-07 23:00:00,9,21.052352776584122,27.39957935018665,61.43064069521984 +2024-09-08 00:00:00,9,25.52311989505786,25.573003992846413,57.49326779981057 +2024-09-08 01:00:00,9,36.18355454205322,25.32271210017408,44.36074717863361 +2024-09-08 02:00:00,9,37.29450625361794,29.348977812959976,45.67799285052772 +2024-09-08 03:00:00,9,22.3147994962719,25.11185184404108,62.783090643001294 +2024-09-08 04:00:00,9,10.04736170223796,23.981696573412737,30.940192878222117 +2024-09-08 05:00:00,9,36.70850292966673,19.91130094644714,39.992500542302636 +2024-09-08 06:00:00,9,36.0606408026929,24.767736921486946,62.55178087390171 +2024-09-08 07:00:00,9,29.087268391215297,29.80713058407445,47.412893508140414 +2024-09-08 08:00:00,9,29.20284786463547,24.796097670569857,67.16846970697786 +2024-09-08 09:00:00,9,24.609819457848968,22.597842518195293,60.12997595573538 +2024-09-08 10:00:00,9,23.295171889239306,25.977746226290535,59.69849302661974 +2024-09-08 11:00:00,9,27.1248110776986,21.817843845552964,50.02399753247214 +2024-09-08 12:00:00,9,8.996742241112806,21.812892703568224,44.8162316896682 +2024-09-08 13:00:00,9,19.659676205752543,24.01396828859924,65.85881668623601 +2024-09-08 14:00:00,9,31.5291829001011,26.432250624949628,54.842727801940114 +2024-09-08 15:00:00,9,30.344794940797097,31.885292684446792,55.80647029730294 +2024-09-08 16:00:00,9,17.209113867910304,20.76628596125034,40.22268201860239 +2024-09-08 17:00:00,9,32.759634982868555,23.55640592546039,44.56039576506568 +2024-09-08 18:00:00,9,41.4687323260582,14.579476606896918,59.7430452504823 +2024-09-08 19:00:00,9,13.043800310929306,27.202973683460897,51.07111697162088 +2024-09-08 20:00:00,9,19.737416265174176,19.965330719894382,44.13476591805542 +2024-09-08 21:00:00,9,34.31290553181193,15.155139957528593,48.40183679545606 +2024-09-08 22:00:00,9,43.051687125663676,18.296044906438553,49.84138292958312 +2024-09-08 23:00:00,9,30.142776947268853,24.18741911922117,52.59730883333844 +2024-09-09 00:00:00,9,17.670383821820984,28.371687367432376,63.389384129714074 +2024-09-09 01:00:00,9,43.15506647308555,22.539532558283433,62.63162870604511 +2024-09-09 02:00:00,9,35.02713832598015,24.956647271622202,43.79103229062537 +2024-09-09 03:00:00,9,18.817943698522406,20.873548896098512,61.58526355460001 +2024-09-09 04:00:00,9,32.78786217678014,27.05546054312663,65.25156501292079 +2024-09-09 05:00:00,9,29.848016384128158,27.08189734224762,46.93240870398779 +2024-09-09 06:00:00,9,48.05255218364031,19.37843550324412,79.58765719779741 +2024-09-09 07:00:00,9,30.489571973741533,16.787933946278773,67.8372431942043 +2024-09-09 08:00:00,9,45.75695781018749,24.9273169058851,47.12739294024167 +2024-09-09 09:00:00,9,24.659912211638296,19.38253024935925,59.441592688897515 +2024-09-09 10:00:00,9,30.61669844318708,23.256570350026337,54.78711884036526 +2024-09-09 11:00:00,9,15.442596770003954,25.09300879593094,52.69066911042228 +2024-09-09 12:00:00,9,27.141860567361455,27.656550851090497,52.481224659255474 +2024-09-09 13:00:00,9,20.80015226517969,19.14631903613628,56.19302544684713 +2024-09-09 14:00:00,9,33.42933814116001,21.2344991808015,52.65213692282913 +2024-09-09 15:00:00,9,21.409302954764208,25.544809856178443,48.21386536078589 +2024-09-09 16:00:00,9,28.06059202003087,25.03847225549869,50.52698205269994 +2024-09-09 17:00:00,9,21.061514040705532,23.28155610749772,40.43504040805716 +2024-09-09 18:00:00,9,20.078248226415617,29.056851420725856,45.51407341908691 +2024-09-09 19:00:00,9,28.351032794939094,22.850594556289934,48.77580284898671 +2024-09-09 20:00:00,9,33.08388012664084,23.299072721684944,64.41008716525751 +2024-09-09 21:00:00,9,45.91130936291568,22.17746105820375,29.84483993552314 +2024-09-09 22:00:00,9,13.299765981004132,18.775955450936397,65.49120274992069 +2024-09-09 23:00:00,9,22.497452122472133,13.35549601589928,63.12465496067171 +2024-09-10 00:00:00,9,22.92816241748696,24.759766282398566,67.3005258301482 +2024-09-10 01:00:00,9,17.542421059459,28.809495371412332,50.05044649035358 +2024-09-10 02:00:00,9,14.950107605480541,26.313670721245284,45.282592201578396 +2024-09-10 03:00:00,9,31.63242395926036,25.496293529376743,63.03854472903677 +2024-09-10 04:00:00,9,24.59269892502798,26.4801705627668,54.06068972805355 +2024-09-10 05:00:00,9,20.073603017208313,22.323655437325108,58.568647068212854 +2024-09-10 06:00:00,9,22.627222052915855,17.456899505750712,50.17287484956714 +2024-09-10 07:00:00,9,34.95758955189353,26.60235977601869,49.02811362775154 +2024-09-10 08:00:00,9,21.945866227767905,29.204662081138864,55.31293464036185 +2024-09-10 09:00:00,9,20.88853234677792,19.88286610718128,54.841449611365526 +2024-09-10 10:00:00,9,24.925716253732666,20.112375495110847,54.52573617589816 +2024-09-10 11:00:00,9,31.64596875014508,27.081652453765958,54.738177293397385 +2024-09-10 12:00:00,9,46.364460067287084,19.073440448804845,58.52497160647326 +2024-09-10 13:00:00,9,36.707009638683445,21.34391328796893,51.24624884534018 +2024-09-10 14:00:00,9,43.64190495808415,21.113451033971554,64.61574281637661 +2024-09-10 15:00:00,9,27.558871247797995,27.451087735992516,46.515177860086055 +2024-09-10 16:00:00,9,9.78220594978859,26.12480237237142,51.439418627858416 +2024-09-10 17:00:00,9,26.476968829997396,24.80000605780869,41.95897909590848 +2024-09-10 18:00:00,9,23.587543594438742,30.097463555905456,62.374940589462504 +2024-09-10 19:00:00,9,18.93946385195851,31.750334259149167,51.819685787233574 +2024-09-10 20:00:00,9,22.358589677136557,30.47021016507725,58.254999444057084 +2024-09-10 21:00:00,9,29.32759245508222,20.584592698289136,51.161464164736934 +2024-09-10 22:00:00,9,22.516136733685975,22.81721705829579,64.80260942072958 +2024-09-10 23:00:00,9,27.826333044433383,19.630120212728286,52.90851299692934 +2024-09-11 00:00:00,9,14.575347682560968,20.1115671379992,55.01098050294343 +2024-09-11 01:00:00,9,5.626846901103658,22.453013701495408,45.3165360747714 +2024-09-11 02:00:00,9,20.833005118352215,29.407787908954184,51.66673151666825 +2024-09-11 03:00:00,9,26.594479484066337,20.303940386563784,65.40856193899027 +2024-09-11 04:00:00,9,23.116426626308836,24.11004138514698,48.12204262616281 +2024-09-11 05:00:00,9,31.974199027226007,25.2267797770813,64.33501532164998 +2024-09-11 06:00:00,9,29.887843134842758,25.887279175281368,37.669793740081005 +2024-09-11 07:00:00,9,25.739052067333773,27.94473293773855,44.87124237058267 +2024-09-11 08:00:00,9,10.690320023764208,26.387246456892367,41.158330936002585 +2024-09-11 09:00:00,9,29.82693146142573,26.41490176087019,45.19962468014563 +2024-09-11 10:00:00,9,18.026083010841706,24.132745027703876,47.29249941894467 +2024-09-11 11:00:00,9,27.478150396388198,28.64501815925511,52.05317230751852 +2024-09-11 12:00:00,9,37.431827640393635,22.135079802249898,54.91745804479392 +2024-09-11 13:00:00,9,26.017257155850025,19.161346185218754,42.14791039256615 +2024-09-11 14:00:00,9,35.523377464533446,23.78802079538508,53.186028318198986 +2024-09-11 15:00:00,9,37.696425264019595,26.472373368770228,55.03465617449148 +2024-09-11 16:00:00,9,27.631836008740706,22.68507238933794,55.31145858228223 +2024-09-11 17:00:00,9,32.534541113665014,21.47291618258221,53.598997794573556 +2024-09-11 18:00:00,9,28.688032600535063,29.720969697985794,52.745741367912466 +2024-09-11 19:00:00,9,22.330682773554962,25.630432316339817,46.67307214190938 +2024-09-11 20:00:00,9,50.98858855281146,23.921392624767712,73.85534096850716 +2024-09-11 21:00:00,9,26.287702066895676,28.907783559113206,45.54949003848112 +2024-09-11 22:00:00,9,10.07999178360967,19.142706401985834,59.13158053264279 +2024-09-11 23:00:00,9,31.04027549685761,14.033194352642447,76.53645891747446 +2024-09-12 00:00:00,9,18.60198404358449,20.86017288368924,55.65502259947768 +2024-09-12 01:00:00,9,41.770604378955646,21.88476735512799,44.50886497755829 +2024-09-12 02:00:00,9,27.48089012798919,25.220272659173272,63.83699258483582 +2024-09-12 03:00:00,9,32.8912520488039,18.49663330665452,44.284960643924784 +2024-09-12 04:00:00,9,26.27119179205574,23.04577097444721,56.53709430707122 +2024-09-12 05:00:00,9,29.565944224189145,26.272700290424343,57.30271973295412 +2024-09-12 06:00:00,9,13.154372625051487,24.627224490742332,53.70107191382981 +2024-09-12 07:00:00,9,21.24454468814136,28.56332702944136,54.788268133753256 +2024-09-12 08:00:00,9,7.741577395861981,27.108223912906833,54.55488374229468 +2024-09-12 09:00:00,9,26.946664206877976,19.964923641451048,51.57847486828102 +2024-09-12 10:00:00,9,23.816211992571173,21.555272059004473,47.615900763544666 +2024-09-12 11:00:00,9,41.663305060224694,26.22016272876705,47.90229486919031 +2024-09-12 12:00:00,9,23.25116353280523,27.52200759541647,55.86460459445393 +2024-09-12 13:00:00,9,30.46643875553326,18.44096592822298,53.74956731944244 +2024-09-12 14:00:00,9,34.43332817970624,25.27174185861395,45.95687564472477 +2024-09-12 15:00:00,9,14.741529443336727,26.845450901251738,40.18201371515049 +2024-09-12 16:00:00,9,19.34851135497762,24.377822135003424,45.383113734934454 +2024-09-12 17:00:00,9,20.44114905275977,21.347107042971217,53.96028515012343 +2024-09-12 18:00:00,9,10.12601076944282,31.945088613538378,53.445943036965915 +2024-09-12 19:00:00,9,27.882847521677068,18.691840104288847,45.28814782384076 +2024-09-12 20:00:00,9,22.64116049688722,26.78244754262066,64.12150213554949 +2024-09-12 21:00:00,9,34.36545058823852,25.50940106219661,70.6364726304405 +2024-09-12 22:00:00,9,9.231351129936797,24.625579394557047,64.13293779058247 +2024-09-12 23:00:00,9,28.53953565744001,17.167000979577764,53.273062323534774 +2024-09-13 00:00:00,9,17.384974893881687,24.115535625584094,54.19852298841948 +2024-09-13 01:00:00,9,18.317027934943575,27.14905683314955,59.52986289959401 +2024-09-13 02:00:00,9,31.28267118204788,27.91217203308787,55.071982600014294 +2024-09-13 03:00:00,9,25.829519672397524,22.603527915380422,53.81928464755434 +2024-09-13 04:00:00,9,36.42937819838839,27.414141792711487,57.42343168343342 +2024-09-13 05:00:00,9,23.884746329977652,22.29424731501539,57.17561835843259 +2024-09-13 06:00:00,9,8.804984081760256,22.869094960941574,53.263043649772214 +2024-09-13 07:00:00,9,20.620999820382227,24.63400176781912,48.49273461210295 +2024-09-13 08:00:00,9,23.656848235298263,19.850178454611147,66.42157085237696 +2024-09-13 09:00:00,9,13.17018652461679,14.633077988290916,55.18221820386145 +2024-09-13 10:00:00,9,20.234484555365363,21.597790348900226,65.01806130163821 +2024-09-13 11:00:00,9,19.524532257664525,21.911500015036907,63.824175956390185 +2024-09-13 12:00:00,9,5.306955636201113,28.500053546586578,59.700033712850185 +2024-09-13 13:00:00,9,31.14073649390349,33.19938753361674,49.03369640635622 +2024-09-13 14:00:00,9,26.084236009598644,27.262723303669862,54.250936100623306 +2024-09-13 15:00:00,9,20.1665233758629,27.561720400175208,47.80449557993237 +2024-09-13 16:00:00,9,24.48543872400408,25.972801184835816,59.64398520066216 +2024-09-13 17:00:00,9,27.24635066835994,23.307180405514313,63.47009067785872 +2024-09-13 18:00:00,9,12.672984999089236,25.06776270254416,50.55771617356674 +2024-09-13 19:00:00,9,20.23467807853235,15.278014828659728,55.24984888246592 +2024-09-13 20:00:00,9,29.01144365785221,26.61853653896673,43.62362079157833 +2024-09-13 21:00:00,9,28.093196871613955,18.614776304146794,62.40096335940253 +2024-09-13 22:00:00,9,19.528825087779836,29.041707221008288,68.6896776635577 +2024-09-13 23:00:00,9,17.194803448421634,27.591700889687598,51.56869183168143 +2024-09-14 00:00:00,9,19.445326124923714,22.21735223244787,46.09053913671692 +2024-09-14 01:00:00,9,32.905094012724135,26.754466053061662,57.421928290746656 +2024-09-14 02:00:00,9,38.55843439305584,20.539681269310968,58.63696228276907 +2024-09-14 03:00:00,9,29.42004135492264,25.729875771059415,63.31723759284861 +2024-09-14 04:00:00,9,36.9709832489779,21.44505314904757,39.611841329047465 +2024-09-14 05:00:00,9,18.166308537133958,18.864830068403354,56.71576577367324 +2024-09-14 06:00:00,9,31.675647476147244,20.40516311760176,61.62521511498715 +2024-09-14 07:00:00,9,30.940544438079677,26.482792513097735,55.6449396771708 +2024-09-14 08:00:00,9,44.15765518463462,26.83912192536258,58.671356148785684 +2024-09-14 09:00:00,9,46.385822563870335,21.70807853333748,51.40884630534548 +2024-09-14 10:00:00,9,18.565602295224316,25.698527364996174,72.84192238151911 +2024-09-14 11:00:00,9,8.714834577728201,26.374464121094807,49.020882452077956 +2024-09-14 12:00:00,9,31.80673562899795,20.22861692409561,57.0633752775042 +2024-09-14 13:00:00,9,36.386869669103675,24.02845459983456,52.62583638880762 +2024-09-14 14:00:00,9,23.928720657130707,26.74372754812919,53.596624477835 +2024-09-14 15:00:00,9,14.23676842985954,26.763708635150838,42.68487326235166 +2024-09-14 16:00:00,9,20.259760599959407,21.458401217434286,66.61998108790294 +2024-09-14 17:00:00,9,23.41861866382557,28.421440772177395,45.06419067580103 +2024-09-14 18:00:00,9,40.502592298715015,22.471829727605723,47.00979083583835 +2024-09-14 19:00:00,9,37.84111205915014,27.11922891486674,58.72188436414987 +2024-09-14 20:00:00,9,23.21634309172017,21.653302217319187,52.710775474484535 +2024-09-14 21:00:00,9,12.15592436629742,17.824547671077134,56.38472333584681 +2024-09-14 22:00:00,9,17.454524985714613,28.2078620699141,51.75838185075149 +2024-09-14 23:00:00,9,17.678666835440815,24.852271054896498,66.71084231858838 +2024-09-15 00:00:00,9,28.89039662372369,13.058619551137522,44.89205388170762 +2024-09-15 01:00:00,9,22.335468356299636,28.852460669841957,61.959511547169505 +2024-09-15 02:00:00,9,35.75636032553325,27.234271729966075,68.53062898551048 +2024-09-15 03:00:00,9,34.70276856924929,23.037674999158394,48.38436742180907 +2024-09-15 04:00:00,9,36.403440118282205,20.917666980595754,53.45361051177654 +2024-09-15 05:00:00,9,24.747526835418032,23.16042764046932,55.036029018092876 +2024-09-15 06:00:00,9,21.986837562963967,25.63701862838851,39.62430390560789 +2024-09-15 07:00:00,9,29.575884233474778,23.851408682909806,57.94735034378954 +2024-09-15 08:00:00,9,25.07322377878407,20.76924088904543,49.0069805791577 +2024-09-15 09:00:00,9,17.62749738635256,17.319601895119465,59.30113722770581 +2024-09-15 10:00:00,9,28.32446374411041,23.958243859629576,65.16792042501399 +2024-09-15 11:00:00,9,30.041012738174267,22.73889097077537,49.66315861803104 +2024-09-15 12:00:00,9,22.5153709946084,22.16087307376947,55.09966460978231 +2024-09-15 13:00:00,9,12.090050217994499,26.46518620842006,59.62796958238273 +2024-09-15 14:00:00,9,21.150542858795905,20.912943096848167,56.16219290301595 +2024-09-15 15:00:00,9,35.95941682911263,27.04516074168845,47.31628225535009 +2024-09-15 16:00:00,9,22.321695616646668,25.15524235885052,54.63404530281771 +2024-09-15 17:00:00,9,13.364648067666495,25.06323642358268,64.9055563106802 +2024-09-15 18:00:00,9,18.473473246404904,23.50344700746628,48.719943325812196 +2024-09-15 19:00:00,9,35.26379057679081,28.64359987312898,41.97787911000797 +2024-09-15 20:00:00,9,30.137580870220035,22.582477931168693,55.43548374810346 +2024-09-15 21:00:00,9,29.561860831753446,17.373681684171586,44.94057904145043 +2024-09-15 22:00:00,9,19.716536977680736,19.557357105185027,66.97335686055837 +2024-09-15 23:00:00,9,19.737138450720362,27.371146254078504,68.52478337452379 +2024-09-16 00:00:00,9,16.94480883549525,29.98457416355982,63.45896620049584 +2024-09-16 01:00:00,9,25.683143328368903,22.549717627394717,42.79449503130546 +2024-09-16 02:00:00,9,41.341623296491406,26.11585461314139,50.39572127878549 +2024-09-16 03:00:00,9,21.926498272710962,21.529461502269204,50.51379265356434 +2024-09-16 04:00:00,9,15.409840306039317,24.109758459244844,50.3376052796056 +2024-09-16 05:00:00,9,15.378237055250569,24.317778235226122,51.81320671782623 +2024-09-16 06:00:00,9,41.45636402563782,27.885146655634163,51.46574744297591 +2024-09-16 07:00:00,9,18.01870784725722,27.549255507751298,43.77177038971014 +2024-09-16 08:00:00,9,14.419739795367754,22.420147542030644,43.850503286829095 +2024-09-16 09:00:00,9,44.78989824606298,27.425410968514115,57.643961186902274 +2024-09-16 10:00:00,9,28.759371392003185,25.301940026822376,59.57103226503843 +2024-09-16 11:00:00,9,23.747737678690143,21.09257453432691,63.170593091787154 +2024-09-16 12:00:00,9,34.479370251413116,21.51979851181312,46.2253333350162 +2024-09-16 13:00:00,9,33.94764072621651,26.30912135747258,63.24580188994328 +2024-09-16 14:00:00,9,25.762756666909283,29.86913382117282,45.31683052611228 +2024-09-16 15:00:00,9,24.587634913029028,18.188209424645027,39.28718013062651 +2024-09-16 16:00:00,9,37.010294161280214,30.63301776929808,38.86112218327058 +2024-09-16 17:00:00,9,31.041616986977598,27.924175499216737,51.94534233590247 +2024-09-16 18:00:00,9,6.044753067897727,22.46358260275492,49.76770043474293 +2024-09-16 19:00:00,9,31.657198891974296,20.742125760033886,61.01121224235387 +2024-09-16 20:00:00,9,30.80460180539107,18.453008100273628,47.36501737706162 +2024-09-16 21:00:00,9,20.871908734808194,25.25309602802128,47.27975977075516 +2024-09-16 22:00:00,9,22.746841362692813,26.702941735847737,40.509952907969975 +2024-09-16 23:00:00,9,28.98157751445368,21.198181794085997,50.665359766070864 +2024-09-17 00:00:00,9,32.68943300138869,20.809643237780286,50.20396576157855 +2024-09-17 01:00:00,9,31.17044243534749,23.23763243358545,45.12743918443793 +2024-09-17 02:00:00,9,9.185657588584501,25.73181904761286,42.050457740198624 +2024-09-17 03:00:00,9,27.47670685539886,22.725067142684527,44.760398028578734 +2024-09-17 04:00:00,9,43.59191195462488,24.45883030203675,53.71698947600895 +2024-09-17 05:00:00,9,14.0008948242473,18.338397086736386,49.92337507176478 +2024-09-17 06:00:00,9,42.51622443965995,17.878551468335186,55.53426708500987 +2024-09-17 07:00:00,9,22.091626147871917,26.426892786864002,43.84704541361994 +2024-09-17 08:00:00,9,11.920969502733808,23.67593244378598,59.211589425864304 +2024-09-17 09:00:00,9,32.2837900072913,17.716936326679786,46.82953323385905 +2024-09-17 10:00:00,9,29.53704593786143,25.233958817816966,60.772367043229025 +2024-09-17 11:00:00,9,17.085214523510142,20.028220778306277,47.98677395869907 +2024-09-17 12:00:00,9,20.569578406240982,24.750766190186262,56.1932577633027 +2024-09-17 13:00:00,9,26.189822162666786,23.067950235351088,47.268265497411015 +2024-09-17 14:00:00,9,31.969722138950498,34.486330707520224,63.97816901896961 +2024-09-17 15:00:00,9,30.063142465485416,24.233172970799384,60.37591522681337 +2024-09-17 16:00:00,9,38.69786800698353,30.405380680221185,56.9943082153722 +2024-09-17 17:00:00,9,44.939508469691205,19.880161916682102,29.05398426456376 +2024-09-17 18:00:00,9,14.249711206709328,29.107124289564027,59.23688876457636 +2024-09-17 19:00:00,9,19.251194342679895,21.95673267946737,58.14192919590268 +2024-09-17 20:00:00,9,16.864182914653078,21.881423252931945,44.69139269488671 +2024-09-17 21:00:00,9,16.42461388054162,30.31440557075387,52.673581606410686 +2024-09-17 22:00:00,9,20.949886983736935,14.353752078604533,63.837699920657016 +2024-09-17 23:00:00,9,20.091262978578754,26.72819249963312,43.18114028171207 +2024-09-18 00:00:00,9,29.281527458324906,17.90617742998782,50.391882818487005 +2024-09-18 01:00:00,9,8.366588552966029,22.576271146000607,46.02628712368367 +2024-09-18 02:00:00,9,32.194218342958685,21.745276377403446,52.2908485232482 +2024-09-18 03:00:00,9,17.212616199036614,24.6078387989083,55.59719725709528 +2024-09-18 04:00:00,9,23.88083119475672,15.812083235629304,51.26616403066373 +2024-09-18 05:00:00,9,20.781745474814027,27.137552573025808,45.877202415116905 +2024-09-18 06:00:00,9,24.91807582176464,26.42530588763476,44.481209707120705 +2024-09-18 07:00:00,9,27.473602203665152,27.12803963217628,55.82701978506326 +2024-09-18 08:00:00,9,21.59351845918591,22.61869491195355,67.65017278360106 +2024-09-18 09:00:00,9,30.630996723178576,18.73295279876217,52.80349477907902 +2024-09-18 10:00:00,9,19.713943838790613,32.50150274545366,39.38822473871703 +2024-09-18 11:00:00,9,11.647544075564706,24.003859711984163,49.32043056862287 +2024-09-18 12:00:00,9,12.133372338175619,24.83983079882441,46.539108770927086 +2024-09-18 13:00:00,9,9.023775484622426,27.34932905068824,62.422387566875926 +2024-09-18 14:00:00,9,22.16331212141443,26.57953306306666,52.53481377519925 +2024-09-18 15:00:00,9,8.762596729407385,27.288725426463508,51.892613409957185 +2024-09-18 16:00:00,9,21.344548239602034,27.301931553026087,62.75159302593911 +2024-09-18 17:00:00,9,22.789129931549862,27.71423520747826,40.93538909482888 +2024-09-18 18:00:00,9,27.76118919772784,26.232283442738492,63.72002129294398 +2024-09-18 19:00:00,9,34.51475588191917,23.124460158409217,58.6652043225366 +2024-09-18 20:00:00,9,19.992511376228165,28.582199713249317,49.230971821495594 +2024-09-18 21:00:00,9,44.76414334704225,21.895914680365827,65.16304289393366 +2024-09-18 22:00:00,9,12.680149027652854,23.161219900153878,65.24085193451089 +2024-09-18 23:00:00,9,14.509334426569684,24.0837503574094,83.6436528640811 +2024-09-19 00:00:00,9,18.63207353931526,22.42643901540655,58.87916552711186 +2024-09-19 01:00:00,9,16.73614324323579,25.603979653333397,55.74770506472367 +2024-09-19 02:00:00,9,33.30865571359413,21.52583659643999,56.248535828967334 +2024-09-19 03:00:00,9,21.762310623239525,22.986140768564894,48.36918389367561 +2024-09-19 04:00:00,9,20.537532938809974,28.34159729020058,52.95105264027391 +2024-09-19 05:00:00,9,16.544415558334727,18.345070138470376,43.139151921032905 +2024-09-19 06:00:00,9,17.559693865965954,20.038885012629542,62.69467292303389 +2024-09-19 07:00:00,9,27.739284222000013,23.190261556889585,46.982849063693386 +2024-09-19 08:00:00,9,34.37143839909929,28.874676881593526,53.42031053858871 +2024-09-19 09:00:00,9,25.084780820324138,22.298835517296567,54.19363151167994 +2024-09-19 10:00:00,9,47.6170902031363,21.945642991192297,53.581922039148395 +2024-09-19 11:00:00,9,21.040081802528956,32.46959829028641,65.92948636216084 +2024-09-19 12:00:00,9,22.482727245219305,22.81355004435919,57.03534357843826 +2024-09-19 13:00:00,9,28.190331295752163,28.50239706474299,69.64790691556038 +2024-09-19 14:00:00,9,28.102107648181317,23.39225066313761,52.03923725391089 +2024-09-19 15:00:00,9,25.315590900594785,22.06089210481425,46.653061707454256 +2024-09-19 16:00:00,9,33.21784018223462,33.097068088642494,33.57863206939054 +2024-09-19 17:00:00,9,26.252692832685895,25.806019393107224,51.845958943262886 +2024-09-19 18:00:00,9,11.737517430354183,26.100669055013157,42.230582385526894 +2024-09-19 19:00:00,9,34.304577211441746,27.609104777214096,58.58397973993103 +2024-09-19 20:00:00,9,23.309738458276218,24.587643644989527,67.94592131352881 +2024-09-19 21:00:00,9,33.52803303506786,27.834154182010366,52.2507326649457 +2024-09-19 22:00:00,9,19.094632504868237,18.208102269092684,56.49274708843055 +2024-09-19 23:00:00,9,26.45785116285525,20.940554840459466,53.77551552524056 +2024-09-20 00:00:00,9,36.30770794516499,23.820346336424,61.088893607833135 +2024-09-20 01:00:00,9,18.38035283971442,20.49262997873405,49.80019257543761 +2024-09-20 02:00:00,9,37.10950422446426,24.07391829708751,58.99385568837714 +2024-09-20 03:00:00,9,25.445124428933752,19.798169328216893,52.76730266098572 +2024-09-20 04:00:00,9,27.39773318993442,25.821133432930672,66.53851077881797 +2024-09-20 05:00:00,9,20.855274395991856,25.367599419634264,47.62336780421767 +2024-09-20 06:00:00,9,32.363563981973286,21.04159678479729,57.48550090022823 +2024-09-20 07:00:00,9,16.33410492756709,21.98799705147806,63.77332322573162 +2024-09-20 08:00:00,9,21.173822938788582,21.267527082351442,58.000179365040495 +2024-09-20 09:00:00,9,25.762043050322703,25.87198236135507,65.99132283051821 +2024-09-20 10:00:00,9,37.26013806006702,25.53731700448805,57.195956343412554 +2024-09-20 11:00:00,9,25.87898155553403,21.07786449549879,56.399165260973724 +2024-09-20 12:00:00,9,23.12815888112391,24.348118820952266,67.19964366201036 +2024-09-20 13:00:00,9,31.395925914087737,24.284982665579797,66.50422808052076 +2024-09-20 14:00:00,9,15.67244213729526,26.683714348875927,63.5487575398458 +2024-09-20 15:00:00,9,23.077966011465215,26.709130309026722,62.26435704926136 +2024-09-20 16:00:00,9,35.12516773841169,18.58135027054,59.32769314465513 +2024-09-20 17:00:00,9,27.362850949490777,30.29129778038954,57.94290006504943 +2024-09-20 18:00:00,9,14.963473739237024,25.231827873444015,68.29876525533764 +2024-09-20 19:00:00,9,27.47533930575161,21.998416970986224,45.00642089332058 +2024-09-20 20:00:00,9,24.526115985982777,18.60506903028459,66.19525717575482 +2024-09-20 21:00:00,9,22.741245328561924,27.133399979195598,55.950656951618114 +2024-09-20 22:00:00,9,40.24019411264663,20.103685623348724,78.98360217770224 +2024-09-20 23:00:00,9,22.480375799268298,28.548585131168423,61.657063733094 +2024-09-21 00:00:00,9,23.55333854588374,17.308019326102876,66.2960851510139 +2024-09-21 01:00:00,9,22.73754222343057,21.09112757401901,49.54277157099243 +2024-09-21 02:00:00,9,21.62970065023579,22.74472781419921,55.808614637203576 +2024-09-21 03:00:00,9,16.97960431969557,24.497994538670937,65.48074252092947 +2024-09-21 04:00:00,9,40.00071417885698,26.066190254544086,42.17266487519412 +2024-09-21 05:00:00,9,32.17070759231945,26.47960141151628,58.648413992429354 +2024-09-21 06:00:00,9,32.85133228016266,23.095149361221228,57.57162241106925 +2024-09-21 07:00:00,9,33.72956813956585,25.945479959944176,45.77698078382522 +2024-09-21 08:00:00,9,27.943913046160553,22.4658601714533,55.65289143344175 +2024-09-21 09:00:00,9,27.454590338874404,24.143687681845908,59.77914863280266 +2024-09-21 10:00:00,9,21.409376986065475,22.840717173162528,59.73956769503346 +2024-09-21 11:00:00,9,37.85207023495266,24.880096627460468,62.03010417163937 +2024-09-21 12:00:00,9,25.883562626023053,27.474633904709474,48.91654168043675 +2024-09-21 13:00:00,9,22.43261929340419,28.534514746554425,51.31344650027809 +2024-09-21 14:00:00,9,15.453304336864502,21.919780878159784,54.548042650641776 +2024-09-21 15:00:00,9,10.40700177943744,24.570935892683952,53.87855779999862 +2024-09-21 16:00:00,9,26.818847441166685,25.222737341346296,75.35053516856529 +2024-09-21 17:00:00,9,27.638223492776582,35.13176844131668,62.60484207709277 +2024-09-21 18:00:00,9,24.00510925390884,20.09103135767841,70.42324680161676 +2024-09-21 19:00:00,9,44.449547023923294,27.55767886959254,39.73630984205087 +2024-09-21 20:00:00,9,12.58198973308857,23.635701578767932,60.85373399774337 +2024-09-21 21:00:00,9,21.727972658843647,30.24156740972165,52.188046145604076 +2024-09-21 22:00:00,9,28.00088206892766,25.142459073014265,64.3901806422835 +2024-09-21 23:00:00,9,27.16346927101937,18.84665505275636,68.26541049898304 +2024-09-22 00:00:00,9,4.904979765206232,23.11052645158823,35.67975625857435 +2024-09-22 01:00:00,9,26.897534611598324,24.195883890705836,45.63659999116904 +2024-09-22 02:00:00,9,19.605107216632703,27.73905689727993,66.54005443161233 +2024-09-22 03:00:00,9,34.89710835029747,27.48654695324668,56.716063866025145 +2024-09-22 04:00:00,9,27.873301721058088,29.829569023712178,60.7586869987751 +2024-09-22 05:00:00,9,27.265540518207874,25.50741752981878,39.505665940203365 +2024-09-22 06:00:00,9,27.56456833744895,24.038283848975045,59.538447925492775 +2024-09-22 07:00:00,9,30.847819539898833,15.634811077336805,67.8930807107276 +2024-09-22 08:00:00,9,34.12500297278055,26.788005312073984,61.09552752351451 +2024-09-22 09:00:00,9,32.45900462522761,25.917145399230485,63.189059153803356 +2024-09-22 10:00:00,9,19.88452451677662,21.23043462882799,71.75357121230195 +2024-09-22 11:00:00,9,2.4928027005231463,25.1950377728434,43.955459298929284 +2024-09-22 12:00:00,9,20.003264793571592,23.749855024103045,56.487098063279035 +2024-09-22 13:00:00,9,38.49855350408798,24.50003447180388,47.570907352581706 +2024-09-22 14:00:00,9,27.56057063714887,25.142238464405246,45.347718463474386 +2024-09-22 15:00:00,9,4.198672659535063,28.0636693028226,43.77717745431639 +2024-09-22 16:00:00,9,33.48679420151862,33.80416414564878,41.61126140674306 +2024-09-22 17:00:00,9,39.81307246866922,27.184202008701075,49.502582901522935 +2024-09-22 18:00:00,9,40.1903796967435,18.491034796060372,60.107570707384234 +2024-09-22 19:00:00,9,38.91192661712857,32.1583431158157,49.09574612242785 +2024-09-22 20:00:00,9,16.93954234412513,23.59114319893991,57.59440178216873 +2024-09-22 21:00:00,9,30.530371651315505,20.244873130251214,47.25203734898971 +2024-09-22 22:00:00,9,16.76386003136657,22.63915060852278,51.60139763323973 +2024-09-22 23:00:00,9,8.772065213626536,23.268133228621338,44.896753252803705 +2024-09-23 00:00:00,9,32.381833557079716,27.055576163024114,55.937436453424105 +2024-09-23 01:00:00,9,27.400647437218247,23.383519420452163,53.04377824499365 +2024-09-23 02:00:00,9,18.106648832145297,26.374313009366343,42.341547942573015 +2024-09-23 03:00:00,9,34.79589817734378,27.209309405514894,46.4866955408441 +2024-09-23 04:00:00,9,41.116716758418164,25.34200042507927,43.87123361895592 +2024-09-23 05:00:00,9,8.301948742640064,22.507114758902752,44.97448172158372 +2024-09-23 06:00:00,9,21.06612670861632,20.245308904552143,49.88286758971907 +2024-09-23 07:00:00,9,14.83801499973903,27.488537017089683,69.69898988330881 +2024-09-23 08:00:00,9,40.96506719741857,23.836367617633393,56.646732683061956 +2024-09-23 09:00:00,9,35.90366401469204,21.83925098045128,40.912958533176116 +2024-09-23 10:00:00,9,27.463545417319896,21.03269445215771,50.081727643613824 +2024-09-23 11:00:00,9,20.484144769280107,17.908252712211514,64.00197800209214 +2024-09-23 12:00:00,9,14.643506366345749,25.385954677585353,63.83699912749421 +2024-09-23 13:00:00,9,24.772576526512413,27.160195288768797,56.78325436267904 +2024-09-23 14:00:00,9,36.246563971115634,22.894495909775927,58.99145185087757 +2024-09-23 15:00:00,9,23.90004169951227,21.789412794583512,51.988807952312406 +2024-09-23 16:00:00,9,31.028542043400392,20.518184303803416,51.062847267289044 +2024-09-23 17:00:00,9,13.491740592948075,23.8007170108907,58.67650206878858 +2024-09-23 18:00:00,9,27.3628882723257,23.866893507125866,49.37576370690353 +2024-09-23 19:00:00,9,25.817072134595666,22.64692813108651,52.51386998647919 +2024-09-23 20:00:00,9,24.738702291410643,19.796485620796435,55.72643995433793 +2024-09-23 21:00:00,9,29.476215679190748,26.5232682520072,55.49695027557527 +2024-09-23 22:00:00,9,36.56263930755422,20.501132240142766,69.85997628875647 +2024-09-23 23:00:00,9,30.315071622511898,21.257651178333088,47.62249540541145 +2024-09-24 00:00:00,9,32.86067943505519,22.94895589719114,43.878225773560445 +2024-09-24 01:00:00,9,27.451475271633523,19.988669981116402,56.620253930766346 +2024-09-24 02:00:00,9,32.491443447345134,19.391316129615426,47.81886745580486 +2024-09-24 03:00:00,9,16.09991021276421,32.076929197170585,54.825678730866564 +2024-09-24 04:00:00,9,6.074474340680535,25.813871218133634,55.94864452975944 +2024-09-24 05:00:00,9,36.88748339463541,27.775836377855722,65.02547736736634 +2024-09-24 06:00:00,9,32.584272997700744,19.42472755139415,37.98774807574104 +2024-09-24 07:00:00,9,32.87579757027456,29.27478534292685,56.746080436279364 +2024-09-24 08:00:00,9,21.335290489691378,22.682279527926937,60.04902084125626 +2024-09-24 09:00:00,9,39.49935541022521,25.931582338150513,46.0636986350375 +2024-09-24 10:00:00,9,18.87007555818868,20.40640363926375,51.71645142300029 +2024-09-24 11:00:00,9,40.600318467546295,24.63295520713572,56.82953136560812 +2024-09-24 12:00:00,9,33.13417805697362,29.311796710524767,56.63772657123325 +2024-09-24 13:00:00,9,30.179192473073616,27.10205190822265,51.99646174471532 +2024-09-24 14:00:00,9,33.08334561223746,30.956995186062414,45.95562883304191 +2024-09-24 15:00:00,9,23.16932505846615,14.329371817111449,42.67663461636265 +2024-09-24 16:00:00,9,33.99749598262129,27.505758781476835,59.985194649117375 +2024-09-24 17:00:00,9,30.979116560108597,22.61460247223347,45.47303603269271 +2024-09-24 18:00:00,9,36.12972985641361,30.29062408372944,58.441902159754996 +2024-09-24 19:00:00,9,11.203493966162643,23.28914528050558,60.02789843154213 +2024-09-24 20:00:00,9,35.597891323435654,20.77551801350226,41.677792806886316 +2024-09-24 21:00:00,9,31.127355647414404,22.65654119280027,47.89706993768182 +2024-09-24 22:00:00,9,29.635367225742957,19.44717092493015,50.18319136557412 +2024-09-24 23:00:00,9,10.76780220375559,21.889912429522006,47.14070426098476 +2024-09-25 00:00:00,9,27.16430485631055,19.262798095694624,57.66924992908612 +2024-09-25 01:00:00,9,27.75927866729207,23.20906259761478,44.8979523700761 +2024-09-25 02:00:00,9,43.298011227362956,25.01788987402695,52.81729592169475 +2024-09-25 03:00:00,9,9.81146103612047,24.822496399871042,49.4220986368956 +2024-09-25 04:00:00,9,28.456198779352952,22.616954206791675,56.375366898711285 +2024-09-25 05:00:00,9,27.48748042768102,28.41134077815046,44.99353722116073 +2024-09-25 06:00:00,9,26.529640682559464,22.814267130156306,71.65469100763252 +2024-09-25 07:00:00,9,26.4650401732218,20.12023050877193,52.21262770952958 +2024-09-25 08:00:00,9,42.88959183605829,22.085165161232005,46.96730210522077 +2024-09-25 09:00:00,9,24.688169996710613,18.54564641702297,50.13723196697753 +2024-09-25 10:00:00,9,16.902857183578163,17.60791890892544,51.02461306322998 +2024-09-25 11:00:00,9,30.126512943764343,24.312487615471973,53.83069886401717 +2024-09-25 12:00:00,9,33.563938968706005,28.0971808590667,43.64847296190169 +2024-09-25 13:00:00,9,32.57728986687567,27.337075823496903,45.824618264006205 +2024-09-25 14:00:00,9,27.38740238428336,21.72422237703508,43.6688848554681 +2024-09-25 15:00:00,9,38.98005614709323,27.485782901631957,56.83631597259621 +2024-09-25 16:00:00,9,19.7095209986326,21.68984387196731,51.787557152280904 +2024-09-25 17:00:00,9,26.882203269693093,21.415822170728884,63.30979916648482 +2024-09-25 18:00:00,9,29.594821767479107,18.199379060054472,52.95542226443494 +2024-09-25 19:00:00,9,22.456854532120925,20.42822892770974,44.44065587491174 +2024-09-25 20:00:00,9,11.426225574119252,12.85093215823727,55.710789702762106 +2024-09-25 21:00:00,9,38.75805707756131,17.538956725981684,57.56797340312143 +2024-09-25 22:00:00,9,41.84456226298395,24.260278680415546,58.93071610132234 +2024-09-25 23:00:00,9,19.024575793242846,22.923557006279395,52.36596207805079 +2024-09-26 00:00:00,9,17.757498015153523,27.794305497332058,47.85270627167597 +2024-09-26 01:00:00,9,29.187916563187837,23.236890278358228,54.933918375768044 +2024-09-26 02:00:00,9,27.391633209248013,24.69921506161406,61.36653435385395 +2024-09-26 03:00:00,9,20.89521753374092,24.250251735078365,52.20197940254893 +2024-09-26 04:00:00,9,30.201082017825172,24.987793424768242,43.87717511627655 +2024-09-26 05:00:00,9,38.869500613989054,25.636950916426063,49.769913531735 +2024-09-26 06:00:00,9,20.586278298679147,23.433908288910356,59.53687925006142 +2024-09-26 07:00:00,9,15.42336937121754,19.42768105173596,49.1419800380546 +2024-09-26 08:00:00,9,30.72771816437495,23.787587978712413,57.981131308839736 +2024-09-26 09:00:00,9,33.157779151907185,21.508914080653184,53.939603106893074 +2024-09-26 10:00:00,9,18.742230010628806,22.565301642247572,61.28822968244614 +2024-09-26 11:00:00,9,5.6856043420117786,23.937440964875762,65.58612924908465 +2024-09-26 12:00:00,9,24.280733698889865,31.5296859135102,59.68770133489717 +2024-09-26 13:00:00,9,33.933973854077976,21.704135995777264,52.311451465699335 +2024-09-26 14:00:00,9,16.826818702858567,26.08898128359224,38.310490596998676 +2024-09-26 15:00:00,9,33.17060583715303,23.51355947752452,46.89443343170986 +2024-09-26 16:00:00,9,43.804455811003116,26.682963287131596,62.72389081275539 +2024-09-26 17:00:00,9,13.227048054548613,23.82823582170951,54.65187682090853 +2024-09-26 18:00:00,9,18.01637248488029,24.86985817443715,64.80535542982628 +2024-09-26 19:00:00,9,51.15666194375856,28.721543597918956,50.12987686962369 +2024-09-26 20:00:00,9,22.372810603673056,27.335895801814473,54.686011579354144 +2024-09-26 21:00:00,9,22.537773039963422,27.811063439848585,62.335283762598195 +2024-09-26 22:00:00,9,32.76386511740754,24.088703572854985,56.78249839554255 +2024-09-26 23:00:00,9,26.066006588970538,17.893761635428454,53.59817368040478 +2024-09-27 00:00:00,9,29.997639491590192,27.16098605817889,56.25568179835545 +2024-09-27 01:00:00,9,38.254202766236766,23.79070034180942,57.60611980555667 +2024-09-27 02:00:00,9,17.19974717971631,26.518137161065987,43.353222031841824 +2024-09-27 03:00:00,9,27.724036144907984,24.87230326893405,45.63448021629845 +2024-09-27 04:00:00,9,20.601009612000112,18.195636742452436,29.984766061010443 +2024-09-27 05:00:00,9,16.48114839483671,26.971789113576293,49.00256126499444 +2024-09-27 06:00:00,9,19.87984225131455,28.30041509918748,57.00752931212809 +2024-09-27 07:00:00,9,22.60987438103518,21.969132533802618,68.69598558880617 +2024-09-27 08:00:00,9,21.520177020884887,26.37473028928241,54.61304654496192 +2024-09-27 09:00:00,9,10.264572264195532,22.663259947104898,63.65638311157418 +2024-09-27 10:00:00,9,36.01157721048373,21.209672296052275,64.88883635822454 +2024-09-27 11:00:00,9,54.118488064758935,28.80212168927836,59.004020310642574 +2024-09-27 12:00:00,9,34.686782304395564,34.231263863666875,29.224135761549952 +2024-09-27 13:00:00,9,20.99995889356365,22.157885046094485,46.17786950339656 +2024-09-27 14:00:00,9,30.44888737949063,25.147837815801267,48.71491508747218 +2024-09-27 15:00:00,9,36.14165104966291,29.28121156706887,48.81917160232348 +2024-09-27 16:00:00,9,27.94136583075458,23.07159661371575,53.28864557924119 +2024-09-27 17:00:00,9,24.5208942499741,26.279234343575673,50.79176164767226 +2024-09-27 18:00:00,9,25.428488584869175,16.53991064112708,55.6688733153411 +2024-09-27 19:00:00,9,20.572021389657458,25.560726254463997,55.70862495966 +2024-09-27 20:00:00,9,32.74433443477735,21.671776564900313,41.02270931877791 +2024-09-27 21:00:00,9,24.223109182064825,19.39279064720421,48.330071062253026 +2024-09-27 22:00:00,9,31.79407769792737,23.596930152474304,54.431601630365314 +2024-09-27 23:00:00,9,28.931917818819976,21.888185744354956,64.37380842289092 +2024-09-28 00:00:00,9,21.957854129955507,27.14931715586168,60.24832956438176 +2024-09-28 01:00:00,9,17.19843567206681,17.652479376751945,43.13214651219171 +2024-09-28 02:00:00,9,34.41733457278208,27.667973424320195,38.943484229272954 +2024-09-28 03:00:00,9,47.619145834731135,19.41784421131333,58.41234682558798 +2024-09-28 04:00:00,9,18.037484737665267,25.301282371761893,51.584083840248056 +2024-09-28 05:00:00,9,28.310016681112252,23.61037600537291,55.520722652681364 +2024-09-28 06:00:00,9,15.365185785068523,29.212771083971838,58.28889721260465 +2024-09-28 07:00:00,9,45.13392851600132,27.56286143517095,66.57298899362212 +2024-09-28 08:00:00,9,34.12069811746762,18.54865297788882,51.956462077128386 +2024-09-28 09:00:00,9,12.879506984316983,17.28629143900188,58.7409694345232 +2024-09-28 10:00:00,9,34.56961708329316,20.50614794316631,59.82147483721606 +2024-09-28 11:00:00,9,32.59144457908123,25.995055592648303,41.012937814736496 +2024-09-28 12:00:00,9,15.731352653419158,27.303659003262638,55.086884248100596 +2024-09-28 13:00:00,9,17.64210013377705,25.500272322303427,63.70603401419792 +2024-09-28 14:00:00,9,18.6322320809652,16.82601044081285,58.338129747278266 +2024-09-28 15:00:00,9,28.592242757353848,24.569224327250776,37.94283725675544 +2024-09-28 16:00:00,9,21.526882292201098,26.28793572145515,62.105102264208575 +2024-09-28 17:00:00,9,29.956789574542285,17.523603112779053,59.97785670409599 +2024-09-28 18:00:00,9,25.77740710732523,24.69883936627269,53.479906560720416 +2024-09-28 19:00:00,9,28.011611289217818,27.845597271855272,42.94080937834785 +2024-09-28 20:00:00,9,13.777653162164103,24.337625916324914,51.83696539003682 +2024-09-28 21:00:00,9,25.93543917487988,21.60600461756977,58.14933333963412 +2024-09-28 22:00:00,9,20.705569909933196,15.12261531105121,57.39327915611351 +2024-09-28 23:00:00,9,20.77580923874546,21.562939037369087,48.74434978007825 +2024-09-29 00:00:00,9,35.51296929224171,24.85585241070234,55.13819366580966 +2024-09-29 01:00:00,9,11.3227215215872,23.22442009539144,61.38458342214649 +2024-09-29 02:00:00,9,7.458469116128001,23.728020688103136,43.55032259785119 +2024-09-29 03:00:00,9,25.664789155855793,23.289958546555056,56.735494404303836 +2024-09-29 04:00:00,9,21.318181437719133,29.23259343041321,62.86555848380694 +2024-09-29 05:00:00,9,35.26611667997456,25.61863622824575,45.804930362504784 +2024-09-29 06:00:00,9,25.375028835561782,23.103641242953934,69.8545516209019 +2024-09-29 07:00:00,9,26.84158713055175,15.1282768956215,64.11270819132979 +2024-09-29 08:00:00,9,29.191628726794423,23.884880223824585,39.741369127943166 +2024-09-29 09:00:00,9,25.225943094932607,24.16409908413783,54.09496487899593 +2024-09-29 10:00:00,9,9.569760763987203,26.94788928076457,53.74170700033267 +2024-09-29 11:00:00,9,19.254133977335574,28.11708086734609,41.51648097325338 +2024-09-29 12:00:00,9,42.84270184196856,23.175628664078147,55.380328928609 +2024-09-29 13:00:00,9,27.838423118255978,24.33838595923737,52.93229445767944 +2024-09-29 14:00:00,9,41.92216365009625,24.473724060094707,46.39527474232009 +2024-09-29 15:00:00,9,18.131784905997456,23.13501753253748,51.96056308850885 +2024-09-29 16:00:00,9,18.859068562576866,23.056919662082446,54.00281084394207 +2024-09-29 17:00:00,9,28.46851052828758,26.580777978657004,50.45759089342823 +2024-09-29 18:00:00,9,11.06603364300657,32.45231108608426,39.58326489223702 +2024-09-29 19:00:00,9,47.47176813840018,20.67250389507762,56.378981883840126 +2024-09-29 20:00:00,9,33.65978864759532,27.64801534830903,75.2275822721289 +2024-09-29 21:00:00,9,12.751875249407354,23.458649352889697,62.99538389158616 +2024-09-29 22:00:00,9,18.082386441860063,19.453763410590078,49.95303155116174 +2024-09-29 23:00:00,9,24.54624554835165,23.245513806176923,61.507458163232506 +2024-09-30 00:00:00,9,32.443223470695735,26.28387722496582,57.04116211501569 +2024-09-30 01:00:00,9,17.525535896650002,22.696382254566657,73.69427596802224 +2024-09-30 02:00:00,9,16.570614657328314,20.48946198263887,65.19542482046529 +2024-09-30 03:00:00,9,21.256208301222657,16.99507883075682,53.307071028649055 +2024-09-30 04:00:00,9,23.650448911732042,21.924197411543602,51.22043257325473 +2024-09-30 05:00:00,9,21.643147126006518,24.723809568927624,52.90372590487739 +2024-09-30 06:00:00,9,27.43612789586776,26.37465559353661,68.97860332810528 +2024-09-30 07:00:00,9,48.82488328593289,24.735948199319715,55.78917142151576 +2024-09-30 08:00:00,9,19.03252723899913,28.917703438225463,53.67014511262257 +2024-09-30 09:00:00,9,20.67461257119978,24.46871531758322,38.78197359033801 +2024-09-30 10:00:00,9,21.990068440797508,21.180608421177237,70.82366359488896 +2024-09-30 11:00:00,9,11.675328660387693,25.409182518927796,54.65699467076154 +2024-09-30 12:00:00,9,22.346980532446825,30.310085647543392,56.778845006149645 +2024-09-30 13:00:00,9,37.02691933873102,28.27115050966592,66.19427603498909 +2024-09-30 14:00:00,9,20.021864569423286,20.136962548377547,74.43779510897514 +2024-09-30 15:00:00,9,29.675998703257708,25.295715937134784,48.13097976966905 +2024-09-30 16:00:00,9,24.331803078939974,31.83167503384857,66.07260675640646 +2024-09-30 17:00:00,9,10.343852405012495,28.589265595730907,62.67868529353731 +2024-09-30 18:00:00,9,35.30956078406845,25.806548668986395,59.65395653560265 +2024-09-30 19:00:00,9,23.032918275255966,23.386247222561874,60.990119305854805 +2024-09-30 20:00:00,9,25.357022949435542,28.513043398632355,55.46548499030301 +2024-09-30 21:00:00,9,18.560394172734025,16.601607870839157,57.97197675567574 +2024-09-30 22:00:00,9,6.1125004698992775,20.416358182262382,70.67621934552321 +2024-09-30 23:00:00,9,44.86896199026685,23.144255439862643,52.390514746771764 +2024-10-01 00:00:00,9,13.52395374999491,25.383133533413908,52.58629631443279 +2024-10-01 01:00:00,9,39.47720204435397,24.607541928913232,46.26208830593622 +2024-10-01 02:00:00,9,28.79582524362046,23.082992426183203,61.51423477206766 +2024-10-01 03:00:00,9,17.5410649347293,26.398157795796863,50.85288882027476 +2024-10-01 04:00:00,9,16.90475190078204,23.366314741887685,62.63710587505241 +2024-10-01 05:00:00,9,30.415795086617994,20.088031046111716,47.8146611713582 +2024-10-01 06:00:00,9,42.23192163595016,21.30493624756424,48.19318872155928 +2024-10-01 07:00:00,9,17.848678772654843,18.13108241304723,52.85753572780388 +2024-10-01 08:00:00,9,35.1622087868305,19.551907510428585,54.3138610368566 +2024-10-01 09:00:00,9,21.400736739547753,22.353083686495026,49.17725079477251 +2024-10-01 10:00:00,9,6.486007805253628,25.86051686123225,62.98795013152643 +2024-10-01 11:00:00,9,27.58161981538907,19.183790119454784,45.28951933443352 +2024-10-01 12:00:00,9,32.535685004476704,24.015124833447125,56.905545915378504 +2024-10-01 13:00:00,9,28.954455849766504,22.33500579784564,48.57295218311318 +2024-10-01 14:00:00,9,24.66758642009178,25.148103431830833,71.46178534749689 +2024-10-01 15:00:00,9,26.346223857739822,29.126600813492402,45.70280594270366 +2024-10-01 16:00:00,9,32.78103691562184,34.56514104917628,44.813728550500535 +2024-10-01 17:00:00,9,26.98670921768844,25.48201902434412,50.41302889797602 +2024-10-01 18:00:00,9,21.66130564477543,32.362011238719965,33.30127232775606 +2024-10-01 19:00:00,9,27.593796698069163,17.323370032020588,55.7778443506216 +2024-10-01 20:00:00,9,27.315384093062953,21.47039185694878,68.07711696726763 +2024-10-01 21:00:00,9,17.36932322616194,21.311750128453177,44.832419759687824 +2024-10-01 22:00:00,9,26.06140682983445,14.617146037725124,42.08650524766709 +2024-10-01 23:00:00,9,23.525361073386495,19.885434543511245,53.117802260843234 +2024-10-02 00:00:00,9,26.962981125435604,22.737005744077965,32.14082074079687 +2024-10-02 01:00:00,9,28.18706752424186,29.849902365984644,58.252456572531294 +2024-10-02 02:00:00,9,33.529581297263185,31.717973346058383,49.163495937033446 +2024-10-02 03:00:00,9,21.988202970261018,22.88508397730956,55.07806815421349 +2024-10-02 04:00:00,9,33.142113932599635,23.753564048516694,39.57900751764866 +2024-10-02 05:00:00,9,22.09562528139977,28.27849731333614,56.02605031308508 +2024-10-02 06:00:00,9,38.12016839506808,23.91375932621023,61.257996876708646 +2024-10-02 07:00:00,9,20.582748502531462,24.99973434603369,59.55751955921362 +2024-10-02 08:00:00,9,24.539297360041097,21.7646430147164,61.15320357032888 +2024-10-02 09:00:00,9,12.596398361656059,29.676013260353695,52.377983407848575 +2024-10-02 10:00:00,9,10.77027680065129,24.658093937507765,54.75740503667534 +2024-10-02 11:00:00,9,22.75442042753677,24.81249399098829,51.94989670189071 +2024-10-02 12:00:00,9,18.06711045945557,22.899951284152063,47.105780910187555 +2024-10-02 13:00:00,9,31.479231747269935,21.295787561256873,40.98922689091843 +2024-10-02 14:00:00,9,38.23742493072457,29.168511361901164,52.95256013151684 +2024-10-02 15:00:00,9,19.048842485680453,23.82052703059481,44.02362617851105 +2024-10-02 16:00:00,9,34.436365867367876,29.94616533847629,37.704125717609465 +2024-10-02 17:00:00,9,21.500657079655426,22.56907129915195,51.11304778639624 +2024-10-02 18:00:00,9,20.739019384293748,23.417718143698373,56.52055542060247 +2024-10-02 19:00:00,9,22.464016536983,19.122492141550563,52.127514492069444 +2024-10-02 20:00:00,9,32.93777103277977,18.49272977558792,73.84030447325644 +2024-10-02 21:00:00,9,18.5630718605139,22.19969625652634,53.05188846325932 +2024-10-02 22:00:00,9,19.709680460944202,20.142395834380917,44.202925118594365 +2024-10-02 23:00:00,9,16.79174477373491,27.95770922680985,58.69125193349272 +2024-10-03 00:00:00,9,30.78757247164381,22.91110648797486,55.21081919410031 +2024-10-03 01:00:00,9,29.349219536444185,20.622577754263805,46.903814901666514 +2024-10-03 02:00:00,9,25.146619596052602,25.15917030172202,46.99930639870675 +2024-10-03 03:00:00,9,36.70892637615385,21.82251045676433,48.528237251033104 +2024-10-03 04:00:00,9,11.401258006035967,20.07499839201194,56.44976388242477 +2024-10-03 05:00:00,9,31.138037165628607,19.92119295384591,46.382032981562915 +2024-10-03 06:00:00,9,26.553221437317543,23.59586114549775,51.228493686883425 +2024-10-03 07:00:00,9,21.264776371800664,24.224787645984044,47.116992281396634 +2024-10-03 08:00:00,9,25.689443954496507,26.31417855125985,58.614492628983875 +2024-10-03 09:00:00,9,14.909091600112303,28.931316926309105,51.82286481015277 +2024-10-03 10:00:00,9,16.434802209344404,27.89215615082654,43.01432666346126 +2024-10-03 11:00:00,9,22.1368401912772,27.816966616206496,67.4067566131563 +2024-10-03 12:00:00,9,30.35477817013163,15.852766449867577,45.93266476967655 +2024-10-03 13:00:00,9,36.2131835867241,24.959469194132044,55.28835947979373 +2024-10-03 14:00:00,9,33.04114962355309,17.861591390015437,66.33313411358243 +2024-10-03 15:00:00,9,33.60072592610271,26.078157306775953,43.48261270249611 +2024-10-03 16:00:00,9,21.56096329382196,30.94245256078617,68.74065790712817 +2024-10-03 17:00:00,9,37.78104664568815,21.488182106291447,54.89808019801037 +2024-10-03 18:00:00,9,38.97002954505063,20.624636321546475,43.28292392750307 +2024-10-03 19:00:00,9,23.871307483331325,24.003389598491633,62.460616617146044 +2024-10-03 20:00:00,9,12.670000019836882,18.50534044214357,59.48248548529333 +2024-10-03 21:00:00,9,26.87046380811871,23.05720669759355,56.29567009737279 +2024-10-03 22:00:00,9,31.3917949366418,28.069043507953115,49.6599456144649 +2024-10-03 23:00:00,9,20.78578161963994,16.81044355301026,63.87807167665594 +2024-10-04 00:00:00,9,20.46385732209375,22.721816923706367,50.3972567938137 +2024-10-04 01:00:00,9,20.6515131834571,17.566550991131795,55.125761423503455 +2024-10-04 02:00:00,9,33.6569645964032,22.021133868828407,48.24259958194402 +2024-10-04 03:00:00,9,21.914745411206432,25.113543502246998,52.17336777672992 +2024-10-04 04:00:00,9,30.99109526792025,22.287991488732565,50.93282171352913 +2024-10-04 05:00:00,9,39.84313311459374,24.503548260483512,38.98335342528033 +2024-10-04 06:00:00,9,16.010344093139025,20.345831706062075,56.81744561947798 +2024-10-04 07:00:00,9,17.082688118337288,23.9171590005958,43.40834646225552 +2024-10-04 08:00:00,9,20.84471892276493,27.569066474286213,65.9539752151632 +2024-10-04 09:00:00,9,34.57988473446785,23.310401888472843,37.82148990236024 +2024-10-04 10:00:00,9,17.85156478224704,26.442799682117965,51.24904991973752 +2024-10-04 11:00:00,9,31.683222757474763,22.01558963863206,47.5501756971653 +2024-10-04 12:00:00,9,30.82854490854948,16.706898493585108,53.34394267263503 +2024-10-04 13:00:00,9,43.01409342494894,28.888851347732512,53.35744455398009 +2024-10-04 14:00:00,9,31.68021624261206,20.551905640785293,57.97400380922679 +2024-10-04 15:00:00,9,21.095398196460366,31.034367675060153,58.535098736141 +2024-10-04 16:00:00,9,20.45671983463484,23.687302491003937,41.06196221927411 +2024-10-04 17:00:00,9,35.314788598792745,25.579584100774973,61.04687871410239 +2024-10-04 18:00:00,9,34.24226327774759,21.869945983824227,50.80629352473666 +2024-10-04 19:00:00,9,40.67322250057538,16.13829969966502,62.40835131713769 +2024-10-04 20:00:00,9,21.55659307301266,17.23761440933626,41.60934236099617 +2024-10-04 21:00:00,9,10.248761723486073,23.501726292885273,59.11096592407202 +2024-10-04 22:00:00,9,36.00224290477034,18.355254336698366,46.869133829783095 +2024-10-04 23:00:00,9,28.42734737587722,24.69915863535919,57.73483865768455 +2024-10-05 00:00:00,9,33.28121842775981,22.565048335562302,56.35726757574372 +2024-10-05 01:00:00,9,32.11666852068731,21.924807574999072,62.02507989487253 +2024-10-05 02:00:00,9,23.332951295576322,21.561760853455468,48.958944522498086 +2024-10-05 03:00:00,9,15.578042666030088,25.885365889472922,44.75835289869141 +2024-10-05 04:00:00,9,34.37308797799712,27.825644091422376,40.86442433862355 +2024-10-05 05:00:00,9,27.616928645177072,26.215830881894412,40.22103451671509 +2024-10-05 06:00:00,9,16.785075619678828,21.465736349532328,45.95453559522763 +2024-10-05 07:00:00,9,30.295844787561407,29.618625628805695,71.14531422655872 +2024-10-05 08:00:00,9,24.10917990837734,24.28181014820383,55.08724772377051 +2024-10-05 09:00:00,9,39.4791326035639,20.109033958089544,73.03712860095054 +2024-10-05 10:00:00,9,30.525851226407582,25.23899448772093,54.8755697461878 +2024-10-05 11:00:00,9,13.81490004907594,27.43076099327653,38.08920307542059 +2024-10-05 12:00:00,9,21.04018320319753,22.890179018583552,63.935605691131386 +2024-10-05 13:00:00,9,50.04297956610948,30.763265045542155,50.51840505075176 +2024-10-05 14:00:00,9,40.66449730631099,28.176214231959534,40.425312868400546 +2024-10-05 15:00:00,9,30.624124812761618,23.339794482736,39.42875737186339 +2024-10-05 16:00:00,9,18.234512484525784,28.934036212933428,58.84969670204441 +2024-10-05 17:00:00,9,23.650252269453244,28.9659942271203,60.63310025678642 +2024-10-05 18:00:00,9,25.988132566801305,21.284412445120406,55.730456482399724 +2024-10-05 19:00:00,9,31.89075479130253,22.591514909463843,54.23538251630529 +2024-10-05 20:00:00,9,34.20151678104274,25.27694550129532,63.155151928877665 +2024-10-05 21:00:00,9,52.7567368123052,28.659180523105498,48.78743300417123 +2024-10-05 22:00:00,9,27.272681594722243,15.304408121776639,72.57675995112965 +2024-10-05 23:00:00,9,23.325953004860946,25.312727271825906,58.32944246771955 +2024-10-06 00:00:00,9,27.321714327917036,28.244523069486764,32.22057344426091 +2024-10-06 01:00:00,9,11.018632956692878,23.484951840894,74.31318505250975 +2024-10-06 02:00:00,9,26.71968615083479,29.48671905719747,50.67412800190373 +2024-10-06 03:00:00,9,21.353408048805626,27.112662362436264,47.74901456325873 +2024-10-06 04:00:00,9,25.32094256042249,24.256150526137702,44.23087414275854 +2024-10-06 05:00:00,9,17.50447463033097,21.21793767919918,55.00146035360504 +2024-10-06 06:00:00,9,31.718887850500632,20.025080957146436,47.99264691899572 +2024-10-06 07:00:00,9,14.928670322419757,18.960505999186147,58.7933392094489 +2024-10-06 08:00:00,9,30.513017826827124,18.215446352048378,48.59279561373047 +2024-10-06 09:00:00,9,38.46005833420373,19.6833816947202,57.563238541882164 +2024-10-06 10:00:00,9,47.37788426962128,22.079766971321593,47.49365175297105 +2024-10-06 11:00:00,9,32.493731311858554,27.627340424677207,41.553277035705776 +2024-10-06 12:00:00,9,19.943920698233278,28.846240566509955,59.30675369671559 +2024-10-06 13:00:00,9,33.943176331633516,24.862894720146624,57.67490643060777 +2024-10-06 14:00:00,9,30.477851968534956,25.6089445740068,49.492451964243685 +2024-10-06 15:00:00,9,34.3989291633998,28.79455749131253,65.46588746044937 +2024-10-06 16:00:00,9,25.532635983414032,30.318196480157688,43.60741130474121 +2024-10-06 17:00:00,9,16.37382766357517,25.23565725649185,66.80696628096095 +2024-10-06 18:00:00,9,29.899996894730652,15.999092730337622,56.16173600545538 +2024-10-06 19:00:00,9,32.803064337767374,29.789794510310397,59.370628532958044 +2024-10-06 20:00:00,9,28.99679179850323,19.874368375972207,47.17796241312519 +2024-10-06 21:00:00,9,17.46937845921621,23.455331927274013,68.45222713276078 +2024-10-06 22:00:00,9,24.346462377095918,17.026008811039567,74.71867327269061 +2024-10-06 23:00:00,9,33.20325232630071,20.903823232273204,73.26395447957212 +2024-10-07 00:00:00,9,17.43423715736109,23.831274299100183,59.16107276101399 +2024-10-07 01:00:00,9,29.077407875263077,26.971342931916855,52.882209178147875 +2024-10-07 02:00:00,9,14.131775309049914,21.927298389155933,52.65824343927378 +2024-10-07 03:00:00,9,37.18235774493029,25.828369295167743,66.31547411544527 +2024-10-07 04:00:00,9,10.191818265222523,27.426904348079383,64.16067294964364 +2024-10-07 05:00:00,9,26.906836395971506,24.653605858373602,58.934247852751014 +2024-10-07 06:00:00,9,32.63377897880148,26.65671494121356,43.921362768508004 +2024-10-07 07:00:00,9,32.791591084274756,20.306215397948034,51.709038912868884 +2024-10-07 08:00:00,9,30.356482544999054,17.924454772669648,45.43906586272425 +2024-10-07 09:00:00,9,23.80728252674284,24.419760235007242,67.55412534059361 +2024-10-07 10:00:00,9,31.272600260873904,25.645614359925304,68.49662505078618 +2024-10-07 11:00:00,9,36.140372671422654,22.437619133828566,39.57395799443684 +2024-10-07 12:00:00,9,39.78498646567347,25.464773014632236,66.20804451891843 +2024-10-07 13:00:00,9,25.960748159117127,28.575085148461135,79.25067447899013 +2024-10-07 14:00:00,9,38.68410113593699,28.783290463544084,58.80660596759145 +2024-10-07 15:00:00,9,21.89417438081741,24.38651055374914,52.167240839334994 +2024-10-07 16:00:00,9,37.864421686953335,30.352679693699336,45.01517218384822 +2024-10-07 17:00:00,9,26.138604539335226,24.816903430441744,51.95411772331951 +2024-10-07 18:00:00,9,28.99139832843602,31.343266624315902,51.35135890452418 +2024-10-07 19:00:00,9,23.79241607019557,33.48017499348972,67.2469680039887 +2024-10-07 20:00:00,9,15.233039246238544,24.230984506964326,67.70202111492671 +2024-10-07 21:00:00,9,21.02434974684124,22.208679480204434,65.80057247886393 +2024-10-07 22:00:00,9,34.24626282261494,19.988927052108323,69.01690189819982 +2024-10-07 23:00:00,9,27.801700772424407,18.762023471043822,52.74688641501192 +2024-10-08 00:00:00,9,17.578166892643203,18.085135673762295,50.16529660294815 +2024-10-08 01:00:00,9,46.64647899322485,30.282422574674342,52.54238417840961 +2024-10-08 02:00:00,9,16.18271986375492,25.27002250517598,66.09064843868832 +2024-10-08 03:00:00,9,22.066669568318623,21.375447810306536,54.53586181406 +2024-10-08 04:00:00,9,34.32646892104301,22.868925424411817,40.028328421032974 +2024-10-08 05:00:00,9,33.95336988223419,22.263744710304756,41.000022835244664 +2024-10-08 06:00:00,9,20.630612725334743,21.95704650378015,69.79129768014914 +2024-10-08 07:00:00,9,46.62755279796994,29.157151798715883,55.7786846983891 +2024-10-08 08:00:00,9,27.380691019804992,23.569429324253715,47.36205041813195 +2024-10-08 09:00:00,9,41.005342162421265,21.556076529521256,51.591560863766794 +2024-10-08 10:00:00,9,31.456062564725155,28.635642796688686,54.713159040506056 +2024-10-08 11:00:00,9,21.800551409103196,20.594059848544056,56.69320103309109 +2024-10-08 12:00:00,9,8.146981593782236,25.721928979787158,45.589980686233694 +2024-10-08 13:00:00,9,33.252877201228095,21.243436743070255,61.47337471881669 +2024-10-08 14:00:00,9,33.45892026857244,27.01294419466507,49.59842071791565 +2024-10-08 15:00:00,9,31.60384873384122,24.29362555681102,58.16833320486957 +2024-10-08 16:00:00,9,18.19489795577121,26.019520379589064,62.33337735030525 +2024-10-08 17:00:00,9,20.211796170859085,26.1480520656857,54.64852635694266 +2024-10-08 18:00:00,9,33.28414888743256,27.50157712266875,70.02622228332586 +2024-10-08 19:00:00,9,30.204078279855896,24.317171437660647,67.81431234356806 +2024-10-08 20:00:00,9,24.66147878894636,21.95338872036389,35.118617454921804 +2024-10-08 21:00:00,9,43.697655462615586,29.787058036827055,61.60214371190988 +2024-10-08 22:00:00,9,27.836171039799193,20.735944216345374,53.679723952148464 +2024-10-08 23:00:00,9,26.889468852253756,23.262380743039614,56.57414453369054 +2024-10-09 00:00:00,9,32.57010355383142,24.353883105132926,69.40365586267083 +2024-10-09 01:00:00,9,21.95212016009579,25.395632004947124,58.325425140158515 +2024-10-09 02:00:00,9,31.13870088231082,22.82510140293725,57.462866287226255 +2024-10-09 03:00:00,9,23.131402858609107,27.090600090883243,67.24923214762615 +2024-10-09 04:00:00,9,24.824415928157052,27.028819477133045,53.294732467789224 +2024-10-09 05:00:00,9,24.668199278355363,29.85178319752815,66.2735874044244 +2024-10-09 06:00:00,9,18.826317740298578,26.975846509749882,61.5131483462195 +2024-10-09 07:00:00,9,24.274712491302207,25.45757020634197,46.15109641559422 +2024-10-09 08:00:00,9,23.813348568084702,16.83325476385902,48.038620220178565 +2024-10-09 09:00:00,9,22.752780091851168,29.483835798721934,48.640514992823846 +2024-10-09 10:00:00,9,35.41941357270007,30.024551253946832,52.28323222375854 +2024-10-09 11:00:00,9,38.58093218936428,29.028065397891726,68.04732724185551 +2024-10-09 12:00:00,9,7.2516241145740885,26.906810116778882,49.2430959216263 +2024-10-09 13:00:00,9,38.154733882356055,26.931220102733967,56.83798486242986 +2024-10-09 14:00:00,9,39.19888715426815,24.890633426550032,57.422555672238914 +2024-10-09 15:00:00,9,32.74618174180395,24.44538947813811,46.12909007785773 +2024-10-09 16:00:00,9,30.10976879402004,28.003890970267857,66.07801290809194 +2024-10-09 17:00:00,9,22.889283564159975,23.112749226965573,59.561473799239835 +2024-10-09 18:00:00,9,34.827001688083456,26.750085325227808,62.14374042836065 +2024-10-09 19:00:00,9,22.343992954232565,28.890259139922605,40.527150737752144 +2024-10-09 20:00:00,9,29.226614532085463,26.458443761832378,58.00312760698983 +2024-10-09 21:00:00,9,14.376604056866197,23.623978183164624,48.63085975758251 +2024-10-09 22:00:00,9,18.177158913272944,16.80279245144881,57.553342184716094 +2024-10-09 23:00:00,9,14.340892922808319,27.57946789714622,66.03097293219771 +2024-10-10 00:00:00,9,22.641318303291705,16.195874585012703,48.182545596924754 +2024-10-10 01:00:00,9,29.645262845000577,19.262453117619614,47.30191808950224 +2024-10-10 02:00:00,9,30.351809056012506,26.346478167786177,40.47758470972519 +2024-10-10 03:00:00,9,22.12833902149915,26.530060577610147,59.0791111989424 +2024-10-10 04:00:00,9,30.069190040272723,25.13777509762453,46.42044816602121 +2024-10-10 05:00:00,9,24.467216844457482,23.18715129761055,57.97202412728058 +2024-10-10 06:00:00,9,15.879332931529357,22.099784918461424,48.779530972126345 +2024-10-10 07:00:00,9,33.4258848366671,19.616874879202726,40.50604337365511 +2024-10-10 08:00:00,9,13.817009763165595,33.20364395117251,56.781803396705605 +2024-10-10 09:00:00,9,23.484575507441672,27.30650416579742,53.81547785612691 +2024-10-10 10:00:00,9,15.682810113460485,25.31841925238693,62.28273362605505 +2024-10-10 11:00:00,9,29.933136498380577,14.769539129152639,49.84686744536326 +2024-10-10 12:00:00,9,22.719206691599183,21.066308081597505,58.895680240290844 +2024-10-10 13:00:00,9,35.777053503289,23.026145416578213,50.21435550204828 +2024-10-10 14:00:00,9,28.871490028235982,27.280261143941328,41.33348760228693 +2024-10-10 15:00:00,9,27.329955934503715,31.23424730620948,46.06101555588381 +2024-10-10 16:00:00,9,16.56298412443485,22.675460334987417,42.49598484959487 +2024-10-10 17:00:00,9,21.927214637213684,20.336797865088606,44.156968718278414 +2024-10-10 18:00:00,9,28.243213742962784,34.41271998455606,54.42792121237357 +2024-10-10 19:00:00,9,27.929675881334074,32.337885274070466,55.31080105002973 +2024-10-10 20:00:00,9,20.630237319523193,29.645658262211526,67.63566725614541 +2024-10-10 21:00:00,9,29.582708954601806,18.953571804929172,72.51173805388079 +2024-10-10 22:00:00,9,30.26329445836388,25.318201553598733,66.67636212253282 +2024-10-10 23:00:00,9,21.1969950353427,18.62076598460827,56.072405281129626 +2024-10-11 00:00:00,9,39.72772353216847,25.292274958467925,61.30192147289643 +2024-10-11 01:00:00,9,20.457519802968715,25.243346555092014,42.009506342100345 +2024-10-11 02:00:00,9,35.624085540714546,23.169083656229947,65.37775199491833 +2024-10-11 03:00:00,9,27.925880800733154,15.485512679940747,42.74994089647258 +2024-10-11 04:00:00,9,30.32999908702655,22.23029850442756,62.022171696467005 +2024-10-11 05:00:00,9,16.64253340986243,31.07418945703626,41.669716406577805 +2024-10-11 06:00:00,9,23.551285054023587,27.937007846516607,54.527944141493954 +2024-10-11 07:00:00,9,21.532251658509253,19.430487337478638,73.73575624689764 +2024-10-11 08:00:00,9,34.26544972763702,22.462622286938114,53.220471507320184 +2024-10-11 09:00:00,9,24.204785167459182,23.61782097543724,44.76705456892027 +2024-10-11 10:00:00,9,25.52210162604673,19.64472766785686,44.24859679323174 +2024-10-11 11:00:00,9,26.784830426596347,27.013868665686495,44.221806795679335 +2024-10-11 12:00:00,9,29.72213641566058,28.17615153942813,58.36354134417466 +2024-10-11 13:00:00,9,24.12350159933878,30.397395044395488,53.40453847999283 +2024-10-11 14:00:00,9,40.72227228209134,25.77621569974989,38.08533465895464 +2024-10-11 15:00:00,9,36.65704431238851,17.821331767192667,70.50323997938206 +2024-10-11 16:00:00,9,26.008675964047992,28.54213267121635,48.63927354958302 +2024-10-11 17:00:00,9,32.92656959156789,22.186088169652994,36.54134225818568 +2024-10-11 18:00:00,9,6.340652224418335,20.72242907058141,42.05404882082102 +2024-10-11 19:00:00,9,34.806171056604484,24.4491711871966,52.88002839731136 +2024-10-11 20:00:00,9,24.512241547506274,25.177887657140936,47.54244480777011 +2024-10-11 21:00:00,9,22.820395172597745,28.139717640806055,35.51970732950945 +2024-10-11 22:00:00,9,20.45951064989264,24.792962835983722,60.57463743284344 +2024-10-11 23:00:00,9,30.63636811806886,28.739216594394435,55.60133754493956 +2024-10-12 00:00:00,9,33.322210424070775,17.649043259913554,47.218538283402175 +2024-10-12 01:00:00,9,38.346618223061625,22.076436321954905,57.27873000367206 +2024-10-12 02:00:00,9,28.991681317878086,20.663298830987333,50.960344649998035 +2024-10-12 03:00:00,9,29.82752723537767,28.874786652088993,60.07188219699651 +2024-10-12 04:00:00,9,21.632942468374118,27.026184101945375,30.680558200287905 +2024-10-12 05:00:00,9,23.696906793665345,22.69640112525005,61.17835682016089 +2024-10-12 06:00:00,9,24.174215847635345,18.400727744291544,55.74096349795099 +2024-10-12 07:00:00,9,33.51032395452999,17.94623877185892,41.29550430007874 +2024-10-12 08:00:00,9,41.612128963149004,15.654024376995084,53.892923957931615 +2024-10-12 09:00:00,9,16.64884531981938,17.790203016656243,55.71563156031068 +2024-10-12 10:00:00,9,9.478592133402495,18.396811971354698,47.37700191569869 +2024-10-12 11:00:00,9,19.798022120831867,25.375008402730657,43.105722528148455 +2024-10-12 12:00:00,9,16.94620288455651,26.662038584320186,59.00402202433596 +2024-10-12 13:00:00,9,19.171709776861512,14.405191265609623,43.97661753443781 +2024-10-12 14:00:00,9,32.852086849714986,22.790798981702075,43.59594955877224 +2024-10-12 15:00:00,9,24.432999380479277,19.248816353013943,56.90292050287957 +2024-10-12 16:00:00,9,15.735573802571478,26.106258111763104,40.44452972309249 +2024-10-12 17:00:00,9,20.995365536548224,27.212173933417105,48.178742519046345 +2024-10-12 18:00:00,9,43.909737667489544,24.759875844309217,49.43753067153482 +2024-10-12 19:00:00,9,33.050506775801146,23.236759544052568,68.75785170972682 +2024-10-12 20:00:00,9,38.41193888036048,23.947897338800413,53.516737944830254 +2024-10-12 21:00:00,9,20.72136089975083,13.922642780736679,70.85712707993888 +2024-10-12 22:00:00,9,20.805032471515467,22.58666007944231,59.067311662299176 +2024-10-12 23:00:00,9,9.73746944312357,19.927127460176322,54.31415331756454 +2024-10-13 00:00:00,9,24.950604173986275,30.631208241326423,52.044833512892495 +2024-10-13 01:00:00,9,24.683382027504724,21.507380041603398,56.044854411769315 +2024-10-13 02:00:00,9,34.67241982117961,27.369599396023382,48.199614944517634 +2024-10-13 03:00:00,9,23.79225144576155,24.90833000975215,49.98799868829985 +2024-10-13 04:00:00,9,25.972268709879742,19.976226419398127,26.644941930735204 +2024-10-13 05:00:00,9,23.138128619769624,29.239442927809677,53.06812142306865 +2024-10-13 06:00:00,9,23.53881837185579,25.277561015347768,44.09114549195806 +2024-10-13 07:00:00,9,36.506181591975064,33.231266454763876,62.67880057612699 +2024-10-13 08:00:00,9,37.85303169353766,25.81761616808653,60.20654410143629 +2024-10-13 09:00:00,9,21.145266213168746,20.968203776923517,37.48361119778629 +2024-10-13 10:00:00,9,20.816608389606998,24.819811768923614,62.93167537134838 +2024-10-13 11:00:00,9,18.539919037169774,26.010741754408723,45.85520181013924 +2024-10-13 12:00:00,9,22.332339164178347,28.393270197183753,55.143084618310255 +2024-10-13 13:00:00,9,12.729982815789219,30.667360105305995,59.008653442668376 +2024-10-13 14:00:00,9,23.1170328665009,24.69173158777462,53.20890808162893 +2024-10-13 15:00:00,9,18.31647170043027,31.025737274176386,58.649983270052715 +2024-10-13 16:00:00,9,19.91536001979118,25.299782993089565,66.73607877530142 +2024-10-13 17:00:00,9,29.278718494696406,21.741262307849613,68.34412706676066 +2024-10-13 18:00:00,9,33.73801438682943,26.006229343254905,51.77435389158269 +2024-10-13 19:00:00,9,22.67682228360082,27.063399793360674,47.994508618071336 +2024-10-13 20:00:00,9,16.33833946864022,18.83748726633965,29.285102595727693 +2024-10-13 21:00:00,9,27.966731132993637,19.78772476810291,73.74557311140151 +2024-10-13 22:00:00,9,18.638729955106818,24.410472227023284,56.37167226658647 +2024-10-13 23:00:00,9,27.45246509956738,25.313000284613885,44.2483933710472 +2024-10-14 00:00:00,9,12.868214908559791,25.327319207254686,50.08242229868265 +2024-10-14 01:00:00,9,17.626655498242215,21.73481945200464,78.80530610666526 +2024-10-14 02:00:00,9,28.670384006975855,25.29287739850522,47.156276755983654 +2024-10-14 03:00:00,9,27.186662957751984,25.55159876149195,48.186728928757674 +2024-10-14 04:00:00,9,28.21581900704557,25.136589650625595,53.891840090725964 +2024-10-14 05:00:00,9,36.24610239219811,23.173270239019672,59.78392712324475 +2024-10-14 06:00:00,9,28.278518070119556,20.286752583283587,55.652199827621416 +2024-10-14 07:00:00,9,34.84639814251307,19.335866781707907,51.08549058333652 +2024-10-14 08:00:00,9,21.4799122350578,22.365360294478556,59.53047426336673 +2024-10-14 09:00:00,9,24.68392322631937,14.899770334069933,56.91334097849199 +2024-10-14 10:00:00,9,25.709563709537804,24.070401730674693,44.79752106076407 +2024-10-14 11:00:00,9,27.344628203485577,17.833514045228256,41.20698774415713 +2024-10-14 12:00:00,9,20.74742271530115,16.847335501466105,70.32881981391266 +2024-10-14 13:00:00,9,25.153128000482408,25.25924875934796,58.41452336148261 +2024-10-14 14:00:00,9,25.098922128732227,23.485327227525662,51.557311575342595 +2024-10-14 15:00:00,9,37.96693860487014,28.454675151273424,65.47031627594704 +2024-10-14 16:00:00,9,44.05048990629487,16.759258838262554,65.08173549273765 +2024-10-14 17:00:00,9,29.688679156930775,28.213184023811877,54.256477432915105 +2024-10-14 18:00:00,9,16.544335448498472,28.70381499606099,39.57177573235787 +2024-10-14 19:00:00,9,29.63509844757087,24.384318352805593,66.67115192911211 +2024-10-14 20:00:00,9,27.96530301145677,28.142238453103097,61.01235188654493 +2024-10-14 21:00:00,9,23.506521465997917,28.17584061701478,61.367431539578675 +2024-10-14 22:00:00,9,27.8980928924713,20.14042178360222,61.62204138023834 +2024-10-14 23:00:00,9,46.13275682870572,20.666357090440425,56.29127529369266 +2024-10-15 00:00:00,9,25.288594180132726,22.938726369934905,46.833003834597235 +2024-10-15 01:00:00,9,30.78168607371567,21.398324653252,61.90074767017584 +2024-10-15 02:00:00,9,25.54058306521188,20.589620569906202,45.71313193922183 +2024-10-15 03:00:00,9,30.297926341000547,25.200619196393806,72.21793952096117 +2024-10-15 04:00:00,9,14.701356210308125,28.676120593851593,39.383522544075795 +2024-10-15 05:00:00,9,15.239953712207285,19.551986427095724,55.80175460826332 +2024-10-15 06:00:00,9,27.285142031746492,19.81725717307405,59.32525251623929 +2024-10-15 07:00:00,9,16.247342018889682,23.750471688947286,49.169400240523984 +2024-10-15 08:00:00,9,25.480386703593,25.785502476016898,42.27583117279864 +2024-10-15 09:00:00,9,42.58514530606644,24.164910112075148,65.82262468292143 +2024-10-15 10:00:00,9,42.10876274869742,21.943476939573134,59.038052449929154 +2024-10-15 11:00:00,9,31.55084716726037,27.762558631181793,63.68434222294765 +2024-10-15 12:00:00,9,21.88095838119693,24.180355219412945,45.86959876113555 +2024-10-15 13:00:00,9,26.00126721177032,22.408567760555542,60.16381608667224 +2024-10-15 14:00:00,9,12.930285620317955,22.383183668338887,55.655308960612736 +2024-10-15 15:00:00,9,22.633459934886098,28.180913350427897,45.07285017768973 +2024-10-15 16:00:00,9,15.553636845707201,25.576477224999515,50.86138318359561 +2024-10-15 17:00:00,9,33.29761909225878,28.734109631491314,61.82841875241901 +2024-10-15 18:00:00,9,47.30481714080594,25.169034903572907,47.63822855623132 +2024-10-15 19:00:00,9,13.814140599489328,26.404744697186608,67.1454378318875 +2024-10-15 20:00:00,9,31.285113908911605,29.93230282034437,35.00739516314859 +2024-10-15 21:00:00,9,32.172698616551955,19.303542895358344,50.21129895535834 +2024-10-15 22:00:00,9,24.50146700707103,18.48764260420368,61.361669607432276 +2024-10-15 23:00:00,9,23.371384139099558,23.248571571165197,51.724382581468774 +2024-10-16 00:00:00,9,29.009175848583524,24.684854978139786,50.99056350931784 +2024-10-16 01:00:00,9,29.21154156840363,23.1137090535841,62.0419306032241 +2024-10-16 02:00:00,9,24.80663005860414,24.322522182272444,46.5986772957164 +2024-10-16 03:00:00,9,35.43838671348404,22.850610635882465,60.81387905695833 +2024-10-16 04:00:00,9,32.33020599472223,23.95351379157039,44.61399143423889 +2024-10-16 05:00:00,9,29.291772739945756,21.15446850990038,54.73940451790488 +2024-10-16 06:00:00,9,26.751868288991655,18.307579838325246,57.04413388499064 +2024-10-16 07:00:00,9,21.885547003267668,23.183915451746024,33.16776239558518 +2024-10-16 08:00:00,9,8.13456359479429,22.15062004217249,45.477236310116155 +2024-10-16 09:00:00,9,18.470201153671248,23.68671226424435,53.38151495396634 +2024-10-16 10:00:00,9,23.392134519573567,25.452630319744884,61.298344792084364 +2024-10-16 11:00:00,9,6.352876602828044,20.72596165218864,56.26603449247687 +2024-10-16 12:00:00,9,24.882236106382564,27.23921830878867,59.02592059683312 +2024-10-16 13:00:00,9,22.30325233459166,21.734893752442474,36.07880005807402 +2024-10-16 14:00:00,9,16.329471725000097,30.64863193806675,54.04035109595671 +2024-10-16 15:00:00,9,22.31621672261175,24.852146643758598,50.07387400728213 +2024-10-16 16:00:00,9,34.91431115351222,26.86115461369633,57.520823893082664 +2024-10-16 17:00:00,9,30.348094669570578,23.083627628526543,49.113959838902126 +2024-10-16 18:00:00,9,28.113923606860553,23.77152058940901,48.3460953328862 +2024-10-16 19:00:00,9,22.48565104544936,18.814706582698406,60.146363666621404 +2024-10-16 20:00:00,9,31.516068567895232,24.520238362435382,47.82617199854037 +2024-10-16 21:00:00,9,32.1280052625069,30.288051665160722,43.943788867905226 +2024-10-16 22:00:00,9,31.15994842582171,23.821451040783426,57.66664047184478 +2024-10-16 23:00:00,9,31.26165488415517,22.83032737558979,49.59536509251451 +2024-10-17 00:00:00,9,25.377494245461232,26.484078744164748,50.55313389574765 +2024-10-17 01:00:00,9,37.451186883694035,24.987093802689525,57.0920824976509 +2024-10-17 02:00:00,9,34.47807031084962,22.558322965227934,53.93080012739277 +2024-10-17 03:00:00,9,22.491954241811325,28.351025135898666,56.72596213176688 +2024-10-17 04:00:00,9,26.230223897958524,27.01302310157113,68.98705857439138 +2024-10-17 05:00:00,9,25.450871587752196,26.937010895205518,47.72171879850433 +2024-10-17 06:00:00,9,32.98228228520704,22.566434564819772,55.141588774136146 +2024-10-17 07:00:00,9,16.979711423985695,20.72861665104159,47.35760570138916 +2024-10-17 08:00:00,9,30.86021828236417,21.549229795463148,53.90612804018879 +2024-10-17 09:00:00,9,21.436367779433397,23.021799718156153,48.06336255744657 +2024-10-17 10:00:00,9,19.51396926603229,21.408881225452234,66.00134938564285 +2024-10-17 11:00:00,9,25.78438637576495,25.513557435203538,60.54371290489212 +2024-10-17 12:00:00,9,33.12438298973092,18.904901390254764,49.542358708740565 +2024-10-17 13:00:00,9,25.788354805861466,32.22773952690178,52.88081886377209 +2024-10-17 14:00:00,9,22.763267573450268,18.86742046405188,60.26848487126982 +2024-10-17 15:00:00,9,18.731553923259888,28.73808107601971,48.08738775503731 +2024-10-17 16:00:00,9,15.233085995923446,16.962640671993263,51.1457752425617 +2024-10-17 17:00:00,9,24.764291936577894,19.075717704959338,58.23887528572897 +2024-10-17 18:00:00,9,37.698411532699595,27.978920636066604,40.51759408943892 +2024-10-17 19:00:00,9,30.991421833446275,20.40051878453709,58.37076343039265 +2024-10-17 20:00:00,9,20.07966261868973,22.86177697119717,61.816327809251035 +2024-10-17 21:00:00,9,19.457396052205766,21.406327254521877,45.9385041394105 +2024-10-17 22:00:00,9,37.58813652298332,23.23554860515433,55.80952828786151 +2024-10-17 23:00:00,9,30.80217938483343,23.78264658723011,45.92188913239944 +2024-10-18 00:00:00,9,30.32746200036565,25.311796670783647,44.031707324840475 +2024-10-18 01:00:00,9,13.03545194860876,19.642589066714482,47.556310814016555 +2024-10-18 02:00:00,9,23.528479504911328,22.08142182763739,60.406126887709554 +2024-10-18 03:00:00,9,21.751708875705027,22.384371707458516,48.08750878486577 +2024-10-18 04:00:00,9,30.158645564846548,22.264289017352702,61.0657422553979 +2024-10-18 05:00:00,9,21.42407071741559,23.78280597226577,52.77845430443206 +2024-10-18 06:00:00,9,33.84538327962682,24.794141651008164,47.99876294469132 +2024-10-18 07:00:00,9,33.001692451063136,21.934081592226327,60.50932858390422 +2024-10-18 08:00:00,9,27.135334815780723,20.004004369041386,45.315946853363585 +2024-10-18 09:00:00,9,20.779122927016413,28.55928420704751,60.843963801960925 +2024-10-18 10:00:00,9,29.03484510955669,23.48202251484294,46.29867896639077 +2024-10-18 11:00:00,9,21.51190368422954,26.820541511399917,56.236461955009744 +2024-10-18 12:00:00,9,15.016557025587355,25.46987815716986,61.84522364042628 +2024-10-18 13:00:00,9,2.706376677572468,21.469355774083876,50.88637566316552 +2024-10-18 14:00:00,9,34.66137407741924,24.804307767468583,36.682988250476484 +2024-10-18 15:00:00,9,31.33097599605674,26.36159195152772,59.21095216282363 +2024-10-18 16:00:00,9,28.11132275879954,32.85504370099883,50.13172497135337 +2024-10-18 17:00:00,9,29.56324494700299,26.617143122025396,58.205197694555 +2024-10-18 18:00:00,9,19.466571226787767,26.461959061241842,56.36474266932163 +2024-10-18 19:00:00,9,41.22385358926988,28.892419925824658,39.81416652944107 +2024-10-18 20:00:00,9,6.976609235617872,19.391432413317883,64.81682762333976 +2024-10-18 21:00:00,9,22.791536871538842,27.54825477805245,38.20065375984646 +2024-10-18 22:00:00,9,23.840187723505167,21.106370799022578,45.11755528069878 +2024-10-18 23:00:00,9,26.31370722079997,16.41300348428736,55.57564295294084 +2024-10-19 00:00:00,9,26.032912668346516,22.620291975042505,57.76583894715751 +2024-10-19 01:00:00,9,30.080114477247,23.0814163688185,54.995973629481234 +2024-10-19 02:00:00,9,25.968742261248774,24.94922987509787,46.60946300414662 +2024-10-19 03:00:00,9,26.584783361282323,29.6845556031904,38.452182286459966 +2024-10-19 04:00:00,9,34.35526352843392,17.92595196712345,48.773132612183915 +2024-10-19 05:00:00,9,30.596529558431772,19.930026290452844,43.32847477878948 +2024-10-19 06:00:00,9,26.592549013618793,23.029382767929764,58.551990670621066 +2024-10-19 07:00:00,9,39.51609774814361,18.51230437949413,51.36272337808211 +2024-10-19 08:00:00,9,23.782754652827798,30.38858168685415,63.404693025306685 +2024-10-19 09:00:00,9,34.08774074349603,23.434780424128462,51.99422802159296 +2024-10-19 10:00:00,9,29.16136148574382,26.83641823035831,46.91171586776958 +2024-10-19 11:00:00,9,33.48754541426804,23.79588564072355,55.47802244735908 +2024-10-19 12:00:00,9,25.665590531040767,21.49652257217842,54.58352957361633 +2024-10-19 13:00:00,9,31.005929214067443,22.346071047911103,55.148867466931804 +2024-10-19 14:00:00,9,11.977744031041864,21.286366381078917,50.66936750409009 +2024-10-19 15:00:00,9,29.278156276717752,30.12296163815063,46.270228005642174 +2024-10-19 16:00:00,9,34.81853381725752,23.13682472990751,42.91473882032314 +2024-10-19 17:00:00,9,5.818321432715187,24.25742471216761,45.07032645623795 +2024-10-19 18:00:00,9,35.50593245338622,34.04677305120661,43.96848177628297 +2024-10-19 19:00:00,9,31.17083863162752,23.815561818258942,57.6196779233692 +2024-10-19 20:00:00,9,28.828980766239276,17.992267816078833,68.01291118788615 +2024-10-19 21:00:00,9,17.33663741103029,23.000549022062017,58.09904044112496 +2024-10-19 22:00:00,9,21.91948566281933,25.336724882809573,55.365972439224585 +2024-10-19 23:00:00,9,33.16404424250698,18.116831918968973,56.50762821242462 +2024-10-20 00:00:00,9,21.140100796718826,24.22808464927675,68.22292270797958 +2024-10-20 01:00:00,9,22.50887142487629,20.449927419973704,64.5372938477947 +2024-10-20 02:00:00,9,26.62273733544494,18.004632391010766,54.83296669824664 +2024-10-20 03:00:00,9,38.96759433640835,27.72327419682586,72.26868844297549 +2024-10-20 04:00:00,9,29.844654786127556,28.204411055632573,67.26208314394074 +2024-10-20 05:00:00,9,30.144466915711007,22.427769211925305,60.942455193640434 +2024-10-20 06:00:00,9,32.24879166936538,27.144028450833655,52.26335882147177 +2024-10-20 07:00:00,9,24.70828256739038,26.199272871923156,53.35219011324994 +2024-10-20 08:00:00,9,31.039186441163487,21.488266357137928,41.0695414799314 +2024-10-20 09:00:00,9,29.712073718747813,22.266831019432047,53.40694895357925 +2024-10-20 10:00:00,9,25.888764851781392,18.307758394519286,59.622866931711584 +2024-10-20 11:00:00,9,22.693183244148944,24.351824422079346,52.636876999193866 +2024-10-20 12:00:00,9,23.80584727837105,27.565735705714307,54.738112152304865 +2024-10-20 13:00:00,9,21.159716498265915,21.630134921890374,61.210579521125695 +2024-10-20 14:00:00,9,4.0966968125771075,32.07393432634033,42.40426713238236 +2024-10-20 15:00:00,9,24.760689397430045,26.85677205785354,56.57200704747813 +2024-10-20 16:00:00,9,36.32545276356101,32.587369614459234,52.034741287755054 +2024-10-20 17:00:00,9,27.800764475296344,26.50528719035344,42.34081066776358 +2024-10-20 18:00:00,9,21.80229334839188,25.63441123672389,49.577437116898274 +2024-10-20 19:00:00,9,34.574687400377144,20.6946436360275,47.29058843774129 +2024-10-20 20:00:00,9,21.502129824121713,22.77045007784542,57.76110857020527 +2024-10-20 21:00:00,9,31.92468111116235,21.377784869840635,58.06275186755026 +2024-10-20 22:00:00,9,16.679932591303192,28.435829448438913,54.42475706215022 +2024-10-20 23:00:00,9,16.901822746888588,23.773256593984982,66.22603705184927 +2024-10-21 00:00:00,9,6.028948773777252,28.491913903199286,47.83123126155016 +2024-10-21 01:00:00,9,38.356869496572116,21.628166772082302,53.29736968543503 +2024-10-21 02:00:00,9,30.15224878410402,27.3263692921029,57.55222537188691 +2024-10-21 03:00:00,9,25.13157023794154,28.701592589023278,50.37605363246182 +2024-10-21 04:00:00,9,28.610595749832797,25.661482999856926,53.94973675042732 +2024-10-21 05:00:00,9,30.688462830069877,17.910836427614367,47.14806845921911 +2024-10-21 06:00:00,9,24.825843647062605,28.925006295893617,53.912336490971995 +2024-10-21 07:00:00,9,16.595352869861824,29.843909297640394,58.17045797022484 +2024-10-21 08:00:00,9,17.74534406176238,28.6631884254465,52.27751767412105 +2024-10-21 09:00:00,9,32.41915001128035,16.767993314969807,40.298047801917235 +2024-10-21 10:00:00,9,23.27330923847084,27.18371788281017,64.54256194747877 +2024-10-21 11:00:00,9,31.220187723596275,24.597844524473892,40.10608885264493 +2024-10-21 12:00:00,9,35.94357380948291,25.943837815374614,54.0497825751071 +2024-10-21 13:00:00,9,25.4839135700289,29.634861547037893,56.96396387217509 +2024-10-21 14:00:00,9,17.53193712612948,21.048803820142403,49.60432016836924 +2024-10-21 15:00:00,9,27.247725369710366,23.971366319527988,56.573968294828745 +2024-10-21 16:00:00,9,18.94670558718366,31.60788446256673,45.321698059546186 +2024-10-21 17:00:00,9,40.41177926594604,29.723984164573814,50.99097011825272 +2024-10-21 18:00:00,9,21.46447587913975,24.65568497525217,63.69211170930373 +2024-10-21 19:00:00,9,20.16053848291572,25.604484211477185,39.643645900428346 +2024-10-21 20:00:00,9,10.867806999365422,28.843248555939418,54.044538948393544 +2024-10-21 21:00:00,9,32.1967121310778,25.905613274031605,53.29353763008442 +2024-10-21 22:00:00,9,22.949128043501346,19.142046145403054,47.97960028617093 +2024-10-21 23:00:00,9,39.918217214081636,15.35444178951007,46.22429243089003 +2024-10-22 00:00:00,9,22.818542131783943,25.578191059327875,39.230472657208566 +2024-10-22 01:00:00,9,25.881516992408464,28.734807236064583,47.96566007194432 +2024-10-22 02:00:00,9,39.690714973943756,24.81346090102258,57.74208179002891 +2024-10-22 03:00:00,9,0.0,26.228615801737092,51.293903072932125 +2024-10-22 04:00:00,9,18.53695941102935,20.678707645376605,35.91394839980683 +2024-10-22 05:00:00,9,31.73495250832167,21.62745432779992,59.479462564783375 +2024-10-22 06:00:00,9,35.891237535151745,25.87046160390223,54.08987420986967 +2024-10-22 07:00:00,9,45.04356865691549,22.90449552700474,44.75400411246152 +2024-10-22 08:00:00,9,26.04232568479358,28.671284225682697,51.65671168168279 +2024-10-22 09:00:00,9,37.93901190283161,28.73410231553428,61.62557552557204 +2024-10-22 10:00:00,9,27.3598788559342,20.44186222951209,56.769402118053804 +2024-10-22 11:00:00,9,22.419094016462346,28.03477309816912,63.0633799386477 +2024-10-22 12:00:00,9,19.74411767960598,20.10596108392687,61.52099155874842 +2024-10-22 13:00:00,9,23.872744938654446,25.118392580418494,59.440214601113276 +2024-10-22 14:00:00,9,9.228666136601255,21.612485187086197,48.49084585716156 +2024-10-22 15:00:00,9,33.34473075952535,30.268233062051173,55.954832089891546 +2024-10-22 16:00:00,9,48.19428479566261,22.0061994699875,54.93201124843513 +2024-10-22 17:00:00,9,23.934687870160534,31.888561687717115,48.8944854591359 +2024-10-22 18:00:00,9,26.517051664190074,32.10279834064511,54.9696026443848 +2024-10-22 19:00:00,9,44.9716623366695,17.124336260424613,33.69017714416694 +2024-10-22 20:00:00,9,24.45098617663588,22.089688373999294,54.294107525716136 +2024-10-22 21:00:00,9,26.618119796032556,24.959376932279255,41.725192688638735 +2024-10-22 22:00:00,9,36.75093237956956,19.21606128643303,44.83140271395056 +2024-10-22 23:00:00,9,25.374460046872567,20.603121688578597,38.54778114264552 +2024-10-23 00:00:00,9,12.725835692830191,27.62982889154769,61.47676758190492 +2024-10-23 01:00:00,9,27.77776485229831,21.36396888412775,57.130144703514524 +2024-10-23 02:00:00,9,20.814758381014162,27.103361907960846,56.0776091954786 +2024-10-23 03:00:00,9,11.820815352710616,15.64760673038135,48.700998199247564 +2024-10-23 04:00:00,9,49.85045597501254,28.661319384057194,63.345714066460964 +2024-10-23 05:00:00,9,31.749829215363803,27.873531679522863,56.70072622887244 +2024-10-23 06:00:00,9,30.732287193028732,26.418514623345676,54.77072322053738 +2024-10-23 07:00:00,9,25.179744749605298,30.092658150297552,50.24730850221755 +2024-10-23 08:00:00,9,30.833459447909124,21.49595668635107,66.19977435466245 +2024-10-23 09:00:00,9,32.82983338176067,21.77517683680399,46.739399343048156 +2024-10-23 10:00:00,9,28.52578669847979,24.64522961070389,49.70018945325561 +2024-10-23 11:00:00,9,31.72869264372867,20.28566145194077,49.86082962238795 +2024-10-23 12:00:00,9,26.961543713116793,32.006087840870926,56.6419087990957 +2024-10-23 13:00:00,9,39.40845537254651,25.704641734886593,46.2060398750586 +2024-10-23 14:00:00,9,26.323189021729466,29.410251976983435,56.08231741320384 +2024-10-23 15:00:00,9,21.605066509559293,19.76183027089114,56.86619853078059 +2024-10-23 16:00:00,9,21.746950803056322,21.309463508064617,56.917626137117466 +2024-10-23 17:00:00,9,24.685006433234882,24.59094483787706,55.52896002579785 +2024-10-23 18:00:00,9,16.679980634948656,26.71637257541309,46.04441372490365 +2024-10-23 19:00:00,9,21.638632334437773,25.85993418232984,51.66370454121714 +2024-10-23 20:00:00,9,22.50672783672975,26.042086412611603,64.92751173689308 +2024-10-23 21:00:00,9,11.042269426490346,20.973612377106022,53.12784699355536 +2024-10-23 22:00:00,9,29.61435533695982,22.704043346736103,65.70156622921357 +2024-10-23 23:00:00,9,30.29078735776227,28.4940734440906,55.79051233264665 +2024-10-24 00:00:00,9,17.047231607304198,24.577082987359557,47.67911647756697 +2024-10-24 01:00:00,9,29.115023411135102,26.059317480564452,62.48068414509841 +2024-10-24 02:00:00,9,35.16978882144431,25.67996677020342,53.08926331750091 +2024-10-24 03:00:00,9,34.48818904753651,18.530094229423458,60.15625253040467 +2024-10-24 04:00:00,9,23.732822861117846,19.361616948507184,64.12182797210441 +2024-10-24 05:00:00,9,38.972052071626614,24.594262371518543,57.172210300339216 +2024-10-24 06:00:00,9,27.733987390601467,25.57636645859097,59.611983986952396 +2024-10-24 07:00:00,9,21.79384700201294,17.477098420968662,64.46225612767883 +2024-10-24 08:00:00,9,29.91776251577801,19.899504217072735,44.33265758728723 +2024-10-24 09:00:00,9,34.39632578217957,19.480640425247447,59.377796179227126 +2024-10-24 10:00:00,9,21.3170856435691,26.627134576758692,58.634323653639655 +2024-10-24 11:00:00,9,9.733780310734653,24.153147214681113,56.8616155344152 +2024-10-24 12:00:00,9,23.64942132975054,21.212064738531662,36.09009093482063 +2024-10-24 13:00:00,9,17.948209851737317,24.781996035770327,49.736114266941115 +2024-10-24 14:00:00,9,49.388561843738344,27.891592597433075,59.115683597893124 +2024-10-24 15:00:00,9,25.636948876285597,27.522716931032196,62.16778179458056 +2024-10-24 16:00:00,9,38.36874345904279,18.396917725019723,51.702548301801606 +2024-10-24 17:00:00,9,23.829194384238214,21.99690897947927,62.52107824461704 +2024-10-24 18:00:00,9,28.930023929632114,25.64660909400578,48.0257306247824 +2024-10-24 19:00:00,9,2.893087003367217,24.137988379383476,53.57250640345613 +2024-10-24 20:00:00,9,33.81094087147259,27.121882585149628,60.0235757279965 +2024-10-24 21:00:00,9,13.6775964272522,22.694407214764144,50.36979173520234 +2024-10-24 22:00:00,9,29.2467416973569,29.32794621345758,60.749597258421275 +2024-10-24 23:00:00,9,20.485624953573623,26.088803477337297,45.83070476492447 +2024-10-25 00:00:00,9,18.573252905953428,27.67621715301821,55.46988605953224 +2024-10-25 01:00:00,9,36.7298302838947,20.81675924854903,62.38297041340794 +2024-10-25 02:00:00,9,17.240369554227534,30.741409506305665,56.88413960307958 +2024-10-25 03:00:00,9,30.332760357003984,24.4335383628451,49.475629653028726 +2024-10-25 04:00:00,9,27.712135573236047,25.23575297291389,65.99541394702885 +2024-10-25 05:00:00,9,10.513574174962704,27.033241316455985,60.63171110715433 +2024-10-25 06:00:00,9,17.88080339001303,23.4888889248394,64.42370149170796 +2024-10-25 07:00:00,9,35.88545109593982,23.023364118341547,52.09056524759986 +2024-10-25 08:00:00,9,18.91323426272373,20.789539602596005,57.53531397034857 +2024-10-25 09:00:00,9,32.16765445218245,25.067040220026684,36.776841318873885 +2024-10-25 10:00:00,9,23.993733219217297,24.505136387137792,52.111452966793586 +2024-10-25 11:00:00,9,19.341390654406876,22.76706890988661,52.72491516084854 +2024-10-25 12:00:00,9,33.72535645210547,26.609505423611523,59.647176728533374 +2024-10-25 13:00:00,9,26.620258679558738,24.598355199977586,68.61936159309865 +2024-10-25 14:00:00,9,16.07221542156023,24.974411598900016,43.53601721188879 +2024-10-25 15:00:00,9,30.245087489867714,28.379085295028357,57.39895244079173 +2024-10-25 16:00:00,9,17.90621955756644,27.07291286416217,50.29149757985897 +2024-10-25 17:00:00,9,13.236455573222747,20.468491145500565,57.74858715922889 +2024-10-25 18:00:00,9,20.163831644810934,26.285328047455753,43.24609722501273 +2024-10-25 19:00:00,9,24.149795768825957,27.713693667404883,60.78021142999381 +2024-10-25 20:00:00,9,16.714996376845143,18.60739885254423,63.016038384598566 +2024-10-25 21:00:00,9,24.904221421955178,17.95847195912262,52.74763719824016 +2024-10-25 22:00:00,9,22.014288548210846,22.15832979592206,54.80355457909497 +2024-10-25 23:00:00,9,24.87830887647438,18.37567354921684,46.129746365688135 +2024-10-26 00:00:00,9,18.790221826935678,27.12396617249562,65.21661550196697 +2024-10-26 01:00:00,9,26.23652406752472,21.25184342295102,51.450218444991094 +2024-10-26 02:00:00,9,16.544192306236276,23.985771721057713,49.05534111429993 +2024-10-26 03:00:00,9,26.426454145693445,20.1118475393137,58.33099930561438 +2024-10-26 04:00:00,9,24.19573271127701,30.17115079402456,46.27423404843333 +2024-10-26 05:00:00,9,17.308425947677677,20.83339716947664,47.19475303887677 +2024-10-26 06:00:00,9,24.579699341844123,27.93107767218473,67.94700379677894 +2024-10-26 07:00:00,9,22.77865621285143,22.005172903237504,54.45540192327418 +2024-10-26 08:00:00,9,13.871287289416273,23.650564298178388,57.38127146438514 +2024-10-26 09:00:00,9,30.006136394653723,28.09738733679432,64.42605516981746 +2024-10-26 10:00:00,9,25.569969598654836,25.435463678110526,53.278297349644106 +2024-10-26 11:00:00,9,8.821252567958858,22.449188458165626,48.334164345839035 +2024-10-26 12:00:00,9,22.27391834199387,22.166567512038185,64.44149510298583 +2024-10-26 13:00:00,9,25.034268622999345,27.431030878944686,46.6682977819447 +2024-10-26 14:00:00,9,13.066057498844579,23.218304967137644,72.93873674700295 +2024-10-26 15:00:00,9,27.78338155516358,23.155450103409137,56.227548926068614 +2024-10-26 16:00:00,9,17.702808484251108,21.286993796389655,55.32187709790075 +2024-10-26 17:00:00,9,31.759839836466664,19.60329145511649,57.14994777329752 +2024-10-26 18:00:00,9,22.406359280210573,27.749200134527065,58.72037746621571 +2024-10-26 19:00:00,9,23.79000946572531,30.612308293999423,54.89617751950715 +2024-10-26 20:00:00,9,16.054270819621856,24.175570356064895,58.784312235768034 +2024-10-26 21:00:00,9,29.019238381687078,26.103870311375726,60.174357582998304 +2024-10-26 22:00:00,9,28.416978575924546,19.036894816552703,46.78370974400383 +2024-10-26 23:00:00,9,26.80934880156559,25.421313366989523,34.16626428737276 +2024-10-27 00:00:00,9,26.683761861451877,15.343382043891182,58.34010120986379 +2024-10-27 01:00:00,9,31.10484338827698,22.347942524338713,38.78668552290372 +2024-10-27 02:00:00,9,13.374516627276524,27.98173873601018,60.743692565705 +2024-10-27 03:00:00,9,27.642890822885672,18.9761499177042,53.3904198612491 +2024-10-27 04:00:00,9,28.409685084191047,24.772997580390815,40.45522190360911 +2024-10-27 05:00:00,9,45.17659814014314,20.150180966105207,61.84803841883718 +2024-10-27 06:00:00,9,28.923748399494958,18.489066109166153,46.409520265849736 +2024-10-27 07:00:00,9,35.35138960621257,21.819785894418978,52.74048034019981 +2024-10-27 08:00:00,9,27.877111990987373,24.93404943440433,43.135984345296535 +2024-10-27 09:00:00,9,22.9249032755898,25.755719307921677,53.64600016950256 +2024-10-27 10:00:00,9,29.526494425355764,24.97430594570381,51.42662384838776 +2024-10-27 11:00:00,9,32.80833923361662,24.824182795816185,55.63850685259419 +2024-10-27 12:00:00,9,38.35752840103673,20.825923738427605,68.53935698050205 +2024-10-27 13:00:00,9,17.583881038625762,25.654004449513856,44.40364932420714 +2024-10-27 14:00:00,9,25.335705594887823,23.719013655861804,35.50336898496201 +2024-10-27 15:00:00,9,26.47226076005208,27.555157507589616,65.03938895589006 +2024-10-27 16:00:00,9,15.684948140959186,28.83298966406706,55.66863527350049 +2024-10-27 17:00:00,9,21.375152260593207,25.642929721081284,32.17418332990023 +2024-10-27 18:00:00,9,27.182960569697947,25.48467022614438,49.91690916574405 +2024-10-27 19:00:00,9,19.695213814196894,23.23915140797848,71.10545852063512 +2024-10-27 20:00:00,9,42.06728761518335,19.52349601947234,56.35129005814603 +2024-10-27 21:00:00,9,17.489462126076816,22.036777441708413,61.70785065055386 +2024-10-27 22:00:00,9,18.170125152704507,25.344341536013115,61.86109803931222 +2024-10-27 23:00:00,9,13.154330513957774,19.214929192098694,59.75286874698988 +2024-10-28 00:00:00,9,14.546379311502028,24.212532138558252,55.78050848410251 +2024-10-28 01:00:00,9,25.833659814751492,24.937215274800554,52.89544921620148 +2024-10-28 02:00:00,9,25.79417409909884,25.2835644252085,53.546903028217066 +2024-10-28 03:00:00,9,16.158457718945463,28.54561953306794,59.91612889673664 +2024-10-28 04:00:00,9,22.082387772037066,23.565167387199352,58.67665072664147 +2024-10-28 05:00:00,9,21.305487017408314,28.5139657055844,54.78930386600429 +2024-10-28 06:00:00,9,29.775468377150013,24.58009484820271,48.550213651209376 +2024-10-28 07:00:00,9,16.78685962452591,20.941175147971823,59.339215571342606 +2024-10-28 08:00:00,9,42.17571648144226,23.980199900072144,52.109235125520584 +2024-10-28 09:00:00,9,20.093591824569643,24.05152978269858,71.2655806744871 +2024-10-28 10:00:00,9,28.199854214987568,22.065237872123433,55.447367053472654 +2024-10-28 11:00:00,9,36.77081328706807,20.334626917689224,52.701298895718786 +2024-10-28 12:00:00,9,23.07983806657297,22.065875977977246,55.74486723966258 +2024-10-28 13:00:00,9,36.41751885936288,23.36631135784596,60.38129955356888 +2024-10-28 14:00:00,9,3.9120613414440655,26.03988167326208,61.38209313078753 +2024-10-28 15:00:00,9,31.251108238930666,24.068524079978978,62.4885887114724 +2024-10-28 16:00:00,9,21.30348319174737,20.922827045445366,65.5157412165578 +2024-10-28 17:00:00,9,29.28863999429819,31.07473782646855,55.34840079249048 +2024-10-28 18:00:00,9,32.98562562262788,29.04263990171721,40.00276489639775 +2024-10-28 19:00:00,9,9.654782051265913,21.947566207225183,41.26392474025879 +2024-10-28 20:00:00,9,25.972179534069202,24.735642550458746,59.544080830359704 +2024-10-28 21:00:00,9,14.76449768454831,22.924496445616043,46.09149865334526 +2024-10-28 22:00:00,9,12.047550106720774,22.800082332945998,52.37211202931631 +2024-10-28 23:00:00,9,37.88439164055784,19.235537092013843,48.09800852335763 +2024-10-29 00:00:00,9,23.58793843548775,28.95131731915396,47.137921888991976 +2024-10-29 01:00:00,9,19.152042512161284,15.547668035452741,72.89426759541432 +2024-10-29 02:00:00,9,30.42388630164529,21.406265789962276,55.78993312124199 +2024-10-29 03:00:00,9,24.022221576718668,22.05435262886842,53.02245599009 +2024-10-29 04:00:00,9,28.182159088019745,24.17067442636216,59.991588320956375 +2024-10-29 05:00:00,9,33.894856129468494,29.561505646355087,65.37093775559552 +2024-10-29 06:00:00,9,30.54377950198552,24.97831076953873,54.472662086764394 +2024-10-29 07:00:00,9,14.85896223878158,23.721692457051198,54.08410561262497 +2024-10-29 08:00:00,9,15.118730870190518,23.553119831582045,41.44481562924145 +2024-10-29 09:00:00,9,30.122809418921662,21.9601731036535,46.041343968253024 +2024-10-29 10:00:00,9,10.59026113566531,19.505569819864302,58.60655417261996 +2024-10-29 11:00:00,9,8.566507142653911,26.12676233616514,67.91121192337164 +2024-10-29 12:00:00,9,22.855516769868686,27.91856657438842,62.10919171692229 +2024-10-29 13:00:00,9,21.654401836224785,23.195357743224257,66.34866663102662 +2024-10-29 14:00:00,9,30.76640101811419,26.880454754386214,61.99577778959673 +2024-10-29 15:00:00,9,27.50193015696403,19.050184460610506,36.13410669749368 +2024-10-29 16:00:00,9,48.220081219244754,21.04153537265457,59.52042476964839 +2024-10-29 17:00:00,9,20.840086631656007,29.949966797075486,45.892389266511806 +2024-10-29 18:00:00,9,38.87070410282138,28.0273532172662,53.30389037658356 +2024-10-29 19:00:00,9,37.57314523647079,23.645921260811146,52.5019506994045 +2024-10-29 20:00:00,9,23.66892657242698,25.31339543955497,63.02332224076344 +2024-10-29 21:00:00,9,17.016220912154193,24.71977066898937,79.295675259639 +2024-10-29 22:00:00,9,8.104734832167253,24.010051004256894,54.176727357728595 +2024-10-29 23:00:00,9,43.86639660006664,20.668179502199926,51.10381873170644 +2024-10-30 00:00:00,9,29.337063727601944,28.733458997219664,53.536717999232344 +2024-10-30 01:00:00,9,24.56864998605111,24.429878977590224,49.30407265108748 +2024-10-30 02:00:00,9,24.22847146044,29.637097397107542,53.187589253052934 +2024-10-30 03:00:00,9,23.193432618497503,28.051724684230347,72.03378206042343 +2024-10-30 04:00:00,9,12.446964021401486,23.301377558012458,58.88384344304086 +2024-10-30 05:00:00,9,30.19964068061383,20.740784012545312,63.860750080259095 +2024-10-30 06:00:00,9,25.57583008636032,23.746509315145957,51.83787727108474 +2024-10-30 07:00:00,9,28.086794682035666,22.793640331376597,40.21861070512366 +2024-10-30 08:00:00,9,17.683043372516636,17.911453426598595,56.98032018729308 +2024-10-30 09:00:00,9,30.17367592430026,22.395513637682985,47.068005155492564 +2024-10-30 10:00:00,9,33.85057673589772,25.94110160768578,57.42025068667626 +2024-10-30 11:00:00,9,44.868247949681205,28.962372199906437,39.217765443566584 +2024-10-30 12:00:00,9,22.206960515359516,32.825444594162974,55.00555320406304 +2024-10-30 13:00:00,9,10.544568801804523,20.804691575626634,58.92164606198808 +2024-10-30 14:00:00,9,35.63516555119268,20.79423859754017,31.55756568243307 +2024-10-30 15:00:00,9,20.37661945385169,23.46375273574543,59.01596998365298 +2024-10-30 16:00:00,9,13.311618479789733,17.974054064975967,48.99161554919407 +2024-10-30 17:00:00,9,43.336140623369346,24.00086065300713,55.438599608261846 +2024-10-30 18:00:00,9,20.42806944801243,30.35574420045915,42.263815049390026 +2024-10-30 19:00:00,9,38.304048036864216,23.057289554654446,60.000525109187436 +2024-10-30 20:00:00,9,22.427296827159104,26.058634812912704,55.223690053354616 +2024-10-30 21:00:00,9,20.764762869709845,26.76450677236642,75.85659315131082 +2024-10-30 22:00:00,9,25.199176979719642,20.142929963654524,60.07336656473849 +2024-10-30 23:00:00,9,38.91367191042121,21.470382566544984,56.37769796062795 +2024-10-31 00:00:00,9,33.670774418240754,24.76558299757707,51.36920691132073 +2024-10-31 01:00:00,9,15.3568181829553,24.774696341840006,49.88313702502979 +2024-10-31 02:00:00,9,22.964821065877352,16.760784312290937,55.18928915250015 +2024-10-31 03:00:00,9,23.79260262594257,27.080498711857047,62.36310106592568 +2024-10-31 04:00:00,9,33.43065699944014,21.480786699106194,38.3306378346655 +2024-10-31 05:00:00,9,15.195843170736397,23.055672012289342,48.05518099942385 +2024-10-31 06:00:00,9,31.376959590786118,28.927684072578387,60.09157496319618 +2024-10-31 07:00:00,9,33.880248192104965,19.423255626751157,44.82542291161853 +2024-10-31 08:00:00,9,19.80071196813109,20.579242577776704,59.79159510375151 +2024-10-31 09:00:00,9,12.022177254474858,23.239603212870342,58.588830313761484 +2024-10-31 10:00:00,9,27.29638499167878,25.28979292980078,74.3048226764171 +2024-10-31 11:00:00,9,13.52998464343756,25.744082623291177,46.111466726028546 +2024-10-31 12:00:00,9,30.49226413900803,27.739056915236787,43.810867874131134 +2024-10-31 13:00:00,9,23.60589966983178,21.343394066617183,42.56857231705674 +2024-10-31 14:00:00,9,31.46515959904241,22.116160972175887,38.26418490553381 +2024-10-31 15:00:00,9,32.9012005196371,28.644055081959657,45.52756581860662 +2024-10-31 16:00:00,9,22.91416655153165,19.24833859477004,50.04062056307211 +2024-10-31 17:00:00,9,20.33761939449097,24.74838023383636,55.400413593123254 +2024-10-31 18:00:00,9,22.300428152197092,29.712508381035466,55.579077220187855 +2024-10-31 19:00:00,9,24.97965019923501,24.80063113807754,69.23181692547803 +2024-10-31 20:00:00,9,24.494531315126853,22.956919070672658,51.32758791725457 +2024-10-31 21:00:00,9,26.519488224511726,20.84101345979613,41.17961811713586 +2024-10-31 22:00:00,9,31.120135776114573,24.93579748073324,38.99558877080665 +2024-10-31 23:00:00,9,43.60798249311045,19.391599853384694,48.97585448363407 +2024-11-01 00:00:00,9,27.75319354578407,22.152052561666768,52.157099680359934 +2024-11-01 01:00:00,9,27.45095275473879,22.91456458518012,63.6269476840375 +2024-11-01 02:00:00,9,26.499539779460093,23.64846514358316,49.38389132131367 +2024-11-01 03:00:00,9,15.82972507205479,20.185407684903367,41.541369625971946 +2024-11-01 04:00:00,9,7.424191502408348,22.53945713811131,42.41857178383874 +2024-11-01 05:00:00,9,35.62299008430107,24.09536310653315,59.2837153575217 +2024-11-01 06:00:00,9,26.759626712011432,24.342676072038685,56.32182747023001 +2024-11-01 07:00:00,9,21.053045646034157,21.276451724858052,55.563613435059054 +2024-11-01 08:00:00,9,39.75308357553542,25.210745841647554,52.39718003302629 +2024-11-01 09:00:00,9,22.102823238351817,22.13312471000236,53.492674477453896 +2024-11-01 10:00:00,9,13.551139499877864,22.045271054142304,61.362978757720946 +2024-11-01 11:00:00,9,14.392031188720926,24.135536327573668,47.689280658437994 +2024-11-01 12:00:00,9,5.475831018340237,26.837300739822844,58.04124782208575 +2024-11-01 13:00:00,9,23.852855722357713,16.79457462815619,52.929151678980396 +2024-11-01 14:00:00,9,30.653894751940623,25.193938868063118,38.77836691167162 +2024-11-01 15:00:00,9,25.249013633049373,25.012539561975945,58.40861872142342 +2024-11-01 16:00:00,9,29.48404271671757,24.642491291605726,33.355004412273715 +2024-11-01 17:00:00,9,30.52206294229022,22.381085392002575,60.85953078572332 +2024-11-01 18:00:00,9,24.50741598699807,23.181208745005645,51.98099129088322 +2024-11-01 19:00:00,9,31.63925756987853,25.91081098146313,50.306728292687566 +2024-11-01 20:00:00,9,24.683133504192252,25.895261769779104,57.31999582892896 +2024-11-01 21:00:00,9,23.0040556879728,27.020071800634568,59.33284863446863 +2024-11-01 22:00:00,9,24.503594494457303,20.713170926408164,47.75164849575664 +2024-11-01 23:00:00,9,32.38271982819828,18.996174613519134,66.79016191298103 +2024-11-02 00:00:00,9,46.41325136886812,18.06159374152774,58.14669370738197 +2024-11-02 01:00:00,9,17.202112685608082,24.66675484634582,49.218858417323425 +2024-11-02 02:00:00,9,27.39554839198607,26.152007187526337,58.034693282519214 +2024-11-02 03:00:00,9,29.185242655579636,24.9005058083591,46.39434081415756 +2024-11-02 04:00:00,9,26.013485262692516,17.35415376999323,46.488881839315226 +2024-08-04 05:00:00,10,20.29802754512977,31.83306785262091,75.4501768634329 +2024-08-04 06:00:00,10,30.611770187745122,26.47827121693171,64.56874816810868 +2024-08-04 07:00:00,10,17.9183289935678,23.23558404440279,46.52908204777552 +2024-08-04 08:00:00,10,30.123559452595522,19.76104576824739,41.8491085033834 +2024-08-04 09:00:00,10,30.413547838092995,20.933092968556508,58.44117923362133 +2024-08-04 10:00:00,10,19.647735318676176,22.728603356310344,60.5621484188034 +2024-08-04 11:00:00,10,18.5909484356657,22.864125106261877,63.70271658387598 +2024-08-04 12:00:00,10,35.79945782199388,19.097626463119234,47.79543614315557 +2024-08-04 13:00:00,10,24.70315293006715,23.426740942226242,65.3044729277897 +2024-08-04 14:00:00,10,31.784956589449926,20.96094773820956,73.56234851783427 +2024-08-04 15:00:00,10,26.60540719292039,22.971312476438232,56.71027444090711 +2024-08-04 16:00:00,10,24.700710795294622,16.33803056791369,53.37303771423469 +2024-08-04 17:00:00,10,11.070739848759096,29.694737204098246,45.488165355399204 +2024-08-04 18:00:00,10,17.976081073834358,21.674060898684004,48.79630741163463 +2024-08-04 19:00:00,10,14.509355379947538,26.808661656622874,44.16992526567343 +2024-08-04 20:00:00,10,27.701418589036738,19.703522807430378,59.689037063931316 +2024-08-04 21:00:00,10,15.64725983779669,20.599119699505028,67.84961042342987 +2024-08-04 22:00:00,10,20.4904743383777,27.615973699108253,32.132181148678185 +2024-08-04 23:00:00,10,35.21402974793755,22.879611778128783,67.93600569811515 +2024-08-05 00:00:00,10,11.339133443874996,27.54509444182592,33.4299425038801 +2024-08-05 01:00:00,10,14.88903854927982,23.495049008181006,38.83537825201927 +2024-08-05 02:00:00,10,19.77948008450853,28.01161559247685,53.47177255212484 +2024-08-05 03:00:00,10,26.448201917945966,29.46795839801529,49.26208594549463 +2024-08-05 04:00:00,10,21.608593797670522,25.856976036141265,56.71281008347067 +2024-08-05 05:00:00,10,10.621260269448479,31.054749485850163,63.05805151004409 +2024-08-05 06:00:00,10,30.798324200454047,35.18203274070524,39.34146419345195 +2024-08-05 07:00:00,10,20.524098812672673,25.15023878596304,78.58139602943771 +2024-08-05 08:00:00,10,27.128008402825493,18.883518271836135,59.94308716247182 +2024-08-05 09:00:00,10,18.408075266729,26.013832123881112,74.43146975149986 +2024-08-05 10:00:00,10,37.55353014798523,26.445250713050456,72.26987058125022 +2024-08-05 11:00:00,10,26.703186780247222,20.151355962876284,38.07866719627165 +2024-08-05 12:00:00,10,19.312395889974695,22.33484471134382,58.07538676783051 +2024-08-05 13:00:00,10,20.540733231361248,21.601436399277006,52.85227235528453 +2024-08-05 14:00:00,10,18.75443722785797,25.846852191795513,73.8165233512708 +2024-08-05 15:00:00,10,27.50445911189069,22.31012089868624,50.84420641536944 +2024-08-05 16:00:00,10,29.20895764042382,16.046824153670016,37.81448828440544 +2024-08-05 17:00:00,10,1.4164096210751467,18.26104157287799,53.30462359559307 +2024-08-05 18:00:00,10,29.625848226465322,17.452908176727995,52.57513074504777 +2024-08-05 19:00:00,10,18.512178609490256,22.757836451388886,62.528617893104894 +2024-08-05 20:00:00,10,16.90929535672641,21.418251553642563,42.99937620854462 +2024-08-05 21:00:00,10,13.64616155893737,24.255610433953674,59.48107506629816 +2024-08-05 22:00:00,10,11.710764274027435,26.94110961429204,42.48795326542164 +2024-08-05 23:00:00,10,21.690862590288674,22.413998725475754,51.89930807795095 +2024-08-06 00:00:00,10,22.1164706577899,21.74489141701359,49.7571041056197 +2024-08-06 01:00:00,10,20.413080555696027,19.610792739726868,57.388889889822906 +2024-08-06 02:00:00,10,16.732834287970743,29.517655406262033,45.66382890194963 +2024-08-06 03:00:00,10,16.98367128490802,28.746804809756746,64.13653058137757 +2024-08-06 04:00:00,10,33.84829560310069,17.43767619946108,50.842012593902005 +2024-08-06 05:00:00,10,33.203362130499414,29.771781956458486,57.3111175458829 +2024-08-06 06:00:00,10,16.050222033606158,28.5122685422457,67.02568445283563 +2024-08-06 07:00:00,10,25.78797951746564,19.482416054643,63.99287434399536 +2024-08-06 08:00:00,10,31.130334600258983,27.38757273976122,59.304267840547546 +2024-08-06 09:00:00,10,10.483293003891758,18.03661575174214,50.90490488734871 +2024-08-06 10:00:00,10,24.891576977890853,26.48532994193666,73.60781127419308 +2024-08-06 11:00:00,10,17.319527234630563,18.21302665305164,69.75574148479122 +2024-08-06 12:00:00,10,13.443753031767258,27.01653878767297,59.615793254882284 +2024-08-06 13:00:00,10,20.03644562855282,17.197819862107657,62.11877442179885 +2024-08-06 14:00:00,10,36.40024293610857,15.902574150008316,52.78991910078683 +2024-08-06 15:00:00,10,22.164831708032647,18.822275223958325,50.86156038308625 +2024-08-06 16:00:00,10,17.350920636413683,19.82281931589153,62.36698459874845 +2024-08-06 17:00:00,10,37.0795294165367,27.953417842021924,52.05094779783649 +2024-08-06 18:00:00,10,15.175199828875,25.02056554783217,52.238626218171405 +2024-08-06 19:00:00,10,30.55542268124725,27.420203914737204,47.04564501461634 +2024-08-06 20:00:00,10,16.822275179538764,22.54023374656765,46.43042980404115 +2024-08-06 21:00:00,10,16.15224873323178,28.789057276471645,55.39559905397999 +2024-08-06 22:00:00,10,18.526760247044034,22.10347038004068,56.25115525499195 +2024-08-06 23:00:00,10,14.147196204204572,24.596768560738084,65.40398536001959 +2024-08-07 00:00:00,10,34.91841025369552,27.30823689735601,50.97381887359049 +2024-08-07 01:00:00,10,27.656315715235753,25.98947614743073,64.28873972371075 +2024-08-07 02:00:00,10,13.915246752548983,27.536278337872986,62.15348021165009 +2024-08-07 03:00:00,10,14.445699038822095,24.693636913916073,60.87028890801763 +2024-08-07 04:00:00,10,35.68638066453518,30.73328660799725,47.02711734646904 +2024-08-07 05:00:00,10,27.772825105398802,31.06054986724279,52.67708211261389 +2024-08-07 06:00:00,10,33.239909786497996,23.857021910698364,70.16549625314985 +2024-08-07 07:00:00,10,19.92378274907739,28.453255974127742,59.40473121848623 +2024-08-07 08:00:00,10,21.09324339896929,21.188150953619534,62.16298340719638 +2024-08-07 09:00:00,10,23.039793886010248,28.25618755873584,69.66683411553952 +2024-08-07 10:00:00,10,28.613604435968174,20.682177364414752,50.53442815399901 +2024-08-07 11:00:00,10,27.75505698737753,22.83514956160571,56.872205963224395 +2024-08-07 12:00:00,10,16.008900159354326,21.12920330095681,53.02838156738589 +2024-08-07 13:00:00,10,19.995434456555245,19.2951042149079,70.3896287534077 +2024-08-07 14:00:00,10,28.717406640018424,21.040231125715394,48.00808455961128 +2024-08-07 15:00:00,10,4.489535423285851,22.72620117364495,57.49023719384969 +2024-08-07 16:00:00,10,17.50635384969989,26.531682635545245,67.62651847164034 +2024-08-07 17:00:00,10,34.130230385769224,24.927307307167656,71.14509607067382 +2024-08-07 18:00:00,10,29.75621910112075,24.165483042658856,48.8663205290778 +2024-08-07 19:00:00,10,29.272954370228362,23.314705478272675,65.33451962520688 +2024-08-07 20:00:00,10,20.715791354735167,25.677259525726974,48.14428409170404 +2024-08-07 21:00:00,10,20.4984358709274,21.824452021810423,52.14209021889473 +2024-08-07 22:00:00,10,37.219029669435926,23.94326614358659,45.27707212388049 +2024-08-07 23:00:00,10,26.334620695106874,30.575940188160573,43.71229666017356 +2024-08-08 00:00:00,10,10.287308681716572,23.79584795037451,65.11590450686458 +2024-08-08 01:00:00,10,16.387871043783285,25.390624582799894,43.93775813650137 +2024-08-08 02:00:00,10,15.18323724512531,24.830250305030532,54.22798834307526 +2024-08-08 03:00:00,10,21.397020771644232,27.46743027343143,49.9221350791068 +2024-08-08 04:00:00,10,38.87981813856326,27.928257338824153,65.54734112215776 +2024-08-08 05:00:00,10,31.265898283666473,27.010080572562817,49.81009787793739 +2024-08-08 06:00:00,10,21.119583346682507,27.520355121909756,61.3520967291543 +2024-08-08 07:00:00,10,35.47869700821338,23.32044052043987,65.25366476116733 +2024-08-08 08:00:00,10,18.495738717159682,25.875574879199796,49.88961092914549 +2024-08-08 09:00:00,10,18.72985421743645,20.63119333007379,47.16308616491059 +2024-08-08 10:00:00,10,27.293040658164102,20.60003017586184,61.92629671640404 +2024-08-08 11:00:00,10,21.743863965379845,28.58441855649331,44.042750733886194 +2024-08-08 12:00:00,10,37.089755180007934,23.811916869351023,73.62423305005406 +2024-08-08 13:00:00,10,17.780405817265414,20.346243090251843,63.226401488955496 +2024-08-08 14:00:00,10,12.003266322153733,19.332662905921918,56.261653189791154 +2024-08-08 15:00:00,10,34.32942966248296,19.90984593803557,62.819262508211324 +2024-08-08 16:00:00,10,26.514677190028554,26.284613224426316,54.57326219589162 +2024-08-08 17:00:00,10,23.555278758924793,21.80706103434461,53.286953275042436 +2024-08-08 18:00:00,10,23.081695097076313,23.112822056035526,61.10631504654852 +2024-08-08 19:00:00,10,4.935599799011467,23.322446715246112,51.90197394640386 +2024-08-08 20:00:00,10,11.91407732178076,23.35150936505547,53.87578162697251 +2024-08-08 21:00:00,10,20.533899354347664,23.76851290934895,43.01665200244105 +2024-08-08 22:00:00,10,21.289954916914475,25.97514623304483,54.830760873290075 +2024-08-08 23:00:00,10,10.624088340076032,26.553896676883436,62.78545388382243 +2024-08-09 00:00:00,10,31.869456199094436,31.189633228769363,51.26521704455481 +2024-08-09 01:00:00,10,7.544196406217637,31.972603413952093,40.9769296700043 +2024-08-09 02:00:00,10,26.62365610484915,24.45303857110329,57.10046942245856 +2024-08-09 03:00:00,10,10.52666735050873,22.215606250600164,50.83934687213517 +2024-08-09 04:00:00,10,36.21166694387774,23.881580437275915,64.36955369308383 +2024-08-09 05:00:00,10,24.62465411053482,26.285474503852683,60.6919307365347 +2024-08-09 06:00:00,10,26.178477868832502,23.281594758555688,54.10605479785008 +2024-08-09 07:00:00,10,13.50545067955217,23.704471139147852,61.05406696553411 +2024-08-09 08:00:00,10,33.03622945260063,30.81347198585989,43.60114448375684 +2024-08-09 09:00:00,10,5.863018525838182,21.19896762552237,75.63500609446079 +2024-08-09 10:00:00,10,37.07199765763373,22.410372397133784,60.694194200516165 +2024-08-09 11:00:00,10,15.706441865100185,21.296094882452348,51.6519700998563 +2024-08-09 12:00:00,10,11.23583419844572,23.378560348051558,58.10587993840154 +2024-08-09 13:00:00,10,24.854699608324932,22.60824013763968,52.215223379742625 +2024-08-09 14:00:00,10,26.429694360823277,25.706736429810274,50.91716095931832 +2024-08-09 15:00:00,10,36.03332023865679,19.083101751268536,66.132757787808 +2024-08-09 16:00:00,10,14.207190851113362,25.7941746197021,67.54242897301893 +2024-08-09 17:00:00,10,18.958990172122665,25.400532455321308,49.08202073682607 +2024-08-09 18:00:00,10,21.509649924518506,20.843462361819014,66.22136337852287 +2024-08-09 19:00:00,10,25.119238973984164,23.87621711074013,55.42107825128316 +2024-08-09 20:00:00,10,18.050329850901058,20.908887207021728,61.12613464315684 +2024-08-09 21:00:00,10,15.687142341833914,15.74246420258211,56.849272938242116 +2024-08-09 22:00:00,10,26.895415706628064,28.438196579049155,68.62047305747573 +2024-08-09 23:00:00,10,18.501671117587037,23.45085073308051,60.68865188747054 +2024-08-10 00:00:00,10,39.51513918060872,25.67404930403054,54.7091792118468 +2024-08-10 01:00:00,10,18.05919313594616,28.324426101604224,63.02285245821039 +2024-08-10 02:00:00,10,31.335506547976124,24.46987251726501,46.89735981702667 +2024-08-10 03:00:00,10,23.3078993665497,24.386495002116682,71.62782757541636 +2024-08-10 04:00:00,10,27.850775991834468,23.784347225695047,41.94004566033685 +2024-08-10 05:00:00,10,9.31894576109131,28.04328202190123,70.06612052728158 +2024-08-10 06:00:00,10,25.64133598399335,20.559133273576062,40.88690297080022 +2024-08-10 07:00:00,10,36.80520806857385,26.401069884453673,62.104533937010736 +2024-08-10 08:00:00,10,17.783525822668388,17.699223722743362,61.80460283633062 +2024-08-10 09:00:00,10,25.33349528512067,22.009794408187904,67.57783459399212 +2024-08-10 10:00:00,10,32.161862817539564,24.745957746925658,60.811365808592754 +2024-08-10 11:00:00,10,30.770678837547198,22.6749382382775,46.71582009215643 +2024-08-10 12:00:00,10,26.83386898765562,23.928143335245643,80.21436429698184 +2024-08-10 13:00:00,10,21.00484758229534,20.205246370509485,49.59770610544088 +2024-08-10 14:00:00,10,18.70700907317039,26.50431612773643,56.706929642569754 +2024-08-10 15:00:00,10,25.407048765012718,16.908323564148496,60.18901857506293 +2024-08-10 16:00:00,10,35.285958391896585,22.498862922278263,48.806039095213094 +2024-08-10 17:00:00,10,49.80209457339507,19.917376646340635,57.48603140513503 +2024-08-10 18:00:00,10,23.183956471626587,22.409840418633497,57.686125713905966 +2024-08-10 19:00:00,10,19.660071099633456,26.007422975937118,68.6018402535814 +2024-08-10 20:00:00,10,21.642101677898722,24.349087410553402,69.73854439862896 +2024-08-10 21:00:00,10,27.068203577776124,24.429237346794643,49.696893351869214 +2024-08-10 22:00:00,10,14.746717257283459,23.52616543911833,48.109839592406956 +2024-08-10 23:00:00,10,32.857051700353296,26.441523322666864,51.73962553524633 +2024-08-11 00:00:00,10,37.829554570631686,28.27649084292796,63.66449008820793 +2024-08-11 01:00:00,10,36.28062873530413,23.80637959472865,65.68039834219293 +2024-08-11 02:00:00,10,15.934735251528371,26.27944555523897,62.285928452771735 +2024-08-11 03:00:00,10,16.18177622017336,28.398451701957722,87.83774224674178 +2024-08-11 04:00:00,10,23.394446610610306,26.330911975028258,49.195586660289244 +2024-08-11 05:00:00,10,22.59451807821411,21.300739385318852,53.2415508484338 +2024-08-11 06:00:00,10,24.09702963384339,20.587121987239804,43.128618458655524 +2024-08-11 07:00:00,10,25.300579719787233,24.710574498175188,46.21536718105259 +2024-08-11 08:00:00,10,14.36539798536115,31.075281947030184,59.76706593132841 +2024-08-11 09:00:00,10,32.38300899497543,22.24250526263143,57.69487259796712 +2024-08-11 10:00:00,10,22.482467688368054,26.095445442192908,54.517747395215935 +2024-08-11 11:00:00,10,20.78260005581859,26.827827299283086,38.13636447636829 +2024-08-11 12:00:00,10,17.729292873712335,25.19155150909714,52.70262153996574 +2024-08-11 13:00:00,10,20.038971423249265,21.260414092999625,62.812657902262636 +2024-08-11 14:00:00,10,26.529026056023916,26.353252469149314,48.67896819346916 +2024-08-11 15:00:00,10,39.53100616175242,21.70049211341993,51.562696371303964 +2024-08-11 16:00:00,10,23.68311901611031,28.353714635215994,55.65677375974244 +2024-08-11 17:00:00,10,19.698229632010573,23.554354460958226,36.13960750496282 +2024-08-11 18:00:00,10,25.03794295121496,19.829699880573635,66.54846186961063 +2024-08-11 19:00:00,10,26.841234820586337,27.273906086390237,34.41515846751504 +2024-08-11 20:00:00,10,25.383295489868125,24.96481031677782,57.89763353536965 +2024-08-11 21:00:00,10,19.56834036849213,21.377165602553603,61.416657059383205 +2024-08-11 22:00:00,10,23.450501331359785,29.105836722495066,58.57144071413223 +2024-08-11 23:00:00,10,11.05679722128481,18.678120038521396,52.79007947665318 +2024-08-12 00:00:00,10,28.559947115452808,26.420164984991402,52.09368021458757 +2024-08-12 01:00:00,10,15.94630803106813,29.467139922073816,56.57145206106148 +2024-08-12 02:00:00,10,22.331110766136177,22.977211087951382,48.31385483254249 +2024-08-12 03:00:00,10,25.679192806067746,29.12790044635479,33.605735662181274 +2024-08-12 04:00:00,10,11.060883811711545,24.466361247151017,65.36199377818993 +2024-08-12 05:00:00,10,11.405573841938708,21.198921232647557,30.947253301321513 +2024-08-12 06:00:00,10,23.199147603181167,27.558029142797956,63.05328399554833 +2024-08-12 07:00:00,10,34.93570363071581,20.734529137729982,56.97254728323007 +2024-08-12 08:00:00,10,25.37467045351965,24.53641302502158,63.96464033186008 +2024-08-12 09:00:00,10,22.657213645796542,25.799653672457712,60.410345487185936 +2024-08-12 10:00:00,10,38.68959420664652,30.819302176485763,61.24999855437763 +2024-08-12 11:00:00,10,15.088283710304243,14.768234492246556,51.10414903332753 +2024-08-12 12:00:00,10,36.21587826839341,27.564379191492367,54.88563882189391 +2024-08-12 13:00:00,10,21.16358830925987,23.290205386274565,58.35796286474502 +2024-08-12 14:00:00,10,22.10980859627402,28.01226932976646,60.71132484577877 +2024-08-12 15:00:00,10,20.3739957939353,24.356122332087516,42.49672768651479 +2024-08-12 16:00:00,10,12.105249497136908,25.805801213738437,42.54127525034023 +2024-08-12 17:00:00,10,38.52271060328398,19.46532251265488,57.639476124884176 +2024-08-12 18:00:00,10,0.0,20.07296872003948,49.66837465141008 +2024-08-12 19:00:00,10,32.345755538790726,25.73133656882832,52.12177688120913 +2024-08-12 20:00:00,10,8.724335846033945,17.448583532688726,54.94940442902213 +2024-08-12 21:00:00,10,19.194766123430828,24.40017432779858,48.01814066764019 +2024-08-12 22:00:00,10,11.536352801463044,22.263506220632014,45.626215799933945 +2024-08-12 23:00:00,10,19.12709411040052,18.30025391874653,48.07031837441955 +2024-08-13 00:00:00,10,0.8428697697943193,34.46526030489171,64.02472731939459 +2024-08-13 01:00:00,10,34.89235305851521,22.937691848149537,34.180058053872195 +2024-08-13 02:00:00,10,32.661355657939964,25.92436357418756,50.75853437805773 +2024-08-13 03:00:00,10,36.16763598340808,26.192106741819504,56.96885794632418 +2024-08-13 04:00:00,10,28.805620753455692,31.300940434843543,57.58010591071389 +2024-08-13 05:00:00,10,35.31979586290974,30.64001050411072,58.326597957669506 +2024-08-13 06:00:00,10,11.109857181497762,24.89045283230248,58.93438291978737 +2024-08-13 07:00:00,10,32.07854319400917,23.66424201456397,39.96638799035813 +2024-08-13 08:00:00,10,30.840673654294413,25.641739063024303,44.66372023803699 +2024-08-13 09:00:00,10,24.5925783148388,24.093941039697853,58.41895991106389 +2024-08-13 10:00:00,10,28.362688335104735,19.430839552525224,59.02671523073752 +2024-08-13 11:00:00,10,36.814042550095415,26.38163144711603,55.54592651363589 +2024-08-13 12:00:00,10,25.745249124489867,14.762072948335668,47.52186912828056 +2024-08-13 13:00:00,10,31.464441343499413,24.202917326304572,60.84852434204694 +2024-08-13 14:00:00,10,18.952625738003793,24.79638788402806,50.64171329315362 +2024-08-13 15:00:00,10,27.660644711573106,23.78065112536247,62.768106278206496 +2024-08-13 16:00:00,10,26.685042798256013,23.809100999843743,71.89777756163122 +2024-08-13 17:00:00,10,7.219250639941443,26.408321758751477,69.56585872284843 +2024-08-13 18:00:00,10,21.29004279183835,24.421764231263264,61.46429550980854 +2024-08-13 19:00:00,10,27.425588111537998,24.36905786063221,47.51564351632951 +2024-08-13 20:00:00,10,19.64671663935925,23.154170473737977,75.97283644583662 +2024-08-13 21:00:00,10,15.361826408779406,20.175889603179876,51.53194047715944 +2024-08-13 22:00:00,10,7.74688208825652,26.431986420465027,43.553717682638826 +2024-08-13 23:00:00,10,17.22301669302546,29.422767133795904,36.66626835944926 +2024-08-14 00:00:00,10,8.076485555923256,26.938885033457716,61.14394625308161 +2024-08-14 01:00:00,10,17.991065308526018,24.50579862940681,27.763139811460277 +2024-08-14 02:00:00,10,29.789502127436855,29.282281162922615,68.22856940099894 +2024-08-14 03:00:00,10,24.877358989571402,24.00394402284632,58.914580596096215 +2024-08-14 04:00:00,10,19.77455348598736,28.06154903291462,67.04492148943781 +2024-08-14 05:00:00,10,26.868541313205935,19.807405005785462,47.315772203246325 +2024-08-14 06:00:00,10,31.26616743122543,23.89420719267198,64.08644538905051 +2024-08-14 07:00:00,10,24.84313245970586,23.407760984632144,72.38136956741627 +2024-08-14 08:00:00,10,20.626207540877225,19.17597773419706,55.70650956658594 +2024-08-14 09:00:00,10,26.880040604786462,23.67206828019338,73.5824259176528 +2024-08-14 10:00:00,10,23.923907146232953,21.996835984148305,47.65132931972138 +2024-08-14 11:00:00,10,20.080715397432375,24.034077622815268,72.34497452074433 +2024-08-14 12:00:00,10,36.25731203131665,25.52644934880757,69.25541288680608 +2024-08-14 13:00:00,10,8.79402354468359,18.469512611546577,40.3723458847505 +2024-08-14 14:00:00,10,30.9830376893006,29.246293678509414,44.04311284950317 +2024-08-14 15:00:00,10,24.348317047135637,24.635965089325996,63.131128575465716 +2024-08-14 16:00:00,10,37.27952111435937,23.206765939905058,60.278378013469144 +2024-08-14 17:00:00,10,15.278828815275704,18.221503650831963,57.00835738024207 +2024-08-14 18:00:00,10,15.680603583998776,25.824207497593065,52.2692633217032 +2024-08-14 19:00:00,10,18.040762086202783,23.3918614907056,52.15163356002314 +2024-08-14 20:00:00,10,27.479401789434284,25.866757651832074,66.11027991845154 +2024-08-14 21:00:00,10,22.272761413372493,23.150519853765342,50.72774481291246 +2024-08-14 22:00:00,10,5.542288411385464,28.25710471003049,33.390684594013834 +2024-08-14 23:00:00,10,10.943912296328062,26.67245448556856,66.58452512355228 +2024-08-15 00:00:00,10,22.757590397932905,24.720139524469502,52.05846637904278 +2024-08-15 01:00:00,10,38.84984813900309,27.250592973543068,57.45295668502329 +2024-08-15 02:00:00,10,27.27498233015384,23.309676133975735,58.779396278345416 +2024-08-15 03:00:00,10,16.211819512583418,28.0731806641405,48.040996294606266 +2024-08-15 04:00:00,10,25.882039037029845,25.007232857878464,38.00478905244712 +2024-08-15 05:00:00,10,27.586736250460714,22.671365212227148,47.46725734626019 +2024-08-15 06:00:00,10,17.860469330127856,29.58324997035309,66.16009816150282 +2024-08-15 07:00:00,10,14.99409550582077,26.795132849443803,61.19774635429103 +2024-08-15 08:00:00,10,16.84446730945072,23.44166186423429,51.27559776835851 +2024-08-15 09:00:00,10,33.38282143148862,22.68865952498402,49.32957570558732 +2024-08-15 10:00:00,10,23.4878580063233,23.33289382217345,59.689212495500165 +2024-08-15 11:00:00,10,13.373200101137973,19.894912547716206,68.75737880807647 +2024-08-15 12:00:00,10,16.139118694199226,20.56579983136407,58.52192639175113 +2024-08-15 13:00:00,10,26.476358110348514,19.47246948060374,66.90039734604166 +2024-08-15 14:00:00,10,27.334727178218397,19.439597091392343,64.04170753226003 +2024-08-15 15:00:00,10,22.07889568765807,22.343123939397717,36.95083059270496 +2024-08-15 16:00:00,10,22.16539827331292,27.94655976991036,56.102085600737844 +2024-08-15 17:00:00,10,20.1179239163764,26.779909055766705,62.99643236369927 +2024-08-15 18:00:00,10,25.53855377938924,24.321799139906226,52.33243783691783 +2024-08-15 19:00:00,10,22.2063882004259,27.27619587591046,49.47053351616243 +2024-08-15 20:00:00,10,21.052236603907314,25.255188698541204,56.350042454292854 +2024-08-15 21:00:00,10,11.593054987689984,25.382298529883915,59.120119097557414 +2024-08-15 22:00:00,10,24.220199404408902,26.337637011682936,62.89003946021394 +2024-08-15 23:00:00,10,21.902626539453976,20.507738595171386,64.87920204733288 +2024-08-16 00:00:00,10,20.10229204176639,20.701040901303983,35.625220560788456 +2024-08-16 01:00:00,10,23.62599672696816,26.680739411025435,55.25863083418973 +2024-08-16 02:00:00,10,24.120458059395318,31.197692018928574,57.07379266891136 +2024-08-16 03:00:00,10,34.17677338597785,27.06699438386383,68.06579514026083 +2024-08-16 04:00:00,10,28.962777060476064,27.303687133231715,48.46381440013932 +2024-08-16 05:00:00,10,4.896138161939042,27.473670607449677,53.25570167828042 +2024-08-16 06:00:00,10,24.632841604792944,26.802110651230123,61.9579284055286 +2024-08-16 07:00:00,10,11.007983695500556,26.98900154482198,70.70103078857632 +2024-08-16 08:00:00,10,20.62926484394803,18.856179678987168,31.396232222254636 +2024-08-16 09:00:00,10,35.982693320277555,26.37803181557782,55.05641786118668 +2024-08-16 10:00:00,10,33.32768658426217,22.79885110251794,64.30716911533408 +2024-08-16 11:00:00,10,30.853538355405945,21.14706963175662,56.46764363055373 +2024-08-16 12:00:00,10,24.02763560317785,21.634702800441772,59.34065402023132 +2024-08-16 13:00:00,10,27.40505879386532,19.509071785614296,67.9153613659827 +2024-08-16 14:00:00,10,18.049768346197066,22.277485814672875,52.47474425541722 +2024-08-16 15:00:00,10,48.04471107994789,19.92249243175019,63.441472013667926 +2024-08-16 16:00:00,10,31.981673390554512,20.062849745675624,61.02889711493564 +2024-08-16 17:00:00,10,20.086526802370997,17.892778333551846,65.18173267246407 +2024-08-16 18:00:00,10,24.411101034611875,21.101027058547075,52.724846271229126 +2024-08-16 19:00:00,10,38.70441430822853,22.57688062551319,38.04482957005153 +2024-08-16 20:00:00,10,41.93826197074057,27.276636234594967,63.47614852575901 +2024-08-16 21:00:00,10,24.8687273652894,25.03699563435074,73.29848287727117 +2024-08-16 22:00:00,10,19.500631263488078,24.733417541603906,49.488667097704756 +2024-08-16 23:00:00,10,46.7598681482436,26.460630964597893,49.04662425864187 +2024-08-17 00:00:00,10,21.082470742128717,26.431495327214346,59.60212022888497 +2024-08-17 01:00:00,10,27.476675770307637,27.738976777041692,53.75916840070786 +2024-08-17 02:00:00,10,17.81354208736846,23.66212178172375,50.15816384590893 +2024-08-17 03:00:00,10,17.996957842654638,21.330822246191126,57.93935959769338 +2024-08-17 04:00:00,10,11.416927604529652,28.862654075222267,53.36077899839314 +2024-08-17 05:00:00,10,25.97026126110507,27.289073302732085,55.02204727374522 +2024-08-17 06:00:00,10,29.16234807532324,25.620386425468755,59.78960617215409 +2024-08-17 07:00:00,10,19.768865384688706,23.090903406907735,58.2647072249049 +2024-08-17 08:00:00,10,24.569623855644572,17.35276714506645,67.82055407424296 +2024-08-17 09:00:00,10,18.253728562956624,21.240932332268812,54.276307419022764 +2024-08-17 10:00:00,10,11.739476762921344,22.318060309583206,70.14068839624008 +2024-08-17 11:00:00,10,32.63800113868196,24.70891064410816,81.86218020186689 +2024-08-17 12:00:00,10,28.787177241889562,27.21427888542725,70.44382441475838 +2024-08-17 13:00:00,10,21.327860540494406,21.63189270403915,46.59217875474042 +2024-08-17 14:00:00,10,30.118326146621314,23.355051957255966,58.27609533483288 +2024-08-17 15:00:00,10,18.079230200143826,23.521052367185245,67.43680334047386 +2024-08-17 16:00:00,10,49.47418064334575,23.826865846194742,66.15706364355178 +2024-08-17 17:00:00,10,7.946051719797264,18.65317776285889,52.72019675367528 +2024-08-17 18:00:00,10,36.19799740981935,27.563709738673463,59.07248545715799 +2024-08-17 19:00:00,10,25.43652307138532,22.013569842495965,63.04053039344657 +2024-08-17 20:00:00,10,33.17975103182012,26.57335877133989,51.296234506467705 +2024-08-17 21:00:00,10,10.224306436163586,22.645030495404033,61.78088788149176 +2024-08-17 22:00:00,10,7.327448985591589,26.616762958332163,58.01832052788635 +2024-08-17 23:00:00,10,26.63612151559564,22.759086801945365,48.600113718462325 +2024-08-18 00:00:00,10,35.51880136286893,30.433461263011754,69.25032508397881 +2024-08-18 01:00:00,10,19.21759804790666,26.040644185742508,59.71630305446443 +2024-08-18 02:00:00,10,39.72978317829722,25.86490015727803,55.992164266708826 +2024-08-18 03:00:00,10,30.973667355622812,23.062398195341537,60.11919186165295 +2024-08-18 04:00:00,10,12.505209371683225,23.811562201051395,62.44618667812017 +2024-08-18 05:00:00,10,18.47834244929986,28.224643477297676,59.35017744769996 +2024-08-18 06:00:00,10,23.838020644713147,25.58906299507222,60.60902842179581 +2024-08-18 07:00:00,10,18.09827789636088,20.25705225182671,51.01076218723482 +2024-08-18 08:00:00,10,26.15378968185868,24.674833953233428,48.33565347956299 +2024-08-18 09:00:00,10,40.013908323791995,20.30525603860645,63.1887826226738 +2024-08-18 10:00:00,10,12.099742096080194,20.204383675756947,53.75198131406192 +2024-08-18 11:00:00,10,20.975779777607904,27.330090262834762,70.42969042700246 +2024-08-18 12:00:00,10,0.0,15.669817876199465,56.59108818680623 +2024-08-18 13:00:00,10,35.62585884898892,22.66332250292164,61.512388850406296 +2024-08-18 14:00:00,10,22.595947256734146,27.14102935438243,46.905921956202235 +2024-08-18 15:00:00,10,28.44831037027381,23.096085535947964,55.435770580486 +2024-08-18 16:00:00,10,15.439138694224068,24.965236026840394,43.314585701634776 +2024-08-18 17:00:00,10,33.49756182047453,20.98853709217687,56.19046435849154 +2024-08-18 18:00:00,10,12.672541577301988,21.988767146403784,74.28603343195964 +2024-08-18 19:00:00,10,26.59272639586358,25.835473373982733,44.70422483480857 +2024-08-18 20:00:00,10,19.219545553077744,22.854206635963276,29.799893704600567 +2024-08-18 21:00:00,10,31.05671711781411,27.225047209769535,48.88653422517698 +2024-08-18 22:00:00,10,25.63943476324735,23.646703802373395,57.94015288932148 +2024-08-18 23:00:00,10,20.664242599632814,26.266994343160405,44.71573257829603 +2024-08-19 00:00:00,10,21.331308190696753,25.88380057577061,59.73135202276246 +2024-08-19 01:00:00,10,20.707935600187664,33.79141272198402,49.13341147324924 +2024-08-19 02:00:00,10,32.31171004490203,28.00584648521759,60.77829591394342 +2024-08-19 03:00:00,10,19.025796852246998,28.00050669485203,49.86216835976748 +2024-08-19 04:00:00,10,18.396962138846817,24.808525208154812,63.25441281776507 +2024-08-19 05:00:00,10,16.058891036445054,26.50670969068006,51.474022092054604 +2024-08-19 06:00:00,10,13.65179088266845,22.367374566333037,67.8177921573237 +2024-08-19 07:00:00,10,25.260861330672086,22.40595057906198,52.60939132088323 +2024-08-19 08:00:00,10,33.52961354385588,24.896276557284057,50.28220198942299 +2024-08-19 09:00:00,10,25.605482022880935,25.910348386124497,70.07368279121994 +2024-08-19 10:00:00,10,30.44048930840316,23.475447202421726,57.91963503828697 +2024-08-19 11:00:00,10,50.092218867430134,20.96105719277051,57.79326699018962 +2024-08-19 12:00:00,10,30.734848536689864,25.42971882291156,50.63163569119191 +2024-08-19 13:00:00,10,37.05887116656108,22.44707060248103,53.06529189307684 +2024-08-19 14:00:00,10,32.244595341993396,26.758594717397408,46.63033315211112 +2024-08-19 15:00:00,10,23.57845277791261,15.237257790482857,58.348533860898804 +2024-08-19 16:00:00,10,29.691982040692245,24.84629070813641,42.640526220763974 +2024-08-19 17:00:00,10,34.25202658695392,29.25947898725219,59.69454665853983 +2024-08-19 18:00:00,10,27.579102935274232,20.6650438380478,53.08985841308233 +2024-08-19 19:00:00,10,25.622228361100035,20.512997784569347,52.67637702138429 +2024-08-19 20:00:00,10,22.02868731910723,21.006727471117912,57.76530105443899 +2024-08-19 21:00:00,10,11.301736296231777,23.481497034493596,66.86807555546052 +2024-08-19 22:00:00,10,16.493317255952945,27.632903356706528,49.73419411784971 +2024-08-19 23:00:00,10,13.345018590066978,30.48070629160792,56.01710245092734 +2024-08-20 00:00:00,10,31.886525108955464,20.823056936170396,54.51301107845286 +2024-08-20 01:00:00,10,14.842624929272228,25.53656574476033,51.22775108324891 +2024-08-20 02:00:00,10,24.799613912415136,27.734569261298674,55.77338833532036 +2024-08-20 03:00:00,10,48.12015568848024,34.564956510383404,66.58186760528261 +2024-08-20 04:00:00,10,31.10815581249596,28.14710159478019,55.948979250252606 +2024-08-20 05:00:00,10,17.42999002262453,31.370619828216302,55.28097791845828 +2024-08-20 06:00:00,10,37.59438207327033,25.560079487276578,54.26882782084881 +2024-08-20 07:00:00,10,20.61746949730756,23.027423169947166,45.32506478947032 +2024-08-20 08:00:00,10,15.770391734122295,26.275245674110028,58.905469923798684 +2024-08-20 09:00:00,10,16.54798561155386,21.90979009269926,36.70699136886397 +2024-08-20 10:00:00,10,15.464879247840141,16.169032251993244,53.19572806423842 +2024-08-20 11:00:00,10,26.068867118415994,20.796184160355025,64.8393559228469 +2024-08-20 12:00:00,10,24.703742845266422,19.49180416081413,62.0064460558417 +2024-08-20 13:00:00,10,16.70570882829643,22.16536853988132,68.38249943455521 +2024-08-20 14:00:00,10,23.40659195123219,23.637399396022243,48.19733327489646 +2024-08-20 15:00:00,10,14.25850806914954,23.802833052039375,45.60331316293614 +2024-08-20 16:00:00,10,18.265732913981683,21.27572898122588,63.55674733891215 +2024-08-20 17:00:00,10,25.878954102302348,21.724743570568357,58.77719118002412 +2024-08-20 18:00:00,10,18.61825882353437,21.564513304986065,45.26271472610171 +2024-08-20 19:00:00,10,24.259816272148466,27.101830461854988,66.05946318344827 +2024-08-20 20:00:00,10,13.01328688925115,22.406641253578037,66.713653823578 +2024-08-20 21:00:00,10,29.484448390585225,28.80609345413597,58.638912417624944 +2024-08-20 22:00:00,10,12.76301107617548,24.13202691542785,67.0834330293187 +2024-08-20 23:00:00,10,34.97080416151568,31.611544556976515,55.73379698865854 +2024-08-21 00:00:00,10,35.88922919167813,26.810413004912537,54.792190148505256 +2024-08-21 01:00:00,10,34.55383838909334,22.022414143522596,57.372408345754046 +2024-08-21 02:00:00,10,21.903005936752287,25.798594339075034,52.00534171793725 +2024-08-21 03:00:00,10,14.511260532722716,27.076677324446297,75.58343318745673 +2024-08-21 04:00:00,10,23.505423617161963,21.91135279942495,48.454300544993394 +2024-08-21 05:00:00,10,17.391441022083537,26.445608750927207,46.105918815898086 +2024-08-21 06:00:00,10,21.85480242404715,30.671392905420976,58.00443739278015 +2024-08-21 07:00:00,10,10.261549874113571,24.694219756836144,47.65179217635 +2024-08-21 08:00:00,10,10.262609610535081,24.170485253889296,52.27361234400984 +2024-08-21 09:00:00,10,23.323216672641887,20.724428003575728,47.60545407312046 +2024-08-21 10:00:00,10,25.203058451251334,22.389549148529067,70.616948237116 +2024-08-21 11:00:00,10,33.313369967349146,18.573561986993898,49.571260746310976 +2024-08-21 12:00:00,10,27.491114238164386,21.29798799972149,58.93832773692771 +2024-08-21 13:00:00,10,30.102063404978278,23.242072880709237,38.45067459251628 +2024-08-21 14:00:00,10,25.887635374914165,24.053164706011042,64.99438861576735 +2024-08-21 15:00:00,10,28.638797638421092,22.201905335019447,56.453647215240295 +2024-08-21 16:00:00,10,16.436408931575137,23.555886240161293,55.869858214552266 +2024-08-21 17:00:00,10,15.905256737594504,23.352540847238334,52.74382557686231 +2024-08-21 18:00:00,10,3.1383424501565287,26.881110018923934,69.76897641942396 +2024-08-21 19:00:00,10,14.278382450642207,27.53843663448904,43.74925252073977 +2024-08-21 20:00:00,10,10.092686994699442,24.81919158775128,53.72849638682177 +2024-08-21 21:00:00,10,34.813900820830824,20.38290710985051,62.82312199766035 +2024-08-21 22:00:00,10,27.691288052540063,24.748666360451388,54.16158227168537 +2024-08-21 23:00:00,10,21.549601919787154,22.344606055738915,61.182848048238206 +2024-08-22 00:00:00,10,27.170059212809857,26.307801967142733,61.772653886433744 +2024-08-22 01:00:00,10,26.19116401413898,21.38034320041239,58.79432551678655 +2024-08-22 02:00:00,10,27.38449942499639,28.21200683100911,53.52378585511407 +2024-08-22 03:00:00,10,36.7444866544518,28.801546118138145,48.85420299197649 +2024-08-22 04:00:00,10,23.023821232299454,31.705435252740582,44.6585064409567 +2024-08-22 05:00:00,10,19.082237949882668,25.126442301072803,50.37697402179906 +2024-08-22 06:00:00,10,22.147161669933954,24.650394411667964,45.17485003016418 +2024-08-22 07:00:00,10,31.66279196907888,22.58076979421616,63.65082145533425 +2024-08-22 08:00:00,10,25.130693757290786,23.12202433895412,51.462582195638134 +2024-08-22 09:00:00,10,29.394849497175848,28.170703244628438,57.91619270986028 +2024-08-22 10:00:00,10,27.78125963609263,26.67448862906234,53.20918677030771 +2024-08-22 11:00:00,10,28.277906229258857,20.34050721914783,55.25451905775563 +2024-08-22 12:00:00,10,20.264525719395902,24.707100913065645,58.147184608573 +2024-08-22 13:00:00,10,34.00469090855606,26.86872325920773,57.172648734559935 +2024-08-22 14:00:00,10,14.699521902294853,25.124401391938058,52.53347091014088 +2024-08-22 15:00:00,10,8.017812183913257,18.06650357957284,50.688528842728054 +2024-08-22 16:00:00,10,43.03586917507246,22.70414345119247,65.44328167346272 +2024-08-22 17:00:00,10,24.64748973311915,23.450939871372622,60.81800831077339 +2024-08-22 18:00:00,10,27.18882261926905,23.073807348368497,40.2647078738034 +2024-08-22 19:00:00,10,15.744384850235242,28.014070408658455,45.472959021207096 +2024-08-22 20:00:00,10,31.81994410913285,23.06429632201933,68.32901578093475 +2024-08-22 21:00:00,10,28.62602520440986,25.630323795169467,52.03685725665807 +2024-08-22 22:00:00,10,15.834909779665285,20.0482380288443,60.671922696391945 +2024-08-22 23:00:00,10,11.318427579195372,27.133968298326426,64.5938387820851 +2024-08-23 00:00:00,10,14.98751164404833,30.696497229493936,49.26975238422676 +2024-08-23 01:00:00,10,24.399032471390093,25.62196308206928,61.15949275833909 +2024-08-23 02:00:00,10,22.05361508117959,29.94324624823589,61.18126880456362 +2024-08-23 03:00:00,10,23.06661967359625,25.748052366935628,46.68970690883598 +2024-08-23 04:00:00,10,22.822525533360537,30.743194995814914,64.72740549966984 +2024-08-23 05:00:00,10,7.370891238480976,26.230995431014833,71.11824635614721 +2024-08-23 06:00:00,10,29.6100788927474,27.943117634033246,44.91017014940988 +2024-08-23 07:00:00,10,18.69719108837873,27.82345359696705,55.701219981222486 +2024-08-23 08:00:00,10,30.156253723146527,22.365314226718358,56.209526406885786 +2024-08-23 09:00:00,10,23.85238928655042,17.865666524015246,76.09329633066758 +2024-08-23 10:00:00,10,12.936034116911003,28.449938511364437,62.78362975886456 +2024-08-23 11:00:00,10,25.126192250421912,21.899708013726425,62.54020246332553 +2024-08-23 12:00:00,10,32.164886946406796,27.933802626617393,61.89637522102259 +2024-08-23 13:00:00,10,32.95632059914027,21.290615878087788,62.9864632284203 +2024-08-23 14:00:00,10,24.88488900468843,23.72516353978982,51.85017165680401 +2024-08-23 15:00:00,10,28.770225676607627,23.199156357316113,55.53093652388447 +2024-08-23 16:00:00,10,32.59737058171296,23.258189621251855,48.4072866530952 +2024-08-23 17:00:00,10,2.538127294430222,21.200918329934066,53.9537998734504 +2024-08-23 18:00:00,10,25.465324081198954,19.706480915462475,48.28451886691293 +2024-08-23 19:00:00,10,6.243071658504398,26.684590530455154,55.58767811315872 +2024-08-23 20:00:00,10,12.796420768077027,16.578840504642116,53.94615856470542 +2024-08-23 21:00:00,10,26.678870442795485,24.70306435854693,41.94153634038329 +2024-08-23 22:00:00,10,4.768468846294912,21.07829206053718,59.61068692018557 +2024-08-23 23:00:00,10,20.792105867176094,24.55725084388753,57.47493859739267 +2024-08-24 00:00:00,10,30.876368912700478,24.74211322708894,36.90055814930443 +2024-08-24 01:00:00,10,21.864922400878108,28.963664941779275,47.53641641711305 +2024-08-24 02:00:00,10,16.544446329299433,20.574893912874625,32.006028433791045 +2024-08-24 03:00:00,10,31.931670743681558,33.00150690150294,55.9616730478051 +2024-08-24 04:00:00,10,25.97346512458542,32.58021956816603,69.94536599253277 +2024-08-24 05:00:00,10,28.620248052093125,26.654161779114062,40.14942038440202 +2024-08-24 06:00:00,10,31.138163584424618,23.131119771479963,58.17518176379398 +2024-08-24 07:00:00,10,11.193233361126067,24.344483158644948,58.24273156035234 +2024-08-24 08:00:00,10,19.268382601563463,22.674134823851094,67.27155663359798 +2024-08-24 09:00:00,10,30.374446677672925,22.92796293060683,46.88709919872221 +2024-08-24 10:00:00,10,18.5284538857297,18.93606725421696,80.11560856277461 +2024-08-24 11:00:00,10,33.34224984546494,20.936578468086857,47.45328276213174 +2024-08-24 12:00:00,10,21.867599922835208,25.86986837670013,61.5754498005341 +2024-08-24 13:00:00,10,37.876479729641176,25.62250527447253,50.2363452364613 +2024-08-24 14:00:00,10,28.750966247281518,21.465748894683255,75.17071725898182 +2024-08-24 15:00:00,10,14.049686823774742,25.303321367092764,54.85529048036927 +2024-08-24 16:00:00,10,13.95020001638749,19.51902312088837,46.80629407141689 +2024-08-24 17:00:00,10,30.30757612441284,25.280524753247608,49.497777548624775 +2024-08-24 18:00:00,10,35.858417316567,18.516717743619143,62.91031928635763 +2024-08-24 19:00:00,10,29.21517411121752,23.824221894105353,60.36900667232949 +2024-08-24 20:00:00,10,31.22608546319038,22.743236259432887,41.3807677685426 +2024-08-24 21:00:00,10,15.469233339965395,22.73662776577959,54.75961062068998 +2024-08-24 22:00:00,10,23.501555000174854,18.47472744802452,62.28216277164624 +2024-08-24 23:00:00,10,20.442811666797493,27.926425225685843,62.27654551352586 +2024-08-25 00:00:00,10,34.160122390406784,27.389122042842384,46.29297866731762 +2024-08-25 01:00:00,10,16.296014282805068,27.289533410805586,47.948227638566806 +2024-08-25 02:00:00,10,17.954222070086924,32.636737437958075,62.390284044701836 +2024-08-25 03:00:00,10,13.188289587894884,29.633367309624646,67.99803623743007 +2024-08-25 04:00:00,10,20.695115912121782,29.33216004678599,56.595463575196845 +2024-08-25 05:00:00,10,39.876305103024066,25.86185446790905,52.84404076463816 +2024-08-25 06:00:00,10,32.926261495930476,25.00944287769451,66.92121018340171 +2024-08-25 07:00:00,10,28.98760967414608,21.948271978433727,59.07344564196711 +2024-08-25 08:00:00,10,19.869288908819144,23.383988820573208,57.47969346617241 +2024-08-25 09:00:00,10,19.18204798814483,23.089623308783708,71.21121195408763 +2024-08-25 10:00:00,10,34.02778231264874,19.92233063901672,58.644098233327696 +2024-08-25 11:00:00,10,24.283687993534894,26.453524054665486,67.66916440825307 +2024-08-25 12:00:00,10,4.842138469812571,25.545156818204713,48.967630548405296 +2024-08-25 13:00:00,10,18.93222257924308,23.95463396336074,55.008092925706656 +2024-08-25 14:00:00,10,15.252819141914916,21.73968370738559,52.59602540807437 +2024-08-25 15:00:00,10,27.08080648344646,17.537389807632366,65.3171014064393 +2024-08-25 16:00:00,10,32.9053238940523,29.09279490737594,41.97466215122574 +2024-08-25 17:00:00,10,20.090723682104354,19.238477426588453,54.2335950300867 +2024-08-25 18:00:00,10,10.64827053525041,23.343151149331156,53.13486085487285 +2024-08-25 19:00:00,10,16.984663292565923,23.032687407278946,57.77716881107825 +2024-08-25 20:00:00,10,29.137505169789108,26.629848455254276,59.8845222959107 +2024-08-25 21:00:00,10,21.392305830901645,31.540964482157186,51.61866972572256 +2024-08-25 22:00:00,10,33.4402154980939,20.60074663977736,46.21302750930809 +2024-08-25 23:00:00,10,29.58959972451339,19.455711955860792,44.1718596976403 +2024-08-26 00:00:00,10,28.803951964553967,34.819762049730045,57.04297579701451 +2024-08-26 01:00:00,10,21.706288794820097,21.217722698757814,45.67743626580128 +2024-08-26 02:00:00,10,40.623422181287836,25.865396939234632,57.28270055730796 +2024-08-26 03:00:00,10,14.183754391100939,21.496162196453717,61.91449019721823 +2024-08-26 04:00:00,10,17.58736301100047,22.320978840219496,54.69949679687458 +2024-08-26 05:00:00,10,32.2535334126035,23.95940403048438,46.577751799695875 +2024-08-26 06:00:00,10,15.545230809688121,36.353944015890235,50.62739994360781 +2024-08-26 07:00:00,10,23.761844485915027,24.79960430692523,71.92309146628074 +2024-08-26 08:00:00,10,30.753151565347743,25.4612175036737,45.956815232658336 +2024-08-26 09:00:00,10,34.60000370416517,29.567371879752173,60.42004130505936 +2024-08-26 10:00:00,10,11.397305549678201,26.237552037394046,60.576156140234545 +2024-08-26 11:00:00,10,26.5589784238072,18.866972651527156,74.9023452728213 +2024-08-26 12:00:00,10,24.154667713399462,23.001953900202203,66.17517348325894 +2024-08-26 13:00:00,10,30.134797236844346,22.762651343636847,69.76322753189554 +2024-08-26 14:00:00,10,16.05657632407769,26.98831466745105,56.534397170299066 +2024-08-26 15:00:00,10,13.4814088575409,23.20116644604879,49.08416472913608 +2024-08-26 16:00:00,10,26.832126842548902,22.279185140396873,54.240579029517406 +2024-08-26 17:00:00,10,26.726107256262875,22.665261857367454,63.85437033000636 +2024-08-26 18:00:00,10,37.432586005696244,28.978435113528644,52.57427632453946 +2024-08-26 19:00:00,10,14.915296400579054,21.279388301237937,55.34958683205109 +2024-08-26 20:00:00,10,31.09548091203491,24.42651154944025,68.03677805217272 +2024-08-26 21:00:00,10,29.650816676330166,24.722453195270706,58.67063326811497 +2024-08-26 22:00:00,10,32.85683886442207,20.393585138668275,59.4766202258911 +2024-08-26 23:00:00,10,24.76362030240168,17.83087605918609,46.188413859557144 +2024-08-27 00:00:00,10,13.535410192647333,24.848395500880894,56.14244050245619 +2024-08-27 01:00:00,10,15.916467508855328,26.46653818629706,67.40936383532046 +2024-08-27 02:00:00,10,11.143672662521837,25.132750263655783,55.80647884561922 +2024-08-27 03:00:00,10,8.284253315543136,28.73943008718407,42.47328569427589 +2024-08-27 04:00:00,10,22.02550561806711,24.304876836514374,41.37257509325936 +2024-08-27 05:00:00,10,33.48424083829138,25.66269593836187,79.21374268879289 +2024-08-27 06:00:00,10,15.631218558257483,28.00248496653235,60.04719307689384 +2024-08-27 07:00:00,10,18.16841089530109,24.756525392213828,53.890033378873994 +2024-08-27 08:00:00,10,28.352230679324588,24.07687288502919,40.13454439171811 +2024-08-27 09:00:00,10,32.98451195204607,29.05914302804554,74.63756151542322 +2024-08-27 10:00:00,10,22.006109904280706,23.61921827052776,61.27317328658865 +2024-08-27 11:00:00,10,27.444711498347473,15.741914339433366,43.93876528012696 +2024-08-27 12:00:00,10,24.73963254082562,25.154038526298812,39.32152161147337 +2024-08-27 13:00:00,10,34.57441700326017,23.469144843633703,51.46078589546988 +2024-08-27 14:00:00,10,39.72894237010482,23.79665967313184,74.1547215064016 +2024-08-27 15:00:00,10,24.016291680855335,22.350855598433057,43.98263360127581 +2024-08-27 16:00:00,10,27.08211958257668,20.118226918206986,54.80533646155438 +2024-08-27 17:00:00,10,37.289149097148695,25.382506418612238,66.48802570421586 +2024-08-27 18:00:00,10,16.85927596985614,24.492716555373065,55.957983269791214 +2024-08-27 19:00:00,10,8.42266388596675,18.997446056857463,47.11454688758623 +2024-08-27 20:00:00,10,0.590792844735148,25.23132374345247,49.88467184657064 +2024-08-27 21:00:00,10,26.932041770656454,22.643181000733634,48.5357143775961 +2024-08-27 22:00:00,10,20.277900639309667,23.439377626100754,53.80703256644786 +2024-08-27 23:00:00,10,12.933350471053677,25.66756556784935,44.625386693563186 +2024-08-28 00:00:00,10,21.935178276387884,30.218405027224055,44.286326593730806 +2024-08-28 01:00:00,10,12.084953802682099,28.083796629983645,43.16823308606342 +2024-08-28 02:00:00,10,12.630413062145145,28.285451233473662,62.93363070360468 +2024-08-28 03:00:00,10,19.702828455937915,22.510870817844665,48.98934234352224 +2024-08-28 04:00:00,10,10.359865983052897,23.348481194899406,37.49057056305053 +2024-08-28 05:00:00,10,41.75269081494304,26.943036506233884,64.53108565698813 +2024-08-28 06:00:00,10,23.840656254997732,30.27389986612305,64.68674792978146 +2024-08-28 07:00:00,10,18.638282398637692,29.08349947899839,56.889501498693015 +2024-08-28 08:00:00,10,20.220032749931665,22.200825643021346,69.20451689208075 +2024-08-28 09:00:00,10,20.527753782565007,24.15063352570984,72.69620919496452 +2024-08-28 10:00:00,10,14.63804807152268,20.326251104075652,62.999655643902166 +2024-08-28 11:00:00,10,25.55618744948341,24.997009760747467,55.54312037190917 +2024-08-28 12:00:00,10,23.597510223537856,26.313255523778253,65.23531582386428 +2024-08-28 13:00:00,10,32.41986401216247,24.924998782319108,60.49319637208161 +2024-08-28 14:00:00,10,32.440901874998005,21.69614984228186,62.32873063013599 +2024-08-28 15:00:00,10,28.737207770720154,23.79011357290028,63.25977109216806 +2024-08-28 16:00:00,10,42.54414768002873,20.525968244638538,37.40370789368844 +2024-08-28 17:00:00,10,29.588267080008695,21.968169022389915,80.33645006661877 +2024-08-28 18:00:00,10,20.864520784207077,22.717546759738493,75.32816882089934 +2024-08-28 19:00:00,10,38.38846376034758,18.780284328099878,58.58946793227221 +2024-08-28 20:00:00,10,2.4142516723940552,20.065669915762157,63.9086988714363 +2024-08-28 21:00:00,10,10.520740596584824,23.033331680101107,46.96104060786202 +2024-08-28 22:00:00,10,9.720366498175766,22.397108222577717,61.8745181980031 +2024-08-28 23:00:00,10,26.737801287003975,27.721634128435188,65.84819299167798 +2024-08-29 00:00:00,10,25.38694413151024,25.23005861203722,47.29045686133692 +2024-08-29 01:00:00,10,23.790918320934402,25.99603884828202,72.92441748768809 +2024-08-29 02:00:00,10,16.345965928984114,22.034108424876973,63.82381978215525 +2024-08-29 03:00:00,10,9.458231720724582,21.356803850145305,55.05757921163936 +2024-08-29 04:00:00,10,23.054360062720356,23.880022774014527,62.75055272916654 +2024-08-29 05:00:00,10,36.11727295058047,28.09825112009457,62.06804126740516 +2024-08-29 06:00:00,10,29.628074611317636,20.66858198561777,62.13967124228742 +2024-08-29 07:00:00,10,14.689955450141579,14.136146223420399,55.33758072241399 +2024-08-29 08:00:00,10,18.525035290925636,24.803059923518944,45.52367160115682 +2024-08-29 09:00:00,10,24.645836054580183,21.67277261552242,53.11275584411363 +2024-08-29 10:00:00,10,15.465870581025353,22.446933301260884,68.1792189695039 +2024-08-29 11:00:00,10,10.60921832853065,25.924885235107624,51.67874116443374 +2024-08-29 12:00:00,10,33.921088044635795,22.477067396849602,74.76368895016864 +2024-08-29 13:00:00,10,5.122631162067545,20.048278018510373,53.018713022872824 +2024-08-29 14:00:00,10,42.17968924248033,22.850377326621757,51.94903120479004 +2024-08-29 15:00:00,10,21.067413508465975,22.03260228977514,47.65669398167988 +2024-08-29 16:00:00,10,35.140373432610346,24.441309193565182,80.92900087672504 +2024-08-29 17:00:00,10,10.143816582385998,24.883206847916586,54.339923651094985 +2024-08-29 18:00:00,10,33.5436492665648,20.27977501943161,54.892748971560934 +2024-08-29 19:00:00,10,35.674683219337545,21.475418731553567,47.9470177613599 +2024-08-29 20:00:00,10,26.79401236179572,25.57990030466245,62.791540421047145 +2024-08-29 21:00:00,10,19.595497754791552,20.4763497423492,59.3994122024196 +2024-08-29 22:00:00,10,36.03066635812043,27.05933588425331,49.439059122542986 +2024-08-29 23:00:00,10,26.487654708418887,24.589451014437454,46.1834101795617 +2024-08-30 00:00:00,10,38.752998389184235,27.855841092815503,63.23681438831141 +2024-08-30 01:00:00,10,14.807389856474792,26.647429640141187,64.26234912811223 +2024-08-30 02:00:00,10,25.865125529598814,27.481845318821616,49.37509501749379 +2024-08-30 03:00:00,10,28.134619016803143,25.012943484217253,62.68668251346162 +2024-08-30 04:00:00,10,17.545919118090296,23.21791573042193,54.394468843701304 +2024-08-30 05:00:00,10,36.11671888371124,24.012361534345537,54.407160606162016 +2024-08-30 06:00:00,10,30.484143609891454,19.77713059615919,48.06720172892558 +2024-08-30 07:00:00,10,27.21729838990189,22.462734269548868,50.4848190585027 +2024-08-30 08:00:00,10,30.97751332491001,20.894630316894933,47.490193181758144 +2024-08-30 09:00:00,10,18.861984526969838,21.894804784196577,57.96259530783666 +2024-08-30 10:00:00,10,47.701350027402384,22.78820504768214,64.58091608943744 +2024-08-30 11:00:00,10,19.157770106509748,22.18365446955543,66.73454579059779 +2024-08-30 12:00:00,10,25.712552640791557,19.656870776544434,46.31512775784863 +2024-08-30 13:00:00,10,25.09136999539971,30.067208224692045,63.05497024859071 +2024-08-30 14:00:00,10,21.98660276461673,20.600304432187478,59.13504538650651 +2024-08-30 15:00:00,10,20.21068396595546,19.9111044882247,61.628472191075176 +2024-08-30 16:00:00,10,21.220343812094562,24.915521209132734,35.419905834371676 +2024-08-30 17:00:00,10,25.926553158202474,24.539691591983793,61.44243015190243 +2024-08-30 18:00:00,10,20.728023620370564,28.52775644856963,53.12825984359719 +2024-08-30 19:00:00,10,30.188335038653666,19.01870146113869,52.40802579271971 +2024-08-30 20:00:00,10,24.421205888380843,22.79586990434273,70.47554585846363 +2024-08-30 21:00:00,10,29.707773918919123,19.7956623662947,59.078958934112165 +2024-08-30 22:00:00,10,5.8109148358967495,29.618105235386196,47.76925704141822 +2024-08-30 23:00:00,10,15.498985844993365,24.033283883708577,55.74148585476825 +2024-08-31 00:00:00,10,43.3868290002248,29.192324435935408,42.08899792709177 +2024-08-31 01:00:00,10,17.956800686487515,28.9771616042246,60.30479963282381 +2024-08-31 02:00:00,10,34.58890417768767,25.86763258387908,64.28841052578694 +2024-08-31 03:00:00,10,20.29079805027419,23.51576923266721,38.96718130020689 +2024-08-31 04:00:00,10,22.000380977589906,29.303115998435942,47.92680554372233 +2024-08-31 05:00:00,10,21.577481846223634,24.860988290335186,64.91205292396663 +2024-08-31 06:00:00,10,31.702531497638574,25.036492915444413,54.3565702846057 +2024-08-31 07:00:00,10,36.03918126719078,21.470416664279178,74.6869721156767 +2024-08-31 08:00:00,10,18.274523873091212,24.82157910155371,67.29236375659535 +2024-08-31 09:00:00,10,1.936238551133016,16.631115965933613,49.39954298223966 +2024-08-31 10:00:00,10,29.180413621688707,19.29993159686252,78.96420157544127 +2024-08-31 11:00:00,10,29.363094348210076,19.614443936085046,67.09244816500633 +2024-08-31 12:00:00,10,12.812184202453999,24.928411946050637,42.95887599021857 +2024-08-31 13:00:00,10,7.047809978648381,26.25832771251324,48.68747841442156 +2024-08-31 14:00:00,10,34.33072294296474,26.064546287725424,58.74200946479667 +2024-08-31 15:00:00,10,24.76803808730106,17.57971609960509,48.30961539373799 +2024-08-31 16:00:00,10,21.546115819657736,22.00687485393266,54.51107327149415 +2024-08-31 17:00:00,10,22.187400143239884,22.513638864447945,61.32433006954824 +2024-08-31 18:00:00,10,17.707523557027677,33.20418259180402,63.60612866122269 +2024-08-31 19:00:00,10,40.353240812840056,24.23307850717729,43.51978091838248 +2024-08-31 20:00:00,10,23.166723417590923,15.948457096746177,61.2619977425087 +2024-08-31 21:00:00,10,14.47203014889342,22.96540869599205,54.82021623471586 +2024-08-31 22:00:00,10,11.888282978054942,18.001487689085742,52.09161885233756 +2024-08-31 23:00:00,10,27.766093233843804,23.582220941692846,49.186622592315544 +2024-09-01 00:00:00,10,28.19187816119666,28.38413528475628,63.13289307788771 +2024-09-01 01:00:00,10,33.23472469469842,28.972884282165708,54.46260439881516 +2024-09-01 02:00:00,10,24.869340773981808,22.473053256825914,59.70265759370228 +2024-09-01 03:00:00,10,20.999799057484896,25.208553901224683,62.78628267492466 +2024-09-01 04:00:00,10,22.513162490381713,24.066766986478495,60.895567910643926 +2024-09-01 05:00:00,10,33.757074290066406,28.817101343920605,43.71445911394393 +2024-09-01 06:00:00,10,27.516846663684657,23.85666827301209,80.07009794553727 +2024-09-01 07:00:00,10,3.317998610029285,22.39055699009125,62.363381778572965 +2024-09-01 08:00:00,10,12.40182629788626,23.72034806670667,70.60227965021944 +2024-09-01 09:00:00,10,26.928660751125378,26.163826596645407,58.182813430008096 +2024-09-01 10:00:00,10,24.186262472372935,24.470246775625704,52.761890580404504 +2024-09-01 11:00:00,10,4.760885967820045,19.538699179787336,57.15397216022671 +2024-09-01 12:00:00,10,33.4584499787869,20.00392074298994,47.907068245715024 +2024-09-01 13:00:00,10,35.470610714415216,23.03241901091492,52.6882944676922 +2024-09-01 14:00:00,10,47.48139733193421,21.1871475414559,75.13521473074414 +2024-09-01 15:00:00,10,27.743794072864198,20.830691413288168,62.00495275915192 +2024-09-01 16:00:00,10,25.922305013501617,25.471391331967396,59.527457610285104 +2024-09-01 17:00:00,10,28.19594436647684,25.3597680550708,46.55872662820699 +2024-09-01 18:00:00,10,25.210908677539067,24.167820353573987,43.70019789832007 +2024-09-01 19:00:00,10,12.4790143569716,21.20925595097194,50.22058436541917 +2024-09-01 20:00:00,10,22.759133912638195,27.25726310857362,42.173086365176 +2024-09-01 21:00:00,10,30.207973402272632,22.108860748973193,44.677240512091096 +2024-09-01 22:00:00,10,30.255712239598864,27.51800751265152,60.71824306739457 +2024-09-01 23:00:00,10,24.02214293383654,21.69194525930509,73.23007077748484 +2024-09-02 00:00:00,10,3.593824850034377,25.327342544710636,68.29967353987895 +2024-09-02 01:00:00,10,15.593749935761995,21.88088264766879,65.51852577248897 +2024-09-02 02:00:00,10,17.794526315095222,27.321132457295793,48.82702055141445 +2024-09-02 03:00:00,10,19.603282574768222,25.91807347541246,53.11107077153416 +2024-09-02 04:00:00,10,12.909016623227187,30.30334905546502,57.28124894306216 +2024-09-02 05:00:00,10,14.997102493103968,21.92820367205692,61.54994146733372 +2024-09-02 06:00:00,10,22.79661681033025,25.33105896081802,53.8850273637784 +2024-09-02 07:00:00,10,20.855630617716546,26.657453875017083,69.40637308250272 +2024-09-02 08:00:00,10,33.704540523736746,23.418234764499072,55.895142592213 +2024-09-02 09:00:00,10,16.659990699383123,23.287768233672885,65.55616701538413 +2024-09-02 10:00:00,10,27.101753604578036,29.258506232978508,40.70492355135655 +2024-09-02 11:00:00,10,12.830825328202426,31.627744894482916,65.52147038570575 +2024-09-02 12:00:00,10,36.48944886946022,21.628491094951183,59.29092135235225 +2024-09-02 13:00:00,10,26.546738129417022,19.869073530788366,59.65587804023465 +2024-09-02 14:00:00,10,23.913886705106204,26.693276097666278,58.89247692642283 +2024-09-02 15:00:00,10,15.816316645432073,24.243412565604196,59.03095050954017 +2024-09-02 16:00:00,10,23.30659877357073,22.00193722215586,54.33165937256229 +2024-09-02 17:00:00,10,29.68347674726538,29.791908648921368,46.02073471044306 +2024-09-02 18:00:00,10,32.6207244483675,21.265411653843785,52.49924363947035 +2024-09-02 19:00:00,10,30.151611171343937,21.60862509754483,40.063646287086904 +2024-09-02 20:00:00,10,35.01274403611898,27.112278710340277,60.1366237283313 +2024-09-02 21:00:00,10,7.011111956161182,20.58574267841843,60.25910139254737 +2024-09-02 22:00:00,10,14.709198481427283,25.02390297700337,61.16185769586692 +2024-09-02 23:00:00,10,20.387027501919302,26.76530094906782,52.505089131073376 +2024-09-03 00:00:00,10,31.169900369471968,29.193041431752874,58.21237093122979 +2024-09-03 01:00:00,10,5.746274246824914,33.951337759297935,39.69350542604222 +2024-09-03 02:00:00,10,33.579387492905276,30.382098387083907,58.646446417501146 +2024-09-03 03:00:00,10,34.215933288944996,24.227504854709775,70.45444907950912 +2024-09-03 04:00:00,10,23.34951155020895,28.01174215714361,60.1496057340531 +2024-09-03 05:00:00,10,38.16591251772035,24.22625540891311,54.19759198139151 +2024-09-03 06:00:00,10,35.20639270025633,20.923132239033364,43.59541238612175 +2024-09-03 07:00:00,10,22.04885828919245,16.440293427206804,63.17512217248414 +2024-09-03 08:00:00,10,29.889925692322944,23.159488027316186,44.625918275459334 +2024-09-03 09:00:00,10,21.52222006026176,25.47361538456442,50.115264752315696 +2024-09-03 10:00:00,10,10.71524327276452,21.480849138568225,48.679326000868485 +2024-09-03 11:00:00,10,27.93584741263627,18.76072725665884,61.858017337634635 +2024-09-03 12:00:00,10,30.314466058806907,21.932761989105316,58.91101595125689 +2024-09-03 13:00:00,10,24.030324054707435,27.144030236051307,53.723512548854 +2024-09-03 14:00:00,10,28.117404861636896,19.21702948978767,70.70873607328596 +2024-09-03 15:00:00,10,27.40111502610841,22.73462780267533,60.782429163354465 +2024-09-03 16:00:00,10,24.135846685426834,23.166356590423273,54.3357564545251 +2024-09-03 17:00:00,10,28.52036089387321,20.16655385981503,52.24539032117264 +2024-09-03 18:00:00,10,22.442947641767326,18.581722625695647,69.45435587834007 +2024-09-03 19:00:00,10,18.196576813943224,24.561573084085154,55.91730545710813 +2024-09-03 20:00:00,10,14.026976231367119,24.722619829224648,50.18744178900427 +2024-09-03 21:00:00,10,15.637963265437328,22.599821532256385,71.61047227911553 +2024-09-03 22:00:00,10,20.32822837184009,24.464073018301743,59.41948900365985 +2024-09-03 23:00:00,10,18.734085922920492,27.293055465007626,37.42042606475464 +2024-09-04 00:00:00,10,27.929831245812164,25.857066828357077,50.45321742115771 +2024-09-04 01:00:00,10,16.323281526288728,31.069808066123954,53.09237631546311 +2024-09-04 02:00:00,10,22.204366986759652,30.117493481334986,59.73875691169768 +2024-09-04 03:00:00,10,22.54062450229057,24.94374442183288,52.55623767652205 +2024-09-04 04:00:00,10,30.672931805138745,25.115252309257965,55.44065971687664 +2024-09-04 05:00:00,10,44.107991542380546,20.590782273995945,58.11809663243096 +2024-09-04 06:00:00,10,16.886803055656415,28.45477604180282,70.17147022668902 +2024-09-04 07:00:00,10,16.483972745076418,21.064619710754076,56.483360047834964 +2024-09-04 08:00:00,10,22.7286095128974,23.30026306925268,54.35536606412631 +2024-09-04 09:00:00,10,27.854135040953352,23.024371738645375,46.873497627910936 +2024-09-04 10:00:00,10,13.022480748253479,18.290665550992053,74.9326167304316 +2024-09-04 11:00:00,10,17.56028138459063,17.735938111079356,65.80430063332199 +2024-09-04 12:00:00,10,33.93806027995012,25.433429425982432,46.61558848629157 +2024-09-04 13:00:00,10,36.77440288298867,24.646165054935487,52.31428533119952 +2024-09-04 14:00:00,10,19.39541668096716,17.870260403651326,72.02461127551874 +2024-09-04 15:00:00,10,20.837875737959777,19.823566374765345,69.49578814952888 +2024-09-04 16:00:00,10,13.91714480526297,23.395657008274714,47.753936853050845 +2024-09-04 17:00:00,10,26.43485125570083,17.497405003227872,45.527272601644974 +2024-09-04 18:00:00,10,13.103310684257789,26.11826406107469,33.282125388665335 +2024-09-04 19:00:00,10,16.005749821563736,22.282743002712934,67.45913978110525 +2024-09-04 20:00:00,10,17.498073183459134,23.71656880192098,56.54672445407042 +2024-09-04 21:00:00,10,14.901733035270212,22.40985653431511,66.8446030168339 +2024-09-04 22:00:00,10,18.060373580121844,25.38832253940239,51.42080923181656 +2024-09-04 23:00:00,10,17.304404489711683,21.665570748803297,62.220469592087554 +2024-09-05 00:00:00,10,17.338236091102473,27.283989576947263,62.85505784596694 +2024-09-05 01:00:00,10,25.978533800358623,24.23993445886634,61.2688517591951 +2024-09-05 02:00:00,10,31.47058779406944,23.788942114710327,54.34367478914622 +2024-09-05 03:00:00,10,31.72081490792023,24.551993642615873,70.85345302245452 +2024-09-05 04:00:00,10,17.181628982040927,25.666272036680613,49.596390228806875 +2024-09-05 05:00:00,10,18.083769398669126,26.03384390037489,40.19642928792115 +2024-09-05 06:00:00,10,20.641374290388274,27.210861505442402,34.48032586179663 +2024-09-05 07:00:00,10,7.837459327621575,24.689669707775117,45.39603262662273 +2024-09-05 08:00:00,10,12.744614771581995,29.368671066523284,55.67589112069161 +2024-09-05 09:00:00,10,33.70796588941804,28.148621339036865,64.48743093935977 +2024-09-05 10:00:00,10,25.766425684518975,15.855310509863585,61.153740963627634 +2024-09-05 11:00:00,10,24.23999557249119,18.916244328974223,81.63614125217288 +2024-09-05 12:00:00,10,19.491345414033532,22.223386454729226,60.39990221961275 +2024-09-05 13:00:00,10,20.267128104959482,22.02783094925167,61.986016526460695 +2024-09-05 14:00:00,10,4.7057400152846895,17.02460086869405,58.27146879341829 +2024-09-05 15:00:00,10,28.31856022285572,23.325031393802696,58.01317845507292 +2024-09-05 16:00:00,10,32.39685867210145,21.995779710544884,63.17820737403319 +2024-09-05 17:00:00,10,25.718831109155083,26.00692926652898,52.58109157959715 +2024-09-05 18:00:00,10,10.000413280399695,20.62214134150455,41.5018899709106 +2024-09-05 19:00:00,10,3.8465665846250943,23.260859861666617,45.0639243191337 +2024-09-05 20:00:00,10,21.15160675907133,27.471389519730117,57.99282681941027 +2024-09-05 21:00:00,10,11.258097209725213,24.33033495927015,54.14659819995859 +2024-09-05 22:00:00,10,18.182434894355804,23.06521647598067,38.35561326610795 +2024-09-05 23:00:00,10,19.65213499553667,27.198212874009993,50.76492108643576 +2024-09-06 00:00:00,10,23.92935769006768,24.177699976574733,52.34888658367514 +2024-09-06 01:00:00,10,23.8125078230336,28.861414538872154,33.01317413944798 +2024-09-06 02:00:00,10,32.28696965737948,22.156511147139625,52.75689407832728 +2024-09-06 03:00:00,10,30.420496291832084,27.222502557191902,33.05454543749502 +2024-09-06 04:00:00,10,26.227882197831853,21.941088800343692,58.12017865367955 +2024-09-06 05:00:00,10,18.572266318668337,22.722598702070076,62.29321526363934 +2024-09-06 06:00:00,10,44.70171961427273,26.206659223360692,72.10097410686151 +2024-09-06 07:00:00,10,17.345940098292957,19.68929065261502,40.81148533740978 +2024-09-06 08:00:00,10,32.65917536843031,18.598089282646036,57.585570386690634 +2024-09-06 09:00:00,10,41.6095367059647,27.214652135898028,56.944268018118066 +2024-09-06 10:00:00,10,23.739772951654075,24.11510093103616,54.535170118924135 +2024-09-06 11:00:00,10,33.90151780988833,24.61689239724645,56.56208438822411 +2024-09-06 12:00:00,10,19.000788008141306,21.663865198476334,57.612019456868985 +2024-09-06 13:00:00,10,25.34231057347212,25.23221665476074,50.77555517933688 +2024-09-06 14:00:00,10,31.224256114961204,22.86152579190526,40.376035903941684 +2024-09-06 15:00:00,10,28.792207507553986,31.455466575370554,45.000308804179674 +2024-09-06 16:00:00,10,15.957610528424974,25.545443652030826,62.55395471846058 +2024-09-06 17:00:00,10,21.215417656860506,20.277052116696197,55.233893582645344 +2024-09-06 18:00:00,10,28.488046615435355,16.467209713200184,66.5613500996733 +2024-09-06 19:00:00,10,37.67752040872636,28.119064482968646,44.06683266421496 +2024-09-06 20:00:00,10,15.395220185720696,24.394054640622826,28.306447002448806 +2024-09-06 21:00:00,10,29.569409494569918,33.19242026181502,39.052366301717285 +2024-09-06 22:00:00,10,18.100557687250557,25.8796604193255,44.033103135093214 +2024-09-06 23:00:00,10,19.981371548135694,25.60089093825698,68.89550119792932 +2024-09-07 00:00:00,10,17.759203317600925,32.01464217289016,58.625428825116884 +2024-09-07 01:00:00,10,18.703408734689248,26.79101738319726,63.98976386335075 +2024-09-07 02:00:00,10,34.891496026146676,24.87463580913936,48.240131876360834 +2024-09-07 03:00:00,10,23.340583976663467,24.78752042883507,49.44260402697633 +2024-09-07 04:00:00,10,26.34821263470178,25.901414676863894,39.53220372137079 +2024-09-07 05:00:00,10,24.012076328558663,23.796109384825638,49.67065343503916 +2024-09-07 06:00:00,10,41.19034378180871,28.15787497101053,52.93197202474123 +2024-09-07 07:00:00,10,18.492759238494045,26.969598934440093,52.78170668057908 +2024-09-07 08:00:00,10,26.172933061863198,21.276440166782457,51.835939978402685 +2024-09-07 09:00:00,10,26.40185533051022,21.88896397183855,52.34740134649558 +2024-09-07 10:00:00,10,12.879499716690054,27.5416653330828,51.74638523909186 +2024-09-07 11:00:00,10,31.00206912541482,24.04213343121511,47.582804390668 +2024-09-07 12:00:00,10,22.836892257662505,23.24250113833219,65.1537824595235 +2024-09-07 13:00:00,10,39.504231352609075,20.74652397928208,61.1063840964435 +2024-09-07 14:00:00,10,16.429235518959356,21.811583870200305,52.77930718950614 +2024-09-07 15:00:00,10,42.670409430083694,22.85165436667477,42.69854383168247 +2024-09-07 16:00:00,10,14.63087549840946,27.371714744225567,63.11928566462381 +2024-09-07 17:00:00,10,28.09309606399722,22.064015162356394,62.84141329005671 +2024-09-07 18:00:00,10,23.78531380728192,22.356300078302002,49.64491366106912 +2024-09-07 19:00:00,10,29.70026508011823,28.79242018308204,54.49176006744108 +2024-09-07 20:00:00,10,30.31953784721874,21.480213584353752,47.57518725896724 +2024-09-07 21:00:00,10,22.893523202424888,25.032306935554153,64.01060450461775 +2024-09-07 22:00:00,10,15.028266367644651,23.50707018179823,55.67157065732302 +2024-09-07 23:00:00,10,29.460778603257083,20.375179040324745,54.60455833051677 +2024-09-08 00:00:00,10,18.414771525717548,23.96903879220551,40.3486969464213 +2024-09-08 01:00:00,10,23.900842296902095,24.974574077212882,53.061883003613495 +2024-09-08 02:00:00,10,23.738203648657848,25.347546071249656,65.35452694008656 +2024-09-08 03:00:00,10,12.165655013587456,26.21452756021353,51.9919891004736 +2024-09-08 04:00:00,10,18.668149655320576,27.02267854230847,59.67415980637346 +2024-09-08 05:00:00,10,20.44596109353011,25.335253896513642,46.59309917718619 +2024-09-08 06:00:00,10,14.183984599597975,24.646924817963935,55.91224755100272 +2024-09-08 07:00:00,10,35.17815956209139,22.358321695214638,64.78753088127247 +2024-09-08 08:00:00,10,5.969919976918767,26.75944969773297,43.882695094489634 +2024-09-08 09:00:00,10,32.66901610427436,25.56554217409029,63.873129865231434 +2024-09-08 10:00:00,10,15.269392410689363,24.960352311996765,61.25257469776117 +2024-09-08 11:00:00,10,16.696579684721215,22.323650970595022,58.30437458196041 +2024-09-08 12:00:00,10,28.852319639019413,22.019061667943895,65.83041218795071 +2024-09-08 13:00:00,10,22.444995796218233,21.19557550603054,71.23404919781888 +2024-09-08 14:00:00,10,13.46996215979529,24.1416558794051,50.87543786228686 +2024-09-08 15:00:00,10,31.664258073526604,28.830125488940574,54.75741117471133 +2024-09-08 16:00:00,10,24.132726251762236,26.55888887298938,51.675502805919805 +2024-09-08 17:00:00,10,21.76805651718897,26.47297594850934,56.357366660385395 +2024-09-08 18:00:00,10,30.04961201474077,20.799976699260526,45.678855347248046 +2024-09-08 19:00:00,10,27.09080656388547,28.4728842253086,42.15571360042242 +2024-09-08 20:00:00,10,28.065961542393445,20.476994368471356,52.51601078305066 +2024-09-08 21:00:00,10,21.15369335545238,28.085755153125046,46.67921419968666 +2024-09-08 22:00:00,10,25.005772583095645,27.956851072587256,53.69832226725374 +2024-09-08 23:00:00,10,18.931065457134768,19.342193733996893,32.733087330109555 +2024-09-09 00:00:00,10,28.55319877798092,25.750506683204662,52.853598317379195 +2024-09-09 01:00:00,10,18.752033765335725,22.759772821109188,45.87281340382428 +2024-09-09 02:00:00,10,34.23907855574876,27.127507863715067,63.71007002143944 +2024-09-09 03:00:00,10,36.53220174863438,23.815854753733426,41.637672529725904 +2024-09-09 04:00:00,10,38.58176188798052,21.99197653100955,50.30750992878031 +2024-09-09 05:00:00,10,9.151412661076069,23.862681207638474,42.85128686530511 +2024-09-09 06:00:00,10,35.63764072674141,22.538322720958053,59.96048407954607 +2024-09-09 07:00:00,10,22.716631714851005,23.319756726491732,36.19758003530515 +2024-09-09 08:00:00,10,30.9063704001782,20.314681265161994,44.904475691518385 +2024-09-09 09:00:00,10,14.328667150748837,26.7168101926405,43.910610096288565 +2024-09-09 10:00:00,10,19.276983389037976,20.961665939762646,81.60802524645938 +2024-09-09 11:00:00,10,27.58840262089153,23.185683906834587,55.00880150014376 +2024-09-09 12:00:00,10,30.20948520750703,23.100728552601538,58.85734567873759 +2024-09-09 13:00:00,10,15.26513130896306,22.301008732829956,61.67765635272885 +2024-09-09 14:00:00,10,17.077747447036764,23.884748640795387,51.082758651391345 +2024-09-09 15:00:00,10,31.478218335751613,22.10659903356067,62.381267282394475 +2024-09-09 16:00:00,10,21.66498273865202,20.562393407614078,48.22043504342079 +2024-09-09 17:00:00,10,12.839340447995673,18.411874915721,55.86049130764423 +2024-09-09 18:00:00,10,12.461527890576132,26.687697342419288,49.88278142555461 +2024-09-09 19:00:00,10,26.691774712200143,24.817015756022133,57.96658000161496 +2024-09-09 20:00:00,10,6.9000252897841285,24.16484300112536,70.66123387471694 +2024-09-09 21:00:00,10,18.18784598966666,26.38045569515994,50.49182228984246 +2024-09-09 22:00:00,10,26.332180724834032,19.860419900270422,57.51538287932497 +2024-09-09 23:00:00,10,25.265198464432434,22.855364373100134,70.75205607391709 +2024-09-10 00:00:00,10,32.762150451238824,30.41312382600335,69.65499290690049 +2024-09-10 01:00:00,10,24.815430671232097,31.472294277087578,54.53081883344327 +2024-09-10 02:00:00,10,23.112340416741123,31.758429443215274,62.83599757164228 +2024-09-10 03:00:00,10,24.171895175181195,24.082840069210356,57.13023534653058 +2024-09-10 04:00:00,10,19.207915371503994,26.966275276095182,57.201122812554836 +2024-09-10 05:00:00,10,31.31361457769311,28.033086826703776,51.85622730044637 +2024-09-10 06:00:00,10,35.33891884724607,22.866615875637073,49.36942337144749 +2024-09-10 07:00:00,10,33.62418521738297,29.254742689240327,55.272989027604396 +2024-09-10 08:00:00,10,7.024601113888906,25.0462670268733,63.77884771954578 +2024-09-10 09:00:00,10,22.669464171294997,31.776755638415985,72.73376294135863 +2024-09-10 10:00:00,10,20.434344730289343,22.569578358353247,62.58668902258659 +2024-09-10 11:00:00,10,30.19006548418556,25.542498155708337,52.47947810412044 +2024-09-10 12:00:00,10,34.71567158043876,24.836131868403555,61.00026074738757 +2024-09-10 13:00:00,10,18.70953031994347,20.70533080208493,69.33513612954539 +2024-09-10 14:00:00,10,27.860488844140782,21.473488695786486,60.136121615394494 +2024-09-10 15:00:00,10,23.21411861510831,24.81578157750231,54.13146746494149 +2024-09-10 16:00:00,10,15.71241981706436,21.40555220207018,52.153690280308716 +2024-09-10 17:00:00,10,24.49922905679872,24.03409422069487,44.68289550577831 +2024-09-10 18:00:00,10,21.817095342493523,20.268557423303847,61.230243797255845 +2024-09-10 19:00:00,10,26.459542937424743,22.94699693289088,71.3731525602152 +2024-09-10 20:00:00,10,33.57721263886698,23.06981036208844,49.74229073739613 +2024-09-10 21:00:00,10,27.41014079305901,26.44921283718707,51.33839918028761 +2024-09-10 22:00:00,10,14.012478147589741,20.361191654375332,51.628281527284635 +2024-09-10 23:00:00,10,27.62892794446919,26.341869505316215,57.935845152211535 +2024-09-11 00:00:00,10,23.380279481131577,30.393369475392326,56.420743489393146 +2024-09-11 01:00:00,10,26.917522751276586,27.69658171616831,52.87280419665456 +2024-09-11 02:00:00,10,8.161913304824601,26.96764919189291,54.54329916769865 +2024-09-11 03:00:00,10,18.516876959409494,31.577250225431005,52.28631790869428 +2024-09-11 04:00:00,10,12.91859154394398,23.919525820261303,46.11493355982209 +2024-09-11 05:00:00,10,16.44124177529849,21.775690270649086,52.59773007150379 +2024-09-11 06:00:00,10,17.5097310257881,24.615753191076802,48.9672356294973 +2024-09-11 07:00:00,10,14.480249397633033,24.7479251888757,65.9441954973205 +2024-09-11 08:00:00,10,32.63083665948691,21.119205887590258,43.39211124767611 +2024-09-11 09:00:00,10,19.46457037309111,22.4888189175082,48.58331864911563 +2024-09-11 10:00:00,10,32.22131613356855,26.47915203616634,65.79552277287253 +2024-09-11 11:00:00,10,38.91090938465234,27.412626741019583,47.49105106184587 +2024-09-11 12:00:00,10,31.65369884946982,22.106619507325405,62.86459980803811 +2024-09-11 13:00:00,10,27.66151558087068,18.7876344794089,70.66362820224894 +2024-09-11 14:00:00,10,29.066916774240042,23.470334304237394,66.07324752759887 +2024-09-11 15:00:00,10,22.799231176916177,22.301542104544676,57.22008438216137 +2024-09-11 16:00:00,10,2.9799452118384764,26.15264772658047,32.77085016604844 +2024-09-11 17:00:00,10,31.83113226201703,21.768839435996988,83.79950011850687 +2024-09-11 18:00:00,10,24.08567690710111,24.38835621861358,36.53122388632291 +2024-09-11 19:00:00,10,4.797834284906028,17.483661709136857,51.768198658469714 +2024-09-11 20:00:00,10,27.06884647178141,22.32868323416296,52.30988226382631 +2024-09-11 21:00:00,10,26.647422109603603,19.05385918116707,59.54455700533027 +2024-09-11 22:00:00,10,22.85937192212464,19.577910145612616,55.61861021499054 +2024-09-11 23:00:00,10,13.329650083840475,27.65712847974916,49.277980821125176 +2024-09-12 00:00:00,10,28.889113038955813,31.108419846397506,64.72406839463734 +2024-09-12 01:00:00,10,39.57209207832189,29.062819800421533,36.12385967157658 +2024-09-12 02:00:00,10,14.698248571463926,24.317478801158103,62.07016042643197 +2024-09-12 03:00:00,10,24.047217801154485,23.860577125969286,56.124878787054314 +2024-09-12 04:00:00,10,20.918521799274387,29.81580797206653,52.105623755730896 +2024-09-12 05:00:00,10,32.01141064800966,20.774457424035567,64.16563693831378 +2024-09-12 06:00:00,10,35.799035466806416,19.69281174862553,61.036929143265134 +2024-09-12 07:00:00,10,22.61437147354329,18.548172678227235,68.70702593436947 +2024-09-12 08:00:00,10,27.142096913975273,25.403699297986297,51.438364022831756 +2024-09-12 09:00:00,10,20.02221049815409,22.10957713302646,76.20262552338025 +2024-09-12 10:00:00,10,23.759667614913617,20.070236975940983,65.03724448237006 +2024-09-12 11:00:00,10,19.847512546281525,17.555097351682164,66.10461582047688 +2024-09-12 12:00:00,10,24.524182330952534,23.927080966430257,54.54127339837324 +2024-09-12 13:00:00,10,8.65757291602601,20.852749599785348,59.495729899242654 +2024-09-12 14:00:00,10,25.08798294902697,22.79700035189758,42.50097863662166 +2024-09-12 15:00:00,10,26.531343024990687,24.96800601688587,66.58451889579298 +2024-09-12 16:00:00,10,11.060520977546544,18.134449961327885,65.09864741250523 +2024-09-12 17:00:00,10,29.54363387436341,20.2900266522303,66.53894214813891 +2024-09-12 18:00:00,10,29.538062014374972,22.45943125705083,45.5320605706276 +2024-09-12 19:00:00,10,16.787441116121965,18.29114143314832,46.61473649751177 +2024-09-12 20:00:00,10,35.20020708206554,25.616413310967474,69.22285483663659 +2024-09-12 21:00:00,10,26.845400389789766,20.96442690949714,65.52046265414437 +2024-09-12 22:00:00,10,11.27219665471196,25.707334684042774,53.12410871151816 +2024-09-12 23:00:00,10,17.850289738393034,24.409962726403567,61.47648838916759 +2024-09-13 00:00:00,10,27.03266648501656,24.56045641783141,70.37166154028219 +2024-09-13 01:00:00,10,28.611275333362045,28.94726617705485,39.700119734304835 +2024-09-13 02:00:00,10,33.725070772049705,24.80202743241648,57.482407789117026 +2024-09-13 03:00:00,10,13.258253779827065,25.553796928395332,51.654108655379126 +2024-09-13 04:00:00,10,26.468014206746812,19.843525107395653,51.409291686579195 +2024-09-13 05:00:00,10,16.526111647278583,29.078732282668803,63.37417614214175 +2024-09-13 06:00:00,10,19.30694383189816,23.460540868770163,44.21364412352674 +2024-09-13 07:00:00,10,18.772911304616624,27.616304809115793,55.8983167838745 +2024-09-13 08:00:00,10,23.998349282538598,24.201581496496683,63.73895157913805 +2024-09-13 09:00:00,10,38.937578150071296,30.63193251371174,51.80512930756012 +2024-09-13 10:00:00,10,13.475463290229412,23.04312110330513,35.1504792313804 +2024-09-13 11:00:00,10,43.308429008308345,25.23675989847302,56.83850430228028 +2024-09-13 12:00:00,10,14.697957540270462,26.89712985918814,55.02829660994558 +2024-09-13 13:00:00,10,11.704946477883874,16.098875687417408,57.048126015402595 +2024-09-13 14:00:00,10,47.28061328055598,20.97682870926838,44.925543213365856 +2024-09-13 15:00:00,10,25.636455751654385,23.86074188023594,47.48059792389206 +2024-09-13 16:00:00,10,27.71323654827764,24.67071029571203,51.845463470049395 +2024-09-13 17:00:00,10,25.07168218086622,23.537857187996572,48.251554712360885 +2024-09-13 18:00:00,10,26.216163615125293,19.26232911338998,40.96418959654348 +2024-09-13 19:00:00,10,27.478076641478253,23.882398898447104,52.880853854176614 +2024-09-13 20:00:00,10,33.602120746630206,27.389690784888337,47.28541346998053 +2024-09-13 21:00:00,10,39.03280217503554,25.99667462231297,55.43354574827134 +2024-09-13 22:00:00,10,41.75830758021077,26.125804670419033,49.84097693875388 +2024-09-13 23:00:00,10,0.0,22.54858705087742,63.76498460069821 +2024-09-14 00:00:00,10,30.261515173092192,29.128929423723807,71.13096223747866 +2024-09-14 01:00:00,10,33.47915443487402,31.153686063916513,62.82784916233691 +2024-09-14 02:00:00,10,34.06236506860609,24.934204760128505,51.909687124428864 +2024-09-14 03:00:00,10,24.04846455980235,33.380079622322846,69.87509762918874 +2024-09-14 04:00:00,10,25.96901705615917,31.123085416550516,58.152286996208616 +2024-09-14 05:00:00,10,38.259586472682884,30.16729635850713,54.3375399238689 +2024-09-14 06:00:00,10,19.089967131579847,24.80420938865563,57.941108636855404 +2024-09-14 07:00:00,10,22.384286552643832,25.105441365638324,55.69201908453953 +2024-09-14 08:00:00,10,1.4544635159148562,25.762284390951624,66.39498441094013 +2024-09-14 09:00:00,10,15.941091574444286,25.389576883005514,64.73813776611918 +2024-09-14 10:00:00,10,8.891598340138541,19.375161708252197,37.593497672734834 +2024-09-14 11:00:00,10,6.974650896476582,21.410203941486262,55.98824925196817 +2024-09-14 12:00:00,10,28.695067922232777,19.384485232315217,54.00447328621566 +2024-09-14 13:00:00,10,28.288344969872632,15.832330056445205,55.70540806117667 +2024-09-14 14:00:00,10,21.803046905357828,15.970414763428206,63.502355819156286 +2024-09-14 15:00:00,10,21.166740131644705,21.019800470097234,61.67105840130369 +2024-09-14 16:00:00,10,17.29911521343132,24.982997540362422,64.78769012635473 +2024-09-14 17:00:00,10,23.717804709459106,26.72997220583209,41.59406987923677 +2024-09-14 18:00:00,10,9.412773250062859,31.822594049563158,59.59212692730665 +2024-09-14 19:00:00,10,27.807416307573295,23.884835819371887,47.16996316951297 +2024-09-14 20:00:00,10,22.297208239787867,19.05745111427759,49.57403366823382 +2024-09-14 21:00:00,10,32.872566662619796,18.523231467566262,59.986723458267335 +2024-09-14 22:00:00,10,10.636093602939528,19.326423888613068,59.22295853511465 +2024-09-14 23:00:00,10,38.02016725491599,30.736844579067277,40.34836828130684 +2024-09-15 00:00:00,10,39.004493071206866,27.828384818326462,37.54338502938514 +2024-09-15 01:00:00,10,21.938662490059283,26.284534080589328,52.824096684528016 +2024-09-15 02:00:00,10,20.010827092415443,31.153102144179236,53.112449496633346 +2024-09-15 03:00:00,10,28.636078572037555,26.479384190119386,59.27985772373765 +2024-09-15 04:00:00,10,18.25155964397684,24.640768825100437,73.51573825640568 +2024-09-15 05:00:00,10,11.447957048257718,30.470118723349476,57.09982384523437 +2024-09-15 06:00:00,10,13.089250886909348,30.348102915534653,49.12674032102534 +2024-09-15 07:00:00,10,5.038878165655298,25.29501977464671,53.037222932049595 +2024-09-15 08:00:00,10,26.227906036729287,25.78745734994292,59.73621959694905 +2024-09-15 09:00:00,10,29.42204980752516,22.46190129717317,73.48898858624246 +2024-09-15 10:00:00,10,21.719992342105307,27.453433830821012,51.417942542389 +2024-09-15 11:00:00,10,8.025358694266645,21.862274830630675,42.8203880197009 +2024-09-15 12:00:00,10,28.463025623764175,25.40667937817284,50.63732437431855 +2024-09-15 13:00:00,10,26.115818047282907,18.816437542916404,48.553081377811665 +2024-09-15 14:00:00,10,25.310411689219983,25.81977174179752,50.66576000046865 +2024-09-15 15:00:00,10,40.92067838744018,20.629910011538044,57.52791123485466 +2024-09-15 16:00:00,10,31.65794664927802,23.15149726229539,49.5279227418552 +2024-09-15 17:00:00,10,23.627407183005214,25.340115215388476,67.09993207259764 +2024-09-15 18:00:00,10,34.152672243679504,24.071988468860766,54.235806496968316 +2024-09-15 19:00:00,10,20.7130463803301,27.376024607680158,65.36999763924074 +2024-09-15 20:00:00,10,11.868869982252058,24.88539118371102,65.09562158393503 +2024-09-15 21:00:00,10,26.230969913117935,24.64918445748181,25.6115078125047 +2024-09-15 22:00:00,10,6.673549577512787,21.72359922530719,72.85132724766014 +2024-09-15 23:00:00,10,20.67535215596023,28.318245593205997,58.86359522122578 +2024-09-16 00:00:00,10,37.220663098439644,23.842187137591665,53.068062671565016 +2024-09-16 01:00:00,10,27.465364282072798,31.242775859629365,60.78860640966102 +2024-09-16 02:00:00,10,25.511408676602283,28.584250893466262,51.81890214919689 +2024-09-16 03:00:00,10,32.54826042332037,25.196778671370108,37.73315257171129 +2024-09-16 04:00:00,10,26.49643051645658,23.42727992711508,61.40213779870958 +2024-09-16 05:00:00,10,25.14874063155876,23.922795207571788,62.38478376720446 +2024-09-16 06:00:00,10,7.069015570943101,25.511896565327383,58.218554063069504 +2024-09-16 07:00:00,10,27.281016493037736,27.81730752781625,47.09007740356717 +2024-09-16 08:00:00,10,15.143050007514123,24.270143497470176,50.20721437312797 +2024-09-16 09:00:00,10,8.006604398855275,21.52577578976406,45.18991471875286 +2024-09-16 10:00:00,10,22.555359992036475,18.671703436512157,56.15660192685581 +2024-09-16 11:00:00,10,25.65802229819466,22.753722024984715,54.8593610211971 +2024-09-16 12:00:00,10,21.884917630897117,20.667755498763096,41.89035743576352 +2024-09-16 13:00:00,10,27.28575984382254,20.34669097713794,47.93002473430699 +2024-09-16 14:00:00,10,25.14148071434505,16.937954368434504,67.70308164168362 +2024-09-16 15:00:00,10,19.402753395779172,25.005729133394663,56.70015819930121 +2024-09-16 16:00:00,10,17.323575260679867,19.649052127874754,58.45163644820103 +2024-09-16 17:00:00,10,18.19092319606247,23.022694529545316,49.0464540275497 +2024-09-16 18:00:00,10,19.319615231169983,29.17712636374342,47.42762712027198 +2024-09-16 19:00:00,10,18.00778676091688,22.295255751748563,68.39508074998845 +2024-09-16 20:00:00,10,23.449119359919212,22.108186356648556,57.51170857313535 +2024-09-16 21:00:00,10,18.82844692288049,24.871111974914527,56.78784731636806 +2024-09-16 22:00:00,10,25.021954117604487,21.83290804985547,51.17424861508568 +2024-09-16 23:00:00,10,13.955315748547829,24.681175712264555,40.077756746763555 +2024-09-17 00:00:00,10,11.365451419165234,24.06922075288938,67.37346812677757 +2024-09-17 01:00:00,10,24.72002353588525,23.94564845370069,50.33987620244435 +2024-09-17 02:00:00,10,43.87110336159965,23.748451201873195,55.48821631299453 +2024-09-17 03:00:00,10,31.37877567927061,28.668871139537828,58.204713603046734 +2024-09-17 04:00:00,10,25.095440069811108,20.33679782403306,41.408882433783546 +2024-09-17 05:00:00,10,15.75938623156714,28.027758071796864,44.89946434679895 +2024-09-17 06:00:00,10,19.619956948650298,22.76430200671435,63.562106653649 +2024-09-17 07:00:00,10,22.476323691477866,29.80582846770977,60.341189944604245 +2024-09-17 08:00:00,10,10.06217420269842,26.174419544853365,60.90510963203546 +2024-09-17 09:00:00,10,32.245386363199486,17.143398757048757,70.2641436224308 +2024-09-17 10:00:00,10,27.1172582725665,20.640724012695046,64.69777554079143 +2024-09-17 11:00:00,10,27.10452135345897,20.984536693295816,51.8633179826184 +2024-09-17 12:00:00,10,15.424201531837857,17.446455801015084,45.63284727280124 +2024-09-17 13:00:00,10,28.512145774027204,21.308846944344214,43.90819755399538 +2024-09-17 14:00:00,10,25.03817066317699,21.050028393562464,50.32245866274551 +2024-09-17 15:00:00,10,28.749851478406356,21.823622540688888,45.763102675026076 +2024-09-17 16:00:00,10,29.136920362745336,23.798550356170086,68.82975320344711 +2024-09-17 17:00:00,10,14.698188277931376,26.928771983686865,53.894609163822025 +2024-09-17 18:00:00,10,33.409136212056275,22.713844255885544,51.619329876127885 +2024-09-17 19:00:00,10,17.946931770132792,17.69213859565972,64.14213183697865 +2024-09-17 20:00:00,10,26.68503566891476,25.123713450011493,52.621534585698804 +2024-09-17 21:00:00,10,36.41470047909007,20.272395036239047,64.79466448741985 +2024-09-17 22:00:00,10,14.799546557002706,22.430996314495403,58.971779632427776 +2024-09-17 23:00:00,10,35.666986412667136,24.708664875459665,61.28485636468943 +2024-09-18 00:00:00,10,24.63948742006183,32.2574705484927,56.57639602548675 +2024-09-18 01:00:00,10,17.350882511967637,21.524510209444617,58.39412754974299 +2024-09-18 02:00:00,10,5.063020657912826,27.205043341829093,61.45959604072273 +2024-09-18 03:00:00,10,16.31895516048007,27.610567197605505,50.12045582152334 +2024-09-18 04:00:00,10,13.69514840705157,24.32047261724258,53.5424241802183 +2024-09-18 05:00:00,10,20.921299616012572,28.871721003575463,72.00738924886579 +2024-09-18 06:00:00,10,12.475846968607142,25.623079177438672,50.888903171153444 +2024-09-18 07:00:00,10,31.140527444975834,26.562812135712818,60.50742210350154 +2024-09-18 08:00:00,10,36.48757087236573,26.911387315062733,47.97139395925837 +2024-09-18 09:00:00,10,4.744811355618452,23.93145462381514,57.78322329500018 +2024-09-18 10:00:00,10,18.374582219008307,20.051320586594507,50.46265650165814 +2024-09-18 11:00:00,10,14.942038238794193,25.094372888892856,69.75505663835708 +2024-09-18 12:00:00,10,16.329402671409607,19.655379045728974,57.72180660153299 +2024-09-18 13:00:00,10,28.966848293244908,20.889615462384697,52.53091052702253 +2024-09-18 14:00:00,10,21.10065088142625,20.877378855079392,53.90544953665918 +2024-09-18 15:00:00,10,22.31948046396616,23.474807198768296,59.04783998301048 +2024-09-18 16:00:00,10,32.94711021195927,14.363244921170415,52.51996186290466 +2024-09-18 17:00:00,10,23.906406175092712,19.01148126034532,58.0025173892732 +2024-09-18 18:00:00,10,31.646807996781217,22.862808732078673,66.23371065010477 +2024-09-18 19:00:00,10,19.79038315246687,25.06286184341567,61.423563929132555 +2024-09-18 20:00:00,10,15.285512381140848,19.734153840905286,49.69067908510105 +2024-09-18 21:00:00,10,25.198722975941806,25.453961139332023,60.00568784563038 +2024-09-18 22:00:00,10,14.922576353134339,18.605325817935892,48.7609948191523 +2024-09-18 23:00:00,10,18.107033624137426,15.073210738286875,41.163908204524425 +2024-09-19 00:00:00,10,13.389851050639267,21.86817007973297,58.178259867632796 +2024-09-19 01:00:00,10,30.040120941566727,32.97654174669943,42.8346582183234 +2024-09-19 02:00:00,10,10.3391214021597,28.916569340895183,63.218397041651826 +2024-09-19 03:00:00,10,12.318749383273126,34.514312720292324,65.2349949859139 +2024-09-19 04:00:00,10,31.377539361940613,22.586316071871828,65.48094868214845 +2024-09-19 05:00:00,10,16.334782708775172,21.571353445202842,54.15694392472416 +2024-09-19 06:00:00,10,35.01328023323378,26.51204988076413,55.397536790337114 +2024-09-19 07:00:00,10,20.982136017092298,25.722454395290633,55.66466715731184 +2024-09-19 08:00:00,10,17.402705779002055,19.478803197737058,62.642231734175375 +2024-09-19 09:00:00,10,13.139014789912846,21.305431608521886,76.40802148040378 +2024-09-19 10:00:00,10,22.568923239057984,22.19187645702771,65.74354839062062 +2024-09-19 11:00:00,10,18.01827470300842,23.416191007604954,54.90246710996923 +2024-09-19 12:00:00,10,3.6113149244886706,15.025539036539492,62.99809934015123 +2024-09-19 13:00:00,10,23.85443721079087,29.907665107973465,61.30693511184951 +2024-09-19 14:00:00,10,26.63552487246021,20.37526203670708,51.15523138932816 +2024-09-19 15:00:00,10,24.31068008250246,22.217616952168342,52.97629741838535 +2024-09-19 16:00:00,10,29.212592716942638,21.054150780899928,48.1446589864822 +2024-09-19 17:00:00,10,20.587929234418944,25.612756564988864,45.36694012169287 +2024-09-19 18:00:00,10,24.70467852458609,22.568340733681868,45.13663949768325 +2024-09-19 19:00:00,10,11.188673100508396,25.793782801555427,47.593507236067204 +2024-09-19 20:00:00,10,27.83930365465467,18.795492965098678,67.14972458711777 +2024-09-19 21:00:00,10,9.377309871396918,22.783761327139118,46.66459826464008 +2024-09-19 22:00:00,10,4.941661207628652,21.724821189202665,45.39819632013048 +2024-09-19 23:00:00,10,45.03858027780632,21.757963569949453,47.42413549512604 +2024-09-20 00:00:00,10,25.10861030323054,21.597663935475467,63.535966287196345 +2024-09-20 01:00:00,10,30.035887163537186,29.73288595163226,56.06264024472112 +2024-09-20 02:00:00,10,29.477613644593227,31.750919783712575,60.57573004125314 +2024-09-20 03:00:00,10,24.212125162444398,29.39664486503272,45.33134250161534 +2024-09-20 04:00:00,10,29.105176458421006,26.14360066704572,58.597161313736265 +2024-09-20 05:00:00,10,26.382300117314905,27.706011400608002,50.082850714441506 +2024-09-20 06:00:00,10,30.870607349141366,19.532488480266434,54.325837552491855 +2024-09-20 07:00:00,10,37.7587126235778,27.170431319194268,56.42086322684817 +2024-09-20 08:00:00,10,12.387649017242246,24.380170275093363,59.85481816966802 +2024-09-20 09:00:00,10,38.47644066477114,23.264406069096147,62.607892647087894 +2024-09-20 10:00:00,10,18.47334483348716,13.15701734970763,57.00011234532701 +2024-09-20 11:00:00,10,15.63692711133773,17.90929938162114,45.31590644876081 +2024-09-20 12:00:00,10,24.159108043798085,20.37451027621492,59.00200112232365 +2024-09-20 13:00:00,10,29.722872632593226,21.70076647422252,60.38132338276218 +2024-09-20 14:00:00,10,33.580516367326,25.98641966541107,52.41406044663037 +2024-09-20 15:00:00,10,33.52495121061297,17.950260652276853,34.00231367703884 +2024-09-20 16:00:00,10,12.710642016074354,21.482129986032728,56.68626113977475 +2024-09-20 17:00:00,10,21.954479117874396,25.695652538995677,53.13376233969346 +2024-09-20 18:00:00,10,26.054831293706116,18.16246394406352,59.392172754848914 +2024-09-20 19:00:00,10,36.63820323290141,23.72587990105013,52.50000288766243 +2024-09-20 20:00:00,10,39.1634908266593,26.40100864020321,55.82610622320908 +2024-09-20 21:00:00,10,25.31617389495327,14.908853912486112,50.55655523610495 +2024-09-20 22:00:00,10,32.24851362573398,25.07634074131893,61.24980228637608 +2024-09-20 23:00:00,10,29.846563094526147,19.384714841198157,41.56246161536634 +2024-09-21 00:00:00,10,13.598526405752079,27.656568735451714,34.08775805888219 +2024-09-21 01:00:00,10,12.57578467883359,27.299514033629322,54.43491823315569 +2024-09-21 02:00:00,10,44.85956365285183,21.103644169167072,52.72800763433741 +2024-09-21 03:00:00,10,29.850135008833888,25.529620527672314,63.69439230770374 +2024-09-21 04:00:00,10,36.23879785108695,27.46513638867077,59.62550467277575 +2024-09-21 05:00:00,10,30.566615778302527,28.640927097627614,56.07244595296715 +2024-09-21 06:00:00,10,28.610456526308138,23.809782559501752,45.6234165695233 +2024-09-21 07:00:00,10,17.238428367131373,25.84972021361091,71.07868185690575 +2024-09-21 08:00:00,10,19.09721985901264,20.057067505986186,58.47554638794196 +2024-09-21 09:00:00,10,13.547826831155897,25.488006784940115,59.067546075913846 +2024-09-21 10:00:00,10,22.395979082074547,21.731477864338906,66.36896729351449 +2024-09-21 11:00:00,10,5.700100946697102,20.253208775469698,55.87109943835687 +2024-09-21 12:00:00,10,33.38213427190105,22.282573413638342,48.88229596735172 +2024-09-21 13:00:00,10,19.122921670500173,26.843435722297954,52.330459725223136 +2024-09-21 14:00:00,10,28.89470885525226,17.025459844024574,66.24731945023889 +2024-09-21 15:00:00,10,26.850335830001033,19.612050526323017,44.2948467276526 +2024-09-21 16:00:00,10,19.944604861473124,19.711779860390607,52.251250445229246 +2024-09-21 17:00:00,10,22.21869672230027,21.97739570135099,58.38313795873029 +2024-09-21 18:00:00,10,30.16353798383109,17.6395019757662,73.57457787487543 +2024-09-21 19:00:00,10,17.527284479761455,26.75488078950524,57.04454682052944 +2024-09-21 20:00:00,10,17.350695082992843,28.45003717364574,61.82700080967541 +2024-09-21 21:00:00,10,14.212112547782946,22.569417409105945,59.984024010751774 +2024-09-21 22:00:00,10,14.438313613463755,19.819402443118555,55.87603505527526 +2024-09-21 23:00:00,10,18.13238772812676,25.07859285096677,62.40386654786526 +2024-09-22 00:00:00,10,19.88249298417689,25.976630525941175,61.99408745487368 +2024-09-22 01:00:00,10,19.30917756000548,26.32069697854821,55.63691393706404 +2024-09-22 02:00:00,10,12.441996340864808,26.8974381865159,42.596823233869 +2024-09-22 03:00:00,10,43.50937403657636,29.461424555284182,52.1056571165301 +2024-09-22 04:00:00,10,14.543241387859208,25.727313084648642,55.107079164528166 +2024-09-22 05:00:00,10,15.66275868184332,30.978528606776095,24.60988649622456 +2024-09-22 06:00:00,10,19.773735503918388,22.46958936713532,56.37910158513128 +2024-09-22 07:00:00,10,22.823582255930805,26.601462035868195,51.63367977830637 +2024-09-22 08:00:00,10,15.747604910069715,25.819884623450037,78.47595068021967 +2024-09-22 09:00:00,10,28.18260940080668,21.60829047009597,43.8475063894558 +2024-09-22 10:00:00,10,33.11756072888384,13.117243920084228,69.88196992626649 +2024-09-22 11:00:00,10,26.739109624568272,19.007039589524926,52.482861301284615 +2024-09-22 12:00:00,10,28.479989322449796,19.952492605746357,54.10177731384279 +2024-09-22 13:00:00,10,38.77192607154739,21.5014945296286,50.152800622226046 +2024-09-22 14:00:00,10,20.829313309916916,20.452480485702417,44.39827484486289 +2024-09-22 15:00:00,10,20.507410541339603,15.897703957977352,65.02052290055543 +2024-09-22 16:00:00,10,33.90719920212969,21.056162500250775,43.712956328871016 +2024-09-22 17:00:00,10,35.17970358530356,26.56029082042838,37.3359470767396 +2024-09-22 18:00:00,10,21.47801813198425,19.967124925179814,57.09547087439404 +2024-09-22 19:00:00,10,17.03557419906266,24.754877023997533,48.17815971182412 +2024-09-22 20:00:00,10,25.351981495084566,25.196929986181598,39.639730847678024 +2024-09-22 21:00:00,10,24.64560565705988,17.904174881843865,38.909100503909556 +2024-09-22 22:00:00,10,18.065859390892797,27.969855216618893,49.917566639562786 +2024-09-22 23:00:00,10,31.772569803804615,21.00948538616025,45.46331767493163 +2024-09-23 00:00:00,10,6.409673126809626,28.274873627955998,67.73374262598549 +2024-09-23 01:00:00,10,22.717739628870863,27.247282541166058,47.263818205843656 +2024-09-23 02:00:00,10,34.195516358342196,26.139450535286898,59.41866629825189 +2024-09-23 03:00:00,10,29.84221898537165,34.19650558244082,66.50274989530992 +2024-09-23 04:00:00,10,27.897284768712744,29.10885598267651,66.65819295426036 +2024-09-23 05:00:00,10,29.81718463701722,31.900991743128785,50.536354477924405 +2024-09-23 06:00:00,10,15.780589557749508,27.312088335761565,66.24771490942017 +2024-09-23 07:00:00,10,6.305941377476685,27.424786226489346,39.049893953788626 +2024-09-23 08:00:00,10,22.707530297316417,30.712802796434936,57.997106820535656 +2024-09-23 09:00:00,10,21.53307880792706,21.496935191483505,59.88003676948567 +2024-09-23 10:00:00,10,9.953573655116223,23.33881281959441,54.926374870642256 +2024-09-23 11:00:00,10,9.631708349310479,18.58171996534684,48.83964061476953 +2024-09-23 12:00:00,10,45.111053500772485,26.284826156391773,50.12700718744174 +2024-09-23 13:00:00,10,26.54489294856199,25.925179312706838,69.61812021807421 +2024-09-23 14:00:00,10,12.519815816171802,23.073018141825738,52.069121237156274 +2024-09-23 15:00:00,10,27.50123949184124,23.54717927517578,43.94950490261781 +2024-09-23 16:00:00,10,36.22492814653753,19.027824566030382,64.17426925995399 +2024-09-23 17:00:00,10,11.882343980644098,22.132981430340426,48.63055837940031 +2024-09-23 18:00:00,10,20.182880473988735,20.663371810940475,58.75449101682556 +2024-09-23 19:00:00,10,30.548190197014243,24.913213791325326,50.80890928124693 +2024-09-23 20:00:00,10,30.314593094542325,23.097967143436527,54.063548377318135 +2024-09-23 21:00:00,10,9.416637289019315,22.318214835706456,42.976612814320006 +2024-09-23 22:00:00,10,18.5387193704668,23.08666627886511,36.92127166415168 +2024-09-23 23:00:00,10,13.19757789546899,22.150182613936792,69.46127745467554 +2024-09-24 00:00:00,10,12.617522244634946,29.48254434540467,64.1271267117146 +2024-09-24 01:00:00,10,23.905981480605334,27.860668849551676,54.533011326592366 +2024-09-24 02:00:00,10,21.934292543790864,33.31905115546722,57.4388137209629 +2024-09-24 03:00:00,10,32.64491139696291,22.894647564032258,55.9132771797758 +2024-09-24 04:00:00,10,27.041445988212043,28.825512982290167,62.40859767803805 +2024-09-24 05:00:00,10,27.683812784175313,22.044874689044818,70.81954192392918 +2024-09-24 06:00:00,10,18.64365168733245,34.77513069199271,47.995588870423575 +2024-09-24 07:00:00,10,28.472919153042092,17.369243440474637,56.010718135637156 +2024-09-24 08:00:00,10,23.17230610295359,21.01143685559526,59.72629720426605 +2024-09-24 09:00:00,10,10.24246144409755,24.31496771620628,62.95350588363737 +2024-09-24 10:00:00,10,18.396510890344388,19.889060044141704,59.96688837879433 +2024-09-24 11:00:00,10,23.312123634574284,24.09948853344322,68.57849122649513 +2024-09-24 12:00:00,10,24.896539008976774,21.671864698411156,54.665422771920774 +2024-09-24 13:00:00,10,18.988208808469548,16.643447150155637,38.104185642588206 +2024-09-24 14:00:00,10,27.253604353111932,23.880641895482654,58.24168317496898 +2024-09-24 15:00:00,10,25.693330212602994,16.821793322402197,68.31130303700958 +2024-09-24 16:00:00,10,25.944661378791007,26.620854904192218,62.62729318422363 +2024-09-24 17:00:00,10,21.977908510912734,23.40683724753322,34.67976914495681 +2024-09-24 18:00:00,10,15.2329757481924,25.39229790451816,53.81105367264542 +2024-09-24 19:00:00,10,28.329205949769126,25.7500347724169,61.517158592385705 +2024-09-24 20:00:00,10,19.79084805989953,22.350695213485356,53.411093964494285 +2024-09-24 21:00:00,10,15.630726092510788,20.578164866749834,50.855767086772715 +2024-09-24 22:00:00,10,26.3443518757405,19.83829034982805,46.467228376422824 +2024-09-24 23:00:00,10,17.668456522035704,22.949756674931752,57.083264548274414 +2024-09-25 00:00:00,10,24.144562500367073,28.308377018134262,59.190151734724154 +2024-09-25 01:00:00,10,33.024631706605824,25.80634235119996,49.97482254514365 +2024-09-25 02:00:00,10,24.066459766557017,30.207763635704126,57.32475603539403 +2024-09-25 03:00:00,10,12.511229265278276,26.607718249063254,48.43031106306187 +2024-09-25 04:00:00,10,34.47153946640154,26.600780678165485,55.77880888930455 +2024-09-25 05:00:00,10,16.55103812479222,27.104300498325635,67.61592651304885 +2024-09-25 06:00:00,10,16.0418871015786,17.194316624027124,55.935998200120125 +2024-09-25 07:00:00,10,19.8896060378954,20.78957100968577,57.31193915092318 +2024-09-25 08:00:00,10,27.67867866949013,21.934671333004246,64.68006348139484 +2024-09-25 09:00:00,10,21.62461311015211,20.33310523416033,40.431366602603376 +2024-09-25 10:00:00,10,15.163918397353468,18.671963848714082,62.12350245612415 +2024-09-25 11:00:00,10,25.760472836886397,24.611715354939747,56.70915708468361 +2024-09-25 12:00:00,10,27.13732520347743,24.81442195180097,78.80435586029003 +2024-09-25 13:00:00,10,15.385012861032585,20.788423729734642,58.47707736505441 +2024-09-25 14:00:00,10,25.36215512552092,16.779779350413932,55.89659190688617 +2024-09-25 15:00:00,10,29.63804892238754,27.64733585906067,60.13483330192568 +2024-09-25 16:00:00,10,24.143265428167126,26.046321011439648,52.60701292658957 +2024-09-25 17:00:00,10,24.79042799980461,18.41682234915836,63.072659634460656 +2024-09-25 18:00:00,10,19.774221947247657,24.224501149047775,67.85469820103783 +2024-09-25 19:00:00,10,14.63206670375003,24.530761678164122,50.643620508350985 +2024-09-25 20:00:00,10,26.736142069093304,23.3863269740481,69.54405495570293 +2024-09-25 21:00:00,10,21.794800906567907,26.667001350776538,52.509500819693535 +2024-09-25 22:00:00,10,27.20159107232403,22.168416872755746,50.44774960188218 +2024-09-25 23:00:00,10,22.55799768290002,25.979821834855017,46.48834250501476 +2024-09-26 00:00:00,10,21.883236409909273,25.403470969188355,51.1997496360615 +2024-09-26 01:00:00,10,41.92241702139902,30.759762689979354,45.82519200655683 +2024-09-26 02:00:00,10,22.122257233984424,26.557187629554534,60.206045844942786 +2024-09-26 03:00:00,10,38.90666311692175,29.68008889225441,67.28313396985102 +2024-09-26 04:00:00,10,7.914097226792531,29.067273690967976,55.11640343263526 +2024-09-26 05:00:00,10,26.741617751896634,27.420366623020833,58.36134932453196 +2024-09-26 06:00:00,10,22.393151850118947,27.064198714580183,50.73332236151247 +2024-09-26 07:00:00,10,18.56381991646267,26.761524895701122,61.561443671234315 +2024-09-26 08:00:00,10,20.899588921035082,23.05946227600563,64.0688832889523 +2024-09-26 09:00:00,10,23.54570804874118,26.11975508674041,35.30683620387669 +2024-09-26 10:00:00,10,12.902570361635378,19.736928402840388,61.467268493123456 +2024-09-26 11:00:00,10,10.095071892148136,22.39679739937172,56.13104489459408 +2024-09-26 12:00:00,10,42.41842633072518,21.478805725401724,70.39230777002014 +2024-09-26 13:00:00,10,27.583271218130022,21.98206174733586,54.47240140392951 +2024-09-26 14:00:00,10,42.012185298083295,24.620746858504518,69.93488860478271 +2024-09-26 15:00:00,10,25.443987389894694,22.557078437666135,69.55673949542165 +2024-09-26 16:00:00,10,24.70494328321303,22.668185840173752,64.79548030240018 +2024-09-26 17:00:00,10,30.293426772106784,27.737388950524263,43.67082598188967 +2024-09-26 18:00:00,10,34.01930577641289,23.371559548584123,56.1936948029657 +2024-09-26 19:00:00,10,20.965010477389526,27.686289991616928,64.60667962025448 +2024-09-26 20:00:00,10,37.193415492790265,25.803148052592697,44.1394099697651 +2024-09-26 21:00:00,10,23.534635518297396,25.111461920285528,60.55146933787148 +2024-09-26 22:00:00,10,7.614285993939113,22.66751795859616,58.51298942562218 +2024-09-26 23:00:00,10,26.543229761885488,19.42339282861847,42.909381647714284 +2024-09-27 00:00:00,10,27.36710773850516,30.1617547624151,53.188063961945794 +2024-09-27 01:00:00,10,34.62087406329183,24.565938235047764,62.772749402505816 +2024-09-27 02:00:00,10,7.477689514745116,30.074776030988588,48.032039171911215 +2024-09-27 03:00:00,10,28.140680278360033,24.168947678026434,65.94171977667054 +2024-09-27 04:00:00,10,9.239328341238204,23.64176493116095,61.56724683682622 +2024-09-27 05:00:00,10,15.619100997404656,27.569194522376012,62.84073512257684 +2024-09-27 06:00:00,10,23.622793973346617,27.7987003312001,70.46621169936151 +2024-09-27 07:00:00,10,28.40027029176388,25.080311347202738,57.42414094072361 +2024-09-27 08:00:00,10,9.85703938204011,21.544877272571597,62.3059572627761 +2024-09-27 09:00:00,10,23.214305886494543,28.779257026941078,48.959459251136494 +2024-09-27 10:00:00,10,24.499788493538418,22.489575839873414,54.596891350637094 +2024-09-27 11:00:00,10,16.24850354742682,21.2355130856728,53.89954680760931 +2024-09-27 12:00:00,10,36.9331272473795,21.01168286855873,67.17291589777818 +2024-09-27 13:00:00,10,17.57241225440548,16.124503732670554,65.88248756048822 +2024-09-27 14:00:00,10,42.77615219859254,19.158609361002732,59.60697072219158 +2024-09-27 15:00:00,10,17.756453721984478,25.49136830275681,64.39756084377122 +2024-09-27 16:00:00,10,11.858810090032193,23.262911769499294,65.10760899114457 +2024-09-27 17:00:00,10,21.899488945168017,19.775505280452425,55.4824990454555 +2024-09-27 18:00:00,10,29.226783222376724,26.323328985597843,65.16369675685144 +2024-09-27 19:00:00,10,19.73653702187768,22.608103217300627,66.87461234891978 +2024-09-27 20:00:00,10,34.50796676501896,24.23223114264729,43.549778293178875 +2024-09-27 21:00:00,10,24.67837265369765,24.46510419479868,45.814155482374105 +2024-09-27 22:00:00,10,31.442878448421364,25.81270821781183,46.67113046494631 +2024-09-27 23:00:00,10,31.28315972255509,25.72024243989453,68.8626074598343 +2024-09-28 00:00:00,10,29.52340112400672,29.339407121078445,62.04269816940948 +2024-09-28 01:00:00,10,31.68216663409882,32.010774386883334,63.26831659718212 +2024-09-28 02:00:00,10,24.469697451838396,28.013463526191693,53.26100788979446 +2024-09-28 03:00:00,10,35.17673748918744,27.12498956390901,57.88652390068839 +2024-09-28 04:00:00,10,2.7510796473629107,28.105293133151466,66.6106686603065 +2024-09-28 05:00:00,10,17.847923498184464,26.31652145099926,30.756576464068914 +2024-09-28 06:00:00,10,20.41987672995417,25.894635127601592,51.63399640253132 +2024-09-28 07:00:00,10,21.452738836620693,23.29459496467227,69.09121908748753 +2024-09-28 08:00:00,10,25.70128207867674,21.70014938466235,52.946734225149775 +2024-09-28 09:00:00,10,26.326229251108558,23.138112504954815,59.42232507543786 +2024-09-28 10:00:00,10,16.543115723615337,17.443841782532857,64.31654318222212 +2024-09-28 11:00:00,10,6.012178490462723,19.453661737331032,45.29521298690302 +2024-09-28 12:00:00,10,41.4416187105263,19.49320242096274,53.65917715867885 +2024-09-28 13:00:00,10,26.72188220689405,20.33071972131264,65.42100171897816 +2024-09-28 14:00:00,10,17.48611808248661,22.85146976359943,51.47624017289776 +2024-09-28 15:00:00,10,17.92077069583783,26.27895207298898,58.69844693445199 +2024-09-28 16:00:00,10,22.869668422382375,20.096954826562406,55.604589508670024 +2024-09-28 17:00:00,10,15.428830806797825,21.481884450742154,58.74885164082407 +2024-09-28 18:00:00,10,24.07151653411424,20.90066264460714,54.84427397197151 +2024-09-28 19:00:00,10,32.123494975303174,24.958797381598597,53.12477217525918 +2024-09-28 20:00:00,10,25.85206343621764,20.4224295999637,59.13673157611258 +2024-09-28 21:00:00,10,39.40472183671744,19.204961103574462,54.96640730897497 +2024-09-28 22:00:00,10,20.436891660124328,27.648263258086672,57.68126632164278 +2024-09-28 23:00:00,10,16.49654102951686,22.932804873113785,59.67840249981507 +2024-09-29 00:00:00,10,23.43940527299875,22.987653416199695,55.016115629190736 +2024-09-29 01:00:00,10,26.847435277146147,30.20282903070122,45.16469783610776 +2024-09-29 02:00:00,10,16.36843601388198,24.6641659258324,62.49386025239891 +2024-09-29 03:00:00,10,21.937672706033904,25.265111115214662,47.83070427628371 +2024-09-29 04:00:00,10,25.068352109401847,24.530628637017855,48.89595157868712 +2024-09-29 05:00:00,10,43.10035098603282,19.272240810338843,54.31269175549768 +2024-09-29 06:00:00,10,26.021508889746933,24.92967247709945,60.479029211954206 +2024-09-29 07:00:00,10,24.489670560474803,26.92194905145952,68.86131762900229 +2024-09-29 08:00:00,10,11.022509178214282,25.929807484186277,60.63943337388208 +2024-09-29 09:00:00,10,29.380734960699687,27.626727281601518,64.05473088116376 +2024-09-29 10:00:00,10,15.984754038900448,20.067573055197784,75.42209871726311 +2024-09-29 11:00:00,10,25.809398669731433,22.5942226952892,62.77329887623255 +2024-09-29 12:00:00,10,35.10811440673039,25.81627038320412,70.4495504334016 +2024-09-29 13:00:00,10,30.766719507787258,25.807131345141723,61.012689053298786 +2024-09-29 14:00:00,10,23.390764972742822,28.409371767636195,58.72755220118268 +2024-09-29 15:00:00,10,27.496328718770762,16.275002711653247,62.43665159258723 +2024-09-29 16:00:00,10,19.094377133788967,22.790216421096847,52.57229485834211 +2024-09-29 17:00:00,10,17.527319482268517,22.406845676745952,61.202569368366454 +2024-09-29 18:00:00,10,18.784315417783034,23.04215824997169,36.47708017433 +2024-09-29 19:00:00,10,33.75043308828477,22.812791923501255,55.80426596780637 +2024-09-29 20:00:00,10,41.54960009996722,21.194875087419874,52.90029862973804 +2024-09-29 21:00:00,10,15.521848718734768,27.087405948821214,58.114122415094215 +2024-09-29 22:00:00,10,27.97523879126196,22.18689586057782,44.63676119957253 +2024-09-29 23:00:00,10,26.222159751532303,24.507083047590385,34.55204820346401 +2024-09-30 00:00:00,10,32.38115381889588,27.14511396646882,45.833587074515904 +2024-09-30 01:00:00,10,8.312359850134857,27.342417102938743,44.194015871581165 +2024-09-30 02:00:00,10,15.535454529697287,23.630360512010466,73.14655237104148 +2024-09-30 03:00:00,10,13.91270287868161,25.251681140090422,54.303535344621224 +2024-09-30 04:00:00,10,16.576463890558376,26.086142970981122,74.56266166227648 +2024-09-30 05:00:00,10,39.84897272484684,35.4047306118442,62.652313405650446 +2024-09-30 06:00:00,10,11.94472147333039,29.58232723441726,51.023090779136396 +2024-09-30 07:00:00,10,26.428501068761616,22.83282793525324,55.04599377130929 +2024-09-30 08:00:00,10,18.636429553230354,23.55836393570868,51.36943947118795 +2024-09-30 09:00:00,10,13.844349605681979,19.661372338602717,59.32461353995911 +2024-09-30 10:00:00,10,18.166833023374252,28.08841172865013,72.03809989990596 +2024-09-30 11:00:00,10,11.95044275614199,20.804937614747878,63.28213962625074 +2024-09-30 12:00:00,10,25.28483966284498,19.835102109581552,64.68001853748652 +2024-09-30 13:00:00,10,15.250739995720036,25.163532028812256,49.19034895227357 +2024-09-30 14:00:00,10,33.57370872336771,20.047247855152857,55.471723484437895 +2024-09-30 15:00:00,10,21.071801454572867,28.729522813239125,67.39188725768791 +2024-09-30 16:00:00,10,42.102741191233605,30.79915053218685,31.504968833227977 +2024-09-30 17:00:00,10,24.29407398132899,21.499615607580843,52.29819668936987 +2024-09-30 18:00:00,10,11.513487796266164,24.63760748843851,59.05934881341806 +2024-09-30 19:00:00,10,30.996097024462607,23.647903723993153,59.66276213551726 +2024-09-30 20:00:00,10,16.81315020452956,24.45396069399351,55.42694808483627 +2024-09-30 21:00:00,10,18.198093539837537,27.869619188963906,41.80403777924873 +2024-09-30 22:00:00,10,13.873160588595232,24.44541609221421,49.834663356986105 +2024-09-30 23:00:00,10,12.159367971205228,17.495645024892724,58.9500056205749 +2024-10-01 00:00:00,10,32.215819317507524,24.136957877102795,68.6508028135868 +2024-10-01 01:00:00,10,10.923884041494984,28.895925150629346,45.10523339122488 +2024-10-01 02:00:00,10,34.60576194526983,26.641157887397064,63.83828273480529 +2024-10-01 03:00:00,10,25.93319612312734,29.860546832436974,70.30643641903222 +2024-10-01 04:00:00,10,32.93259035194293,28.235875026868833,62.65581765340541 +2024-10-01 05:00:00,10,28.304242729270676,24.32618976559724,57.083089982287845 +2024-10-01 06:00:00,10,19.762531392468254,25.77625576789379,58.28404672566531 +2024-10-01 07:00:00,10,34.66416012187344,26.451477805918035,56.09256036000838 +2024-10-01 08:00:00,10,33.96231673774917,20.932905048129772,57.6061853995868 +2024-10-01 09:00:00,10,49.04621348016131,23.389094078841385,51.01752398384592 +2024-10-01 10:00:00,10,25.762487968014785,27.89823537389365,43.940933071452804 +2024-10-01 11:00:00,10,20.198804227998142,25.282420591503723,66.88368206333168 +2024-10-01 12:00:00,10,30.240645941588173,22.390708559492385,52.5445798174614 +2024-10-01 13:00:00,10,24.410885433576418,24.432498519199108,66.43728401137271 +2024-10-01 14:00:00,10,20.164199247020505,21.347508684779907,72.39390010824745 +2024-10-01 15:00:00,10,20.35771987231844,21.46329447295408,56.48108889825672 +2024-10-01 16:00:00,10,48.26544154495284,21.51048084886596,38.35912803518426 +2024-10-01 17:00:00,10,37.183000139063935,23.038142953357962,71.96361000453264 +2024-10-01 18:00:00,10,28.201228021580167,20.983607339743624,53.49268001985945 +2024-10-01 19:00:00,10,16.845220225909113,23.48116442146775,57.26075450658902 +2024-10-01 20:00:00,10,28.88558258634271,21.392216017739305,52.26221162370536 +2024-10-01 21:00:00,10,28.439901142685738,18.94679274247161,39.98149493682111 +2024-10-01 22:00:00,10,23.149408515184483,23.778190665292172,53.65732456090242 +2024-10-01 23:00:00,10,9.030476423576925,27.527384598469933,51.432634683946326 +2024-10-02 00:00:00,10,42.1258551230319,29.677688780455714,44.73394287782299 +2024-10-02 01:00:00,10,33.02609006404289,30.882240913563322,75.33364438150875 +2024-10-02 02:00:00,10,19.652046210163007,26.20187617133806,59.43201406541467 +2024-10-02 03:00:00,10,15.100452881331409,24.835192604847137,61.79951908235407 +2024-10-02 04:00:00,10,17.786234820978027,29.586465846906943,46.59447767698673 +2024-10-02 05:00:00,10,31.754725602935224,23.72321751793062,57.471161866848945 +2024-10-02 06:00:00,10,24.531973144606013,23.89733365214822,57.04402407241913 +2024-10-02 07:00:00,10,4.146825155244976,23.376109329144796,63.827214815899005 +2024-10-02 08:00:00,10,34.94447981480987,17.944332730938182,74.53573365877153 +2024-10-02 09:00:00,10,34.27430170596098,26.79771755147623,58.80378742943097 +2024-10-02 10:00:00,10,7.950921513962092,16.326575458249213,50.601896902983626 +2024-10-02 11:00:00,10,25.146059675207688,20.22648966818732,55.76594444075785 +2024-10-02 12:00:00,10,24.62923781252827,24.04725787187524,39.40044432332838 +2024-10-02 13:00:00,10,36.678896771980135,28.908740233512617,66.17900997002424 +2024-10-02 14:00:00,10,31.62633008934757,26.844904842685693,52.22352391647437 +2024-10-02 15:00:00,10,27.65155179716258,25.197537136707986,50.923115434814534 +2024-10-02 16:00:00,10,17.434091256231056,21.969795494316273,64.15647743497968 +2024-10-02 17:00:00,10,30.803985597079006,25.63292825370331,61.47255955324216 +2024-10-02 18:00:00,10,29.6343593575148,24.296055634402624,53.57304233973557 +2024-10-02 19:00:00,10,10.342042475385282,20.517322704917177,43.897772108410535 +2024-10-02 20:00:00,10,25.355698314173434,24.849554776494607,58.63434562492396 +2024-10-02 21:00:00,10,30.828178276367087,25.874310303121554,29.03258258492331 +2024-10-02 22:00:00,10,22.625148444000924,26.37930885137196,45.84849012559465 +2024-10-02 23:00:00,10,16.533019810564507,27.429802344282628,56.75630032117296 +2024-10-03 00:00:00,10,24.042288622725312,27.011721779693847,69.5519174403015 +2024-10-03 01:00:00,10,28.08125091013093,24.11771146774449,44.6036286900196 +2024-10-03 02:00:00,10,33.41129930886744,31.30057964873141,58.132468143357116 +2024-10-03 03:00:00,10,31.957849589466186,33.08770294536855,59.446074804115845 +2024-10-03 04:00:00,10,26.190613405678565,25.504601587408956,57.959295639091195 +2024-10-03 05:00:00,10,31.935489729141718,26.838993968252897,56.25699732392868 +2024-10-03 06:00:00,10,18.634935571559463,21.92230111825236,57.46376412739991 +2024-10-03 07:00:00,10,27.987947511750512,22.385630250406834,63.62851586634141 +2024-10-03 08:00:00,10,18.641106334388652,19.287763822757316,60.03541471394956 +2024-10-03 09:00:00,10,20.263032517810903,18.841228687481106,58.407494652813526 +2024-10-03 10:00:00,10,23.404538611206622,20.61128819297209,55.67981409975917 +2024-10-03 11:00:00,10,14.22047158483133,23.853515473083,61.638335144425355 +2024-10-03 12:00:00,10,21.249697792908922,20.1710906623488,58.87793253389358 +2024-10-03 13:00:00,10,34.261863072297864,17.919419850626802,52.056476278296 +2024-10-03 14:00:00,10,25.489642274023858,21.65085358988623,57.87379170250843 +2024-10-03 15:00:00,10,41.02116453881305,17.74700841176494,61.390811728402085 +2024-10-03 16:00:00,10,29.066047601182547,21.772240953094197,48.05506763765285 +2024-10-03 17:00:00,10,21.57263597040921,23.908167230527408,35.8902800933697 +2024-10-03 18:00:00,10,25.67321774801442,22.345289457322174,49.28919900142304 +2024-10-03 19:00:00,10,19.81145667768601,17.165836559165154,61.53124650404558 +2024-10-03 20:00:00,10,21.907515795401864,20.94386247295887,64.7085556699009 +2024-10-03 21:00:00,10,6.174089583729611,30.361985916930067,64.43343840599461 +2024-10-03 22:00:00,10,16.887563338417042,24.034045262754567,51.868989488483756 +2024-10-03 23:00:00,10,22.59412568304701,24.951877228575192,51.35996617055812 +2024-10-04 00:00:00,10,19.699010167083813,29.295951809595184,74.49449825428952 +2024-10-04 01:00:00,10,23.82066284998866,27.879125529766046,49.6440014731609 +2024-10-04 02:00:00,10,19.779444915430627,27.07783828466236,54.58872709089451 +2024-10-04 03:00:00,10,14.708689694190118,21.610282975982827,65.2671858007553 +2024-10-04 04:00:00,10,27.69283849608431,28.652513319805553,76.74570310005453 +2024-10-04 05:00:00,10,26.607582405756986,33.72153992414262,45.20921021429655 +2024-10-04 06:00:00,10,38.4864221330169,26.490681089261,51.59296654425978 +2024-10-04 07:00:00,10,15.935615342785555,20.82670478834016,67.18418210825288 +2024-10-04 08:00:00,10,23.430309496477783,25.28167049288354,62.21102918785655 +2024-10-04 09:00:00,10,23.65821449372769,23.42021611421568,49.377518358956216 +2024-10-04 10:00:00,10,29.65745972735136,22.097700930444706,66.12600358854836 +2024-10-04 11:00:00,10,12.160423799295815,26.18492281478299,62.34007598360676 +2024-10-04 12:00:00,10,11.089397303933625,21.17238919083652,57.82467037964747 +2024-10-04 13:00:00,10,31.793173886563267,14.744667543497158,72.20281573463217 +2024-10-04 14:00:00,10,22.88915587723859,27.310144241847155,50.58435925271449 +2024-10-04 15:00:00,10,15.614775158107655,20.44085812777044,43.58021321069992 +2024-10-04 16:00:00,10,21.98422498934486,21.879194926120487,74.62404710933203 +2024-10-04 17:00:00,10,26.029000358503446,24.388320080881506,61.913826964148846 +2024-10-04 18:00:00,10,31.469700798571825,25.529911832005908,74.22087788741149 +2024-10-04 19:00:00,10,37.51908434320069,20.114093939804945,52.26161682656495 +2024-10-04 20:00:00,10,7.668458651808876,21.867844199989804,39.89948280007665 +2024-10-04 21:00:00,10,16.569427183838926,25.006930867269006,43.175197549108496 +2024-10-04 22:00:00,10,17.83940697287477,19.432390085112456,60.5841817813965 +2024-10-04 23:00:00,10,13.171098849918192,28.638059308984978,45.75874627710585 +2024-10-05 00:00:00,10,33.29764489179891,31.29707011000905,47.52956802734838 +2024-10-05 01:00:00,10,24.840388716875918,29.985536974085207,69.31333085034217 +2024-10-05 02:00:00,10,27.54382792968339,28.318753727112934,68.93922574829706 +2024-10-05 03:00:00,10,25.044132072285553,26.325592722523258,50.90961968578339 +2024-10-05 04:00:00,10,17.53173368343873,22.437341833105748,51.57190612630655 +2024-10-05 05:00:00,10,25.22493695621013,25.74383757799942,53.695875513624436 +2024-10-05 06:00:00,10,21.025776643383995,25.654957136980737,60.41621523007491 +2024-10-05 07:00:00,10,30.271047088891596,27.292581051613528,48.41997360094178 +2024-10-05 08:00:00,10,33.17244711683593,24.257801800311576,70.37339706900383 +2024-10-05 09:00:00,10,33.213311429613846,24.044079268031485,69.70730250978981 +2024-10-05 10:00:00,10,23.02606312312898,23.348768380566938,55.26254659693742 +2024-10-05 11:00:00,10,22.73174230646384,24.09020054261505,71.86919709668294 +2024-10-05 12:00:00,10,13.526208221539996,21.973950247448908,59.60328581623208 +2024-10-05 13:00:00,10,21.069178753369158,22.35847214722207,53.08900602169747 +2024-10-05 14:00:00,10,31.48341412065538,21.30131400589649,65.33118507936378 +2024-10-05 15:00:00,10,27.697193319923358,21.45659264777786,67.24062483232044 +2024-10-05 16:00:00,10,32.37281335287641,23.038259749962975,53.733963358204875 +2024-10-05 17:00:00,10,39.67661151211366,21.55420696659596,56.93502736141345 +2024-10-05 18:00:00,10,23.66225999334768,20.437273392263286,55.42294365409176 +2024-10-05 19:00:00,10,17.563348392679494,21.3469818479937,49.78238762631046 +2024-10-05 20:00:00,10,13.411179507891081,27.311905110330574,55.23829797716979 +2024-10-05 21:00:00,10,29.117696442556404,21.131846725730593,37.8575451830407 +2024-10-05 22:00:00,10,10.961406289567444,23.288264422998374,59.79690230138256 +2024-10-05 23:00:00,10,19.357646666051536,26.791580453084713,57.861984609217906 +2024-10-06 00:00:00,10,29.628544373606957,27.609819465443824,49.49641155592324 +2024-10-06 01:00:00,10,28.442705250220122,29.22915317174072,57.98495739480334 +2024-10-06 02:00:00,10,19.77073081980734,26.869289398259554,56.32527818675861 +2024-10-06 03:00:00,10,19.638373628311808,27.54526431268544,62.720503634513115 +2024-10-06 04:00:00,10,36.064136140856306,27.369769534068862,48.185792747178304 +2024-10-06 05:00:00,10,34.94589557607577,26.0548777124719,51.76618849348643 +2024-10-06 06:00:00,10,32.32130011646769,30.31436575728017,45.71400124657231 +2024-10-06 07:00:00,10,16.49684134157066,27.77125757424419,51.829590371754016 +2024-10-06 08:00:00,10,37.150158477540145,22.49353943203219,53.38078676279315 +2024-10-06 09:00:00,10,25.894473949739996,24.692382415497566,79.37022475427273 +2024-10-06 10:00:00,10,28.630992080377453,19.667604990597475,63.6032052108026 +2024-10-06 11:00:00,10,25.696455674032528,23.098026522667052,59.15797492862235 +2024-10-06 12:00:00,10,31.873171415463837,16.01032915417568,63.58328985593181 +2024-10-06 13:00:00,10,26.87359690741959,19.290314909345668,55.3534789476726 +2024-10-06 14:00:00,10,25.387912077367314,20.74676603805754,43.01487241099102 +2024-10-06 15:00:00,10,18.65043374254462,17.508475113682273,53.69490038461191 +2024-10-06 16:00:00,10,40.71778893827969,23.185830857094118,60.620894812696974 +2024-10-06 17:00:00,10,17.198777677614608,24.751398576609365,68.89669489435747 +2024-10-06 18:00:00,10,25.61487428451538,24.400601350220196,50.712961556351 +2024-10-06 19:00:00,10,26.782465225564376,25.042956254862833,44.49066427241163 +2024-10-06 20:00:00,10,18.01118015641594,22.890754508884285,63.575191299413554 +2024-10-06 21:00:00,10,33.3930950309312,24.519643876852147,55.330374830004324 +2024-10-06 22:00:00,10,29.39248936558588,28.60327189614085,50.58276505950841 +2024-10-06 23:00:00,10,31.9257585685683,21.19117999152927,45.72872190122131 +2024-10-07 00:00:00,10,19.15437794030175,26.240023861789712,60.94752873116 +2024-10-07 01:00:00,10,15.839676274502896,30.761612069656763,61.99855660939174 +2024-10-07 02:00:00,10,20.925382663781413,21.78982859953468,50.113413201420244 +2024-10-07 03:00:00,10,38.7868988895835,24.883854795106252,72.14071652744387 +2024-10-07 04:00:00,10,30.405753011061382,21.26621245420644,59.95056085338997 +2024-10-07 05:00:00,10,26.928345539481594,25.42940506317465,60.477864138196075 +2024-10-07 06:00:00,10,36.0825843440747,26.027340352810917,56.29109940649218 +2024-10-07 07:00:00,10,28.31018473060275,25.478129647657106,64.46021035490308 +2024-10-07 08:00:00,10,35.26688949049658,21.92319459845512,57.88666396557116 +2024-10-07 09:00:00,10,9.720829332986595,22.91439912966677,60.49476549671027 +2024-10-07 10:00:00,10,13.853720648440593,24.514047441604784,55.10218348601136 +2024-10-07 11:00:00,10,13.661435284608253,26.085807623561166,46.13241640258965 +2024-10-07 12:00:00,10,0.0,21.381886731378422,35.73426206459773 +2024-10-07 13:00:00,10,25.339477089394624,26.565927311918713,50.19085538717911 +2024-10-07 14:00:00,10,18.513156766999646,24.952951731772487,69.2580965943251 +2024-10-07 15:00:00,10,21.123872286134795,24.06738791814003,56.557554380475416 +2024-10-07 16:00:00,10,29.428991085562394,26.662974287713833,66.22535614063457 +2024-10-07 17:00:00,10,34.51766350696779,24.921941607032768,56.71680567852471 +2024-10-07 18:00:00,10,29.309025057874777,15.15523832662149,65.20837228585809 +2024-10-07 19:00:00,10,30.205956393925685,24.083820428404902,51.66950777480175 +2024-10-07 20:00:00,10,15.395086860822296,28.081525951562526,75.36115010350248 +2024-10-07 21:00:00,10,23.347586883954428,24.95474172125677,65.34874743236514 +2024-10-07 22:00:00,10,28.469059652816185,25.66902025855429,57.598391985250885 +2024-10-07 23:00:00,10,37.55676636021578,21.575300730071273,71.68800390174019 +2024-10-08 00:00:00,10,29.673139058881432,25.795627082488632,55.350545233148104 +2024-10-08 01:00:00,10,27.888740715612077,24.176614299308373,62.85687251717846 +2024-10-08 02:00:00,10,28.531073727281186,22.36077997232858,42.14527401651033 +2024-10-08 03:00:00,10,17.396970938517363,27.808485563606503,47.42516048771374 +2024-10-08 04:00:00,10,14.253548324350382,23.57298987862099,37.28237695666764 +2024-10-08 05:00:00,10,34.26575978917059,24.27621525419974,62.97412691279658 +2024-10-08 06:00:00,10,21.192838951676805,29.810767101607524,57.87708887369433 +2024-10-08 07:00:00,10,11.352006266173147,27.180545040438577,51.580626724677295 +2024-10-08 08:00:00,10,28.504004621748642,19.07248980667869,41.80311178448848 +2024-10-08 09:00:00,10,14.99722108738146,22.08793558103862,67.92263987858358 +2024-10-08 10:00:00,10,22.266333633907855,21.751698379023903,50.55101000461141 +2024-10-08 11:00:00,10,23.186738481445566,25.388620782355943,52.01361707243178 +2024-10-08 12:00:00,10,27.66189234970706,28.605298802527898,40.880074936937945 +2024-10-08 13:00:00,10,8.864686464075273,19.585320164626605,42.83235468050404 +2024-10-08 14:00:00,10,11.084802648254518,22.38394312854691,44.84177141976099 +2024-10-08 15:00:00,10,35.42054980768147,25.435613341686317,53.993874097199914 +2024-10-08 16:00:00,10,28.496890653261808,25.392821194612996,53.48040134917749 +2024-10-08 17:00:00,10,28.43492229634246,26.725952783346138,50.830590382820596 +2024-10-08 18:00:00,10,24.505336435680945,26.74320092232422,60.05452132618386 +2024-10-08 19:00:00,10,10.896265069820924,26.971532958128293,40.534214730180366 +2024-10-08 20:00:00,10,12.987713015666529,27.64213686295944,42.91668007969046 +2024-10-08 21:00:00,10,25.501143556914187,27.665026200499554,42.86669192639154 +2024-10-08 22:00:00,10,40.48024924063835,25.143588687049306,60.840609251157844 +2024-10-08 23:00:00,10,17.774077794439087,22.13367031751966,55.12113442208578 +2024-10-09 00:00:00,10,37.84127615079037,21.89960184357529,47.94515198673707 +2024-10-09 01:00:00,10,29.37322293946398,30.18971253804429,62.33474377289708 +2024-10-09 02:00:00,10,27.222180404409396,27.824726489471626,45.9337646072226 +2024-10-09 03:00:00,10,15.482667713583831,29.2361905788172,44.43297617696176 +2024-10-09 04:00:00,10,35.4430474515506,25.20640334756512,73.27123317388602 +2024-10-09 05:00:00,10,36.22936899841556,32.04956474391556,59.596176425878035 +2024-10-09 06:00:00,10,32.32985687545597,26.56409824777675,46.10229148526712 +2024-10-09 07:00:00,10,26.184187489623472,21.563903902676138,73.24564686639235 +2024-10-09 08:00:00,10,29.506620939780454,18.102436939756952,57.199454329600684 +2024-10-09 09:00:00,10,5.066243048230252,25.73787615438473,65.64910171976506 +2024-10-09 10:00:00,10,11.963470748834474,22.45135536589726,66.36696856552865 +2024-10-09 11:00:00,10,16.97174024170215,24.782699030396756,57.775778826154095 +2024-10-09 12:00:00,10,25.790582982985036,25.59699343287329,56.25761187081975 +2024-10-09 13:00:00,10,31.55302211950403,20.757395136422492,62.665756235589456 +2024-10-09 14:00:00,10,33.80839494575646,22.265940190217712,44.481770237697056 +2024-10-09 15:00:00,10,19.729376319263807,21.246786955774994,48.90054716856573 +2024-10-09 16:00:00,10,19.881362409290414,21.622457446236087,57.27131474687734 +2024-10-09 17:00:00,10,33.07712800548465,28.719922429405106,44.03520378424195 +2024-10-09 18:00:00,10,26.98095109597967,22.138630413669205,47.200008844789124 +2024-10-09 19:00:00,10,20.288727992937574,19.60810883458078,49.446955977675245 +2024-10-09 20:00:00,10,29.508182308723903,22.31312590876503,56.23495446461878 +2024-10-09 21:00:00,10,23.035197588928895,27.761194854535354,61.58359882447205 +2024-10-09 22:00:00,10,22.115575163900836,23.162680757185537,52.23290229837207 +2024-10-09 23:00:00,10,33.74500601241724,23.247979189636034,50.45413583542756 +2024-10-10 00:00:00,10,9.211155229589231,24.379963007615917,55.81135632508074 +2024-10-10 01:00:00,10,42.17616221223311,24.02692867402844,65.53057712212743 +2024-10-10 02:00:00,10,25.60953145677683,23.59498866131444,41.92317788552704 +2024-10-10 03:00:00,10,24.972160138021085,26.237833258667017,47.202112161405886 +2024-10-10 04:00:00,10,16.77723041175946,27.25176714032127,38.4532076944713 +2024-10-10 05:00:00,10,16.084671904138098,24.78354474957319,54.1684105452081 +2024-10-10 06:00:00,10,34.4596694726074,27.197881286251114,61.76687882521107 +2024-10-10 07:00:00,10,18.309024032174186,30.113934228052848,57.1629674068022 +2024-10-10 08:00:00,10,22.687515077608275,21.51110781915969,54.92443501381918 +2024-10-10 09:00:00,10,39.550912276379464,20.15689736019364,72.68586362497558 +2024-10-10 10:00:00,10,16.23246106694816,16.90198577565927,59.763176304195795 +2024-10-10 11:00:00,10,15.735774391856463,24.212782555248012,53.630297125190644 +2024-10-10 12:00:00,10,27.391952599323965,25.099728003401335,60.434603964871904 +2024-10-10 13:00:00,10,22.87143858225502,25.1011587626512,49.66584827689253 +2024-10-10 14:00:00,10,18.678453965049627,24.602152860473698,51.63538549650438 +2024-10-10 15:00:00,10,28.327190530778868,26.188298795426046,51.713883704447056 +2024-10-10 16:00:00,10,1.3919966102467605,28.408396255420516,46.95066663177302 +2024-10-10 17:00:00,10,17.10495821329851,21.19926262275349,54.334412419059525 +2024-10-10 18:00:00,10,18.472501104453386,24.492823703967634,60.44474091042411 +2024-10-10 19:00:00,10,15.696339806480204,25.292403585882347,64.63603846010528 +2024-10-10 20:00:00,10,30.295465042449386,24.082235613283444,71.48305079737207 +2024-10-10 21:00:00,10,30.78467358113432,22.319697683510604,44.870133396815035 +2024-10-10 22:00:00,10,27.910520894964847,23.91509011029137,53.653348829299176 +2024-10-10 23:00:00,10,9.501538568439472,26.454683443364626,55.446442798989374 +2024-10-11 00:00:00,10,16.03547277413096,24.291128116195672,73.50758213527862 +2024-10-11 01:00:00,10,36.51889308150564,28.493063073788782,53.24156565123328 +2024-10-11 02:00:00,10,28.338815102988804,31.688956218034775,53.490397183873796 +2024-10-11 03:00:00,10,30.65765051214425,28.391977866070313,60.02577039519908 +2024-10-11 04:00:00,10,20.444398531828096,29.472278248864086,47.20524390155467 +2024-10-11 05:00:00,10,30.091556190221294,26.023888257044025,63.780692267309384 +2024-10-11 06:00:00,10,17.066659961421642,24.277543334064497,47.977069519154334 +2024-10-11 07:00:00,10,7.315466718558472,22.17884549833118,56.949225615436866 +2024-10-11 08:00:00,10,43.57945147062466,19.793766065708432,60.28484784459305 +2024-10-11 09:00:00,10,25.586382586511956,26.51035115358864,65.00623345356405 +2024-10-11 10:00:00,10,19.65762395790814,25.69303953533638,67.53002923264543 +2024-10-11 11:00:00,10,12.266737991718557,20.578928261045608,57.917415294848894 +2024-10-11 12:00:00,10,30.952998177478005,21.58805915304161,57.558989960324496 +2024-10-11 13:00:00,10,21.044340465854823,24.778675692501995,59.83126142062914 +2024-10-11 14:00:00,10,38.471624207672164,26.513087824948872,61.839836903541865 +2024-10-11 15:00:00,10,19.6263930073259,20.41847625295166,44.98946777179178 +2024-10-11 16:00:00,10,25.01480051271644,21.62343418700936,50.39188140848113 +2024-10-11 17:00:00,10,21.36973810463213,24.5145099954354,57.54569859461306 +2024-10-11 18:00:00,10,32.9471183292837,21.902839489092504,66.16047679333403 +2024-10-11 19:00:00,10,24.217212049262436,29.01346034152545,43.64594648531533 +2024-10-11 20:00:00,10,26.21597722296558,21.718334724220153,61.18807319977099 +2024-10-11 21:00:00,10,25.13706314222813,20.882191165245032,54.210629040312696 +2024-10-11 22:00:00,10,28.739452085047528,23.119146072307824,45.88293626613331 +2024-10-11 23:00:00,10,16.996849308270942,27.80726704560641,43.83638366650183 +2024-10-12 00:00:00,10,25.7643132247429,33.78571946006832,40.91321497742802 +2024-10-12 01:00:00,10,38.795336070059896,24.255387964129703,44.47947043610017 +2024-10-12 02:00:00,10,26.68191729650361,24.329922343845247,69.8397142829894 +2024-10-12 03:00:00,10,0.0,24.23959692413642,58.79818873300016 +2024-10-12 04:00:00,10,22.43963881367818,27.344794833518947,63.0514107062719 +2024-10-12 05:00:00,10,5.85309003247362,27.46020151616883,65.55736564177587 +2024-10-12 06:00:00,10,22.216816812500436,27.496068319302026,64.43386349713907 +2024-10-12 07:00:00,10,43.657992413165545,22.77008058176453,56.680737327337965 +2024-10-12 08:00:00,10,32.31976723236732,21.37196998116029,66.52265228753355 +2024-10-12 09:00:00,10,22.708813526885926,26.309666586249367,56.830437659596846 +2024-10-12 10:00:00,10,9.591279935075272,27.729823505036904,52.79675400879981 +2024-10-12 11:00:00,10,42.06014634316894,23.268527662169205,51.78759291131468 +2024-10-12 12:00:00,10,22.767258484826403,21.333894647476715,51.821242758993144 +2024-10-12 13:00:00,10,5.370470343901875,22.039605254358438,62.88977696495135 +2024-10-12 14:00:00,10,24.835085991069874,26.74060898475687,61.440020327961236 +2024-10-12 15:00:00,10,40.78161293580408,32.282640655109475,66.4414140364946 +2024-10-12 16:00:00,10,29.924518989533563,19.57973892227806,67.43791938839456 +2024-10-12 17:00:00,10,22.950475649500635,24.704056907391827,59.88542802698831 +2024-10-12 18:00:00,10,17.417147681293727,24.1542986924448,54.294027240276236 +2024-10-12 19:00:00,10,37.03609725481994,25.61417043166142,56.98074633183032 +2024-10-12 20:00:00,10,15.68711924998992,26.948715847488227,47.21639393065185 +2024-10-12 21:00:00,10,31.84563873736726,26.809534967990793,60.852414293377656 +2024-10-12 22:00:00,10,7.734224372598179,22.944612666442463,65.11929401146658 +2024-10-12 23:00:00,10,29.962132264161113,23.486849164344306,41.48937945760027 +2024-10-13 00:00:00,10,18.060721065833736,27.866792566278562,60.67059278630154 +2024-10-13 01:00:00,10,5.080867253186444,24.34693020543879,63.24537195950174 +2024-10-13 02:00:00,10,24.832452675737496,22.941966359337368,39.756305660658256 +2024-10-13 03:00:00,10,16.01520525434082,21.950299443858604,44.836306573638616 +2024-10-13 04:00:00,10,20.862325055403975,31.76713097783349,55.77537929494935 +2024-10-13 05:00:00,10,32.35499455925308,29.65290559348901,64.4663589439619 +2024-10-13 06:00:00,10,21.743722148904347,26.609755429227995,54.9290046829601 +2024-10-13 07:00:00,10,25.2814898185539,21.69253127224722,51.91581272389681 +2024-10-13 08:00:00,10,28.287470753482456,23.472468913333724,63.790505498145095 +2024-10-13 09:00:00,10,30.606392862631946,22.719844574033807,47.602508218851895 +2024-10-13 10:00:00,10,32.924622538851956,18.858981820415906,58.338701073728224 +2024-10-13 11:00:00,10,12.066323063200677,19.281216188361032,60.09470829346486 +2024-10-13 12:00:00,10,32.18794847923116,23.63439008087385,67.23326113294267 +2024-10-13 13:00:00,10,32.8530250464876,26.709766042689548,60.71682983330203 +2024-10-13 14:00:00,10,24.872787010044966,24.213129264332906,57.7683939972696 +2024-10-13 15:00:00,10,30.949865152018052,27.16250285700659,54.31131039420605 +2024-10-13 16:00:00,10,22.473699413686465,25.669145047435194,51.8748631368949 +2024-10-13 17:00:00,10,4.853248304267964,19.564351881471513,70.0398805306438 +2024-10-13 18:00:00,10,0.0,20.98760961048656,57.69718685668817 +2024-10-13 19:00:00,10,26.816602314710355,18.09569465301543,46.12791510876917 +2024-10-13 20:00:00,10,28.606046337634893,25.625660376477715,58.53836614907281 +2024-10-13 21:00:00,10,26.181795657155313,22.606110956641206,54.54885212864337 +2024-10-13 22:00:00,10,22.463585974176027,25.807472628755967,47.47703977645953 +2024-10-13 23:00:00,10,22.773244807478918,25.028581206664192,46.386464921371314 +2024-10-14 00:00:00,10,23.53229911314955,26.48083091349725,44.65834510886246 +2024-10-14 01:00:00,10,14.233753154822105,25.342243188759003,45.7887862682068 +2024-10-14 02:00:00,10,37.20313892669693,33.030273077321766,53.09321188685493 +2024-10-14 03:00:00,10,20.064635769872588,27.78367564685844,34.39973242625061 +2024-10-14 04:00:00,10,18.533475724167307,25.786606947386936,58.31092740473179 +2024-10-14 05:00:00,10,9.903336394908578,22.656526768589018,62.46427835360136 +2024-10-14 06:00:00,10,23.367983046870272,25.056594999514047,63.50768802784656 +2024-10-14 07:00:00,10,10.996076747448415,24.726284064166375,52.228440048187345 +2024-10-14 08:00:00,10,29.070812029242717,28.142553263767866,48.415228983051826 +2024-10-14 09:00:00,10,19.791303560871523,26.36265106487457,65.19287990753024 +2024-10-14 10:00:00,10,6.944843972157903,23.94409122231674,64.59689392017913 +2024-10-14 11:00:00,10,24.4496368489305,22.63403279422581,39.23440116646014 +2024-10-14 12:00:00,10,25.012764106800404,21.487717067115764,66.22855305583519 +2024-10-14 13:00:00,10,32.280666505095695,29.75989175386563,65.58277348579351 +2024-10-14 14:00:00,10,22.531100043623923,20.71121829466909,60.47463911666531 +2024-10-14 15:00:00,10,24.85475694578835,23.423991873491566,42.034222845394396 +2024-10-14 16:00:00,10,19.819171739022245,23.708635570849964,59.944365320503195 +2024-10-14 17:00:00,10,12.616597212451346,22.007871196315516,55.830671302795515 +2024-10-14 18:00:00,10,11.669161743154506,24.878412881380484,48.66774028014554 +2024-10-14 19:00:00,10,23.860502321292447,17.655333100856044,57.41395537182587 +2024-10-14 20:00:00,10,30.44360987397555,20.689896426696592,47.36938268488359 +2024-10-14 21:00:00,10,15.859475100880148,26.430181834148815,46.03201262952695 +2024-10-14 22:00:00,10,20.325546994387505,27.684687507985508,59.722710903452786 +2024-10-14 23:00:00,10,20.009745111865648,22.144083063963755,62.52185361363016 +2024-10-15 00:00:00,10,27.231590886607645,25.697684976645892,33.31724573057231 +2024-10-15 01:00:00,10,18.909651083103043,21.750482675021114,48.756356433832515 +2024-10-15 02:00:00,10,27.020939511400762,20.724482203269538,55.890894267951474 +2024-10-15 03:00:00,10,17.780740020407464,29.272798121903385,51.227444254731566 +2024-10-15 04:00:00,10,30.906896967647718,33.403593436021666,60.09884987995056 +2024-10-15 05:00:00,10,11.091301345712488,20.868918322350197,67.32231600020837 +2024-10-15 06:00:00,10,32.6356450331766,28.881201732956033,51.91033898295953 +2024-10-15 07:00:00,10,40.661654924044385,20.15198292325196,69.23562686808663 +2024-10-15 08:00:00,10,24.606632751353786,20.395187425405208,64.28358710751375 +2024-10-15 09:00:00,10,15.363403259340696,20.401524825963044,63.524743112602856 +2024-10-15 10:00:00,10,20.837650003742436,19.76280787015652,50.81530744636912 +2024-10-15 11:00:00,10,24.2250233078366,19.624970217892454,66.86223192929886 +2024-10-15 12:00:00,10,25.695029157153975,18.24609337630654,70.13269034276858 +2024-10-15 13:00:00,10,34.43792354920249,21.942351136872993,58.23518222846018 +2024-10-15 14:00:00,10,22.379320280045683,19.29212167470922,63.503535246716055 +2024-10-15 15:00:00,10,37.784103147573234,23.65654709431057,74.60214598164143 +2024-10-15 16:00:00,10,29.163111665036947,31.760659423845976,69.28968720065593 +2024-10-15 17:00:00,10,23.86139938499413,19.778045805476875,48.86852298617256 +2024-10-15 18:00:00,10,22.565556519335626,20.751846109674233,44.76405448147925 +2024-10-15 19:00:00,10,33.32761246263159,22.89822308769985,57.0053128012864 +2024-10-15 20:00:00,10,28.247948037943154,29.382675382146722,54.91542216670163 +2024-10-15 21:00:00,10,25.5698564516451,22.780245274473902,55.525907963921256 +2024-10-15 22:00:00,10,24.767462001886678,28.990744020122897,67.78622372379782 +2024-10-15 23:00:00,10,20.815286409281285,22.586256031175157,45.527129980359256 +2024-10-16 00:00:00,10,34.39449529409694,24.26770387471646,70.04683051684663 +2024-10-16 01:00:00,10,22.469339296499545,28.329712512444868,48.914454117626164 +2024-10-16 02:00:00,10,38.354347134267385,27.492482008792887,62.29528785920504 +2024-10-16 03:00:00,10,23.664720949634685,28.90932593276581,56.30916201559431 +2024-10-16 04:00:00,10,20.605643887488597,28.74419921544826,42.62200321392122 +2024-10-16 05:00:00,10,19.11183036491672,25.681869744806228,52.29009318010158 +2024-10-16 06:00:00,10,24.154378407937884,24.828975294382534,57.12819103214643 +2024-10-16 07:00:00,10,24.93017633695625,20.071045735448685,50.4873606135355 +2024-10-16 08:00:00,10,25.06335836417558,22.932569749323825,54.43339862458292 +2024-10-16 09:00:00,10,35.48169572858455,25.450854773622723,55.65515667430706 +2024-10-16 10:00:00,10,28.713483190013022,17.391187361449106,68.79884445196153 +2024-10-16 11:00:00,10,25.61769328451468,16.573538847806496,56.27113313200232 +2024-10-16 12:00:00,10,22.942759476270933,19.650140334851184,67.61657833678841 +2024-10-16 13:00:00,10,10.732369627182106,19.160917485077277,84.37046134696587 +2024-10-16 14:00:00,10,19.82537287880661,20.34333072581056,74.62594905636396 +2024-10-16 15:00:00,10,7.345638024652903,21.33014416913882,59.900696834188146 +2024-10-16 16:00:00,10,33.40681881759893,19.735673264435647,68.64108312964588 +2024-10-16 17:00:00,10,15.630661539867905,21.808378774292322,50.163088687769594 +2024-10-16 18:00:00,10,36.73104696656574,26.485939887495924,52.60005225118841 +2024-10-16 19:00:00,10,23.84614841099697,28.056638779871076,51.13648548567656 +2024-10-16 20:00:00,10,22.296788034612252,15.162189293431464,48.39370126086744 +2024-10-16 21:00:00,10,18.02526514529793,21.57126669545947,55.19259205809728 +2024-10-16 22:00:00,10,25.68817442021796,22.750786788429252,44.85308849946432 +2024-10-16 23:00:00,10,27.779972240947117,26.242924723359977,48.98329117485106 +2024-10-17 00:00:00,10,26.64131102749927,27.545916871403552,56.828588244139006 +2024-10-17 01:00:00,10,26.820511611267705,27.952899326959304,37.08974346620722 +2024-10-17 02:00:00,10,23.93647982935139,32.3815980789851,57.32141287089853 +2024-10-17 03:00:00,10,27.703683328521315,26.410612154737283,52.284517324372565 +2024-10-17 04:00:00,10,11.38808760648974,22.75320453465317,56.056553415761364 +2024-10-17 05:00:00,10,18.59609837244729,30.455456656566483,59.00193663092946 +2024-10-17 06:00:00,10,14.427154795824663,25.12491088005855,50.12220033597297 +2024-10-17 07:00:00,10,33.91291772538675,22.263419009925613,49.03156053870666 +2024-10-17 08:00:00,10,18.971827728431435,27.19859641490833,67.8428075398748 +2024-10-17 09:00:00,10,7.948678122481125,19.992539634483546,59.292494160728516 +2024-10-17 10:00:00,10,25.68391682974819,20.6462692288393,49.667382360534646 +2024-10-17 11:00:00,10,14.535194349894818,14.813423614280236,56.54862291769796 +2024-10-17 12:00:00,10,26.981500777830373,24.123495239564427,56.42973775294422 +2024-10-17 13:00:00,10,27.417529748112855,17.38474772043329,64.21729838270598 +2024-10-17 14:00:00,10,17.27373109139345,25.891412490199176,53.23414551655242 +2024-10-17 15:00:00,10,16.832862702604793,31.45894090570656,43.31474520213423 +2024-10-17 16:00:00,10,29.215003818925386,20.209141336199547,71.95186863706152 +2024-10-17 17:00:00,10,12.88238455173535,21.239447448428763,66.36688105282118 +2024-10-17 18:00:00,10,19.058733921982192,18.266820237526005,48.44567193342575 +2024-10-17 19:00:00,10,17.59918183212846,19.6607332532098,50.61636716940537 +2024-10-17 20:00:00,10,38.810170606808036,26.171472982095104,43.87151197988053 +2024-10-17 21:00:00,10,7.804805925779949,23.308303583573487,55.38383059946496 +2024-10-17 22:00:00,10,9.69975271283124,19.039195486828195,54.32876508443288 +2024-10-17 23:00:00,10,20.625637084882165,30.75697182623655,59.02935663892 +2024-10-18 00:00:00,10,25.94731629116933,29.553238368722127,51.22539679699513 +2024-10-18 01:00:00,10,15.07819094657893,27.630390160684208,57.2973882970573 +2024-10-18 02:00:00,10,23.407970992335073,28.243624520170204,54.33634413540285 +2024-10-18 03:00:00,10,12.837093116981725,27.593604009500933,71.85839955313807 +2024-10-18 04:00:00,10,20.583649325785927,34.779649821741764,61.677444997877224 +2024-10-18 05:00:00,10,27.630206307542526,26.14221033824964,46.76083228908812 +2024-10-18 06:00:00,10,12.498529986345034,27.848778230788596,56.01730280630657 +2024-10-18 07:00:00,10,22.29218034053222,22.174890911282215,52.633291437786674 +2024-10-18 08:00:00,10,26.055214679262505,27.516147976302406,52.93007629113412 +2024-10-18 09:00:00,10,5.605846949631619,18.81362192384643,70.73919053757926 +2024-10-18 10:00:00,10,15.079212429630097,20.34160966592718,74.78059621535196 +2024-10-18 11:00:00,10,17.901824194766345,21.34984015447075,66.25519809654541 +2024-10-18 12:00:00,10,37.99652985364247,23.234026026010998,51.75122317118439 +2024-10-18 13:00:00,10,30.321619653181166,23.576938860750225,70.6668148470995 +2024-10-18 14:00:00,10,42.43412322252496,23.726379756136666,60.09567244061642 +2024-10-18 15:00:00,10,29.143437226873317,18.28052193343511,67.5039826564904 +2024-10-18 16:00:00,10,32.17583598610231,25.480753644371593,48.18994253969267 +2024-10-18 17:00:00,10,29.41006335808303,22.762903077994093,50.000453284823294 +2024-10-18 18:00:00,10,16.924495712280976,30.77665173285486,47.640747208142315 +2024-10-18 19:00:00,10,22.39378247652056,20.530399610007,58.73287420655399 +2024-10-18 20:00:00,10,19.29324973176832,30.569686676788194,60.58388377488113 +2024-10-18 21:00:00,10,8.223841094431263,33.33594327069395,33.66072315989169 +2024-10-18 22:00:00,10,17.296794867747458,24.35319636443772,54.700914472332975 +2024-10-18 23:00:00,10,0.8299203679223055,27.623465485424227,53.37633392364229 +2024-10-19 00:00:00,10,18.08580205057008,26.266776690846257,76.57341563244434 +2024-10-19 01:00:00,10,17.636792819103942,31.888222723781816,60.224492725290496 +2024-10-19 02:00:00,10,34.9663445656427,22.963327380570743,46.30501661473029 +2024-10-19 03:00:00,10,17.202469923936356,23.89682364833095,55.67080853280785 +2024-10-19 04:00:00,10,25.325266903573343,33.10411110930487,45.21431853727797 +2024-10-19 05:00:00,10,29.01621872134107,21.55183271932982,57.91843620270113 +2024-10-19 06:00:00,10,30.48242256462561,22.261488112080475,62.15532827863259 +2024-10-19 07:00:00,10,32.27987072877508,29.00758043066751,49.86087796859003 +2024-10-19 08:00:00,10,16.8547346076355,28.068655689917094,64.46398510089011 +2024-10-19 09:00:00,10,34.77765054502247,18.99337815843611,62.15601067178909 +2024-10-19 10:00:00,10,25.083836242399823,21.90416227436471,61.3526027907932 +2024-10-19 11:00:00,10,27.11111111256605,20.189430090427336,54.04293002694411 +2024-10-19 12:00:00,10,17.350191200748228,21.670586884122073,67.47980326475741 +2024-10-19 13:00:00,10,22.131285121303115,21.074896235459107,68.43234695575455 +2024-10-19 14:00:00,10,27.115004298466882,26.149927674776922,51.87924738801286 +2024-10-19 15:00:00,10,15.733531475371066,17.011652424681404,59.324985980804485 +2024-10-19 16:00:00,10,26.36598940805313,19.976554117321424,54.33143529880228 +2024-10-19 17:00:00,10,28.086939512776873,21.06835099200731,63.64174803689234 +2024-10-19 18:00:00,10,25.55602229221646,21.302294280960723,55.47988110241262 +2024-10-19 19:00:00,10,34.7088715135452,26.181229586554082,30.29826767537398 +2024-10-19 20:00:00,10,11.31970815806514,23.59013607596617,50.096391591101415 +2024-10-19 21:00:00,10,26.871336261171987,25.302507726645878,56.181176805460794 +2024-10-19 22:00:00,10,24.934020243744065,22.38868666636859,53.38643797354714 +2024-10-19 23:00:00,10,19.354878581479184,16.7513589424839,52.769502190755865 +2024-10-20 00:00:00,10,24.18253753570216,27.069596181091832,53.40168751222649 +2024-10-20 01:00:00,10,22.303026752918246,24.914150814933958,56.57517530167456 +2024-10-20 02:00:00,10,31.05848061669679,27.50305394707111,54.28439543498725 +2024-10-20 03:00:00,10,22.316311469899972,24.692187456396496,54.62339888928501 +2024-10-20 04:00:00,10,18.790112049652784,26.051711932186954,58.75576039827507 +2024-10-20 05:00:00,10,31.431457027788085,23.485453002678835,59.19936431315601 +2024-10-20 06:00:00,10,14.32586630651085,26.79639484472139,54.46007761072118 +2024-10-20 07:00:00,10,30.558651895633812,24.755865082761947,61.93618380855378 +2024-10-20 08:00:00,10,26.16723191881774,29.350749481695726,40.10149859183514 +2024-10-20 09:00:00,10,23.236915807964678,19.832009634716517,79.57053981666705 +2024-10-20 10:00:00,10,42.84483058262644,30.83469455645932,44.426830493835865 +2024-10-20 11:00:00,10,39.59018943187458,24.48617140585852,60.458781847147584 +2024-10-20 12:00:00,10,22.346536753915167,14.577293606858554,51.29690427218961 +2024-10-20 13:00:00,10,41.01883838488453,22.698105625384464,51.079509092524546 +2024-10-20 14:00:00,10,31.2836179126098,22.37525722104699,66.28025161702625 +2024-10-20 15:00:00,10,25.261142786643532,29.902527797466767,49.32122935018908 +2024-10-20 16:00:00,10,29.202804659585645,24.61181942292765,63.667076291566865 +2024-10-20 17:00:00,10,15.863447744693575,23.64635123817295,64.43248733067206 +2024-10-20 18:00:00,10,39.798807176512895,33.75931435208136,66.29906389733551 +2024-10-20 19:00:00,10,19.661949495196446,27.452511184536533,65.3867932383977 +2024-10-20 20:00:00,10,22.29185427178526,24.24744764155038,38.095989218024734 +2024-10-20 21:00:00,10,24.315676771003275,27.575266935554833,45.670434590827355 +2024-10-20 22:00:00,10,6.931589819319713,21.292165067398532,45.5712800040155 +2024-10-20 23:00:00,10,9.217544483003921,27.33402441837312,51.928029659364725 +2024-10-21 00:00:00,10,15.93362484600657,30.036571482670666,53.31435377727017 +2024-10-21 01:00:00,10,30.21637868367621,28.587806138584483,62.20751857421594 +2024-10-21 02:00:00,10,18.64378405903028,23.44263376310385,47.09955617237277 +2024-10-21 03:00:00,10,24.37933722051284,27.536425076199688,51.86938265493097 +2024-10-21 04:00:00,10,26.272238837934268,26.246597419548035,73.88754641053478 +2024-10-21 05:00:00,10,11.673621850430798,26.15279610434825,61.77734153523758 +2024-10-21 06:00:00,10,27.53300630495947,29.997539351589367,47.96902039994449 +2024-10-21 07:00:00,10,28.17972294438534,26.038474555248722,70.02325412377695 +2024-10-21 08:00:00,10,25.118762870908096,19.744703586400384,48.643145804951146 +2024-10-21 09:00:00,10,27.886610240400476,23.81902115029528,68.20199348807935 +2024-10-21 10:00:00,10,21.860053666204095,20.241162588919416,49.57732725961722 +2024-10-21 11:00:00,10,18.967738509838725,21.379420443181374,57.56027387635215 +2024-10-21 12:00:00,10,24.87644404531208,21.64041710167787,46.752129154657 +2024-10-21 13:00:00,10,25.854490872563638,23.067627266209666,60.972877900725074 +2024-10-21 14:00:00,10,8.282222852965926,24.907883907215048,58.525019446375296 +2024-10-21 15:00:00,10,14.068439060821758,24.441276618933422,55.1554998125392 +2024-10-21 16:00:00,10,13.352936433231633,24.113292386060763,72.45538012982314 +2024-10-21 17:00:00,10,19.805494652643876,21.7495727034058,50.460161560665235 +2024-10-21 18:00:00,10,9.002034020589784,19.582951950223208,47.418330277870176 +2024-10-21 19:00:00,10,17.38278039270928,21.932770338450055,51.925906736052646 +2024-10-21 20:00:00,10,32.34684298634725,25.84732028424673,41.07535569273883 +2024-10-21 21:00:00,10,7.949332877430498,23.35981291634225,48.294271286075286 +2024-10-21 22:00:00,10,8.554033335255845,26.423984352006368,54.10707671556457 +2024-10-21 23:00:00,10,39.03916089690411,20.226012427919258,49.54629775468168 +2024-10-22 00:00:00,10,29.83502554453495,28.330084327451413,62.779887565177546 +2024-10-22 01:00:00,10,21.76145584356954,25.35070486435452,57.331421321617725 +2024-10-22 02:00:00,10,38.42071943876634,24.294671184998673,61.0050128355581 +2024-10-22 03:00:00,10,19.460185296004337,24.752729469792435,62.919123151618464 +2024-10-22 04:00:00,10,40.74129055409642,26.632660785972554,59.46706764573977 +2024-10-22 05:00:00,10,28.36567550453559,23.484583516772673,55.730959549691335 +2024-10-22 06:00:00,10,30.64379278696658,24.749963483105866,48.827843624062666 +2024-10-22 07:00:00,10,34.44580982449244,32.00342455500275,58.9664303627967 +2024-10-22 08:00:00,10,38.92660800312522,28.152116027738863,47.97661414870052 +2024-10-22 09:00:00,10,22.440724264543572,25.120516300134785,74.08202422756263 +2024-10-22 10:00:00,10,29.43632477549015,21.883871781427267,47.29877724103439 +2024-10-22 11:00:00,10,0.0,23.338846923128337,54.272987592121666 +2024-10-22 12:00:00,10,24.619497719206016,23.790563831249916,63.94444614732627 +2024-10-22 13:00:00,10,18.37044845744629,24.44545918958795,46.30474707847458 +2024-10-22 14:00:00,10,14.925157159302826,21.33265763803619,47.48957827490187 +2024-10-22 15:00:00,10,25.770723586552762,23.636060913126784,52.18289114398853 +2024-10-22 16:00:00,10,28.573197994625627,17.032069126013365,48.03692306762133 +2024-10-22 17:00:00,10,36.72441004104738,20.187194834090977,46.242057243546185 +2024-10-22 18:00:00,10,20.86233426612551,21.588099810158894,69.00537202486551 +2024-10-22 19:00:00,10,16.730404199311135,24.553517916387655,63.03860542727257 +2024-10-22 20:00:00,10,16.014024240287707,29.099687339535578,45.909250915762755 +2024-10-22 21:00:00,10,24.2922560406914,24.381786019625384,52.96870295595467 +2024-10-22 22:00:00,10,18.64971328786074,25.66104375213901,56.99966193510927 +2024-10-22 23:00:00,10,21.053564452558778,21.614376445828167,37.86764714232725 +2024-10-23 00:00:00,10,18.2402026210617,25.365255818465766,57.30374783379448 +2024-10-23 01:00:00,10,25.241523593814684,27.369610354509295,57.946000644733104 +2024-10-23 02:00:00,10,5.301769372925801,26.866070012596307,60.980068360203305 +2024-10-23 03:00:00,10,44.69682452577743,27.480089640642994,50.942807798750366 +2024-10-23 04:00:00,10,26.293855589880454,27.669501987160146,54.57227946794847 +2024-10-23 05:00:00,10,19.920848952573056,29.381864624325864,51.62136086861507 +2024-10-23 06:00:00,10,19.21103080710176,27.828391798476986,54.54172915699327 +2024-10-23 07:00:00,10,24.635263557390093,23.618514065332256,64.23276926848541 +2024-10-23 08:00:00,10,26.094727570208725,24.499394779230187,69.08675642446038 +2024-10-23 09:00:00,10,14.889503697595643,25.30813079104897,70.19825377918369 +2024-10-23 10:00:00,10,41.22328532167715,22.09451438001974,58.87134123644921 +2024-10-23 11:00:00,10,31.016593929607794,27.388763047799454,63.89078456768505 +2024-10-23 12:00:00,10,11.632414772309733,27.288099340694483,64.21805193139747 +2024-10-23 13:00:00,10,20.277107660153295,26.749370706316917,71.02852414131434 +2024-10-23 14:00:00,10,22.34249823887968,20.483468646225013,61.6551958494802 +2024-10-23 15:00:00,10,20.897151718730935,24.82688805669251,43.13829782918647 +2024-10-23 16:00:00,10,30.22993665097709,21.774683289527243,60.26209257793232 +2024-10-23 17:00:00,10,6.416278721639575,27.6635433524645,73.09934321828398 +2024-10-23 18:00:00,10,4.098947366256375,21.631926245894036,39.821535677403745 +2024-10-23 19:00:00,10,16.80896062605565,28.091360697280738,64.84133253059267 +2024-10-23 20:00:00,10,14.3607235955612,20.876370503873964,40.27941748574361 +2024-10-23 21:00:00,10,33.437186994059914,22.290529580653804,55.13339547390487 +2024-10-23 22:00:00,10,8.944036643506085,26.152241813999762,61.532535567106976 +2024-10-23 23:00:00,10,20.393891499002642,25.6115353163451,45.544738443752856 +2024-10-24 00:00:00,10,16.44779170629221,32.47355143472578,58.289028354647414 +2024-10-24 01:00:00,10,18.1461977232067,27.749100820453048,48.156444178717656 +2024-10-24 02:00:00,10,9.795033430559902,25.99649148582865,50.78231363344613 +2024-10-24 03:00:00,10,15.905267651921358,23.812099537882712,41.089096557650514 +2024-10-24 04:00:00,10,13.233295988361194,23.14177511195662,64.39827506142046 +2024-10-24 05:00:00,10,15.456726347523032,25.606070261536207,56.339450829789456 +2024-10-24 06:00:00,10,21.25699764437278,28.197162491148575,75.92193042360907 +2024-10-24 07:00:00,10,6.76647135220653,32.01635939841415,59.63828555365918 +2024-10-24 08:00:00,10,23.722155478473685,15.136521144437506,59.517509073617674 +2024-10-24 09:00:00,10,27.70525946846525,21.377128866941945,67.51955221630077 +2024-10-24 10:00:00,10,21.208426385569187,27.68386709682507,56.17239164167846 +2024-10-24 11:00:00,10,24.78161657190348,24.34777136612928,45.19298363713305 +2024-10-24 12:00:00,10,23.20583189943466,21.917824477192447,46.4892477951738 +2024-10-24 13:00:00,10,13.258446693747267,24.233529310562723,66.41863251332961 +2024-10-24 14:00:00,10,39.59031187291458,25.465937952410933,48.43023659617971 +2024-10-24 15:00:00,10,26.00612235464184,22.38651925004468,59.49827550146501 +2024-10-24 16:00:00,10,21.742551033759476,22.445736703229155,64.40480497621691 +2024-10-24 17:00:00,10,19.26333233565625,17.95242805966653,55.91904598700438 +2024-10-24 18:00:00,10,16.32127654870839,18.905924328436924,57.082164103394334 +2024-10-24 19:00:00,10,35.678249822347695,19.831942513542245,60.09932844936883 +2024-10-24 20:00:00,10,17.997384484632544,23.124490137891193,61.03255233308469 +2024-10-24 21:00:00,10,19.293663089954478,24.025737862166032,48.24124183180641 +2024-10-24 22:00:00,10,29.943652317829923,24.719500484695974,66.84678712033926 +2024-10-24 23:00:00,10,30.471961543832627,24.336074722621067,55.62382595226457 +2024-10-25 00:00:00,10,31.207805958902213,28.698399999939177,70.01918973691578 +2024-10-25 01:00:00,10,34.30175741980724,28.753864582006315,52.841422339441586 +2024-10-25 02:00:00,10,24.328315146990715,29.151005011119665,62.00316035323227 +2024-10-25 03:00:00,10,13.582402577375529,27.85463463182305,56.44162865038795 +2024-10-25 04:00:00,10,16.10754072793724,26.06114648170047,31.297224180157414 +2024-10-25 05:00:00,10,33.76680718812842,28.305500259507593,49.45046509472479 +2024-10-25 06:00:00,10,23.705012608301892,22.714869953185858,82.78072165007907 +2024-10-25 07:00:00,10,8.011572282279383,26.60060004545474,59.278720669143944 +2024-10-25 08:00:00,10,5.842606018568784,30.599575649735552,53.482256809980484 +2024-10-25 09:00:00,10,31.582399935592925,21.91959469484283,71.83401214774386 +2024-10-25 10:00:00,10,12.980246461526107,12.817612219556787,55.888237468197815 +2024-10-25 11:00:00,10,19.0609929271885,24.908978905236854,40.50831088213161 +2024-10-25 12:00:00,10,37.8885840839457,17.31255993673947,49.43255547520623 +2024-10-25 13:00:00,10,37.002028454644375,23.003138943581256,66.28134037153274 +2024-10-25 14:00:00,10,37.557354109716,24.906498055954543,53.42940693030524 +2024-10-25 15:00:00,10,15.598546081329273,27.24000685994538,57.906254286696466 +2024-10-25 16:00:00,10,10.303965712035827,18.268569371082044,49.49761156478293 +2024-10-25 17:00:00,10,19.635777348226696,25.215846436357975,54.628005008753526 +2024-10-25 18:00:00,10,19.889304649949306,24.27113956670611,67.07970515594273 +2024-10-25 19:00:00,10,16.775143876342227,24.139451747539614,52.81179362350159 +2024-10-25 20:00:00,10,22.17868148235522,21.78575983509496,72.85919402504699 +2024-10-25 21:00:00,10,17.213841069427648,25.151141330411104,51.53786948868028 +2024-10-25 22:00:00,10,10.968644855088192,23.135778254829916,60.6073354726206 +2024-10-25 23:00:00,10,30.38369664543408,17.652029918716465,55.253723092882474 +2024-10-26 00:00:00,10,32.45089039692475,24.23500303776825,58.40728370050403 +2024-10-26 01:00:00,10,26.596887739111594,29.984697282824012,60.33379628504386 +2024-10-26 02:00:00,10,40.744501073483306,27.630209744356364,52.148534149438 +2024-10-26 03:00:00,10,20.56477217422179,24.399679905231412,52.699076670756675 +2024-10-26 04:00:00,10,15.884599136174469,24.593905527699896,63.52258422754389 +2024-10-26 05:00:00,10,34.77407254119299,27.43649408220506,60.71512403887842 +2024-10-26 06:00:00,10,20.68786248653201,19.728241656664174,52.0993249031941 +2024-10-26 07:00:00,10,18.799253356839614,29.766763113102495,63.367192262213976 +2024-10-26 08:00:00,10,26.46093307016891,29.6305651583166,67.91052599939958 +2024-10-26 09:00:00,10,26.75766774363101,24.18533457250417,66.93025308076942 +2024-10-26 10:00:00,10,20.7570023565298,24.589060637647894,51.7433609510817 +2024-10-26 11:00:00,10,40.02511104535061,22.92787526174365,75.11700850027268 +2024-10-26 12:00:00,10,9.78055006329285,22.785231837040616,40.80863217293013 +2024-10-26 13:00:00,10,20.83459443371178,22.77818983368002,48.62680505062768 +2024-10-26 14:00:00,10,23.670097577754476,23.925428970640322,57.89364647468753 +2024-10-26 15:00:00,10,18.700650673561377,20.49881625163595,63.67665500042951 +2024-10-26 16:00:00,10,36.78015352009679,20.70972190669645,31.725749602790113 +2024-10-26 17:00:00,10,20.77561355368685,20.43484278528949,65.01918656551914 +2024-10-26 18:00:00,10,25.372607687960727,25.177973025243077,67.58541371213246 +2024-10-26 19:00:00,10,19.242127409436215,22.026605154701606,70.13220574306999 +2024-10-26 20:00:00,10,20.625315990086637,24.290747473335422,59.894174700993645 +2024-10-26 21:00:00,10,20.403904108102417,23.97285387907135,52.80406411469639 +2024-10-26 22:00:00,10,40.12632325325159,24.294488811954253,59.965482833933805 +2024-10-26 23:00:00,10,7.543633175880766,26.066735507916018,61.7317513085418 +2024-10-27 00:00:00,10,27.04035541416112,24.936410987376153,64.82407038140211 +2024-10-27 01:00:00,10,9.331269154531295,23.885735725080078,56.63660292137058 +2024-10-27 02:00:00,10,22.00265124352933,24.374011273822017,66.46100883849589 +2024-10-27 03:00:00,10,28.138416296708424,29.307468044720892,59.360218647044455 +2024-10-27 04:00:00,10,42.608790621075656,26.176736786454022,32.84260502199393 +2024-10-27 05:00:00,10,39.51629348001031,26.88885246390511,58.79900396973787 +2024-10-27 06:00:00,10,13.322093403627974,23.027149371872945,46.354489342440644 +2024-10-27 07:00:00,10,27.0604402757463,23.70150614326,50.4755552340724 +2024-10-27 08:00:00,10,3.145391268337818,24.862551779890413,54.291304594252466 +2024-10-27 09:00:00,10,28.467190944174078,22.176214566449303,59.75696096972334 +2024-10-27 10:00:00,10,20.056297012242023,25.145567298111075,62.4936623589771 +2024-10-27 11:00:00,10,21.16826192665428,21.236836168770086,58.31993124365899 +2024-10-27 12:00:00,10,32.46714296281977,16.924408468001722,63.551136180796846 +2024-10-27 13:00:00,10,33.95020338488209,22.382543470541062,50.61062107467039 +2024-10-27 14:00:00,10,20.627614369616662,24.03552095478543,56.066218388154404 +2024-10-27 15:00:00,10,30.927939345570504,27.01414259289076,53.21514799317352 +2024-10-27 16:00:00,10,25.839875829540745,21.819033272841125,50.140722795781706 +2024-10-27 17:00:00,10,27.09257550809839,20.93096910644813,48.737412566846494 +2024-10-27 18:00:00,10,21.26506037902602,23.22657605459575,39.98169848946671 +2024-10-27 19:00:00,10,24.57073509740883,21.12939697621935,59.542479918036946 +2024-10-27 20:00:00,10,32.39592943905305,22.205518537940108,40.179094012233904 +2024-10-27 21:00:00,10,14.60547495182503,21.300649064026917,53.21363415321595 +2024-10-27 22:00:00,10,22.483898606696904,26.993852267215217,50.0535094462303 +2024-10-27 23:00:00,10,27.467473656719427,19.487519726653563,41.32210902707433 +2024-10-28 00:00:00,10,36.54910744180197,27.812913861717828,55.79890285669008 +2024-10-28 01:00:00,10,33.619310763746654,24.9142014485008,67.416093607951 +2024-10-28 02:00:00,10,20.41283586273344,27.95907565763467,51.16730747756752 +2024-10-28 03:00:00,10,22.91263676917895,31.062478874395133,69.74683921280666 +2024-10-28 04:00:00,10,14.874734562683134,27.819107808720812,62.13191609206968 +2024-10-28 05:00:00,10,19.84982553418663,27.680390317829676,47.37028903210726 +2024-10-28 06:00:00,10,17.89393969023158,27.346719844326785,54.97664742559211 +2024-10-28 07:00:00,10,32.371708425452645,19.10612200200574,60.63647011062406 +2024-10-28 08:00:00,10,34.15712873818415,21.408287761514785,68.98633761017933 +2024-10-28 09:00:00,10,5.858511228489242,26.448663527924477,66.3160408271454 +2024-10-28 10:00:00,10,30.79621887671409,25.85135244573292,65.69706367791767 +2024-10-28 11:00:00,10,24.91620095687897,22.75595896056125,61.10777207507817 +2024-10-28 12:00:00,10,10.003066635988239,22.341342240046835,51.01329394489696 +2024-10-28 13:00:00,10,8.734848130111194,22.988921913045527,47.54723870834476 +2024-10-28 14:00:00,10,12.529655751408995,23.835050725562958,64.80316552945104 +2024-10-28 15:00:00,10,18.624799017890194,21.487769992001112,49.545431538516034 +2024-10-28 16:00:00,10,35.92170961831613,25.11486920331081,62.24302837382954 +2024-10-28 17:00:00,10,10.25750838192381,26.10308094394865,54.37930934716802 +2024-10-28 18:00:00,10,37.71653202191878,18.52930847824866,48.643091521683324 +2024-10-28 19:00:00,10,26.13027640110283,24.40781455460255,49.99435413750169 +2024-10-28 20:00:00,10,19.774779107099363,22.628853033391163,54.677915962968505 +2024-10-28 21:00:00,10,17.36600581929165,20.833131387386768,42.58567174172815 +2024-10-28 22:00:00,10,8.6476033363406,26.992777524253874,47.26001389100469 +2024-10-28 23:00:00,10,11.876441775616579,20.734596254258197,37.93686093037688 +2024-10-29 00:00:00,10,42.61180584248868,26.57338521803845,53.84485893288105 +2024-10-29 01:00:00,10,36.21090491031638,23.940529319700932,55.135561030105514 +2024-10-29 02:00:00,10,34.54252160286659,30.758273160269752,62.18120467801282 +2024-10-29 03:00:00,10,26.644697070978232,27.41590184903078,58.64202352910185 +2024-10-29 04:00:00,10,36.122859832834884,29.061965935205002,43.64118544403985 +2024-10-29 05:00:00,10,26.245988085234245,25.99593936872058,57.421693650814326 +2024-10-29 06:00:00,10,17.657062717522535,26.417024765423072,51.33109166635052 +2024-10-29 07:00:00,10,31.744172363358885,20.739819484721053,45.46317862113225 +2024-10-29 08:00:00,10,18.067739925087913,24.546615381188154,69.89541002499313 +2024-10-29 09:00:00,10,21.57806091741689,25.638125202976035,48.52360792519943 +2024-10-29 10:00:00,10,43.14148773976487,22.038932031749177,69.23973787846666 +2024-10-29 11:00:00,10,22.55620989306235,15.551955312144145,45.58146337733536 +2024-10-29 12:00:00,10,27.291363108445587,27.834290451405572,81.51808681829696 +2024-10-29 13:00:00,10,28.804552458201506,17.168082946136824,68.92770569153481 +2024-10-29 14:00:00,10,41.73076083852552,20.158614412044866,70.20802581316467 +2024-10-29 15:00:00,10,30.73707963193748,22.685871208856206,73.69095058068405 +2024-10-29 16:00:00,10,29.79527226550563,17.868835977838867,58.35765733470156 +2024-10-29 17:00:00,10,31.142163993709755,21.733584451836116,48.96431111348282 +2024-10-29 18:00:00,10,19.116683566311572,24.305513908850337,29.507561274156906 +2024-10-29 19:00:00,10,12.94565383210528,26.495853343043514,49.723779658426174 +2024-10-29 20:00:00,10,22.314704102812293,24.77533934696255,63.167784889654726 +2024-10-29 21:00:00,10,22.420015531601674,20.131952913996408,50.59262085934775 +2024-10-29 22:00:00,10,22.680952622660303,22.965193573240875,57.03442064492029 +2024-10-29 23:00:00,10,26.139972746263766,23.906686993921483,44.65851235763271 +2024-10-30 00:00:00,10,25.263883793858245,24.438369553492507,58.47240619214569 +2024-10-30 01:00:00,10,26.38057349625804,24.51651999992602,61.00237959489587 +2024-10-30 02:00:00,10,31.550694917910796,23.30014211672973,58.260882731033824 +2024-10-30 03:00:00,10,31.161776985142826,28.518416906499453,42.343967940202155 +2024-10-30 04:00:00,10,34.655589602061255,26.852077388818994,55.6598331074818 +2024-10-30 05:00:00,10,28.05121602326739,25.64208626163741,83.37438167450192 +2024-10-30 06:00:00,10,20.121539199413313,24.59447758587014,37.42696361260612 +2024-10-30 07:00:00,10,28.27143961353253,26.683373349707114,49.2025393929351 +2024-10-30 08:00:00,10,22.341726867462533,22.651717936798217,52.330813770380196 +2024-10-30 09:00:00,10,30.273549500877603,22.77182827882811,69.99627276278795 +2024-10-30 10:00:00,10,20.488006879624322,23.09300795812088,61.01256228418148 +2024-10-30 11:00:00,10,25.53149582811023,20.11387309193559,69.99283433199707 +2024-10-30 12:00:00,10,29.583406593856743,22.721194756385618,47.04869809456631 +2024-10-30 13:00:00,10,22.177442791648296,19.04561161350806,48.16772218821794 +2024-10-30 14:00:00,10,0.0,19.95605907740699,48.51248505067124 +2024-10-30 15:00:00,10,17.14930182145206,25.11286803392357,74.89742619152562 +2024-10-30 16:00:00,10,24.977960797891075,23.911916777586327,57.68343813426212 +2024-10-30 17:00:00,10,32.52121558351192,20.80069344586327,61.94354257168202 +2024-10-30 18:00:00,10,31.594979055083456,22.93593849094042,66.44484520916001 +2024-10-30 19:00:00,10,31.812849891632055,18.837627894179043,41.70362274699028 +2024-10-30 20:00:00,10,21.916960816290985,27.97655895508121,65.03400138171479 +2024-10-30 21:00:00,10,26.483057764837486,22.002968544460956,44.333896831756064 +2024-10-30 22:00:00,10,18.17675244860708,24.83296365924852,53.70532506764497 +2024-10-30 23:00:00,10,12.325957461722593,27.626925072858064,45.20005354184129 +2024-10-31 00:00:00,10,17.446923228079427,23.120586097791254,53.11274214461011 +2024-10-31 01:00:00,10,17.304139917569643,31.526843609829534,54.62983350926431 +2024-10-31 02:00:00,10,27.564307070542963,28.946457562442955,58.15533572134672 +2024-10-31 03:00:00,10,46.75605088105458,22.880602061399447,58.89164217546795 +2024-10-31 04:00:00,10,20.54185042069733,27.245531252137052,47.550326940557454 +2024-10-31 05:00:00,10,22.495722124987743,29.30896952983895,64.14903266901095 +2024-10-31 06:00:00,10,28.226563660198998,23.94609955501101,75.55351246747166 +2024-10-31 07:00:00,10,18.6303457606926,27.83242677792125,50.4604531522337 +2024-10-31 08:00:00,10,19.08724418577578,26.597471625345616,32.21404828643871 +2024-10-31 09:00:00,10,11.628302279652406,25.828082738376242,60.805021345522235 +2024-10-31 10:00:00,10,13.428381792928706,17.4580717064161,62.41320185886963 +2024-10-31 11:00:00,10,14.577405317652595,22.18043176742305,62.21535672016598 +2024-10-31 12:00:00,10,14.564457267471449,21.634188872060292,73.03588249626637 +2024-10-31 13:00:00,10,35.01429156604908,20.565020424240657,70.94044782912822 +2024-10-31 14:00:00,10,25.90213724977406,21.408108908013872,52.19431377064262 +2024-10-31 15:00:00,10,21.857446949614193,23.56286316357842,45.68812172061673 +2024-10-31 16:00:00,10,23.91377054529001,23.034518104468972,62.216167328286915 +2024-10-31 17:00:00,10,21.05249174940377,28.986887737813408,50.273455908477374 +2024-10-31 18:00:00,10,3.3641999434520287,21.399569635824932,45.53579845722638 +2024-10-31 19:00:00,10,26.162567740611124,22.05547620918294,38.83094294059452 +2024-10-31 20:00:00,10,15.322035642834612,18.82728179383845,55.161491723955926 +2024-10-31 21:00:00,10,13.887780367728014,26.766656916962148,40.938954986565264 +2024-10-31 22:00:00,10,26.65572915282622,26.301235312386073,47.61316987972799 +2024-10-31 23:00:00,10,25.748724069925736,25.345900008866238,58.089627329484344 +2024-11-01 00:00:00,10,23.319355212239138,24.935089146963318,44.05527585430887 +2024-11-01 01:00:00,10,33.577306534277476,26.52889220125927,48.51053920200247 +2024-11-01 02:00:00,10,31.599197367427784,28.930548504202036,56.321198291736195 +2024-11-01 03:00:00,10,36.91430844035923,24.61435564460936,57.682863420312266 +2024-11-01 04:00:00,10,12.027702032469646,27.098116560950164,51.05748908771864 +2024-11-01 05:00:00,10,25.667070943547454,26.334233803141153,45.86997230219115 +2024-11-01 06:00:00,10,28.357103100518003,23.97854098681762,66.90358998949704 +2024-11-01 07:00:00,10,5.947347824586547,18.22721841758714,62.4509080452358 +2024-11-01 08:00:00,10,38.30938262807355,22.511992760837728,62.88628545178257 +2024-11-01 09:00:00,10,31.25440088725547,25.63341344649897,63.43637435568423 +2024-11-01 10:00:00,10,29.138030000088722,15.948065192150388,50.99561528888679 +2024-11-01 11:00:00,10,31.210003910931338,21.959500361398923,70.159123714949 +2024-11-01 12:00:00,10,27.380401467844838,28.911902819449868,51.61837815131872 +2024-11-01 13:00:00,10,28.49726225047911,22.85677262990505,74.43993060452709 +2024-11-01 14:00:00,10,39.528976857653426,23.18610059547493,66.94579186808633 +2024-11-01 15:00:00,10,17.125577276499342,23.001181506930042,51.77334765302435 +2024-11-01 16:00:00,10,23.92189616931598,25.073962267164866,65.52715322368552 +2024-11-01 17:00:00,10,27.63301940074435,21.59454481908961,49.2891998608025 +2024-11-01 18:00:00,10,22.142478488542093,19.721366553117235,34.13801214467517 +2024-11-01 19:00:00,10,27.542014366400615,24.81878322082048,51.44465045883269 +2024-11-01 20:00:00,10,26.114222170153553,19.764185675818418,55.64307405483781 +2024-11-01 21:00:00,10,25.81667197976654,19.757266553663996,69.3101567988544 +2024-11-01 22:00:00,10,37.55253951623312,23.270796468881503,52.17795553341093 +2024-11-01 23:00:00,10,30.462140046100245,21.677219847567667,52.7276276667863 +2024-11-02 00:00:00,10,39.203805670451416,27.922222715803038,52.02917893854069 +2024-11-02 01:00:00,10,22.420909598342284,26.878192710407276,54.776376063597304 +2024-11-02 02:00:00,10,22.656571164792446,26.80296309042541,75.98046460014712 +2024-11-02 03:00:00,10,21.51033397656976,33.84851210905514,29.574737878809287 +2024-11-02 04:00:00,10,30.120544664145363,25.00973090352314,48.429435091796236 diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/profiles_runtime.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/profiles_runtime.py new file mode 100644 index 000000000..36dfcef67 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/profiles_runtime.py @@ -0,0 +1,218 @@ +from pathlib import Path +from typing import Dict, Optional +import numpy as np +import pandas as pd +BASE_DIR = Path(__file__).resolve().parent +MODELS_DIR = BASE_DIR / "reports" / "models" +MODELS_DIR.mkdir(parents=True, exist_ok=True) + +SENSORS = ["Soil_Moisture", "Ambient_Temperature", "Humidity"] + +# ---------- Helpers ---------- +def _safe_interp(series: pd.Series, full_index): + s = series.reindex(full_index) + return s.interpolate(limit_direction="both") + +# ---------- Export (train) ---------- +def export_profiles(df: pd.DataFrame) -> None: + if "Plant_ID" not in df.columns or "Timestamp" not in df.columns: + raise ValueError("DataFrame must contain 'Plant_ID' and 'Timestamp'") + df = df.dropna(subset=["Timestamp"]).sort_values("Timestamp").copy() + + for plant in df["Plant_ID"].dropna().unique(): + d0 = df[df["Plant_ID"] == plant] + for col in SENSORS: + if col not in d0.columns: + continue + d = d0[["Timestamp", col]].dropna().copy() + if d.empty: + continue + + # ----- daily ----- + x = d.copy() + x["hod"] = x["Timestamp"].dt.hour + med_hod = _safe_interp(x.groupby("hod")[col].median(), range(24)) + std_hod = x.groupby("hod")[col].std().reindex(range(24)) + std_hod = std_hod.fillna(std_hod.mean() if not np.isnan(std_hod.mean()) else 0.0) + out = pd.DataFrame({"hod": range(24), "median": med_hod.values, "std": std_hod.values}) + (MODELS_DIR / f"daily_{plant}_{col}.csv").write_text(out.to_csv(index=False), encoding="utf-8") + + # ----- weekly ----- + if len(d) >= 24 * 7: + xw = d.copy() + xw["dow"] = xw["Timestamp"].dt.dayofweek + xw["hod"] = xw["Timestamp"].dt.hour + med_week = xw.groupby(["dow", "hod"])[col].median().unstack() + med_week = med_week.reindex(index=range(7), columns=range(24)) + med_week = med_week.apply(lambda s: s.interpolate(limit_direction="both"), axis=1)\ + .interpolate(axis=0, limit_direction="both") + + std_week = xw.groupby(["dow", "hod"])[col].std().unstack() + std_week = std_week.reindex(index=range(7), columns=range(24)) + fill_val = std_week.stack().mean() if std_week.stack().notna().any() else 0.0 + std_week = std_week.fillna(fill_val) + + med_week.reset_index(inplace=True) + std_week.reset_index(inplace=True) + med_week = med_week.melt(id_vars="dow", var_name="hod", value_name="median").sort_values(["dow", "hod"]) + std_week = std_week.melt(id_vars="dow", var_name="hod", value_name="std").sort_values(["dow", "hod"]) + ww = pd.merge(med_week, std_week, on=["dow","hod"], how="inner") + (MODELS_DIR / f"weekly_{plant}_{col}.csv").write_text(ww.to_csv(index=False), encoding="utf-8") + + # ----- seasonal ----- + if d["Timestamp"].dt.year.nunique() >= 2 or len(d) >= 24 * 60: + xs = d.copy() + xs["doy"] = xs["Timestamp"].dt.dayofyear + xs["hod"] = xs["Timestamp"].dt.hour + med_doy = _safe_interp(xs.groupby("doy")[col].median(), range(1, 367)) + med_hod2 = _safe_interp(xs.groupby("hod")[col].median(), range(24)) + std_h = xs.groupby("hod")[col].std().reindex(range(24)) + std_h = std_h.fillna(std_h.mean() if not np.isnan(std_h.mean()) else 0.0) + pd.DataFrame({"doy": range(1, 367), "median": med_doy.values})\ + .to_csv(MODELS_DIR / f"seasonal_doy_{plant}_{col}.csv", index=False) + pd.DataFrame({"hod": range(24), "median": med_hod2.values, "std": std_h.values})\ + .to_csv(MODELS_DIR / f"seasonal_hod_{plant}_{col}.csv", index=False) + +# ---------- Load ---------- +def load_profiles(plant_id, sensor) -> Dict[str, pd.DataFrame]: + out: Dict[str, pd.DataFrame] = {} + p = MODELS_DIR + daily = p / f"daily_{plant_id}_{sensor}.csv" + weekly = p / f"weekly_{plant_id}_{sensor}.csv" + sdoy = p / f"seasonal_doy_{plant_id}_{sensor}.csv" + shod = p / f"seasonal_hod_{plant_id}_{sensor}.csv" + if daily.exists(): out["daily"] = pd.read_csv(daily) + if weekly.exists(): out["weekly"] = pd.read_csv(weekly) + if sdoy.exists(): out["seasonal_doy"] = pd.read_csv(sdoy) + if shod.exists(): out["seasonal_hod"] = pd.read_csv(shod) + return out + +# ---------- Expectation (baseline/std) ---------- +def expected_from_profiles(ts: pd.Timestamp, profiles: Dict[str, pd.DataFrame]) -> Optional[Dict[str, float]]: + if not profiles: + return None + ts = pd.Timestamp(ts) + hod = int(ts.hour) + dow = int(ts.dayofweek) + doy = int(ts.dayofyear) + + # Seasonal: DOY + HOD + if "seasonal_doy" in profiles and "seasonal_hod" in profiles: + sdoy = profiles["seasonal_doy"].set_index("doy") + shod = profiles["seasonal_hod"].set_index("hod") + + med_doy = float(sdoy.loc[doy, "median"]) if doy in sdoy.index else float(sdoy["median"].iloc[-1]) + if hod in shod.index: + med_hod = float(shod.loc[hod, "median"]) + std_hod = float(shod.loc[hod, "std"]) + else: + med_hod = float(shod["median"].iloc[-1]) + std_hod = float(shod["std"].mean()) if pd.notna(shod["std"].mean()) else 0.0 + if not np.isfinite(std_hod) or std_hod <= 0.0: + std_hod = max(float(shod["std"].mean()), 1e-6) + + base = 0.5 * med_doy + 0.5 * med_hod + return {"baseline": float(base), "band_std": float(std_hod), "bl_type": "seasonal"} + + # Weekly: DOW + HOD + if "weekly" in profiles: + w = profiles["weekly"].copy() + if {"dow", "hod", "median", "std"}.issubset(set(w.columns)): + w["dow"] = w["dow"].astype(int) + w["hod"] = w["hod"].astype(int) + row = w[(w["dow"] == dow) & (w["hod"] == hod)] + if not row.empty: + base = float(row["median"].iloc[0]) + std = float(row["std"].iloc[0]) + if not np.isfinite(std) or std <= 0.0: + std = max(float(w["std"].mean()), 1e-6) + return {"baseline": base, "band_std": std, "bl_type": "weekly"} + + # Daily: HOD only + if "daily" in profiles: + d = profiles["daily"].set_index("hod") + if hod in d.index: + base = float(d.loc[hod, "median"]) + std = float(d.loc[hod, "std"]) + else: + base = float(d["median"].iloc[-1]) + std = float(d["std"].mean()) if pd.notna(d["std"].mean()) else 0.0 + if not np.isfinite(std) or std <= 0.0: + std = max(float(d["std"].mean()), 1e-6) + return {"baseline": base, "band_std": std, "bl_type": "daily"} + + return None + +# ---------- Streaming scoring ---------- +class StreamingState: + def __init__(self, alpha: float = 2/25, bias_alpha: float = 0.002): + self.prev_value: Optional[float] = None + self.ema_abs_res: Optional[float] = None + self.alpha = float(alpha) + self.bias: float = 0.0 + self.bias_alpha: float = float(bias_alpha) + + def update_ema(self, value: float): + if self.ema_abs_res is None: + self.ema_abs_res = value + else: + self.ema_abs_res = self.alpha * value + (1 - self.alpha) * self.ema_abs_res + + def update_bias(self, residual: float): + self.bias = (1 - self.bias_alpha) * self.bias + self.bias_alpha * residual + +def score_new_point( + ts: pd.Timestamp, + value: float, + profiles: Dict[str, pd.DataFrame], + state: StreamingState, + k_band: float = 2.0, + spike_z_like: float = 3.0, + break_mult: float = 1.5, +) -> Dict[str, object]: + exp = expected_from_profiles(ts, profiles) + if exp is None: + return {"ok": False, "reason": "no_profiles"} + + baseline = exp["baseline"] + band_std = exp["band_std"] + adaptive_baseline = baseline + (state.bias or 0.0) + lower = adaptive_baseline - k_band * band_std + upper = adaptive_baseline + k_band * band_std + flag_band = (value < lower) or (value > upper) + + flag_spike = False + if state.prev_value is not None: + diff = value - state.prev_value + denom = band_std if band_std > 1e-9 else 1.0 + flag_spike = abs(diff) > spike_z_like * denom + + residual = value - adaptive_baseline + state.update_ema(abs(residual)) + state.update_bias(residual) + flag_break = False + if state.ema_abs_res is not None: + flag_break = state.ema_abs_res > (break_mult * band_std) + + state.prev_value = value + + is_anomaly = flag_band or flag_spike or flag_break + return { + "ok": True, + "ts": ts, + "baseline": float(baseline), + "adaptive_baseline": float(adaptive_baseline), + "band_std": float(band_std), + "lower": float(lower), + "upper": float(upper), + "value": float(value), + "flags": { + "band": bool(flag_band), + "spike": bool(flag_spike), + "break": bool(flag_break), + }, + "is_anomaly": bool(is_anomaly), + "bl_type": exp["bl_type"], + "ema_abs_res": float(state.ema_abs_res) if state.ema_abs_res is not None else None, + "bias": float(state.bias), + } diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/requirements.txt b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/requirements.txt new file mode 100644 index 000000000..12ce1818c --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/requirements.txt @@ -0,0 +1,15 @@ +pandas==2.2.2 +numpy==1.26.4 +matplotlib==3.8.4 +statsmodels==0.14.2 +scipy==1.11.4 +#fastapi==0.110.0 + + + +uvicorn==0.29.0 +scikit-learn==1.3.2 +joblib==1.3.2 +protobuf>=3.20.3,<5 +grpcio>=1.54.0 +shapely diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/conftest.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/conftest.py new file mode 100644 index 000000000..a6ae30046 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/conftest.py @@ -0,0 +1,39 @@ + +import os +import sys +from pathlib import Path +import pandas as pd +import numpy as np +import pytest +from datetime import datetime, timedelta + +# Ensure project src is importable +HERE = Path(__file__).resolve().parent +SRC = HERE.parent / "sensor-anomaly-pro" / "sensor-anomaly-pro" +if SRC.exists(): + sys.path.insert(0, str(SRC)) + +@pytest.fixture(scope="session") +def mini_csv(tmp_path_factory): + # Create a tiny, valid sensors CSV for tests and set DATA_PATH to it. + tmpdir = tmp_path_factory.mktemp("data") + csv_path = tmpdir / "mini_plant_health.csv" + + # Build 3 days hourly for 2 plants + start = datetime(2024, 1, 1, 0, 0, 0) + rows = [] + for plant in ["P1", "P2"]: + for h in range(0, 24 * 3): + ts = start + timedelta(hours=h) + rows.append({ + "Plant_ID": plant, + "Timestamp": ts.isoformat(), + "Soil_Moisture": 40 + 10*np.sin(h/6.0) + (np.random.rand()-0.5), + "Ambient_Temperature": 20 + 5*np.sin(h/12.0) + (np.random.rand()-0.5), + "Humidity": 60 + 8*np.cos(h/8.0) + (np.random.rand()-0.5), + }) + df = pd.DataFrame(rows) + df.to_csv(csv_path, index=False) + + os.environ["DATA_PATH"] = str(csv_path) + return csv_path diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/pytest.ini b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/pytest.ini new file mode 100644 index 000000000..de40b9629 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -q --maxfail=1 --disable-warnings diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_app_score.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_app_score.py new file mode 100644 index 000000000..bb3357a7c --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_app_score.py @@ -0,0 +1,30 @@ +# tests/test_app_score.py +import os +from pathlib import Path + +import analyze_sensors as az +import sensorAnomalyPro.profiles_runtime as pr +import sensorAnomalyPro.app as appmod + +def test_score_function_end_to_end(tmp_path): + df = az.read_and_clean(Path(os.getenv("DATA_PATH", "/mnt/data/plant_health_data.csv"))) + + pr.MODELS_DIR = tmp_path / "reports" / "models" + pr.MODELS_DIR.mkdir(parents=True, exist_ok=True) + + pr.export_profiles(df) + + + row0 = df[df["Plant_ID"].notna()].iloc[0] + plant = row0["Plant_ID"] + sensor = "Soil_Moisture" if "Soil_Moisture" in df.columns else "Humidity" + + + ts = row0["Timestamp"].isoformat() + value = float(row0[sensor]) + req = appmod.ScoreRequest(plant_id=str(plant), sensor=sensor, ts=ts, value=value) + resp = appmod.score(req) + + assert resp.ok is True + for field in ["is_anomaly", "lower", "upper", "band_std", "flags", "ts", "baseline"]: + assert getattr(resp, field, None) is not None diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_detect_flags.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_detect_flags.py new file mode 100644 index 000000000..a79c13d00 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_detect_flags.py @@ -0,0 +1,35 @@ +# tests/test_detect_flags.py +import os +from pathlib import Path +import numpy as np + +import analyze_sensors as az + +def test_detect_flags_spike_is_caught(): + data_path = Path(os.getenv("DATA_PATH", "/mnt/data/plant_health_data.csv")) + df = az.read_and_clean(data_path) + + col = "Soil_Moisture" if "Soil_Moisture" in df.columns else "Humidity" + + + plant_val = df["Plant_ID"].dropna().unique()[0] + d = df[df["Plant_ID"] == plant_val].copy() + assert not d.empty, "no rows for selected plant" + + mid = len(d) // 2 + base = float(d.iloc[mid][col]) + d.iloc[mid, d.columns.get_loc(col)] = base + 50.0 + + + db = az.baseline_daily(d, col) + out = az.detect_flags(db, col) + + expected = {"flag_band", "flag_spike", "flag_break", "is_anomaly"} + assert expected.issubset(set(out.columns)), f"missing columns: {expected - set(out.columns)}" + + + flag_df = out[["flag_band", "flag_spike", "flag_break", "is_anomaly"]].fillna(False) + assert flag_df.to_numpy().any(), "expected at least one flagged point for the injected spike" + + + assert flag_df["flag_spike"].any() or flag_df["is_anomaly"].any(), "expected spike detection" diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_profiles_runtime.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_profiles_runtime.py new file mode 100644 index 000000000..90f069890 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_profiles_runtime.py @@ -0,0 +1,17 @@ +# tests/test_profiles_runtime.py +import os +from pathlib import Path + +import analyze_sensors as az +import sensorAnomalyPro.profiles_runtime as pr + +def test_export_and_load_profiles(tmp_path): + df = az.read_and_clean(Path(os.getenv("DATA_PATH", "/mnt/data/plant_health_data.csv"))) + pr.MODELS_DIR = tmp_path / "reports" / "models" + pr.MODELS_DIR.mkdir(parents=True, exist_ok=True) # לוודא שהתיקייה קיימת + pr.export_profiles(df) + + plant = df["Plant_ID"].dropna().iloc[0] + sensor = "Soil_Moisture" if "Soil_Moisture" in df.columns else "Humidity" + prof = pr.load_profiles(plant, sensor) + assert prof is not None diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_read_clean.py b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_read_clean.py new file mode 100644 index 000000000..d8b39c3d5 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_read_clean.py @@ -0,0 +1,17 @@ +import os +from pathlib import Path +import pandas as pd + +import analyze_sensors as az + +def test_read_and_clean_smoke(): + data_path = Path(os.getenv("DATA_PATH", "/mnt/data/plant_health_data.csv")) + df = az.read_and_clean(data_path) + assert not df.empty, "cleaned dataframe should not be empty" + # required columns + required = {"Plant_ID", "Timestamp"} + assert required.issubset(df.columns), f"missing columns: {required - set(df.columns)}" + # dtypes + assert pd.api.types.is_datetime64_any_dtype(df["Timestamp"]), "Timestamp must be datetime" + for col in ["Soil_Moisture", "Ambient_Temperature", "Humidity"]: + assert col in df.columns, f"missing sensor column {col}" diff --git a/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/zones.geojson b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/zones.geojson new file mode 100644 index 000000000..423245ec0 --- /dev/null +++ b/AgCloud/services/sensorAnomalyPro/sensorAnomalyPro/zones.geojson @@ -0,0 +1,22 @@ + +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {"name": "Zone A"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[34.75, 32.00], [34.90, 32.00], [34.90, 32.10], [34.75, 32.10], [34.75, 32.00]]] + } + }, + { + "type": "Feature", + "properties": {"name": "Zone B"}, + "geometry": { + "type": "Polygon", + "coordinates": [[[34.90, 31.95], [35.05, 31.95], [35.05, 32.05], [34.90, 32.05], [34.90, 31.95]]] + } + } + ] +} diff --git a/AgCloud/services/sensorGuard/Dockerfile.flink b/AgCloud/services/sensorGuard/Dockerfile.flink new file mode 100644 index 000000000..8d94314f5 --- /dev/null +++ b/AgCloud/services/sensorGuard/Dockerfile.flink @@ -0,0 +1,135 @@ +FROM flink:1.19.3-scala_2.12-java11 + +USER root + +COPY certs/*.crt /app/certs/ +RUN if [ -d ./certs ] && [ "$(ls ./certs/*.crt 2>/dev/null)" ]; then \ + echo "Configuring NetFree certificates..."; \ + cp ./certs/*.crt /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ + fi +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip ca-certificates curl libgomp1; \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +# Ensure lib directory exists +RUN mkdir -p /opt/flink/lib + +# venv + PATH +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python \ + PYTHONUNBUFFERED=1 + +# Configure pip to use SSL certificates +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +COPY requirements.txt /tmp/requirements.txt +# Install Python dependencies including PyFlink +RUN pip install -r /tmp/requirements.txt && \ + pip install "apache-flink==1.19.3" + +# Compatible versions for PyFlink 1.19.3 + +# kafka-clients +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +WORKDIR /opt/app +COPY flink_app/main.py /opt/app/main.py +COPY flink_app/core /opt/app/core +COPY flink_app/io_mod /opt/app/io_mod +COPY flink_app/config /opt/app/config +COPY flink_app/api /opt/app/api + +RUN mkdir -p /opt/app/resources + +RUN chown -R flink:flink /opt/app /opt/flink && chmod -R g+rwX /opt/app +USER flink + +# Default environment variables +ENV KAFKA_BROKERS=kafka:9092 \ + IN_TOPIC=sensors \ + OUT_TOPIC=event_logs_sensors \ + KAFKA_GROUP_ID=flink-device-pipeline \ + PYTHONPATH=/opt/app \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python + +USER root + +COPY netfree-ca.crt /usr/local/share/ca-certificates/corp-ca.crt +RUN chmod 644 /usr/local/share/ca-certificates/corp-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip ca-certificates curl libgomp1; \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +RUN mkdir -p /opt/flink/lib + +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python \ + PYTHONUNBUFFERED=1 + +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +COPY requirements.txt /tmp/requirements.txt +RUN pip install -r /tmp/requirements.txt && \ + pip install "apache-flink==1.19.3" + + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +WORKDIR /opt/app +COPY flink_app/main.py /opt/app/main.py +COPY flink_app/core /opt/app/core +COPY flink_app/io_mod /opt/app/io_mod +COPY flink_app/config /opt/app/config +COPY flink_app/api /opt/app/api + +RUN mkdir -p /opt/app/secrets && \ + chown -R flink:flink /opt/app /opt/flink /opt/app/secrets && \ + chmod -R g+rwX /opt/app && \ + chmod 775 /opt/app/secrets + +# Copy and set up entrypoint script +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["jobmanager"] + +ENV KAFKA_BROKERS=kafka:9092 \ + IN_TOPIC=sensors \ + OUT_TOPIC=event_logs_sensors \ + KAFKA_GROUP_ID=flink-device-pipeline \ + PYTHONPATH=/opt/app \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python diff --git a/AgCloud/services/sensorGuard/README.md b/AgCloud/services/sensorGuard/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/docker-compose.yml b/AgCloud/services/sensorGuard/docker-compose.yml new file mode 100644 index 000000000..bacbeb99d --- /dev/null +++ b/AgCloud/services/sensorGuard/docker-compose.yml @@ -0,0 +1,42 @@ +version: "3.9" + +services: + jobmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-jobmanager + command: jobmanager + ports: + - "8081:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - KAFKA_GROUP_ID=flink-device-pipeline + networks: + - ag_cloud + volumes: + - ./secrets:/opt/app/secrets + + taskmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + depends_on: + - jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - taskmanager.numberOfTaskSlots=4 + - KAFKA_GROUP_ID=flink-device-pipeline + networks: + - ag_cloud + volumes: + - ./secrets:/opt/app/secrets + +networks: + ag_cloud: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/services/sensorGuard/entrypoint.sh b/AgCloud/services/sensorGuard/entrypoint.sh new file mode 100644 index 000000000..a301d88f6 --- /dev/null +++ b/AgCloud/services/sensorGuard/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +# Fix permissions for secrets directory if it exists (runs as root) +if [ -d "/opt/app/secrets" ]; then + echo "Fixing permissions for /opt/app/secrets..." + chown -R flink:flink /opt/app/secrets + chmod 775 /opt/app/secrets +fi + +# Call the original Flink entrypoint +exec /docker-entrypoint.sh "$@" diff --git a/AgCloud/services/sensorGuard/flink_app/__init__.py b/AgCloud/services/sensorGuard/flink_app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/flink_app/api/__init__.py b/AgCloud/services/sensorGuard/flink_app/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/flink_app/api/auth.py b/AgCloud/services/sensorGuard/flink_app/api/auth.py new file mode 100644 index 000000000..ed57b5d02 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/api/auth.py @@ -0,0 +1,74 @@ +import os +import pathlib +import requests +import time + +# === CONFIG === +DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001") +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/opt/app/secrets/db_api_token") +DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "flink_job_sensors") +ROTATE_IF_EXISTS = True # Can be set to False if token rotation is not desired on restart + +# === PATH HELPERS === +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + +def _read_token_from_file(path: str) -> str | None: + try: + p = pathlib.Path(path) + if p.exists(): + token = p.read_text(encoding="utf-8").strip() + if token and len(token) > 10: + return token + except Exception: + pass + return None + +def _write_token_to_file(path: str, token: str) -> None: + p = pathlib.Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + print(f"[AUTH] Token saved to {path}", flush=True) + +# === FETCH LOGIC === +def _fetch_token_via_bootstrap(base: str, retries: int = 3, backoff: float = 1.0) -> str | None: + url = _safe_join_url(base, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": ROTATE_IF_EXISTS} + + for attempt in range(1, retries + 1): + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code not in (200, 201): + print(f"[AUTH] Bootstrap failed ({r.status_code}): {r.text[:200]}", flush=True) + time.sleep(backoff * attempt) + continue + + data = r.json() + raw = (data.get("service_account", {}) or {}).get("raw_token") \ + or (data.get("service_account", {}) or {}).get("token") + + if raw and isinstance(raw, str) and raw.strip() and "***" not in raw: + print("[AUTH] Token fetched successfully", flush=True) + return raw.strip() + except Exception as e: + print(f"[AUTH] Exception: {e}", flush=True) + time.sleep(backoff * attempt) + print("[AUTH] Failed to bootstrap service token", flush=True) + return None + +# === PUBLIC API === +def get_access_token(base_url: str | None = None) -> str: + """ + Loads token from file if exists, otherwise bootstraps new one via /auth/_dev_bootstrap. + Returns a valid token string. + """ + base = base_url or DB_API_BASE + token = _read_token_from_file(DB_API_TOKEN_FILE) + if token: + return token + + new_token = _fetch_token_via_bootstrap(base) + if new_token: + _write_token_to_file(DB_API_TOKEN_FILE, new_token) + return new_token + raise RuntimeError("[AUTH] Could not obtain or save service token") diff --git a/AgCloud/services/sensorGuard/flink_app/api/devices_client.py b/AgCloud/services/sensorGuard/flink_app/api/devices_client.py new file mode 100644 index 000000000..7dfa2924f --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/api/devices_client.py @@ -0,0 +1,74 @@ +""" +api/devices_client.py +----------------------------------- +Fetches all active sensors (devices) from the API +and returns their IDs and models. +""" +import requests +from typing import Iterable, Tuple + + +def list_active_sensors(api_base: str, token: str, timeout: float = 10.0) -> Iterable[str]: + """ + Fetch all sensors from the devices_sensor table. + + Args: + api_base: Base URL of the API (e.g., "http://localhost:8001") + token: Access token (returned from get_access_token) + timeout: HTTP request timeout in seconds + + Yields: + Device IDs as strings. + """ + url = f"{api_base.rstrip('/')}/api/tables/devices_sensor" + headers = {"X-Service-Token": token} + + try: + response = requests.get(url, headers=headers, timeout=timeout) + if response.status_code != 200: + print(f"[DEVICES] Failed ({response.status_code}): {response.text[:120]}") + return + + items = (response.json() or {}).get("rows", []) + print(f"[DEVICES] Fetched {len(items)} sensors from API") + for dev in items: + # All sensors in table are active, just return the IDs + device_id = dev.get("id", "") + if device_id: + print(f"[DEVICES] Adding sensor: id={device_id}") + yield str(device_id) + + except requests.RequestException as e: + print(f"[DEVICES] Request error: {e}") + return + + +def get_sensors_last_seen(api_base: str, token: str, timeout: float = 10.0): + """ + Fetch all sensors from devices_sensor with their last_seen timestamp. + Used for silence sweep. + + Args: + api_base: Base URL of the API. + token: Service token. + timeout: Request timeout. + + Returns: + List of dicts like: [{"id": "dev-a", "sensor_type": "temp", "last_seen": "2025-11-11T13:00:00Z"}, ...] + """ + url = f"{api_base.rstrip('/')}/api/tables/devices_sensor" + headers = {"X-Service-Token": token} + + try: + response = requests.get(url, headers=headers, timeout=timeout) + if response.status_code != 200: + print(f"[DEVICES] Failed ({response.status_code}): {response.text[:120]}") + return [] + + items = (response.json() or {}).get("rows", []) + print(f"[DEVICES] Fetched {len(items)} sensors (with last_seen) from API") + return items + + except requests.RequestException as e: + print(f"[DEVICES][ERROR] {e}") + return [] diff --git a/AgCloud/services/sensorGuard/flink_app/api/devices_updater.py b/AgCloud/services/sensorGuard/flink_app/api/devices_updater.py new file mode 100644 index 000000000..534d2893f --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/api/devices_updater.py @@ -0,0 +1,30 @@ +import requests +from datetime import datetime, timezone +from api.auth import get_access_token + +def update_device_last_seen(device_id: str): + """ + Updates the 'last_seen' field for a specific device in the devices_sensor table. + Uses PATCH /api/tables/devices_sensor + """ + api_base = "http://host.docker.internal:8001" + token = get_access_token(api_base) + headers = { + "X-Service-Token": token, + "Content-Type": "application/json" + } + url = f"{api_base}/api/tables/devices_sensor" + + payload = { + "keys": {"id": device_id}, + "data": {"last_seen": datetime.now(timezone.utc).isoformat()} + } + + try: + r = requests.patch(url, json=payload, headers=headers, timeout=10) + if r.status_code == 200: + print(f"[DB-UPDATER] Updated last_seen for device {device_id}") + else: + print(f"[DB-UPDATER] Failed ({r.status_code}): {r.text}") + except Exception as e: + print(f"[DB-UPDATER] Exception: {e}") diff --git a/AgCloud/services/sensorGuard/flink_app/config/__init__.py b/AgCloud/services/sensorGuard/flink_app/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/flink_app/config/rules.yaml b/AgCloud/services/sensorGuard/flink_app/config/rules.yaml new file mode 100644 index 000000000..dd718a617 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/config/rules.yaml @@ -0,0 +1,25 @@ +defaults: + expected_interval_seconds: 60 + keepalive_miss_factor: 2 + prolonged_silence_seconds: 120 + silence_sweep_interval_seconds: 120 + +ranges: + temperature: { min: 21, max: 29 } + Ambient_Temperature: { min: 21, max: 29 } + humidity: { min: 0, max: 100 } + Humidity: { min: 0, max: 100 } + soil_moist: { min: 0, max: 100 } + Soil_Moisture: { min: 0, max: 100 } + unknown_sensor: { min: 21, max: 29 } # Default range - using temperature range for testing + +stuck: + epsilon: 0.1 + min_run_length: 3 + min_duration_seconds: 600 + +features: + corrupted: true + out_of_range: true + stuck_sensor: true + silence: true \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/flink_app/config/settings.py b/AgCloud/services/sensorGuard/flink_app/config/settings.py new file mode 100644 index 000000000..59853c063 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/config/settings.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +class Settings: + # --- API Configuration --- + DEVICES_API_BASE = os.getenv("DEVICES_API_BASE", "http://host.docker.internal:8001") + DEVICES_API_TOKEN = os.getenv("DEVICES_API_TOKEN", None) + + # --- Kafka Configuration --- + KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") + IN_TOPIC = os.getenv("IN_TOPIC", "sensors") + OUT_TOPIC = os.getenv("OUT_TOPIC", "event_logs_sensors") + KAFKA_GROUP_ID = os.getenv("KAFKA_GROUP_ID", "flink-device-pipeline") + + # --- Flink runtime paths --- + PYTHON_EXEC = os.getenv("PYFLINK_PYTHON", "/opt/venv/bin/python") + RULES_FILE = BASE_DIR / "config" / "rules.yaml" + +settings = Settings() diff --git a/AgCloud/services/sensorGuard/flink_app/core/__init__.py b/AgCloud/services/sensorGuard/flink_app/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/flink_app/core/engine.py b/AgCloud/services/sensorGuard/flink_app/core/engine.py new file mode 100644 index 000000000..c4669b16b --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/core/engine.py @@ -0,0 +1,181 @@ +# core/engine.py + +from .state import StateStore +from .types import Event, Alert +from .rules import corrupted, out_of_range, stuck_sensor +from datetime import datetime, timezone +import time + +from api.devices_updater import update_device_last_seen +from api.devices_client import get_sensors_last_seen +from api.auth import get_access_token + + +class Engine: + def __init__(self, cfg, writer, state: StateStore | None = None): + """ + cfg: dict read from rules.yaml (includes features/ranges/defaults/stuck) + writer: either a single object with write(alert) or a list of writers + """ + self.cfg = cfg + self.writers = writer if isinstance(writer, (list, tuple)) else [writer] + self.state = state or StateStore() + + # --- API info & persistent token --- + self.api_base = "http://host.docker.internal:8001" + self.token = get_access_token(self.api_base) + if self.token: + print("[ENGINE] Access token acquired successfully.") + else: + print("[ENGINE][WARN] Failed to get API token at startup.") + + # --- Utilities --------------------------------------------------------- + + def _emit(self, alert: Alert): + for w in self.writers: + w.write(alert) + + def _open_once(self, dev_state, alert: Alert): + """Open an event only if it’s not already open for the same issue_type.""" + if alert.issue_type not in dev_state.open_alerts: + dev_state.open_alerts[alert.issue_type] = alert + print(f"[ENGINE] Opening new alert: {alert.issue_type} for device {alert.device_id}") + self._emit(alert) + + def _close_if_open(self, dev_state, issue_type: str, ts): + """Close an open event (if exists) and update end_ts.""" + if issue_type in dev_state.open_alerts: + a = dev_state.open_alerts.pop(issue_type) + a.end_ts = ts + print(f"[ENGINE] Closing alert: {issue_type} for device {a.device_id}") + self._emit(a) + + def _close_all_keepalive_alerts(self, dev_state, ts): + """Close missing_keepalive alert when sensor sends valid data.""" + print(f"[ENGINE] Checking open alerts before close_all_keepalive_alerts: {list(dev_state.open_alerts.keys())}") + + if "missing_keepalive" in dev_state.open_alerts: + a = dev_state.open_alerts.pop("missing_keepalive") + a.end_ts = ts + print(f"[ENGINE] Closing missing_keepalive alert (sensor back online) for {a.device_id}") + self._emit(a) + else: + print(f"[ENGINE] No missing_keepalive alert to close for this device") + + # ---------------------------------------------------------------------- + + def sweep_silence(self, now): + """ + Periodic silence check based on DB 'devices_sensor.last_seen'. + Checks for missing_keepalive (not prolonged_silence). + """ + print("[ENGINE] Starting silence sweep (via DB API)...") + + # Fetch sensors via API (single attempt) + sensors = get_sensors_last_seen(self.api_base, self.token) + + if not sensors: + print("[ENGINE][ERROR] No sensors retrieved. Skipping silence sweep.") + return + + expected = self.cfg.get("expected_interval_seconds", 60) + miss_factor = self.cfg.get("keepalive_miss_factor", 3) + miss_thr = miss_factor * expected + print(f"[ENGINE] Checking {len(sensors)} sensors for missing keepalive > {miss_thr}s") + + for s in sensors: + sensor_id = s.get("id") + last_seen_str = s.get("last_seen") + if not sensor_id or not last_seen_str: + continue + + try: + last_seen = datetime.fromisoformat(last_seen_str.replace("Z", "+00:00")) + except Exception: + print(f"[ENGINE][WARN] Invalid timestamp for {sensor_id}: {last_seen_str}") + continue + + gap = (now - last_seen).total_seconds() + + # Check for missing_keepalive only + dev_state = self.state.get(sensor_id) + if dev_state and gap >= miss_thr and "missing_keepalive" not in dev_state.open_alerts: + alert = Alert( + issue_type="missing_keepalive", + device_id=sensor_id, + sensor_type=s.get("sensor_type", "unknown"), + site_id=None, + severity="critical", + start_ts=last_seen, + end_ts=None, + details={"gap_sec": int(gap), "expected": expected}, + ) + print(f"[ENGINE] Sensor {sensor_id} missing keepalive for {int(gap)}s — creating alert.") + dev_state.open_alerts["missing_keepalive"] = alert + self._emit(alert) + elif dev_state and gap < miss_thr and "missing_keepalive" in dev_state.open_alerts: + # Close the alert if gap is back to normal + alert = dev_state.open_alerts.pop("missing_keepalive") + alert.end_ts = now + print(f"[ENGINE] Sensor {sensor_id} keepalive restored — closing alert.") + self._emit(alert) + + # ---------------------------------------------------------------------- + + def process_event(self, ev: Event): + """Process a single event and manage open/close logic for alerts.""" + print(f"[ENGINE] Processing event: device_id={ev.device_id}, msg_type={ev.msg_type}, sensor_type={ev.sensor_type}") + + if not self.state.is_known_device(ev.device_id): + print(f"[ENGINE] Unknown device {ev.device_id} - skipping") + return + + print(f"[ENGINE] Known device {ev.device_id} - processing") + feats = (self.cfg.get("features") or {}) + dev = self.state.get(ev.device_id) + + # === Step 1: Update device state and DB === + print(f"[ENGINE] Updating device {ev.device_id} last_seen_ts from {dev.last_seen_ts} to {ev.ts}") + dev.last_seen_ts = ev.ts + dev.last_value = ev.value + + # --- Update API record --- + update_device_last_seen(ev.device_id) + + if ev.sensor_type and ev.sensor_type != "unknown_sensor": + dev.sensor_type = ev.sensor_type + + # === Step 2: Close keepalive-related alerts === + self._close_all_keepalive_alerts(dev, ev.ts) + + # === Step 3: Corrupted readings === + if feats.get("corrupted", True): + a = corrupted(ev, self.cfg) + if a: + print(f"[ENGINE] Corrupted reading detected for device {ev.device_id}") + self._open_once(dev, a) + self._close_if_open(dev, "out_of_range", ev.ts) + self._close_if_open(dev, "stuck_sensor", ev.ts) + return + print(f"[ENGINE] No corrupted reading for device {ev.device_id}") + + # === Step 4: Out-of-range checks === + if feats.get("out_of_range", True): + print(f"[ENGINE] Checking out_of_range for device {ev.device_id}, value={ev.value}") + a = out_of_range(ev, self.cfg) + if a: + print(f"[ENGINE] Out-of-range detected for device {ev.device_id}: {a}") + self._open_once(dev, a) + else: + print(f"[ENGINE] Value in range for device {ev.device_id}") + self._close_if_open(dev, "out_of_range", ev.ts) + + # === Step 5: Stuck sensor checks === + if feats.get("stuck_sensor", True): + print(f"[ENGINE] Checking stuck_sensor for device {ev.device_id}") + a = stuck_sensor(ev, dev, self.cfg) + if a: + print(f"[ENGINE] Stuck sensor detected for device {ev.device_id}") + self._open_once(dev, a) + else: + self._close_if_open(dev, "stuck_sensor", ev.ts) diff --git a/AgCloud/services/sensorGuard/flink_app/core/rules.py b/AgCloud/services/sensorGuard/flink_app/core/rules.py new file mode 100644 index 000000000..36f22d537 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/core/rules.py @@ -0,0 +1,159 @@ +from typing import Optional, Dict, Any +from datetime import timezone +from .types import Event, Alert, DeviceState + +def _utc(dt): + """Return datetime with UTC tzinfo.""" + return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc) + +def out_of_range(event: Event, cfg: Dict[str, Any]) -> Optional[Alert]: + """Check if sensor value is outside configured min/max.""" + if event.msg_type not in ["reading", "telemetry"] or event.value is None: + return None + rngs = (cfg or {}).get("ranges", {}) + lim = rngs.get(event.sensor_type, {}) + vmin, vmax = lim.get("min"), lim.get("max") + print(f"[RULES] out_of_range: sensor_type={event.sensor_type}, value={event.value}, lim={lim}, vmin={vmin}, vmax={vmax}") + if vmin is not None and event.value < vmin: + return Alert( + device_id=event.device_id, issue_type="out_of_range", + start_ts=_utc(event.ts), end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"value": event.value, "min": vmin, "max": vmax} + ) + if vmax is not None and event.value > vmax: + return Alert( + device_id=event.device_id, issue_type="out_of_range", + start_ts=_utc(event.ts), end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"value": event.value, "min": vmin, "max": vmax} + ) + return None + +def corrupted(event: Event, cfg: Dict[str, Any]) -> Optional[Alert]: + """Check if reading is invalid (null, non-numeric, bad quality).""" + if event.msg_type not in ["reading", "telemetry"]: + return None + if event.value is None: + return Alert( + device_id=event.device_id, issue_type="corrupted", + start_ts=_utc(event.ts), end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"reason": "null value"} + ) + + if not isinstance(event.value, (int, float)): + return Alert( + device_id=event.device_id, issue_type="corrupted", + start_ts=_utc(event.ts), end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"reason": "non-numeric"} + ) + + if event.quality and event.quality != "ok": + return Alert( + device_id=event.device_id, issue_type="corrupted", + start_ts=_utc(event.ts), end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"quality": event.quality} + ) + return None + +def stuck_sensor(event: Event, state: DeviceState, cfg) -> Alert | None: + """Check if sensor value is stuck (no change over time).""" + if event.msg_type not in ["reading", "telemetry"] or event.value is None: + state.last_seen_ts = _utc(event.ts) + return None + + eps = cfg.get("stuck", {}).get("epsilon", 0.1) + min_run = cfg.get("stuck", {}).get("min_run_length", 6) + min_dur = cfg.get("stuck", {}).get("min_duration_seconds", 1800) # Default to 1800 instead of 0! + + # Debug log + if state.last_value is None: + print(f"[STUCK_SENSOR] Config for {event.device_id}: eps={eps}, min_run={min_run}, min_dur={min_dur}") + + if state.last_value is None: + state.last_value = event.value + state.run_length = 1 + state.last_seen_ts = _utc(event.ts) + state.stuck_since_ts = None + return None + + if abs(event.value - state.last_value) < eps: + state.run_length += 1 + if state.stuck_since_ts is None: + state.stuck_since_ts = _utc(event.ts) + print(f"[STUCK_SENSOR] Device {event.device_id}: run_length={state.run_length}, value={event.value}, stuck_since={state.stuck_since_ts}") + else: + print(f"[STUCK_SENSOR] Device {event.device_id}: value changed {state.last_value} -> {event.value}, resetting run_length") + state.run_length = 1 + state.stuck_since_ts = None + state.last_value = event.value + + state.last_seen_ts = _utc(event.ts) + + if state.run_length >= min_run: + if min_dur <= 0: + print(f"[STUCK_SENSOR] Device {event.device_id}: ALERT triggered (no min_dur)") + return Alert( + device_id=event.device_id, issue_type="stuck_sensor", + start_ts=state.stuck_since_ts or _utc(event.ts), end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"run_length": state.run_length, "epsilon": eps} + ) + else: + dur = (_utc(event.ts) - (state.stuck_since_ts or _utc(event.ts))).total_seconds() + print(f"[STUCK_SENSOR] Device {event.device_id}: run_length={state.run_length} >= {min_run}, dur={dur}s, min_dur={min_dur}s") + if dur >= min_dur: + print(f"[STUCK_SENSOR] Device {event.device_id}: ALERT triggered (dur >= min_dur)") + return Alert( + device_id=event.device_id, issue_type="stuck_sensor", + start_ts=state.stuck_since_ts, end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"run_length": state.run_length, "duration_sec": int(dur), "epsilon": eps} + ) + return None + +def silence_checks(event: Event, state: DeviceState, cfg) -> list[Alert]: + """Check for missing keepalive or prolonged silence alerts.""" + alerts: list[Alert] = [] + now_ts = _utc(event.ts) + + expected = cfg.get("expected_interval_seconds", 60) + miss_factor = cfg.get("keepalive_miss_factor", 3) + silence_sec = cfg.get("prolonged_silence_seconds", 1800) + + if state.last_seen_ts is None: + state.last_seen_ts = now_ts + return alerts + + gap = (now_ts - state.last_seen_ts).total_seconds() + + if gap >= silence_sec and "prolonged_silence" not in state.open_alerts: + a = Alert(device_id=event.device_id, issue_type="prolonged_silence", + start_ts=state.last_seen_ts, end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"gap_sec": int(gap)}) + state.open_alerts["prolonged_silence"] = a + alerts.append(a) + elif gap < silence_sec and "prolonged_silence" in state.open_alerts: + a = state.open_alerts.pop("prolonged_silence") + a.end_ts = now_ts + alerts.append(a) + + miss_thr = miss_factor * expected + if gap >= miss_thr and gap < silence_sec and "missing_keepalive" not in state.open_alerts: + a = Alert(device_id=event.device_id, issue_type="missing_keepalive", + start_ts=state.last_seen_ts, end_ts=None, severity="critical", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"gap_sec": int(gap), "expected": expected}) + state.open_alerts["missing_keepalive"] = a + alerts.append(a) + elif (gap < miss_thr or gap >= silence_sec) and "missing_keepalive" in state.open_alerts: + a = state.open_alerts.pop("missing_keepalive") + a.end_ts = now_ts + alerts.append(a) + + state.last_seen_ts = now_ts + return alerts diff --git a/AgCloud/services/sensorGuard/flink_app/core/state.py b/AgCloud/services/sensorGuard/flink_app/core/state.py new file mode 100644 index 000000000..5efa13420 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/core/state.py @@ -0,0 +1,32 @@ +from .types import DeviceState +from typing import Dict, Set + +class StateStore: + def __init__(self): + self._devices: Dict[str, DeviceState] = {} + self._known_device_ids: Set[str] = set() + + @property + def devices(self): + """Expose internal devices dictionary (read-only).""" + return self._devices + + def add_device(self, device_id: str, sensor_type: str = None) -> None: + """Initialize state for a known device.""" + device_id = str(device_id) + self._known_device_ids.add(device_id) + if device_id not in self._devices: + self._devices[device_id] = DeviceState(device_id=device_id, sensor_type=sensor_type) + + def get(self, device_id: str) -> DeviceState: + """Return state for known device, or None if unknown.""" + device_id = str(device_id) + return self._devices.get(device_id) + + def is_known_device(self, device_id: str) -> bool: + """Check if device was loaded from API.""" + return str(device_id) in self._known_device_ids + + def all_states(self): + """Iterator over all registered device states.""" + return self._devices.items() diff --git a/AgCloud/services/sensorGuard/flink_app/core/types.py b/AgCloud/services/sensorGuard/flink_app/core/types.py new file mode 100644 index 000000000..b256537fb --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/core/types.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass, field +from typing import Optional, Dict +from datetime import datetime + +@dataclass +class Event: + ts: datetime + device_id: str + sensor_type: str + site_id: Optional[str] + msg_type: str # "reading" | "keepalive" + value: Optional[float] + seq: Optional[int] + quality: Optional[str] # "ok"/"corrupted"/None + +@dataclass +class Alert: + device_id: str + issue_type: str + start_ts: datetime + end_ts: Optional[datetime] + severity: str + sensor_type: Optional[str] = None + site_id: Optional[str] = None + details: Dict = field(default_factory=dict) + +@dataclass +class DeviceState: + device_id: str + sensor_type: Optional[str] = None + last_seen_ts: Optional[datetime] = None + last_value: Optional[float] = None + run_length: int = 0 + stuck_since_ts: Optional[datetime] = None + open_alerts: Dict[str, "Alert"] = field(default_factory=dict) diff --git a/AgCloud/services/sensorGuard/flink_app/io_mod/__init__.py b/AgCloud/services/sensorGuard/flink_app/io_mod/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/flink_app/io_mod/writer_console.py b/AgCloud/services/sensorGuard/flink_app/io_mod/writer_console.py new file mode 100644 index 000000000..2f62cb0d4 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/io_mod/writer_console.py @@ -0,0 +1,11 @@ +from core.types import Alert + +class ConsoleWriter: + def write(self, alert: Alert) -> None: + end = alert.end_ts.isoformat() if alert.end_ts else "-" + print( + f"[ALERT] type={alert.issue_type} dev={alert.device_id} " + f"sensor={alert.sensor_type} value={alert.details.get('value')} " + f"range=[{alert.details.get('min')},{alert.details.get('max')}] " + f"ts={alert.start_ts.isoformat()} sev={alert.severity}" + ) diff --git a/AgCloud/services/sensorGuard/flink_app/io_mod/writer_kafka.py b/AgCloud/services/sensorGuard/flink_app/io_mod/writer_kafka.py new file mode 100644 index 000000000..2ffb6cfd4 --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/io_mod/writer_kafka.py @@ -0,0 +1,123 @@ +import os +import json +from datetime import datetime +from typing import Any, Dict, Optional + +from kafka import KafkaProducer +from core.types import Alert +from config.settings import settings + + +# Convert Alert dataclass to a stable dict for JSON serialization and downstream consumers. +def _alert_to_dict(alert: Alert) -> Dict[str, Any]: + """ + Convert Alert to an ordered, serialization-friendly dict. + Suitable for downstream consumers (DB / API / BI). + """ + details: Dict[str, Any] = getattr(alert, "details", {}) or {} + + def iso(dt: Optional[datetime]) -> Optional[str]: + return dt.isoformat() if dt else None + + d = { + "issue_type": getattr(alert, "issue_type", None), + "device_id": getattr(alert, "device_id", None), + "severity": getattr(alert, "severity", None), + "start_ts": iso(getattr(alert, "start_ts", None)), + "details": details, + } + + end_ts = getattr(alert, "end_ts", None) + if end_ts is not None: + d["end_ts"] = iso(end_ts) + + return d + + + + +# Kafka writer: send Alert objects as JSON value-only messages. +# Lazy-initialize producer to avoid early network/socket creation. +class KafkaWriter: + """ + Write Alerts to Kafka as JSON (value-only). + Defaults: + - topic from OUT_TOPIC env or 'dev-robot-telemetry-raw' + - brokers from KAFKA_BROKERS env or 'kafka:9092' + """ + def __init__( + self, + topic: str | None = None, + brokers: str | None = None, + linger_ms: int = 10, + acks: str = "all", + retries: int = 5, + ) -> None: + # Topic and bootstrap configuration. + self.topic = topic or settings.OUT_TOPIC + self.bootstrap = brokers or settings.KAFKA_BROKERS + # Producer tuning parameters. + self.linger_ms = linger_ms + self.acks = acks + self.retries = retries + # Producer instance created on first use. + self._producer: Optional[KafkaProducer] = None + + # Ensure a KafkaProducer exists; create it lazily with safe JSON serializer. + def _ensure_producer(self) -> None: + """Lazy-init KafkaProducer with JSON serializer and configured options.""" + if self._producer is None: + print(f"[KafkaWriter] Creating KafkaProducer: brokers={self.bootstrap}, topic={self.topic}") + try: + self._producer = KafkaProducer( + bootstrap_servers=self.bootstrap, + value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode("utf-8"), + linger_ms=self.linger_ms, + acks=self.acks, + retries=self.retries, + ) + print("[KafkaWriter] KafkaProducer created successfully.") + except Exception as e: + print(f"[KafkaWriter][ERROR] Failed to create KafkaProducer: {e!r}") + raise + + # Serialize and send an Alert to Kafka; log errors to stdout. + def write(self, alert: Alert) -> None: + """Send an Alert to Kafka (non-blocking send).""" + print(f"[KafkaWriter] write() called for alert: device={getattr(alert, 'device_id', None)}, issue={getattr(alert, 'issue_type', None)}") + try: + if getattr(alert, "issue_type", None) == "unknown_device": + print(f"[KafkaWriter] Skipping unknown device alert for {alert.device_id}") + return + + self._ensure_producer() + payload = _alert_to_dict(alert) + print(f"[KafkaWriter] Sending payload: {payload}") + self._producer.send(self.topic, payload) + + print( + f"[KafkaWriter] Alert sent → topic='{self.topic}', " + f"device='{alert.device_id}', issue='{alert.issue_type}', " + f"time={payload.get('start_ts')}" + ) + except Exception as e: + print(f"[KafkaWriter][ERROR] send failed: {e!r}") + + # Flush producer buffers if the producer exists. + def flush(self) -> None: + """Flush any buffered messages to Kafka.""" + try: + if self._producer: + self._producer.flush() + except Exception as e: + print(f"[KafkaWriter] flush failed: {e!r}") + + # Flush and close the underlying producer if present. + def close(self) -> None: + """Gracefully flush and close the Kafka producer.""" + try: + if self._producer: + self._producer.flush() + self._producer.close() + except Exception as e: + print(f"[KafkaWriter] close failed: {e!r}") diff --git a/AgCloud/services/sensorGuard/flink_app/main.py b/AgCloud/services/sensorGuard/flink_app/main.py new file mode 100644 index 000000000..b5292b58e --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/main.py @@ -0,0 +1,221 @@ +import os, json, yaml, threading, time +from datetime import datetime, timezone +from pathlib import Path + +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import KafkaSource +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy +from pyflink.common.typeinfo import Types +from pyflink.datastream.functions import MapFunction, ProcessFunction, RuntimeContext + +from core.engine import Engine +from core.types import Event +from io_mod.writer_console import ConsoleWriter +from io_mod.writer_kafka import KafkaWriter +from api.auth import get_access_token +from api.devices_client import list_active_sensors +from core.state import StateStore + + +DROP_INVALID = True + + +# ------------------------------------------------------------- +# Convert incoming Kafka message to Event +# ------------------------------------------------------------- +def to_event(obj: dict) -> Event | None: + if not isinstance(obj, dict): + print("[to_event] Invalid object type, expected dict.") + return None + + ts = datetime.now(timezone.utc) + device_id = obj.get("id") + sensor_type = obj.get("sensor_type") or obj.get("sensor_name", "unknown_sensor") + + if not device_id: + if DROP_INVALID: + print("[to_event] Dropping event due to missing device_id.") + return None + device_id = "unknown_device" + else: + device_id = str(device_id) + + value_str = obj.get("value") + try: + value = float(value_str) if value_str is not None else None + except ValueError: + print(f"[to_event] Invalid numeric value: {value_str}") + value = None + + print(f"[to_event] Parsed event: device_id={device_id}, sensor_type={sensor_type}") + + return Event( + ts=ts, + device_id=device_id, + sensor_type=sensor_type, + site_id=obj.get("site_id"), + msg_type=obj.get("msg_type", "reading"), + value=value, + seq=obj.get("seq"), + quality=obj.get("quality"), + ) + + +# ------------------------------------------------------------- +# Engine Mapper: applies Engine logic for each event +# ------------------------------------------------------------- +class EngineMapper(MapFunction): + def __init__(self, cfg_path: str, state: StateStore): + self.cfg_path = cfg_path + self.state = state + self.engine = None + + def open(self, runtime_context: RuntimeContext): + with open(self.cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + writers = [ConsoleWriter(), KafkaWriter()] + self.engine = Engine(cfg, writers, state=self.state) + + def map(self, ev: Event) -> str: + if ev is None: + return "" + print(f"[EngineMapper] Processing event for device_id={ev.device_id}") + self.engine.process_event(ev) + return ev.device_id or "" + + +# ------------------------------------------------------------- +# Silence Sweep Process (background thread) +# ------------------------------------------------------------- +class SilenceSweepProcess(ProcessFunction): + def __init__(self, cfg_path: str, interval_sec: int, state: StateStore): + self.cfg_path = cfg_path + self.interval_sec = interval_sec + self.state = state + self.engine = None + self._thread = None + self._stop = False + + def open(self, runtime_context: RuntimeContext): + print("[SilenceSweepProcess] Initializing background silence checker.") + with open(self.cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + + writers = [ConsoleWriter(), KafkaWriter()] + self.engine = Engine(cfg, writers, state=self.state) + + # Run silence sweep periodically in a separate thread + def loop(): + print(f"[SilenceSweep] Thread started! Interval={self.interval_sec}s") + time.sleep(self.interval_sec) + while not self._stop: + now = datetime.now(timezone.utc) + print(f"[SilenceSweep] Checking silence at {now.isoformat()}") + try: + self.engine.sweep_silence(now) + print("[SilenceSweep] Sweep completed.") + except Exception as e: + print(f"[SilenceSweep][ERROR] {e}") + time.sleep(self.interval_sec) + print("[SilenceSweep] Thread stopped.") + + self._thread = threading.Thread(target=loop, daemon=True) + self._thread.start() + + def close(self): + self._stop = True + if self._thread: + self._thread.join(timeout=2) + self._thread = None + + def process_element(self, value, ctx: 'ProcessFunction.Context'): + # No per-event logic needed + pass + + +# ------------------------------------------------------------- +# Main entry point +# ------------------------------------------------------------- +def main(): + print("=== STARTING FLINK APPLICATION ===") + base_dir = Path(__file__).resolve().parent + cfg_path = base_dir / "config" / "rules.yaml" + + # --- Load sensors from API --- + api_base = os.getenv("DEVICES_API_BASE", "http://host.docker.internal:8001") + print(f"[INIT] Fetching active sensors from {api_base}...") + + token = get_access_token(api_base) + print(f"[INIT] Token received: {'YES' if token else 'NO'}") + + shared_state = StateStore() + if token: + for device_id in list_active_sensors(api_base, token): + shared_state.add_device(device_id) + print(f"[INIT] Loaded {len(shared_state.devices)} active sensors.") + else: + print("[INIT][WARN] No token, running with empty device list.") + + # --- Flink Setup --- + bootstrap = os.getenv("KAFKA_BROKERS", "kafka:9092") + topic_in = os.getenv("IN_TOPIC", "sensors") + group_id = os.getenv("KAFKA_GROUP_ID", "flink-device-pipeline") + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(1) + env.enable_checkpointing(10_000) + + # Kafka source + source = ( + KafkaSource.builder() + .set_bootstrap_servers(bootstrap) + .set_topics(topic_in) + .set_group_id(group_id) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + stream = env.from_source( + source, + WatermarkStrategy.no_watermarks(), + f"kafka-source:{topic_in}" + ) + + # --- Processing pipeline --- + def parse_json(s: str): + try: + parsed = json.loads(s) + print(f"[FLINK-KAFKA] Parsed JSON: {s[:100]}...") + return parsed + except Exception as e: + print(f"[FLINK-KAFKA] Parse error: {e}") + return None + + events = ( + stream.map(parse_json) + .filter(lambda e: e is not None) + .map(to_event) + .filter(lambda e: e is not None) + ) + + # --- Apply Engine --- + mapper = EngineMapper(str(cfg_path), shared_state) + mapped = events.map(mapper, output_type=Types.STRING()).name("engine-run") + mapped.print().name("debug-print") + + # --- Silence check background thread --- + with open(cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + interval = cfg.get("defaults", {}).get("silence_sweep_interval_seconds", 300) + + silence_checker = SilenceSweepProcess(str(cfg_path), interval, shared_state) + events.process(silence_checker).name("silence-sweeper") + + print("[INIT] Starting Flink job...") + env.execute("DevicePipeline-With-SilenceSweep") + + +if __name__ == "__main__": + print("=== FLINK MAIN.PY EXECUTED ===") + main() diff --git a/AgCloud/services/sensorGuard/flink_app/resources/synthetic_stream.csv b/AgCloud/services/sensorGuard/flink_app/resources/synthetic_stream.csv new file mode 100644 index 000000000..71ee492ee --- /dev/null +++ b/AgCloud/services/sensorGuard/flink_app/resources/synthetic_stream.csv @@ -0,0 +1,523 @@ +timestamp,id,sensor_type,site_id,msg_type,value,seq,quality +2025-09-01T08:00:00Z,hum-A2,humidity,field-01,keepalive,,181,ok +2025-09-01T08:00:00Z,hum-A2,humidity,field-01,reading,54.617,1,ok +2025-09-01T08:00:00Z,soil-A3,soil_moist,field-02,keepalive,,91,ok +2025-09-01T08:00:00Z,soil-A3,soil_moist,field-02,reading,35.313,1,ok +2025-09-01T08:00:00Z,temp-A1,temperature,field-01,keepalive,,151,ok +2025-09-01T08:00:00Z,temp-A1,temperature,field-01,reading,27.338,1,ok +2025-09-01T08:01:00Z,hum-A2,humidity,field-01,reading,55.572,2,ok +2025-09-01T08:01:00Z,temp-A1,temperature,field-01,reading,26.959,2,ok +2025-09-01T08:02:00Z,hum-A2,humidity,field-01,reading,55.767,3,ok +2025-09-01T08:02:00Z,soil-A3,soil_moist,field-02,reading,35.63,2,ok +2025-09-01T08:02:00Z,temp-A1,temperature,field-01,reading,27.112,3,ok +2025-09-01T08:03:00Z,hum-A2,humidity,field-01,reading,56.764,4,ok +2025-09-01T08:03:00Z,temp-A1,temperature,field-01,reading,27.239,4,ok +2025-09-01T08:04:00Z,hum-A2,humidity,field-01,reading,56.144,5,ok +2025-09-01T08:04:00Z,soil-A3,soil_moist,field-02,reading,35.011,3,ok +2025-09-01T08:04:00Z,temp-A1,temperature,field-01,reading,27.052,5,ok +2025-09-01T08:05:00Z,hum-A2,humidity,field-01,keepalive,,182,ok +2025-09-01T08:05:00Z,hum-A2,humidity,field-01,reading,54.829,6,ok +2025-09-01T08:05:00Z,soil-A3,soil_moist,field-02,keepalive,,92,ok +2025-09-01T08:05:00Z,temp-A1,temperature,field-01,keepalive,,152,ok +2025-09-01T08:05:00Z,temp-A1,temperature,field-01,reading,27.263,6,ok +2025-09-01T08:06:00Z,hum-A2,humidity,field-01,reading,55.477,7,ok +2025-09-01T08:06:00Z,soil-A3,soil_moist,field-02,reading,35.495,4,ok +2025-09-01T08:06:00Z,temp-A1,temperature,field-01,reading,27.314,7,ok +2025-09-01T08:07:00Z,hum-A2,humidity,field-01,reading,57.417,8,ok +2025-09-01T08:07:00Z,temp-A1,temperature,field-01,reading,27.016,8,ok +2025-09-01T08:08:00Z,hum-A2,humidity,field-01,reading,56.884,9,ok +2025-09-01T08:08:00Z,soil-A3,soil_moist,field-02,reading,35.178,5,ok +2025-09-01T08:08:00Z,temp-A1,temperature,field-01,reading,27.622,9,ok +2025-09-01T08:09:00Z,hum-A2,humidity,field-01,reading,58.258,10,ok +2025-09-01T08:09:00Z,temp-A1,temperature,field-01,reading,27.59,10,ok +2025-09-01T08:10:00Z,hum-A2,humidity,field-01,keepalive,,183,ok +2025-09-01T08:10:00Z,hum-A2,humidity,field-01,reading,57.821,11,ok +2025-09-01T08:10:00Z,soil-A3,soil_moist,field-02,keepalive,,93,ok +2025-09-01T08:10:00Z,soil-A3,soil_moist,field-02,reading,35.237,6,ok +2025-09-01T08:10:00Z,temp-A1,temperature,field-01,keepalive,,153,ok +2025-09-01T08:10:00Z,temp-A1,temperature,field-01,reading,27.395,11,ok +2025-09-01T08:11:00Z,hum-A2,humidity,field-01,reading,57.256,12,ok +2025-09-01T08:11:00Z,temp-A1,temperature,field-01,reading,27.537,12,ok +2025-09-01T08:12:00Z,hum-A2,humidity,field-01,reading,58.973,13,ok +2025-09-01T08:12:00Z,soil-A3,soil_moist,field-02,reading,35.794,7,ok +2025-09-01T08:12:00Z,temp-A1,temperature,field-01,reading,27.722,13,ok +2025-09-01T08:13:00Z,hum-A2,humidity,field-01,reading,57.71,14,ok +2025-09-01T08:13:00Z,temp-A1,temperature,field-01,reading,27.619,14,ok +2025-09-01T08:14:00Z,hum-A2,humidity,field-01,reading,56.961,15,ok +2025-09-01T08:14:00Z,soil-A3,soil_moist,field-02,reading,36.138,8,ok +2025-09-01T08:14:00Z,temp-A1,temperature,field-01,reading,27.672,15,ok +2025-09-01T08:15:00Z,hum-A2,humidity,field-01,keepalive,,184,ok +2025-09-01T08:15:00Z,hum-A2,humidity,field-01,reading,60.498,16,ok +2025-09-01T08:15:00Z,soil-A3,soil_moist,field-02,keepalive,,94,ok +2025-09-01T08:15:00Z,temp-A1,temperature,field-01,keepalive,,154,ok +2025-09-01T08:15:00Z,temp-A1,temperature,field-01,reading,27.479,16,ok +2025-09-01T08:16:00Z,hum-A2,humidity,field-01,reading,57.56,17,ok +2025-09-01T08:16:00Z,soil-A3,soil_moist,field-02,reading,35.716,9,ok +2025-09-01T08:16:00Z,temp-A1,temperature,field-01,reading,27.929,17,ok +2025-09-01T08:17:00Z,hum-A2,humidity,field-01,reading,59.837,18,ok +2025-09-01T08:17:00Z,temp-A1,temperature,field-01,reading,27.89,18,ok +2025-09-01T08:18:00Z,hum-A2,humidity,field-01,reading,60.314,19,ok +2025-09-01T08:18:00Z,soil-A3,soil_moist,field-02,reading,35.009,10,ok +2025-09-01T08:18:00Z,temp-A1,temperature,field-01,reading,27.968,19,ok +2025-09-01T08:19:00Z,hum-A2,humidity,field-01,reading,59.914,20,ok +2025-09-01T08:19:00Z,temp-A1,temperature,field-01,reading,27.654,20,ok +2025-09-01T08:20:00Z,hum-A2,humidity,field-01,keepalive,,185,ok +2025-09-01T08:20:00Z,hum-A2,humidity,field-01,reading,58.679,21,ok +2025-09-01T08:20:00Z,soil-A3,soil_moist,field-02,keepalive,,95,ok +2025-09-01T08:20:00Z,soil-A3,soil_moist,field-02,reading,130.0,11,corrupted +2025-09-01T08:20:00Z,temp-A1,temperature,field-01,keepalive,,155,ok +2025-09-01T08:20:00Z,temp-A1,temperature,field-01,reading,28.335,21,ok +2025-09-01T08:21:00Z,hum-A2,humidity,field-01,reading,61.063,22,ok +2025-09-01T08:21:00Z,temp-A1,temperature,field-01,reading,28.081,22,ok +2025-09-01T08:22:00Z,hum-A2,humidity,field-01,reading,59.32,23,ok +2025-09-01T08:22:00Z,soil-A3,soil_moist,field-02,reading,35.55,12,ok +2025-09-01T08:22:00Z,temp-A1,temperature,field-01,reading,28.017,23,ok +2025-09-01T08:23:00Z,hum-A2,humidity,field-01,reading,59.617,24,ok +2025-09-01T08:23:00Z,temp-A1,temperature,field-01,reading,28.544,24,ok +2025-09-01T08:24:00Z,hum-A2,humidity,field-01,reading,60.617,25,ok +2025-09-01T08:24:00Z,soil-A3,soil_moist,field-02,reading,36.521,13,ok +2025-09-01T08:24:00Z,temp-A1,temperature,field-01,reading,28.172,25,ok +2025-09-01T08:25:00Z,hum-A2,humidity,field-01,keepalive,,186,ok +2025-09-01T08:25:00Z,hum-A2,humidity,field-01,reading,61.747,26,ok +2025-09-01T08:25:00Z,soil-A3,soil_moist,field-02,keepalive,,96,ok +2025-09-01T08:25:00Z,temp-A1,temperature,field-01,keepalive,,156,ok +2025-09-01T08:25:00Z,temp-A1,temperature,field-01,reading,27.933,26,ok +2025-09-01T08:26:00Z,hum-A2,humidity,field-01,reading,61.563,27,ok +2025-09-01T08:26:00Z,soil-A3,soil_moist,field-02,reading,37.459,14,ok +2025-09-01T08:26:00Z,temp-A1,temperature,field-01,reading,28.183,27,ok +2025-09-01T08:27:00Z,hum-A2,humidity,field-01,reading,62.173,28,ok +2025-09-01T08:27:00Z,temp-A1,temperature,field-01,reading,27.847,28,ok +2025-09-01T08:28:00Z,hum-A2,humidity,field-01,reading,61.367,29,ok +2025-09-01T08:28:00Z,soil-A3,soil_moist,field-02,reading,36.668,15,ok +2025-09-01T08:28:00Z,temp-A1,temperature,field-01,reading,28.554,29,ok +2025-09-01T08:29:00Z,hum-A2,humidity,field-01,reading,61.777,30,ok +2025-09-01T08:29:00Z,temp-A1,temperature,field-01,reading,28.3,30,ok +2025-09-01T08:30:00Z,hum-A2,humidity,field-01,keepalive,,187,ok +2025-09-01T08:30:00Z,hum-A2,humidity,field-01,reading,61.192,31,ok +2025-09-01T08:30:00Z,soil-A3,soil_moist,field-02,keepalive,,97,ok +2025-09-01T08:30:00Z,soil-A3,soil_moist,field-02,reading,36.186,16,ok +2025-09-01T08:30:00Z,temp-A1,temperature,field-01,keepalive,,157,ok +2025-09-01T08:30:00Z,temp-A1,temperature,field-01,reading,28.272,31,ok +2025-09-01T08:31:00Z,hum-A2,humidity,field-01,reading,62.031,32,ok +2025-09-01T08:31:00Z,temp-A1,temperature,field-01,reading,28.671,32,ok +2025-09-01T08:32:00Z,hum-A2,humidity,field-01,reading,62.937,33,ok +2025-09-01T08:32:00Z,soil-A3,soil_moist,field-02,reading,36.151,17,ok +2025-09-01T08:32:00Z,temp-A1,temperature,field-01,reading,28.162,33,ok +2025-09-01T08:33:00Z,hum-A2,humidity,field-01,reading,62.395,34,ok +2025-09-01T08:33:00Z,temp-A1,temperature,field-01,reading,28.634,34,ok +2025-09-01T08:34:00Z,hum-A2,humidity,field-01,reading,63.183,35,ok +2025-09-01T08:34:00Z,soil-A3,soil_moist,field-02,reading,37.564,18,ok +2025-09-01T08:34:00Z,temp-A1,temperature,field-01,reading,28.148,35,ok +2025-09-01T08:35:00Z,hum-A2,humidity,field-01,keepalive,,188,ok +2025-09-01T08:35:00Z,hum-A2,humidity,field-01,reading,62.506,36,ok +2025-09-01T08:35:00Z,soil-A3,soil_moist,field-02,keepalive,,98,ok +2025-09-01T08:35:00Z,temp-A1,temperature,field-01,keepalive,,158,ok +2025-09-01T08:35:00Z,temp-A1,temperature,field-01,reading,28.46,36,ok +2025-09-01T08:36:00Z,hum-A2,humidity,field-01,reading,62.647,37,ok +2025-09-01T08:36:00Z,soil-A3,soil_moist,field-02,reading,37.064,19,ok +2025-09-01T08:36:00Z,temp-A1,temperature,field-01,reading,28.383,37,ok +2025-09-01T08:37:00Z,hum-A2,humidity,field-01,reading,63.303,38,ok +2025-09-01T08:37:00Z,temp-A1,temperature,field-01,reading,28.947,38,ok +2025-09-01T08:38:00Z,hum-A2,humidity,field-01,reading,63.261,39,ok +2025-09-01T08:38:00Z,soil-A3,soil_moist,field-02,reading,37.432,20,ok +2025-09-01T08:38:00Z,temp-A1,temperature,field-01,reading,29.037,39,ok +2025-09-01T08:39:00Z,hum-A2,humidity,field-01,reading,64.03,40,ok +2025-09-01T08:39:00Z,temp-A1,temperature,field-01,reading,28.645,40,ok +2025-09-01T08:40:00Z,hum-A2,humidity,field-01,keepalive,,189,ok +2025-09-01T08:40:00Z,hum-A2,humidity,field-01,reading,63.832,41,ok +2025-09-01T08:40:00Z,soil-A3,soil_moist,field-02,keepalive,,99,ok +2025-09-01T08:40:00Z,soil-A3,soil_moist,field-02,reading,37.711,21,ok +2025-09-01T08:40:00Z,temp-A1,temperature,field-01,keepalive,,159,ok +2025-09-01T08:40:00Z,temp-A1,temperature,field-01,reading,28.906,41,ok +2025-09-01T08:41:00Z,hum-A2,humidity,field-01,reading,65.209,42,ok +2025-09-01T08:41:00Z,temp-A1,temperature,field-01,reading,28.727,42,ok +2025-09-01T08:42:00Z,hum-A2,humidity,field-01,reading,63.762,43,ok +2025-09-01T08:42:00Z,soil-A3,soil_moist,field-02,reading,37.306,22,ok +2025-09-01T08:42:00Z,temp-A1,temperature,field-01,reading,28.901,43,ok +2025-09-01T08:43:00Z,hum-A2,humidity,field-01,reading,63.871,44,ok +2025-09-01T08:43:00Z,temp-A1,temperature,field-01,reading,28.66,44,ok +2025-09-01T08:44:00Z,hum-A2,humidity,field-01,reading,63.329,45,ok +2025-09-01T08:44:00Z,soil-A3,soil_moist,field-02,reading,37.254,23,ok +2025-09-01T08:44:00Z,temp-A1,temperature,field-01,reading,28.491,45,ok +2025-09-01T08:45:00Z,hum-A2,humidity,field-01,keepalive,,190,ok +2025-09-01T08:45:00Z,hum-A2,humidity,field-01,reading,64.282,46,ok +2025-09-01T08:45:00Z,soil-A3,soil_moist,field-02,keepalive,,100,ok +2025-09-01T08:45:00Z,temp-A1,temperature,field-01,keepalive,,160,ok +2025-09-01T08:45:00Z,temp-A1,temperature,field-01,reading,28.492,46,ok +2025-09-01T08:46:00Z,hum-A2,humidity,field-01,reading,64.906,47,ok +2025-09-01T08:46:00Z,soil-A3,soil_moist,field-02,reading,37.008,24,ok +2025-09-01T08:46:00Z,temp-A1,temperature,field-01,reading,28.949,47,ok +2025-09-01T08:47:00Z,hum-A2,humidity,field-01,reading,63.857,48,ok +2025-09-01T08:47:00Z,temp-A1,temperature,field-01,reading,29.339,48,ok +2025-09-01T08:48:00Z,hum-A2,humidity,field-01,reading,64.492,49,ok +2025-09-01T08:48:00Z,soil-A3,soil_moist,field-02,reading,36.01,25,ok +2025-09-01T08:48:00Z,temp-A1,temperature,field-01,reading,28.96,49,ok +2025-09-01T08:49:00Z,hum-A2,humidity,field-01,reading,65.183,50,ok +2025-09-01T08:49:00Z,temp-A1,temperature,field-01,reading,28.817,50,ok +2025-09-01T08:50:00Z,hum-A2,humidity,field-01,keepalive,,191,ok +2025-09-01T08:50:00Z,hum-A2,humidity,field-01,reading,64.576,51,ok +2025-09-01T08:50:00Z,soil-A3,soil_moist,field-02,keepalive,,101,ok +2025-09-01T08:50:00Z,soil-A3,soil_moist,field-02,reading,36.857,26,ok +2025-09-01T08:50:00Z,temp-A1,temperature,field-01,keepalive,,161,ok +2025-09-01T08:50:00Z,temp-A1,temperature,field-01,reading,29.318,51,ok +2025-09-01T08:51:00Z,hum-A2,humidity,field-01,reading,64.326,52,ok +2025-09-01T08:51:00Z,temp-A1,temperature,field-01,reading,28.996,52,ok +2025-09-01T08:52:00Z,hum-A2,humidity,field-01,reading,65.575,53,ok +2025-09-01T08:52:00Z,soil-A3,soil_moist,field-02,reading,37.641,27,ok +2025-09-01T08:52:00Z,temp-A1,temperature,field-01,reading,28.98,53,ok +2025-09-01T08:53:00Z,hum-A2,humidity,field-01,reading,64.924,54,ok +2025-09-01T08:53:00Z,temp-A1,temperature,field-01,reading,29.02,54,ok +2025-09-01T08:54:00Z,hum-A2,humidity,field-01,reading,65.206,55,ok +2025-09-01T08:54:00Z,soil-A3,soil_moist,field-02,reading,37.643,28,ok +2025-09-01T08:54:00Z,temp-A1,temperature,field-01,reading,28.951,55,ok +2025-09-01T08:55:00Z,hum-A2,humidity,field-01,keepalive,,192,ok +2025-09-01T08:55:00Z,hum-A2,humidity,field-01,reading,65.862,56,ok +2025-09-01T08:55:00Z,soil-A3,soil_moist,field-02,keepalive,,102,ok +2025-09-01T08:55:00Z,temp-A1,temperature,field-01,keepalive,,162,ok +2025-09-01T08:55:00Z,temp-A1,temperature,field-01,reading,28.923,56,ok +2025-09-01T08:56:00Z,hum-A2,humidity,field-01,reading,65.87,57,ok +2025-09-01T08:56:00Z,soil-A3,soil_moist,field-02,reading,37.982,29,ok +2025-09-01T08:56:00Z,temp-A1,temperature,field-01,reading,28.704,57,ok +2025-09-01T08:57:00Z,hum-A2,humidity,field-01,reading,64.698,58,ok +2025-09-01T08:57:00Z,temp-A1,temperature,field-01,reading,29.095,58,ok +2025-09-01T08:58:00Z,hum-A2,humidity,field-01,reading,64.57,59,ok +2025-09-01T08:58:00Z,soil-A3,soil_moist,field-02,reading,37.763,30,ok +2025-09-01T08:58:00Z,temp-A1,temperature,field-01,reading,28.979,59,ok +2025-09-01T08:59:00Z,hum-A2,humidity,field-01,reading,65.226,60,ok +2025-09-01T08:59:00Z,temp-A1,temperature,field-01,reading,29.238,60,ok +2025-09-01T09:00:00Z,hum-A2,humidity,field-01,keepalive,,193,ok +2025-09-01T09:00:00Z,hum-A2,humidity,field-01,reading,64.893,61,ok +2025-09-01T09:00:00Z,soil-A3,soil_moist,field-02,keepalive,,103,ok +2025-09-01T09:00:00Z,soil-A3,soil_moist,field-02,reading,37.946,31,ok +2025-09-01T09:01:00Z,hum-A2,humidity,field-01,reading,63.474,62,ok +2025-09-01T09:02:00Z,hum-A2,humidity,field-01,reading,66.001,63,ok +2025-09-01T09:02:00Z,soil-A3,soil_moist,field-02,reading,38.295,32,ok +2025-09-01T09:03:00Z,hum-A2,humidity,field-01,reading,64.977,64,ok +2025-09-01T09:04:00Z,hum-A2,humidity,field-01,reading,65.524,65,ok +2025-09-01T09:04:00Z,soil-A3,soil_moist,field-02,reading,37.488,33,ok +2025-09-01T09:05:00Z,hum-A2,humidity,field-01,keepalive,,194,ok +2025-09-01T09:05:00Z,hum-A2,humidity,field-01,reading,63.963,66,ok +2025-09-01T09:05:00Z,soil-A3,soil_moist,field-02,keepalive,,104,ok +2025-09-01T09:06:00Z,hum-A2,humidity,field-01,reading,65.892,67,ok +2025-09-01T09:06:00Z,soil-A3,soil_moist,field-02,reading,37.812,34,ok +2025-09-01T09:07:00Z,hum-A2,humidity,field-01,reading,64.344,68,ok +2025-09-01T09:08:00Z,hum-A2,humidity,field-01,reading,64.674,69,ok +2025-09-01T09:08:00Z,soil-A3,soil_moist,field-02,reading,37.171,35,ok +2025-09-01T09:09:00Z,hum-A2,humidity,field-01,reading,63.579,70,ok +2025-09-01T09:10:00Z,hum-A2,humidity,field-01,keepalive,,195,ok +2025-09-01T09:10:00Z,hum-A2,humidity,field-01,reading,64.499,71,ok +2025-09-01T09:10:00Z,soil-A3,soil_moist,field-02,keepalive,,105,ok +2025-09-01T09:10:00Z,soil-A3,soil_moist,field-02,reading,38.475,36,ok +2025-09-01T09:11:00Z,hum-A2,humidity,field-01,reading,64.374,72,ok +2025-09-01T09:12:00Z,hum-A2,humidity,field-01,reading,64.329,73,ok +2025-09-01T09:12:00Z,soil-A3,soil_moist,field-02,reading,38.016,37,ok +2025-09-01T09:13:00Z,hum-A2,humidity,field-01,reading,64.741,74,ok +2025-09-01T09:14:00Z,hum-A2,humidity,field-01,reading,64.345,75,ok +2025-09-01T09:14:00Z,soil-A3,soil_moist,field-02,reading,38.245,38,ok +2025-09-01T09:15:00Z,hum-A2,humidity,field-01,keepalive,,196,ok +2025-09-01T09:15:00Z,hum-A2,humidity,field-01,reading,64.977,76,ok +2025-09-01T09:15:00Z,soil-A3,soil_moist,field-02,keepalive,,106,ok +2025-09-01T09:16:00Z,hum-A2,humidity,field-01,reading,63.715,77,ok +2025-09-01T09:16:00Z,soil-A3,soil_moist,field-02,reading,38.769,39,ok +2025-09-01T09:17:00Z,hum-A2,humidity,field-01,reading,63.18,78,ok +2025-09-01T09:18:00Z,hum-A2,humidity,field-01,reading,63.842,79,ok +2025-09-01T09:18:00Z,soil-A3,soil_moist,field-02,reading,38.53,40,ok +2025-09-01T09:19:00Z,hum-A2,humidity,field-01,reading,64.235,80,ok +2025-09-01T09:20:00Z,hum-A2,humidity,field-01,keepalive,,197,ok +2025-09-01T09:20:00Z,hum-A2,humidity,field-01,reading,64.597,81,ok +2025-09-01T09:20:00Z,soil-A3,soil_moist,field-02,keepalive,,107,ok +2025-09-01T09:20:00Z,soil-A3,soil_moist,field-02,reading,37.947,41,ok +2025-09-01T09:21:00Z,hum-A2,humidity,field-01,reading,64.597,82,ok +2025-09-01T09:22:00Z,hum-A2,humidity,field-01,reading,64.597,83,ok +2025-09-01T09:22:00Z,soil-A3,soil_moist,field-02,reading,38.353,42,ok +2025-09-01T09:23:00Z,hum-A2,humidity,field-01,reading,64.597,84,ok +2025-09-01T09:24:00Z,hum-A2,humidity,field-01,reading,64.597,85,ok +2025-09-01T09:24:00Z,soil-A3,soil_moist,field-02,reading,37.219,43,ok +2025-09-01T09:25:00Z,hum-A2,humidity,field-01,keepalive,,198,ok +2025-09-01T09:25:00Z,hum-A2,humidity,field-01,reading,64.597,86,ok +2025-09-01T09:25:00Z,soil-A3,soil_moist,field-02,keepalive,,108,ok +2025-09-01T09:26:00Z,hum-A2,humidity,field-01,reading,64.597,87,ok +2025-09-01T09:26:00Z,soil-A3,soil_moist,field-02,reading,38.123,44,ok +2025-09-01T09:27:00Z,hum-A2,humidity,field-01,reading,64.597,88,ok +2025-09-01T09:28:00Z,hum-A2,humidity,field-01,reading,64.597,89,ok +2025-09-01T09:28:00Z,soil-A3,soil_moist,field-02,reading,37.942,45,ok +2025-09-01T09:29:00Z,hum-A2,humidity,field-01,reading,64.597,90,ok +2025-09-01T09:30:00Z,hum-A2,humidity,field-01,keepalive,,199,ok +2025-09-01T09:30:00Z,hum-A2,humidity,field-01,reading,64.597,91,ok +2025-09-01T09:30:00Z,soil-A3,soil_moist,field-02,keepalive,,109,ok +2025-09-01T09:30:00Z,soil-A3,soil_moist,field-02,reading,37.024,46,ok +2025-09-01T09:30:00Z,temp-A1,temperature,field-01,keepalive,,163,ok +2025-09-01T09:30:00Z,temp-A1,temperature,field-01,reading,28.505,61,ok +2025-09-01T09:31:00Z,hum-A2,humidity,field-01,reading,64.597,92,ok +2025-09-01T09:31:00Z,temp-A1,temperature,field-01,reading,28.493,62,ok +2025-09-01T09:32:00Z,hum-A2,humidity,field-01,reading,64.597,93,ok +2025-09-01T09:32:00Z,soil-A3,soil_moist,field-02,reading,38.457,47,ok +2025-09-01T09:32:00Z,temp-A1,temperature,field-01,reading,28.316,63,ok +2025-09-01T09:33:00Z,hum-A2,humidity,field-01,reading,64.597,94,ok +2025-09-01T09:33:00Z,temp-A1,temperature,field-01,reading,28.263,64,ok +2025-09-01T09:34:00Z,hum-A2,humidity,field-01,reading,64.597,95,ok +2025-09-01T09:34:00Z,soil-A3,soil_moist,field-02,reading,37.387,48,ok +2025-09-01T09:34:00Z,temp-A1,temperature,field-01,reading,28.102,65,ok +2025-09-01T09:35:00Z,hum-A2,humidity,field-01,keepalive,,200,ok +2025-09-01T09:35:00Z,hum-A2,humidity,field-01,reading,64.597,96,ok +2025-09-01T09:35:00Z,soil-A3,soil_moist,field-02,keepalive,,110,ok +2025-09-01T09:35:00Z,temp-A1,temperature,field-01,keepalive,,164,ok +2025-09-01T09:35:00Z,temp-A1,temperature,field-01,reading,28.184,66,ok +2025-09-01T09:36:00Z,hum-A2,humidity,field-01,reading,64.597,97,ok +2025-09-01T09:36:00Z,soil-A3,soil_moist,field-02,reading,38.027,49,ok +2025-09-01T09:36:00Z,temp-A1,temperature,field-01,reading,28.605,67,ok +2025-09-01T09:37:00Z,hum-A2,humidity,field-01,reading,64.597,98,ok +2025-09-01T09:37:00Z,temp-A1,temperature,field-01,reading,28.283,68,ok +2025-09-01T09:38:00Z,hum-A2,humidity,field-01,reading,64.597,99,ok +2025-09-01T09:38:00Z,soil-A3,soil_moist,field-02,reading,37.416,50,ok +2025-09-01T09:38:00Z,temp-A1,temperature,field-01,reading,27.997,69,ok +2025-09-01T09:39:00Z,hum-A2,humidity,field-01,reading,64.597,100,ok +2025-09-01T09:39:00Z,temp-A1,temperature,field-01,reading,27.926,70,ok +2025-09-01T09:40:00Z,hum-A2,humidity,field-01,keepalive,,201,ok +2025-09-01T09:40:00Z,hum-A2,humidity,field-01,reading,64.597,101,ok +2025-09-01T09:40:00Z,soil-A3,soil_moist,field-02,keepalive,,111,ok +2025-09-01T09:40:00Z,soil-A3,soil_moist,field-02,reading,-10.0,51,corrupted +2025-09-01T09:40:00Z,temp-A1,temperature,field-01,keepalive,,165,ok +2025-09-01T09:40:00Z,temp-A1,temperature,field-01,reading,27.802,71,ok +2025-09-01T09:41:00Z,hum-A2,humidity,field-01,reading,64.597,102,ok +2025-09-01T09:41:00Z,temp-A1,temperature,field-01,reading,28.003,72,ok +2025-09-01T09:42:00Z,hum-A2,humidity,field-01,reading,64.597,103,ok +2025-09-01T09:42:00Z,soil-A3,soil_moist,field-02,reading,38.15,52,ok +2025-09-01T09:42:00Z,temp-A1,temperature,field-01,reading,27.96,73,ok +2025-09-01T09:43:00Z,hum-A2,humidity,field-01,reading,64.597,104,ok +2025-09-01T09:43:00Z,temp-A1,temperature,field-01,reading,27.545,74,ok +2025-09-01T09:44:00Z,hum-A2,humidity,field-01,reading,64.597,105,ok +2025-09-01T09:44:00Z,soil-A3,soil_moist,field-02,reading,37.006,53,ok +2025-09-01T09:44:00Z,temp-A1,temperature,field-01,reading,27.883,75,ok +2025-09-01T09:45:00Z,hum-A2,humidity,field-01,keepalive,,202,ok +2025-09-01T09:45:00Z,hum-A2,humidity,field-01,reading,64.597,106,ok +2025-09-01T09:45:00Z,soil-A3,soil_moist,field-02,keepalive,,112,ok +2025-09-01T09:45:00Z,temp-A1,temperature,field-01,keepalive,,166,ok +2025-09-01T09:45:00Z,temp-A1,temperature,field-01,reading,27.623,76,ok +2025-09-01T09:46:00Z,hum-A2,humidity,field-01,reading,64.597,107,ok +2025-09-01T09:46:00Z,soil-A3,soil_moist,field-02,reading,36.578,54,ok +2025-09-01T09:46:00Z,temp-A1,temperature,field-01,reading,27.973,77,ok +2025-09-01T09:47:00Z,hum-A2,humidity,field-01,reading,64.597,108,ok +2025-09-01T09:47:00Z,temp-A1,temperature,field-01,reading,27.669,78,ok +2025-09-01T09:48:00Z,hum-A2,humidity,field-01,reading,64.597,109,ok +2025-09-01T09:48:00Z,soil-A3,soil_moist,field-02,reading,37.196,55,ok +2025-09-01T09:48:00Z,temp-A1,temperature,field-01,reading,27.931,79,ok +2025-09-01T09:49:00Z,hum-A2,humidity,field-01,reading,64.597,110,ok +2025-09-01T09:49:00Z,temp-A1,temperature,field-01,reading,27.446,80,ok +2025-09-01T09:50:00Z,hum-A2,humidity,field-01,keepalive,,203,ok +2025-09-01T09:50:00Z,hum-A2,humidity,field-01,reading,64.597,111,ok +2025-09-01T09:50:00Z,soil-A3,soil_moist,field-02,keepalive,,113,ok +2025-09-01T09:50:00Z,soil-A3,soil_moist,field-02,reading,37.601,56,ok +2025-09-01T09:50:00Z,temp-A1,temperature,field-01,keepalive,,167,ok +2025-09-01T09:50:00Z,temp-A1,temperature,field-01,reading,27.429,81,ok +2025-09-01T09:51:00Z,hum-A2,humidity,field-01,reading,64.597,112,ok +2025-09-01T09:51:00Z,temp-A1,temperature,field-01,reading,27.495,82,ok +2025-09-01T09:52:00Z,hum-A2,humidity,field-01,reading,64.597,113,ok +2025-09-01T09:52:00Z,soil-A3,soil_moist,field-02,reading,36.902,57,ok +2025-09-01T09:52:00Z,temp-A1,temperature,field-01,reading,27.595,83,ok +2025-09-01T09:53:00Z,hum-A2,humidity,field-01,reading,64.597,114,ok +2025-09-01T09:53:00Z,temp-A1,temperature,field-01,reading,27.445,84,ok +2025-09-01T09:54:00Z,hum-A2,humidity,field-01,reading,64.597,115,ok +2025-09-01T09:54:00Z,soil-A3,soil_moist,field-02,reading,36.687,58,ok +2025-09-01T09:54:00Z,temp-A1,temperature,field-01,reading,27.033,85,ok +2025-09-01T09:55:00Z,hum-A2,humidity,field-01,keepalive,,204,ok +2025-09-01T09:55:00Z,hum-A2,humidity,field-01,reading,64.597,116,ok +2025-09-01T09:55:00Z,soil-A3,soil_moist,field-02,keepalive,,114,ok +2025-09-01T09:55:00Z,temp-A1,temperature,field-01,keepalive,,168,ok +2025-09-01T09:55:00Z,temp-A1,temperature,field-01,reading,27.264,86,ok +2025-09-01T09:56:00Z,hum-A2,humidity,field-01,reading,64.597,117,ok +2025-09-01T09:56:00Z,soil-A3,soil_moist,field-02,reading,37.142,59,ok +2025-09-01T09:56:00Z,temp-A1,temperature,field-01,reading,27.18,87,ok +2025-09-01T09:57:00Z,hum-A2,humidity,field-01,reading,64.597,118,ok +2025-09-01T09:57:00Z,temp-A1,temperature,field-01,reading,27.037,88,ok +2025-09-01T09:58:00Z,hum-A2,humidity,field-01,reading,64.597,119,ok +2025-09-01T09:58:00Z,soil-A3,soil_moist,field-02,reading,35.417,60,ok +2025-09-01T09:58:00Z,temp-A1,temperature,field-01,reading,26.941,89,ok +2025-09-01T09:59:00Z,hum-A2,humidity,field-01,reading,64.597,120,ok +2025-09-01T09:59:00Z,temp-A1,temperature,field-01,reading,27.367,90,ok +2025-09-01T10:00:00Z,hum-A2,humidity,field-01,keepalive,,205,ok +2025-09-01T10:00:00Z,hum-A2,humidity,field-01,reading,55.088,121,ok +2025-09-01T10:00:00Z,soil-A3,soil_moist,field-02,keepalive,,115,ok +2025-09-01T10:00:00Z,soil-A3,soil_moist,field-02,reading,36.362,61,ok +2025-09-01T10:00:00Z,temp-A1,temperature,field-01,keepalive,,169,ok +2025-09-01T10:00:00Z,temp-A1,temperature,field-01,reading,26.887,91,ok +2025-09-01T10:01:00Z,hum-A2,humidity,field-01,reading,54.422,122,ok +2025-09-01T10:01:00Z,temp-A1,temperature,field-01,reading,26.743,92,ok +2025-09-01T10:02:00Z,hum-A2,humidity,field-01,reading,53.028,123,ok +2025-09-01T10:02:00Z,soil-A3,soil_moist,field-02,reading,35.896,62,ok +2025-09-01T10:02:00Z,temp-A1,temperature,field-01,reading,26.987,93,ok +2025-09-01T10:03:00Z,hum-A2,humidity,field-01,reading,54.243,124,ok +2025-09-01T10:03:00Z,temp-A1,temperature,field-01,reading,26.833,94,ok +2025-09-01T10:04:00Z,hum-A2,humidity,field-01,reading,54.521,125,ok +2025-09-01T10:04:00Z,soil-A3,soil_moist,field-02,reading,36.626,63,ok +2025-09-01T10:04:00Z,temp-A1,temperature,field-01,reading,26.74,95,ok +2025-09-01T10:05:00Z,hum-A2,humidity,field-01,keepalive,,206,ok +2025-09-01T10:05:00Z,hum-A2,humidity,field-01,reading,53.395,126,ok +2025-09-01T10:05:00Z,soil-A3,soil_moist,field-02,keepalive,,116,ok +2025-09-01T10:05:00Z,temp-A1,temperature,field-01,keepalive,,170,ok +2025-09-01T10:05:00Z,temp-A1,temperature,field-01,reading,26.859,96,ok +2025-09-01T10:06:00Z,hum-A2,humidity,field-01,reading,53.198,127,ok +2025-09-01T10:06:00Z,soil-A3,soil_moist,field-02,reading,35.999,64,ok +2025-09-01T10:06:00Z,temp-A1,temperature,field-01,reading,26.749,97,ok +2025-09-01T10:07:00Z,hum-A2,humidity,field-01,reading,54.11,128,ok +2025-09-01T10:07:00Z,temp-A1,temperature,field-01,reading,26.672,98,ok +2025-09-01T10:08:00Z,hum-A2,humidity,field-01,reading,51.738,129,ok +2025-09-01T10:08:00Z,soil-A3,soil_moist,field-02,reading,35.695,65,ok +2025-09-01T10:08:00Z,temp-A1,temperature,field-01,reading,26.74,99,ok +2025-09-01T10:09:00Z,hum-A2,humidity,field-01,reading,51.284,130,ok +2025-09-01T10:09:00Z,temp-A1,temperature,field-01,reading,26.553,100,ok +2025-09-01T10:10:00Z,hum-A2,humidity,field-01,keepalive,,207,ok +2025-09-01T10:10:00Z,hum-A2,humidity,field-01,reading,51.705,131,ok +2025-09-01T10:10:00Z,soil-A3,soil_moist,field-02,keepalive,,117,ok +2025-09-01T10:10:00Z,soil-A3,soil_moist,field-02,reading,35.959,66,ok +2025-09-01T10:10:00Z,temp-A1,temperature,field-01,keepalive,,171,ok +2025-09-01T10:10:00Z,temp-A1,temperature,field-01,reading,26.322,101,ok +2025-09-01T10:11:00Z,hum-A2,humidity,field-01,reading,51.019,132,ok +2025-09-01T10:11:00Z,temp-A1,temperature,field-01,reading,26.323,102,ok +2025-09-01T10:12:00Z,hum-A2,humidity,field-01,reading,52.804,133,ok +2025-09-01T10:12:00Z,soil-A3,soil_moist,field-02,reading,35.283,67,ok +2025-09-01T10:12:00Z,temp-A1,temperature,field-01,reading,26.241,103,ok +2025-09-01T10:13:00Z,hum-A2,humidity,field-01,reading,51.727,134,ok +2025-09-01T10:13:00Z,temp-A1,temperature,field-01,reading,26.338,104,ok +2025-09-01T10:14:00Z,hum-A2,humidity,field-01,reading,50.543,135,ok +2025-09-01T10:14:00Z,soil-A3,soil_moist,field-02,reading,35.89,68,ok +2025-09-01T10:14:00Z,temp-A1,temperature,field-01,reading,26.031,105,ok +2025-09-01T10:15:00Z,hum-A2,humidity,field-01,keepalive,,208,ok +2025-09-01T10:15:00Z,hum-A2,humidity,field-01,reading,50.5,136,ok +2025-09-01T10:15:00Z,soil-A3,soil_moist,field-02,keepalive,,118,ok +2025-09-01T10:15:00Z,temp-A1,temperature,field-01,keepalive,,172,ok +2025-09-01T10:15:00Z,temp-A1,temperature,field-01,reading,25.832,106,ok +2025-09-01T10:16:00Z,hum-A2,humidity,field-01,reading,53.041,137,ok +2025-09-01T10:16:00Z,soil-A3,soil_moist,field-02,reading,35.434,69,ok +2025-09-01T10:16:00Z,temp-A1,temperature,field-01,reading,26.168,107,ok +2025-09-01T10:17:00Z,hum-A2,humidity,field-01,reading,50.027,138,ok +2025-09-01T10:17:00Z,temp-A1,temperature,field-01,reading,25.836,108,ok +2025-09-01T10:18:00Z,hum-A2,humidity,field-01,reading,49.672,139,ok +2025-09-01T10:18:00Z,soil-A3,soil_moist,field-02,reading,35.463,70,ok +2025-09-01T10:18:00Z,temp-A1,temperature,field-01,reading,25.666,109,ok +2025-09-01T10:19:00Z,hum-A2,humidity,field-01,reading,50.294,140,ok +2025-09-01T10:19:00Z,temp-A1,temperature,field-01,reading,26.085,110,ok +2025-09-01T10:20:00Z,hum-A2,humidity,field-01,keepalive,,209,ok +2025-09-01T10:20:00Z,hum-A2,humidity,field-01,reading,50.334,141,ok +2025-09-01T10:20:00Z,soil-A3,soil_moist,field-02,keepalive,,119,ok +2025-09-01T10:20:00Z,soil-A3,soil_moist,field-02,reading,35.168,71,ok +2025-09-01T10:20:00Z,temp-A1,temperature,field-01,keepalive,,173,ok +2025-09-01T10:20:00Z,temp-A1,temperature,field-01,reading,25.823,111,ok +2025-09-01T10:21:00Z,hum-A2,humidity,field-01,reading,49.778,142,ok +2025-09-01T10:21:00Z,temp-A1,temperature,field-01,reading,26.019,112,ok +2025-09-01T10:22:00Z,hum-A2,humidity,field-01,reading,48.654,143,ok +2025-09-01T10:22:00Z,soil-A3,soil_moist,field-02,reading,35.857,72,ok +2025-09-01T10:22:00Z,temp-A1,temperature,field-01,reading,25.77,113,ok +2025-09-01T10:23:00Z,hum-A2,humidity,field-01,reading,48.237,144,ok +2025-09-01T10:23:00Z,temp-A1,temperature,field-01,reading,25.609,114,ok +2025-09-01T10:24:00Z,hum-A2,humidity,field-01,reading,49.43,145,ok +2025-09-01T10:24:00Z,soil-A3,soil_moist,field-02,reading,35.221,73,ok +2025-09-01T10:24:00Z,temp-A1,temperature,field-01,reading,25.542,115,ok +2025-09-01T10:25:00Z,hum-A2,humidity,field-01,keepalive,,210,ok +2025-09-01T10:25:00Z,hum-A2,humidity,field-01,reading,48.702,146,ok +2025-09-01T10:25:00Z,soil-A3,soil_moist,field-02,keepalive,,120,ok +2025-09-01T10:25:00Z,temp-A1,temperature,field-01,keepalive,,174,ok +2025-09-01T10:25:00Z,temp-A1,temperature,field-01,reading,25.646,116,ok +2025-09-01T10:26:00Z,hum-A2,humidity,field-01,reading,47.229,147,ok +2025-09-01T10:26:00Z,soil-A3,soil_moist,field-02,reading,35.41,74,ok +2025-09-01T10:26:00Z,temp-A1,temperature,field-01,reading,25.654,117,ok +2025-09-01T10:27:00Z,hum-A2,humidity,field-01,reading,49.28,148,ok +2025-09-01T10:27:00Z,temp-A1,temperature,field-01,reading,25.504,118,ok +2025-09-01T10:28:00Z,hum-A2,humidity,field-01,reading,48.77,149,ok +2025-09-01T10:28:00Z,soil-A3,soil_moist,field-02,reading,35.261,75,ok +2025-09-01T10:28:00Z,temp-A1,temperature,field-01,reading,25.574,119,ok +2025-09-01T10:29:00Z,hum-A2,humidity,field-01,reading,47.767,150,ok +2025-09-01T10:29:00Z,temp-A1,temperature,field-01,reading,25.285,120,ok +2025-09-01T10:30:00Z,hum-A2,humidity,field-01,keepalive,,211,ok +2025-09-01T10:30:00Z,hum-A2,humidity,field-01,reading,47.567,151,ok +2025-09-01T10:30:00Z,soil-A3,soil_moist,field-02,keepalive,,121,ok +2025-09-01T10:30:00Z,soil-A3,soil_moist,field-02,reading,34.491,76,ok +2025-09-01T10:30:00Z,temp-A1,temperature,field-01,keepalive,,175,ok +2025-09-01T10:30:00Z,temp-A1,temperature,field-01,reading,25.558,121,ok +2025-09-01T10:31:00Z,hum-A2,humidity,field-01,reading,47.781,152,ok +2025-09-01T10:31:00Z,temp-A1,temperature,field-01,reading,25.16,122,ok +2025-09-01T10:32:00Z,hum-A2,humidity,field-01,reading,46.872,153,ok +2025-09-01T10:32:00Z,soil-A3,soil_moist,field-02,reading,35.052,77,ok +2025-09-01T10:32:00Z,temp-A1,temperature,field-01,reading,25.446,123,ok +2025-09-01T10:33:00Z,hum-A2,humidity,field-01,reading,46.174,154,ok +2025-09-01T10:33:00Z,temp-A1,temperature,field-01,reading,25.59,124,ok +2025-09-01T10:34:00Z,hum-A2,humidity,field-01,reading,47.347,155,ok +2025-09-01T10:34:00Z,soil-A3,soil_moist,field-02,reading,34.815,78,ok +2025-09-01T10:34:00Z,temp-A1,temperature,field-01,reading,25.636,125,ok +2025-09-01T10:35:00Z,hum-A2,humidity,field-01,keepalive,,212,ok +2025-09-01T10:35:00Z,hum-A2,humidity,field-01,reading,45.779,156,ok +2025-09-01T10:35:00Z,soil-A3,soil_moist,field-02,keepalive,,122,ok +2025-09-01T10:35:00Z,temp-A1,temperature,field-01,keepalive,,176,ok +2025-09-01T10:35:00Z,temp-A1,temperature,field-01,reading,25.729,126,ok +2025-09-01T10:36:00Z,hum-A2,humidity,field-01,reading,47.09,157,ok +2025-09-01T10:36:00Z,soil-A3,soil_moist,field-02,reading,34.672,79,ok +2025-09-01T10:36:00Z,temp-A1,temperature,field-01,reading,25.044,127,ok +2025-09-01T10:37:00Z,hum-A2,humidity,field-01,reading,45.478,158,ok +2025-09-01T10:37:00Z,temp-A1,temperature,field-01,reading,25.478,128,ok +2025-09-01T10:38:00Z,hum-A2,humidity,field-01,reading,46.41,159,ok +2025-09-01T10:38:00Z,soil-A3,soil_moist,field-02,reading,34.191,80,ok +2025-09-01T10:38:00Z,temp-A1,temperature,field-01,reading,25.539,129,ok +2025-09-01T10:39:00Z,hum-A2,humidity,field-01,reading,46.246,160,ok +2025-09-01T10:39:00Z,temp-A1,temperature,field-01,reading,25.467,130,ok +2025-09-01T10:40:00Z,hum-A2,humidity,field-01,keepalive,,213,ok +2025-09-01T10:40:00Z,hum-A2,humidity,field-01,reading,47.651,161,ok +2025-09-01T10:40:00Z,soil-A3,soil_moist,field-02,keepalive,,123,ok +2025-09-01T10:40:00Z,soil-A3,soil_moist,field-02,reading,34.053,81,ok +2025-09-01T10:40:00Z,temp-A1,temperature,field-01,keepalive,,177,ok +2025-09-01T10:40:00Z,temp-A1,temperature,field-01,reading,25.059,131,ok +2025-09-01T10:41:00Z,hum-A2,humidity,field-01,reading,45.25,162,ok +2025-09-01T10:41:00Z,temp-A1,temperature,field-01,reading,25.303,132,ok +2025-09-01T10:42:00Z,hum-A2,humidity,field-01,reading,44.853,163,ok +2025-09-01T10:42:00Z,soil-A3,soil_moist,field-02,reading,34.537,82,ok +2025-09-01T10:42:00Z,temp-A1,temperature,field-01,reading,25.569,133,ok +2025-09-01T10:43:00Z,hum-A2,humidity,field-01,reading,45.357,164,ok +2025-09-01T10:43:00Z,temp-A1,temperature,field-01,reading,24.875,134,ok +2025-09-01T10:44:00Z,hum-A2,humidity,field-01,reading,44.871,165,ok +2025-09-01T10:44:00Z,soil-A3,soil_moist,field-02,reading,35.114,83,ok +2025-09-01T10:44:00Z,temp-A1,temperature,field-01,reading,25.249,135,ok +2025-09-01T10:45:00Z,hum-A2,humidity,field-01,keepalive,,214,ok +2025-09-01T10:45:00Z,hum-A2,humidity,field-01,reading,45.2,166,ok +2025-09-01T10:45:00Z,soil-A3,soil_moist,field-02,keepalive,,124,ok +2025-09-01T10:45:00Z,temp-A1,temperature,field-01,keepalive,,178,ok +2025-09-01T10:45:00Z,temp-A1,temperature,field-01,reading,25.357,136,ok +2025-09-01T10:46:00Z,hum-A2,humidity,field-01,reading,45.917,167,ok +2025-09-01T10:46:00Z,soil-A3,soil_moist,field-02,reading,33.488,84,ok +2025-09-01T10:46:00Z,temp-A1,temperature,field-01,reading,24.977,137,ok +2025-09-01T10:47:00Z,hum-A2,humidity,field-01,reading,46.112,168,ok +2025-09-01T10:47:00Z,temp-A1,temperature,field-01,reading,25.216,138,ok +2025-09-01T10:48:00Z,hum-A2,humidity,field-01,reading,46.744,169,ok +2025-09-01T10:48:00Z,soil-A3,soil_moist,field-02,reading,34.053,85,ok +2025-09-01T10:48:00Z,temp-A1,temperature,field-01,reading,24.869,139,ok +2025-09-01T10:49:00Z,hum-A2,humidity,field-01,reading,44.755,170,ok +2025-09-01T10:49:00Z,temp-A1,temperature,field-01,reading,24.906,140,ok +2025-09-01T10:50:00Z,hum-A2,humidity,field-01,keepalive,,215,ok +2025-09-01T10:50:00Z,hum-A2,humidity,field-01,reading,47.39,171,ok +2025-09-01T10:50:00Z,soil-A3,soil_moist,field-02,keepalive,,125,ok +2025-09-01T10:50:00Z,soil-A3,soil_moist,field-02,reading,33.488,86,ok +2025-09-01T10:50:00Z,temp-A1,temperature,field-01,keepalive,,179,ok +2025-09-01T10:50:00Z,temp-A1,temperature,field-01,reading,24.892,141,ok +2025-09-01T10:51:00Z,hum-A2,humidity,field-01,reading,46.807,172,ok +2025-09-01T10:51:00Z,temp-A1,temperature,field-01,reading,25.083,142,ok +2025-09-01T10:52:00Z,hum-A2,humidity,field-01,reading,45.007,173,ok +2025-09-01T10:52:00Z,soil-A3,soil_moist,field-02,reading,33.951,87,ok +2025-09-01T10:52:00Z,temp-A1,temperature,field-01,reading,25.016,143,ok +2025-09-01T10:53:00Z,hum-A2,humidity,field-01,reading,43.291,174,ok +2025-09-01T10:53:00Z,temp-A1,temperature,field-01,reading,25.216,144,ok +2025-09-01T10:54:00Z,hum-A2,humidity,field-01,reading,45.02,175,ok +2025-09-01T10:54:00Z,soil-A3,soil_moist,field-02,reading,33.667,88,ok +2025-09-01T10:54:00Z,temp-A1,temperature,field-01,reading,24.829,145,ok +2025-09-01T10:55:00Z,hum-A2,humidity,field-01,keepalive,,216,ok +2025-09-01T10:55:00Z,hum-A2,humidity,field-01,reading,45.113,176,ok +2025-09-01T10:55:00Z,soil-A3,soil_moist,field-02,keepalive,,126,ok +2025-09-01T10:55:00Z,temp-A1,temperature,field-01,keepalive,,180,ok +2025-09-01T10:55:00Z,temp-A1,temperature,field-01,reading,24.985,146,ok +2025-09-01T10:56:00Z,hum-A2,humidity,field-01,reading,43.192,177,ok +2025-09-01T10:56:00Z,soil-A3,soil_moist,field-02,reading,33.871,89,ok +2025-09-01T10:56:00Z,temp-A1,temperature,field-01,reading,25.198,147,ok +2025-09-01T10:57:00Z,hum-A2,humidity,field-01,reading,45.073,178,ok +2025-09-01T10:57:00Z,temp-A1,temperature,field-01,reading,25.115,148,ok +2025-09-01T10:58:00Z,hum-A2,humidity,field-01,reading,44.843,179,ok +2025-09-01T10:58:00Z,soil-A3,soil_moist,field-02,reading,31.839,90,ok +2025-09-01T10:58:00Z,temp-A1,temperature,field-01,reading,24.736,149,ok +2025-09-01T10:59:00Z,hum-A2,humidity,field-01,reading,44.371,180,ok +2025-09-01T10:59:00Z,temp-A1,temperature,field-01,reading,25.133,150,ok diff --git a/AgCloud/services/sensorGuard/pytest.ini b/AgCloud/services/sensorGuard/pytest.ini new file mode 100644 index 000000000..678c989ca --- /dev/null +++ b/AgCloud/services/sensorGuard/pytest.ini @@ -0,0 +1,16 @@ +[tool:pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + --verbose + --tb=short + --cov=flink_app + --cov-report=html:coverage_html + --cov-report=term-missing + --cov-fail-under=70 +markers = + unit: Unit tests + integration: Integration tests + slow: Slow tests \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/requirements.txt b/AgCloud/services/sensorGuard/requirements.txt new file mode 100644 index 000000000..e70d76a76 --- /dev/null +++ b/AgCloud/services/sensorGuard/requirements.txt @@ -0,0 +1,7 @@ +pyyaml +kafka-python>=2.0.2 +requests +urllib3 +grpcio +avro-python3==1.10.2 +protobuf==3.20.3 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/Dockerfile.flink b/AgCloud/services/sensorGuard/sensorGuard/Dockerfile.flink new file mode 100644 index 000000000..8d94314f5 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/Dockerfile.flink @@ -0,0 +1,135 @@ +FROM flink:1.19.3-scala_2.12-java11 + +USER root + +COPY certs/*.crt /app/certs/ +RUN if [ -d ./certs ] && [ "$(ls ./certs/*.crt 2>/dev/null)" ]; then \ + echo "Configuring NetFree certificates..."; \ + cp ./certs/*.crt /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ + fi +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip ca-certificates curl libgomp1; \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +# Ensure lib directory exists +RUN mkdir -p /opt/flink/lib + +# venv + PATH +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python \ + PYTHONUNBUFFERED=1 + +# Configure pip to use SSL certificates +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +COPY requirements.txt /tmp/requirements.txt +# Install Python dependencies including PyFlink +RUN pip install -r /tmp/requirements.txt && \ + pip install "apache-flink==1.19.3" + +# Compatible versions for PyFlink 1.19.3 + +# kafka-clients +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +WORKDIR /opt/app +COPY flink_app/main.py /opt/app/main.py +COPY flink_app/core /opt/app/core +COPY flink_app/io_mod /opt/app/io_mod +COPY flink_app/config /opt/app/config +COPY flink_app/api /opt/app/api + +RUN mkdir -p /opt/app/resources + +RUN chown -R flink:flink /opt/app /opt/flink && chmod -R g+rwX /opt/app +USER flink + +# Default environment variables +ENV KAFKA_BROKERS=kafka:9092 \ + IN_TOPIC=sensors \ + OUT_TOPIC=event_logs_sensors \ + KAFKA_GROUP_ID=flink-device-pipeline \ + PYTHONPATH=/opt/app \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python + +USER root + +COPY netfree-ca.crt /usr/local/share/ca-certificates/corp-ca.crt +RUN chmod 644 /usr/local/share/ca-certificates/corp-ca.crt && update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + python3 python3-venv python3-pip ca-certificates curl libgomp1; \ + rm -rf /var/lib/apt/lists/* + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +RUN mkdir -p /opt/flink/lib + +RUN python3 -m venv /opt/venv +ENV PATH="/opt/venv/bin:${PATH}" \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python \ + PYTHONUNBUFFERED=1 + +RUN printf "[global]\ntrusted-host = pypi.org\n\tfiles.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +COPY requirements.txt /tmp/requirements.txt +RUN pip install -r /tmp/requirements.txt && \ + pip install "apache-flink==1.19.3" + + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar \ + -o /opt/flink/lib/kafka-clients-3.7.0.jar + +RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar \ + -o /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar + +WORKDIR /opt/app +COPY flink_app/main.py /opt/app/main.py +COPY flink_app/core /opt/app/core +COPY flink_app/io_mod /opt/app/io_mod +COPY flink_app/config /opt/app/config +COPY flink_app/api /opt/app/api + +RUN mkdir -p /opt/app/secrets && \ + chown -R flink:flink /opt/app /opt/flink /opt/app/secrets && \ + chmod -R g+rwX /opt/app && \ + chmod 775 /opt/app/secrets + +# Copy and set up entrypoint script +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["jobmanager"] + +ENV KAFKA_BROKERS=kafka:9092 \ + IN_TOPIC=sensors \ + OUT_TOPIC=event_logs_sensors \ + KAFKA_GROUP_ID=flink-device-pipeline \ + PYTHONPATH=/opt/app \ + PYFLINK_CLIENT_EXECUTABLE=/opt/venv/bin/python \ + PYFLINK_PYTHON=/opt/venv/bin/python diff --git a/AgCloud/services/sensorGuard/sensorGuard/README.md b/AgCloud/services/sensorGuard/sensorGuard/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/sensorGuard/docker-compose.yml b/AgCloud/services/sensorGuard/sensorGuard/docker-compose.yml new file mode 100644 index 000000000..bacbeb99d --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/docker-compose.yml @@ -0,0 +1,42 @@ +version: "3.9" + +services: + jobmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-jobmanager + command: jobmanager + ports: + - "8081:8081" + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - KAFKA_GROUP_ID=flink-device-pipeline + networks: + - ag_cloud + volumes: + - ./secrets:/opt/app/secrets + + taskmanager: + build: + context: . + dockerfile: Dockerfile.flink + container_name: flink-taskmanager + command: taskmanager -D taskmanager.numberOfTaskSlots=4 + depends_on: + - jobmanager + environment: + - JOB_MANAGER_RPC_ADDRESS=jobmanager + - KAFKA_BROKERS=kafka:9092 + - taskmanager.numberOfTaskSlots=4 + - KAFKA_GROUP_ID=flink-device-pipeline + networks: + - ag_cloud + volumes: + - ./secrets:/opt/app/secrets + +networks: + ag_cloud: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/services/sensorGuard/sensorGuard/entrypoint.sh b/AgCloud/services/sensorGuard/sensorGuard/entrypoint.sh new file mode 100644 index 000000000..a301d88f6 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +# Fix permissions for secrets directory if it exists (runs as root) +if [ -d "/opt/app/secrets" ]; then + echo "Fixing permissions for /opt/app/secrets..." + chown -R flink:flink /opt/app/secrets + chmod 775 /opt/app/secrets +fi + +# Call the original Flink entrypoint +exec /docker-entrypoint.sh "$@" diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/__init__.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/__init__.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/auth.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/auth.py new file mode 100644 index 000000000..ed57b5d02 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/auth.py @@ -0,0 +1,74 @@ +import os +import pathlib +import requests +import time + +# === CONFIG === +DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001") +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/opt/app/secrets/db_api_token") +DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "flink_job_sensors") +ROTATE_IF_EXISTS = True # Can be set to False if token rotation is not desired on restart + +# === PATH HELPERS === +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + +def _read_token_from_file(path: str) -> str | None: + try: + p = pathlib.Path(path) + if p.exists(): + token = p.read_text(encoding="utf-8").strip() + if token and len(token) > 10: + return token + except Exception: + pass + return None + +def _write_token_to_file(path: str, token: str) -> None: + p = pathlib.Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + print(f"[AUTH] Token saved to {path}", flush=True) + +# === FETCH LOGIC === +def _fetch_token_via_bootstrap(base: str, retries: int = 3, backoff: float = 1.0) -> str | None: + url = _safe_join_url(base, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": ROTATE_IF_EXISTS} + + for attempt in range(1, retries + 1): + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code not in (200, 201): + print(f"[AUTH] Bootstrap failed ({r.status_code}): {r.text[:200]}", flush=True) + time.sleep(backoff * attempt) + continue + + data = r.json() + raw = (data.get("service_account", {}) or {}).get("raw_token") \ + or (data.get("service_account", {}) or {}).get("token") + + if raw and isinstance(raw, str) and raw.strip() and "***" not in raw: + print("[AUTH] Token fetched successfully", flush=True) + return raw.strip() + except Exception as e: + print(f"[AUTH] Exception: {e}", flush=True) + time.sleep(backoff * attempt) + print("[AUTH] Failed to bootstrap service token", flush=True) + return None + +# === PUBLIC API === +def get_access_token(base_url: str | None = None) -> str: + """ + Loads token from file if exists, otherwise bootstraps new one via /auth/_dev_bootstrap. + Returns a valid token string. + """ + base = base_url or DB_API_BASE + token = _read_token_from_file(DB_API_TOKEN_FILE) + if token: + return token + + new_token = _fetch_token_via_bootstrap(base) + if new_token: + _write_token_to_file(DB_API_TOKEN_FILE, new_token) + return new_token + raise RuntimeError("[AUTH] Could not obtain or save service token") diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/devices_client.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/devices_client.py new file mode 100644 index 000000000..7dfa2924f --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/devices_client.py @@ -0,0 +1,74 @@ +""" +api/devices_client.py +----------------------------------- +Fetches all active sensors (devices) from the API +and returns their IDs and models. +""" +import requests +from typing import Iterable, Tuple + + +def list_active_sensors(api_base: str, token: str, timeout: float = 10.0) -> Iterable[str]: + """ + Fetch all sensors from the devices_sensor table. + + Args: + api_base: Base URL of the API (e.g., "http://localhost:8001") + token: Access token (returned from get_access_token) + timeout: HTTP request timeout in seconds + + Yields: + Device IDs as strings. + """ + url = f"{api_base.rstrip('/')}/api/tables/devices_sensor" + headers = {"X-Service-Token": token} + + try: + response = requests.get(url, headers=headers, timeout=timeout) + if response.status_code != 200: + print(f"[DEVICES] Failed ({response.status_code}): {response.text[:120]}") + return + + items = (response.json() or {}).get("rows", []) + print(f"[DEVICES] Fetched {len(items)} sensors from API") + for dev in items: + # All sensors in table are active, just return the IDs + device_id = dev.get("id", "") + if device_id: + print(f"[DEVICES] Adding sensor: id={device_id}") + yield str(device_id) + + except requests.RequestException as e: + print(f"[DEVICES] Request error: {e}") + return + + +def get_sensors_last_seen(api_base: str, token: str, timeout: float = 10.0): + """ + Fetch all sensors from devices_sensor with their last_seen timestamp. + Used for silence sweep. + + Args: + api_base: Base URL of the API. + token: Service token. + timeout: Request timeout. + + Returns: + List of dicts like: [{"id": "dev-a", "sensor_type": "temp", "last_seen": "2025-11-11T13:00:00Z"}, ...] + """ + url = f"{api_base.rstrip('/')}/api/tables/devices_sensor" + headers = {"X-Service-Token": token} + + try: + response = requests.get(url, headers=headers, timeout=timeout) + if response.status_code != 200: + print(f"[DEVICES] Failed ({response.status_code}): {response.text[:120]}") + return [] + + items = (response.json() or {}).get("rows", []) + print(f"[DEVICES] Fetched {len(items)} sensors (with last_seen) from API") + return items + + except requests.RequestException as e: + print(f"[DEVICES][ERROR] {e}") + return [] diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/devices_updater.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/devices_updater.py new file mode 100644 index 000000000..534d2893f --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/api/devices_updater.py @@ -0,0 +1,30 @@ +import requests +from datetime import datetime, timezone +from api.auth import get_access_token + +def update_device_last_seen(device_id: str): + """ + Updates the 'last_seen' field for a specific device in the devices_sensor table. + Uses PATCH /api/tables/devices_sensor + """ + api_base = "http://host.docker.internal:8001" + token = get_access_token(api_base) + headers = { + "X-Service-Token": token, + "Content-Type": "application/json" + } + url = f"{api_base}/api/tables/devices_sensor" + + payload = { + "keys": {"id": device_id}, + "data": {"last_seen": datetime.now(timezone.utc).isoformat()} + } + + try: + r = requests.patch(url, json=payload, headers=headers, timeout=10) + if r.status_code == 200: + print(f"[DB-UPDATER] Updated last_seen for device {device_id}") + else: + print(f"[DB-UPDATER] Failed ({r.status_code}): {r.text}") + except Exception as e: + print(f"[DB-UPDATER] Exception: {e}") diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/__init__.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/rules.yaml b/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/rules.yaml new file mode 100644 index 000000000..dd718a617 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/rules.yaml @@ -0,0 +1,25 @@ +defaults: + expected_interval_seconds: 60 + keepalive_miss_factor: 2 + prolonged_silence_seconds: 120 + silence_sweep_interval_seconds: 120 + +ranges: + temperature: { min: 21, max: 29 } + Ambient_Temperature: { min: 21, max: 29 } + humidity: { min: 0, max: 100 } + Humidity: { min: 0, max: 100 } + soil_moist: { min: 0, max: 100 } + Soil_Moisture: { min: 0, max: 100 } + unknown_sensor: { min: 21, max: 29 } # Default range - using temperature range for testing + +stuck: + epsilon: 0.1 + min_run_length: 3 + min_duration_seconds: 600 + +features: + corrupted: true + out_of_range: true + stuck_sensor: true + silence: true \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/settings.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/settings.py new file mode 100644 index 000000000..59853c063 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/config/settings.py @@ -0,0 +1,21 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +class Settings: + # --- API Configuration --- + DEVICES_API_BASE = os.getenv("DEVICES_API_BASE", "http://host.docker.internal:8001") + DEVICES_API_TOKEN = os.getenv("DEVICES_API_TOKEN", None) + + # --- Kafka Configuration --- + KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") + IN_TOPIC = os.getenv("IN_TOPIC", "sensors") + OUT_TOPIC = os.getenv("OUT_TOPIC", "event_logs_sensors") + KAFKA_GROUP_ID = os.getenv("KAFKA_GROUP_ID", "flink-device-pipeline") + + # --- Flink runtime paths --- + PYTHON_EXEC = os.getenv("PYFLINK_PYTHON", "/opt/venv/bin/python") + RULES_FILE = BASE_DIR / "config" / "rules.yaml" + +settings = Settings() diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/__init__.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/engine.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/engine.py new file mode 100644 index 000000000..c4669b16b --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/engine.py @@ -0,0 +1,181 @@ +# core/engine.py + +from .state import StateStore +from .types import Event, Alert +from .rules import corrupted, out_of_range, stuck_sensor +from datetime import datetime, timezone +import time + +from api.devices_updater import update_device_last_seen +from api.devices_client import get_sensors_last_seen +from api.auth import get_access_token + + +class Engine: + def __init__(self, cfg, writer, state: StateStore | None = None): + """ + cfg: dict read from rules.yaml (includes features/ranges/defaults/stuck) + writer: either a single object with write(alert) or a list of writers + """ + self.cfg = cfg + self.writers = writer if isinstance(writer, (list, tuple)) else [writer] + self.state = state or StateStore() + + # --- API info & persistent token --- + self.api_base = "http://host.docker.internal:8001" + self.token = get_access_token(self.api_base) + if self.token: + print("[ENGINE] Access token acquired successfully.") + else: + print("[ENGINE][WARN] Failed to get API token at startup.") + + # --- Utilities --------------------------------------------------------- + + def _emit(self, alert: Alert): + for w in self.writers: + w.write(alert) + + def _open_once(self, dev_state, alert: Alert): + """Open an event only if it’s not already open for the same issue_type.""" + if alert.issue_type not in dev_state.open_alerts: + dev_state.open_alerts[alert.issue_type] = alert + print(f"[ENGINE] Opening new alert: {alert.issue_type} for device {alert.device_id}") + self._emit(alert) + + def _close_if_open(self, dev_state, issue_type: str, ts): + """Close an open event (if exists) and update end_ts.""" + if issue_type in dev_state.open_alerts: + a = dev_state.open_alerts.pop(issue_type) + a.end_ts = ts + print(f"[ENGINE] Closing alert: {issue_type} for device {a.device_id}") + self._emit(a) + + def _close_all_keepalive_alerts(self, dev_state, ts): + """Close missing_keepalive alert when sensor sends valid data.""" + print(f"[ENGINE] Checking open alerts before close_all_keepalive_alerts: {list(dev_state.open_alerts.keys())}") + + if "missing_keepalive" in dev_state.open_alerts: + a = dev_state.open_alerts.pop("missing_keepalive") + a.end_ts = ts + print(f"[ENGINE] Closing missing_keepalive alert (sensor back online) for {a.device_id}") + self._emit(a) + else: + print(f"[ENGINE] No missing_keepalive alert to close for this device") + + # ---------------------------------------------------------------------- + + def sweep_silence(self, now): + """ + Periodic silence check based on DB 'devices_sensor.last_seen'. + Checks for missing_keepalive (not prolonged_silence). + """ + print("[ENGINE] Starting silence sweep (via DB API)...") + + # Fetch sensors via API (single attempt) + sensors = get_sensors_last_seen(self.api_base, self.token) + + if not sensors: + print("[ENGINE][ERROR] No sensors retrieved. Skipping silence sweep.") + return + + expected = self.cfg.get("expected_interval_seconds", 60) + miss_factor = self.cfg.get("keepalive_miss_factor", 3) + miss_thr = miss_factor * expected + print(f"[ENGINE] Checking {len(sensors)} sensors for missing keepalive > {miss_thr}s") + + for s in sensors: + sensor_id = s.get("id") + last_seen_str = s.get("last_seen") + if not sensor_id or not last_seen_str: + continue + + try: + last_seen = datetime.fromisoformat(last_seen_str.replace("Z", "+00:00")) + except Exception: + print(f"[ENGINE][WARN] Invalid timestamp for {sensor_id}: {last_seen_str}") + continue + + gap = (now - last_seen).total_seconds() + + # Check for missing_keepalive only + dev_state = self.state.get(sensor_id) + if dev_state and gap >= miss_thr and "missing_keepalive" not in dev_state.open_alerts: + alert = Alert( + issue_type="missing_keepalive", + device_id=sensor_id, + sensor_type=s.get("sensor_type", "unknown"), + site_id=None, + severity="critical", + start_ts=last_seen, + end_ts=None, + details={"gap_sec": int(gap), "expected": expected}, + ) + print(f"[ENGINE] Sensor {sensor_id} missing keepalive for {int(gap)}s — creating alert.") + dev_state.open_alerts["missing_keepalive"] = alert + self._emit(alert) + elif dev_state and gap < miss_thr and "missing_keepalive" in dev_state.open_alerts: + # Close the alert if gap is back to normal + alert = dev_state.open_alerts.pop("missing_keepalive") + alert.end_ts = now + print(f"[ENGINE] Sensor {sensor_id} keepalive restored — closing alert.") + self._emit(alert) + + # ---------------------------------------------------------------------- + + def process_event(self, ev: Event): + """Process a single event and manage open/close logic for alerts.""" + print(f"[ENGINE] Processing event: device_id={ev.device_id}, msg_type={ev.msg_type}, sensor_type={ev.sensor_type}") + + if not self.state.is_known_device(ev.device_id): + print(f"[ENGINE] Unknown device {ev.device_id} - skipping") + return + + print(f"[ENGINE] Known device {ev.device_id} - processing") + feats = (self.cfg.get("features") or {}) + dev = self.state.get(ev.device_id) + + # === Step 1: Update device state and DB === + print(f"[ENGINE] Updating device {ev.device_id} last_seen_ts from {dev.last_seen_ts} to {ev.ts}") + dev.last_seen_ts = ev.ts + dev.last_value = ev.value + + # --- Update API record --- + update_device_last_seen(ev.device_id) + + if ev.sensor_type and ev.sensor_type != "unknown_sensor": + dev.sensor_type = ev.sensor_type + + # === Step 2: Close keepalive-related alerts === + self._close_all_keepalive_alerts(dev, ev.ts) + + # === Step 3: Corrupted readings === + if feats.get("corrupted", True): + a = corrupted(ev, self.cfg) + if a: + print(f"[ENGINE] Corrupted reading detected for device {ev.device_id}") + self._open_once(dev, a) + self._close_if_open(dev, "out_of_range", ev.ts) + self._close_if_open(dev, "stuck_sensor", ev.ts) + return + print(f"[ENGINE] No corrupted reading for device {ev.device_id}") + + # === Step 4: Out-of-range checks === + if feats.get("out_of_range", True): + print(f"[ENGINE] Checking out_of_range for device {ev.device_id}, value={ev.value}") + a = out_of_range(ev, self.cfg) + if a: + print(f"[ENGINE] Out-of-range detected for device {ev.device_id}: {a}") + self._open_once(dev, a) + else: + print(f"[ENGINE] Value in range for device {ev.device_id}") + self._close_if_open(dev, "out_of_range", ev.ts) + + # === Step 5: Stuck sensor checks === + if feats.get("stuck_sensor", True): + print(f"[ENGINE] Checking stuck_sensor for device {ev.device_id}") + a = stuck_sensor(ev, dev, self.cfg) + if a: + print(f"[ENGINE] Stuck sensor detected for device {ev.device_id}") + self._open_once(dev, a) + else: + self._close_if_open(dev, "stuck_sensor", ev.ts) diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/rules.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/rules.py new file mode 100644 index 000000000..36f22d537 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/rules.py @@ -0,0 +1,159 @@ +from typing import Optional, Dict, Any +from datetime import timezone +from .types import Event, Alert, DeviceState + +def _utc(dt): + """Return datetime with UTC tzinfo.""" + return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc) + +def out_of_range(event: Event, cfg: Dict[str, Any]) -> Optional[Alert]: + """Check if sensor value is outside configured min/max.""" + if event.msg_type not in ["reading", "telemetry"] or event.value is None: + return None + rngs = (cfg or {}).get("ranges", {}) + lim = rngs.get(event.sensor_type, {}) + vmin, vmax = lim.get("min"), lim.get("max") + print(f"[RULES] out_of_range: sensor_type={event.sensor_type}, value={event.value}, lim={lim}, vmin={vmin}, vmax={vmax}") + if vmin is not None and event.value < vmin: + return Alert( + device_id=event.device_id, issue_type="out_of_range", + start_ts=_utc(event.ts), end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"value": event.value, "min": vmin, "max": vmax} + ) + if vmax is not None and event.value > vmax: + return Alert( + device_id=event.device_id, issue_type="out_of_range", + start_ts=_utc(event.ts), end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"value": event.value, "min": vmin, "max": vmax} + ) + return None + +def corrupted(event: Event, cfg: Dict[str, Any]) -> Optional[Alert]: + """Check if reading is invalid (null, non-numeric, bad quality).""" + if event.msg_type not in ["reading", "telemetry"]: + return None + if event.value is None: + return Alert( + device_id=event.device_id, issue_type="corrupted", + start_ts=_utc(event.ts), end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"reason": "null value"} + ) + + if not isinstance(event.value, (int, float)): + return Alert( + device_id=event.device_id, issue_type="corrupted", + start_ts=_utc(event.ts), end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"reason": "non-numeric"} + ) + + if event.quality and event.quality != "ok": + return Alert( + device_id=event.device_id, issue_type="corrupted", + start_ts=_utc(event.ts), end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"quality": event.quality} + ) + return None + +def stuck_sensor(event: Event, state: DeviceState, cfg) -> Alert | None: + """Check if sensor value is stuck (no change over time).""" + if event.msg_type not in ["reading", "telemetry"] or event.value is None: + state.last_seen_ts = _utc(event.ts) + return None + + eps = cfg.get("stuck", {}).get("epsilon", 0.1) + min_run = cfg.get("stuck", {}).get("min_run_length", 6) + min_dur = cfg.get("stuck", {}).get("min_duration_seconds", 1800) # Default to 1800 instead of 0! + + # Debug log + if state.last_value is None: + print(f"[STUCK_SENSOR] Config for {event.device_id}: eps={eps}, min_run={min_run}, min_dur={min_dur}") + + if state.last_value is None: + state.last_value = event.value + state.run_length = 1 + state.last_seen_ts = _utc(event.ts) + state.stuck_since_ts = None + return None + + if abs(event.value - state.last_value) < eps: + state.run_length += 1 + if state.stuck_since_ts is None: + state.stuck_since_ts = _utc(event.ts) + print(f"[STUCK_SENSOR] Device {event.device_id}: run_length={state.run_length}, value={event.value}, stuck_since={state.stuck_since_ts}") + else: + print(f"[STUCK_SENSOR] Device {event.device_id}: value changed {state.last_value} -> {event.value}, resetting run_length") + state.run_length = 1 + state.stuck_since_ts = None + state.last_value = event.value + + state.last_seen_ts = _utc(event.ts) + + if state.run_length >= min_run: + if min_dur <= 0: + print(f"[STUCK_SENSOR] Device {event.device_id}: ALERT triggered (no min_dur)") + return Alert( + device_id=event.device_id, issue_type="stuck_sensor", + start_ts=state.stuck_since_ts or _utc(event.ts), end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"run_length": state.run_length, "epsilon": eps} + ) + else: + dur = (_utc(event.ts) - (state.stuck_since_ts or _utc(event.ts))).total_seconds() + print(f"[STUCK_SENSOR] Device {event.device_id}: run_length={state.run_length} >= {min_run}, dur={dur}s, min_dur={min_dur}s") + if dur >= min_dur: + print(f"[STUCK_SENSOR] Device {event.device_id}: ALERT triggered (dur >= min_dur)") + return Alert( + device_id=event.device_id, issue_type="stuck_sensor", + start_ts=state.stuck_since_ts, end_ts=None, severity="warn", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"run_length": state.run_length, "duration_sec": int(dur), "epsilon": eps} + ) + return None + +def silence_checks(event: Event, state: DeviceState, cfg) -> list[Alert]: + """Check for missing keepalive or prolonged silence alerts.""" + alerts: list[Alert] = [] + now_ts = _utc(event.ts) + + expected = cfg.get("expected_interval_seconds", 60) + miss_factor = cfg.get("keepalive_miss_factor", 3) + silence_sec = cfg.get("prolonged_silence_seconds", 1800) + + if state.last_seen_ts is None: + state.last_seen_ts = now_ts + return alerts + + gap = (now_ts - state.last_seen_ts).total_seconds() + + if gap >= silence_sec and "prolonged_silence" not in state.open_alerts: + a = Alert(device_id=event.device_id, issue_type="prolonged_silence", + start_ts=state.last_seen_ts, end_ts=None, severity="error", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"gap_sec": int(gap)}) + state.open_alerts["prolonged_silence"] = a + alerts.append(a) + elif gap < silence_sec and "prolonged_silence" in state.open_alerts: + a = state.open_alerts.pop("prolonged_silence") + a.end_ts = now_ts + alerts.append(a) + + miss_thr = miss_factor * expected + if gap >= miss_thr and gap < silence_sec and "missing_keepalive" not in state.open_alerts: + a = Alert(device_id=event.device_id, issue_type="missing_keepalive", + start_ts=state.last_seen_ts, end_ts=None, severity="critical", + sensor_type=event.sensor_type, site_id=event.site_id, + details={"gap_sec": int(gap), "expected": expected}) + state.open_alerts["missing_keepalive"] = a + alerts.append(a) + elif (gap < miss_thr or gap >= silence_sec) and "missing_keepalive" in state.open_alerts: + a = state.open_alerts.pop("missing_keepalive") + a.end_ts = now_ts + alerts.append(a) + + state.last_seen_ts = now_ts + return alerts diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/state.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/state.py new file mode 100644 index 000000000..5efa13420 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/state.py @@ -0,0 +1,32 @@ +from .types import DeviceState +from typing import Dict, Set + +class StateStore: + def __init__(self): + self._devices: Dict[str, DeviceState] = {} + self._known_device_ids: Set[str] = set() + + @property + def devices(self): + """Expose internal devices dictionary (read-only).""" + return self._devices + + def add_device(self, device_id: str, sensor_type: str = None) -> None: + """Initialize state for a known device.""" + device_id = str(device_id) + self._known_device_ids.add(device_id) + if device_id not in self._devices: + self._devices[device_id] = DeviceState(device_id=device_id, sensor_type=sensor_type) + + def get(self, device_id: str) -> DeviceState: + """Return state for known device, or None if unknown.""" + device_id = str(device_id) + return self._devices.get(device_id) + + def is_known_device(self, device_id: str) -> bool: + """Check if device was loaded from API.""" + return str(device_id) in self._known_device_ids + + def all_states(self): + """Iterator over all registered device states.""" + return self._devices.items() diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/types.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/types.py new file mode 100644 index 000000000..b256537fb --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/core/types.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass, field +from typing import Optional, Dict +from datetime import datetime + +@dataclass +class Event: + ts: datetime + device_id: str + sensor_type: str + site_id: Optional[str] + msg_type: str # "reading" | "keepalive" + value: Optional[float] + seq: Optional[int] + quality: Optional[str] # "ok"/"corrupted"/None + +@dataclass +class Alert: + device_id: str + issue_type: str + start_ts: datetime + end_ts: Optional[datetime] + severity: str + sensor_type: Optional[str] = None + site_id: Optional[str] = None + details: Dict = field(default_factory=dict) + +@dataclass +class DeviceState: + device_id: str + sensor_type: Optional[str] = None + last_seen_ts: Optional[datetime] = None + last_value: Optional[float] = None + run_length: int = 0 + stuck_since_ts: Optional[datetime] = None + open_alerts: Dict[str, "Alert"] = field(default_factory=dict) diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/__init__.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/writer_console.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/writer_console.py new file mode 100644 index 000000000..2f62cb0d4 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/writer_console.py @@ -0,0 +1,11 @@ +from core.types import Alert + +class ConsoleWriter: + def write(self, alert: Alert) -> None: + end = alert.end_ts.isoformat() if alert.end_ts else "-" + print( + f"[ALERT] type={alert.issue_type} dev={alert.device_id} " + f"sensor={alert.sensor_type} value={alert.details.get('value')} " + f"range=[{alert.details.get('min')},{alert.details.get('max')}] " + f"ts={alert.start_ts.isoformat()} sev={alert.severity}" + ) diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/writer_kafka.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/writer_kafka.py new file mode 100644 index 000000000..2ffb6cfd4 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/io_mod/writer_kafka.py @@ -0,0 +1,123 @@ +import os +import json +from datetime import datetime +from typing import Any, Dict, Optional + +from kafka import KafkaProducer +from core.types import Alert +from config.settings import settings + + +# Convert Alert dataclass to a stable dict for JSON serialization and downstream consumers. +def _alert_to_dict(alert: Alert) -> Dict[str, Any]: + """ + Convert Alert to an ordered, serialization-friendly dict. + Suitable for downstream consumers (DB / API / BI). + """ + details: Dict[str, Any] = getattr(alert, "details", {}) or {} + + def iso(dt: Optional[datetime]) -> Optional[str]: + return dt.isoformat() if dt else None + + d = { + "issue_type": getattr(alert, "issue_type", None), + "device_id": getattr(alert, "device_id", None), + "severity": getattr(alert, "severity", None), + "start_ts": iso(getattr(alert, "start_ts", None)), + "details": details, + } + + end_ts = getattr(alert, "end_ts", None) + if end_ts is not None: + d["end_ts"] = iso(end_ts) + + return d + + + + +# Kafka writer: send Alert objects as JSON value-only messages. +# Lazy-initialize producer to avoid early network/socket creation. +class KafkaWriter: + """ + Write Alerts to Kafka as JSON (value-only). + Defaults: + - topic from OUT_TOPIC env or 'dev-robot-telemetry-raw' + - brokers from KAFKA_BROKERS env or 'kafka:9092' + """ + def __init__( + self, + topic: str | None = None, + brokers: str | None = None, + linger_ms: int = 10, + acks: str = "all", + retries: int = 5, + ) -> None: + # Topic and bootstrap configuration. + self.topic = topic or settings.OUT_TOPIC + self.bootstrap = brokers or settings.KAFKA_BROKERS + # Producer tuning parameters. + self.linger_ms = linger_ms + self.acks = acks + self.retries = retries + # Producer instance created on first use. + self._producer: Optional[KafkaProducer] = None + + # Ensure a KafkaProducer exists; create it lazily with safe JSON serializer. + def _ensure_producer(self) -> None: + """Lazy-init KafkaProducer with JSON serializer and configured options.""" + if self._producer is None: + print(f"[KafkaWriter] Creating KafkaProducer: brokers={self.bootstrap}, topic={self.topic}") + try: + self._producer = KafkaProducer( + bootstrap_servers=self.bootstrap, + value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode("utf-8"), + linger_ms=self.linger_ms, + acks=self.acks, + retries=self.retries, + ) + print("[KafkaWriter] KafkaProducer created successfully.") + except Exception as e: + print(f"[KafkaWriter][ERROR] Failed to create KafkaProducer: {e!r}") + raise + + # Serialize and send an Alert to Kafka; log errors to stdout. + def write(self, alert: Alert) -> None: + """Send an Alert to Kafka (non-blocking send).""" + print(f"[KafkaWriter] write() called for alert: device={getattr(alert, 'device_id', None)}, issue={getattr(alert, 'issue_type', None)}") + try: + if getattr(alert, "issue_type", None) == "unknown_device": + print(f"[KafkaWriter] Skipping unknown device alert for {alert.device_id}") + return + + self._ensure_producer() + payload = _alert_to_dict(alert) + print(f"[KafkaWriter] Sending payload: {payload}") + self._producer.send(self.topic, payload) + + print( + f"[KafkaWriter] Alert sent → topic='{self.topic}', " + f"device='{alert.device_id}', issue='{alert.issue_type}', " + f"time={payload.get('start_ts')}" + ) + except Exception as e: + print(f"[KafkaWriter][ERROR] send failed: {e!r}") + + # Flush producer buffers if the producer exists. + def flush(self) -> None: + """Flush any buffered messages to Kafka.""" + try: + if self._producer: + self._producer.flush() + except Exception as e: + print(f"[KafkaWriter] flush failed: {e!r}") + + # Flush and close the underlying producer if present. + def close(self) -> None: + """Gracefully flush and close the Kafka producer.""" + try: + if self._producer: + self._producer.flush() + self._producer.close() + except Exception as e: + print(f"[KafkaWriter] close failed: {e!r}") diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/main.py b/AgCloud/services/sensorGuard/sensorGuard/flink_app/main.py new file mode 100644 index 000000000..b5292b58e --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/main.py @@ -0,0 +1,221 @@ +import os, json, yaml, threading, time +from datetime import datetime, timezone +from pathlib import Path + +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import KafkaSource +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy +from pyflink.common.typeinfo import Types +from pyflink.datastream.functions import MapFunction, ProcessFunction, RuntimeContext + +from core.engine import Engine +from core.types import Event +from io_mod.writer_console import ConsoleWriter +from io_mod.writer_kafka import KafkaWriter +from api.auth import get_access_token +from api.devices_client import list_active_sensors +from core.state import StateStore + + +DROP_INVALID = True + + +# ------------------------------------------------------------- +# Convert incoming Kafka message to Event +# ------------------------------------------------------------- +def to_event(obj: dict) -> Event | None: + if not isinstance(obj, dict): + print("[to_event] Invalid object type, expected dict.") + return None + + ts = datetime.now(timezone.utc) + device_id = obj.get("id") + sensor_type = obj.get("sensor_type") or obj.get("sensor_name", "unknown_sensor") + + if not device_id: + if DROP_INVALID: + print("[to_event] Dropping event due to missing device_id.") + return None + device_id = "unknown_device" + else: + device_id = str(device_id) + + value_str = obj.get("value") + try: + value = float(value_str) if value_str is not None else None + except ValueError: + print(f"[to_event] Invalid numeric value: {value_str}") + value = None + + print(f"[to_event] Parsed event: device_id={device_id}, sensor_type={sensor_type}") + + return Event( + ts=ts, + device_id=device_id, + sensor_type=sensor_type, + site_id=obj.get("site_id"), + msg_type=obj.get("msg_type", "reading"), + value=value, + seq=obj.get("seq"), + quality=obj.get("quality"), + ) + + +# ------------------------------------------------------------- +# Engine Mapper: applies Engine logic for each event +# ------------------------------------------------------------- +class EngineMapper(MapFunction): + def __init__(self, cfg_path: str, state: StateStore): + self.cfg_path = cfg_path + self.state = state + self.engine = None + + def open(self, runtime_context: RuntimeContext): + with open(self.cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + writers = [ConsoleWriter(), KafkaWriter()] + self.engine = Engine(cfg, writers, state=self.state) + + def map(self, ev: Event) -> str: + if ev is None: + return "" + print(f"[EngineMapper] Processing event for device_id={ev.device_id}") + self.engine.process_event(ev) + return ev.device_id or "" + + +# ------------------------------------------------------------- +# Silence Sweep Process (background thread) +# ------------------------------------------------------------- +class SilenceSweepProcess(ProcessFunction): + def __init__(self, cfg_path: str, interval_sec: int, state: StateStore): + self.cfg_path = cfg_path + self.interval_sec = interval_sec + self.state = state + self.engine = None + self._thread = None + self._stop = False + + def open(self, runtime_context: RuntimeContext): + print("[SilenceSweepProcess] Initializing background silence checker.") + with open(self.cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + + writers = [ConsoleWriter(), KafkaWriter()] + self.engine = Engine(cfg, writers, state=self.state) + + # Run silence sweep periodically in a separate thread + def loop(): + print(f"[SilenceSweep] Thread started! Interval={self.interval_sec}s") + time.sleep(self.interval_sec) + while not self._stop: + now = datetime.now(timezone.utc) + print(f"[SilenceSweep] Checking silence at {now.isoformat()}") + try: + self.engine.sweep_silence(now) + print("[SilenceSweep] Sweep completed.") + except Exception as e: + print(f"[SilenceSweep][ERROR] {e}") + time.sleep(self.interval_sec) + print("[SilenceSweep] Thread stopped.") + + self._thread = threading.Thread(target=loop, daemon=True) + self._thread.start() + + def close(self): + self._stop = True + if self._thread: + self._thread.join(timeout=2) + self._thread = None + + def process_element(self, value, ctx: 'ProcessFunction.Context'): + # No per-event logic needed + pass + + +# ------------------------------------------------------------- +# Main entry point +# ------------------------------------------------------------- +def main(): + print("=== STARTING FLINK APPLICATION ===") + base_dir = Path(__file__).resolve().parent + cfg_path = base_dir / "config" / "rules.yaml" + + # --- Load sensors from API --- + api_base = os.getenv("DEVICES_API_BASE", "http://host.docker.internal:8001") + print(f"[INIT] Fetching active sensors from {api_base}...") + + token = get_access_token(api_base) + print(f"[INIT] Token received: {'YES' if token else 'NO'}") + + shared_state = StateStore() + if token: + for device_id in list_active_sensors(api_base, token): + shared_state.add_device(device_id) + print(f"[INIT] Loaded {len(shared_state.devices)} active sensors.") + else: + print("[INIT][WARN] No token, running with empty device list.") + + # --- Flink Setup --- + bootstrap = os.getenv("KAFKA_BROKERS", "kafka:9092") + topic_in = os.getenv("IN_TOPIC", "sensors") + group_id = os.getenv("KAFKA_GROUP_ID", "flink-device-pipeline") + + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(1) + env.enable_checkpointing(10_000) + + # Kafka source + source = ( + KafkaSource.builder() + .set_bootstrap_servers(bootstrap) + .set_topics(topic_in) + .set_group_id(group_id) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + stream = env.from_source( + source, + WatermarkStrategy.no_watermarks(), + f"kafka-source:{topic_in}" + ) + + # --- Processing pipeline --- + def parse_json(s: str): + try: + parsed = json.loads(s) + print(f"[FLINK-KAFKA] Parsed JSON: {s[:100]}...") + return parsed + except Exception as e: + print(f"[FLINK-KAFKA] Parse error: {e}") + return None + + events = ( + stream.map(parse_json) + .filter(lambda e: e is not None) + .map(to_event) + .filter(lambda e: e is not None) + ) + + # --- Apply Engine --- + mapper = EngineMapper(str(cfg_path), shared_state) + mapped = events.map(mapper, output_type=Types.STRING()).name("engine-run") + mapped.print().name("debug-print") + + # --- Silence check background thread --- + with open(cfg_path, "r", encoding="utf-8") as f: + cfg = yaml.safe_load(f) or {} + interval = cfg.get("defaults", {}).get("silence_sweep_interval_seconds", 300) + + silence_checker = SilenceSweepProcess(str(cfg_path), interval, shared_state) + events.process(silence_checker).name("silence-sweeper") + + print("[INIT] Starting Flink job...") + env.execute("DevicePipeline-With-SilenceSweep") + + +if __name__ == "__main__": + print("=== FLINK MAIN.PY EXECUTED ===") + main() diff --git a/AgCloud/services/sensorGuard/sensorGuard/flink_app/resources/synthetic_stream.csv b/AgCloud/services/sensorGuard/sensorGuard/flink_app/resources/synthetic_stream.csv new file mode 100644 index 000000000..71ee492ee --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/flink_app/resources/synthetic_stream.csv @@ -0,0 +1,523 @@ +timestamp,id,sensor_type,site_id,msg_type,value,seq,quality +2025-09-01T08:00:00Z,hum-A2,humidity,field-01,keepalive,,181,ok +2025-09-01T08:00:00Z,hum-A2,humidity,field-01,reading,54.617,1,ok +2025-09-01T08:00:00Z,soil-A3,soil_moist,field-02,keepalive,,91,ok +2025-09-01T08:00:00Z,soil-A3,soil_moist,field-02,reading,35.313,1,ok +2025-09-01T08:00:00Z,temp-A1,temperature,field-01,keepalive,,151,ok +2025-09-01T08:00:00Z,temp-A1,temperature,field-01,reading,27.338,1,ok +2025-09-01T08:01:00Z,hum-A2,humidity,field-01,reading,55.572,2,ok +2025-09-01T08:01:00Z,temp-A1,temperature,field-01,reading,26.959,2,ok +2025-09-01T08:02:00Z,hum-A2,humidity,field-01,reading,55.767,3,ok +2025-09-01T08:02:00Z,soil-A3,soil_moist,field-02,reading,35.63,2,ok +2025-09-01T08:02:00Z,temp-A1,temperature,field-01,reading,27.112,3,ok +2025-09-01T08:03:00Z,hum-A2,humidity,field-01,reading,56.764,4,ok +2025-09-01T08:03:00Z,temp-A1,temperature,field-01,reading,27.239,4,ok +2025-09-01T08:04:00Z,hum-A2,humidity,field-01,reading,56.144,5,ok +2025-09-01T08:04:00Z,soil-A3,soil_moist,field-02,reading,35.011,3,ok +2025-09-01T08:04:00Z,temp-A1,temperature,field-01,reading,27.052,5,ok +2025-09-01T08:05:00Z,hum-A2,humidity,field-01,keepalive,,182,ok +2025-09-01T08:05:00Z,hum-A2,humidity,field-01,reading,54.829,6,ok +2025-09-01T08:05:00Z,soil-A3,soil_moist,field-02,keepalive,,92,ok +2025-09-01T08:05:00Z,temp-A1,temperature,field-01,keepalive,,152,ok +2025-09-01T08:05:00Z,temp-A1,temperature,field-01,reading,27.263,6,ok +2025-09-01T08:06:00Z,hum-A2,humidity,field-01,reading,55.477,7,ok +2025-09-01T08:06:00Z,soil-A3,soil_moist,field-02,reading,35.495,4,ok +2025-09-01T08:06:00Z,temp-A1,temperature,field-01,reading,27.314,7,ok +2025-09-01T08:07:00Z,hum-A2,humidity,field-01,reading,57.417,8,ok +2025-09-01T08:07:00Z,temp-A1,temperature,field-01,reading,27.016,8,ok +2025-09-01T08:08:00Z,hum-A2,humidity,field-01,reading,56.884,9,ok +2025-09-01T08:08:00Z,soil-A3,soil_moist,field-02,reading,35.178,5,ok +2025-09-01T08:08:00Z,temp-A1,temperature,field-01,reading,27.622,9,ok +2025-09-01T08:09:00Z,hum-A2,humidity,field-01,reading,58.258,10,ok +2025-09-01T08:09:00Z,temp-A1,temperature,field-01,reading,27.59,10,ok +2025-09-01T08:10:00Z,hum-A2,humidity,field-01,keepalive,,183,ok +2025-09-01T08:10:00Z,hum-A2,humidity,field-01,reading,57.821,11,ok +2025-09-01T08:10:00Z,soil-A3,soil_moist,field-02,keepalive,,93,ok +2025-09-01T08:10:00Z,soil-A3,soil_moist,field-02,reading,35.237,6,ok +2025-09-01T08:10:00Z,temp-A1,temperature,field-01,keepalive,,153,ok +2025-09-01T08:10:00Z,temp-A1,temperature,field-01,reading,27.395,11,ok +2025-09-01T08:11:00Z,hum-A2,humidity,field-01,reading,57.256,12,ok +2025-09-01T08:11:00Z,temp-A1,temperature,field-01,reading,27.537,12,ok +2025-09-01T08:12:00Z,hum-A2,humidity,field-01,reading,58.973,13,ok +2025-09-01T08:12:00Z,soil-A3,soil_moist,field-02,reading,35.794,7,ok +2025-09-01T08:12:00Z,temp-A1,temperature,field-01,reading,27.722,13,ok +2025-09-01T08:13:00Z,hum-A2,humidity,field-01,reading,57.71,14,ok +2025-09-01T08:13:00Z,temp-A1,temperature,field-01,reading,27.619,14,ok +2025-09-01T08:14:00Z,hum-A2,humidity,field-01,reading,56.961,15,ok +2025-09-01T08:14:00Z,soil-A3,soil_moist,field-02,reading,36.138,8,ok +2025-09-01T08:14:00Z,temp-A1,temperature,field-01,reading,27.672,15,ok +2025-09-01T08:15:00Z,hum-A2,humidity,field-01,keepalive,,184,ok +2025-09-01T08:15:00Z,hum-A2,humidity,field-01,reading,60.498,16,ok +2025-09-01T08:15:00Z,soil-A3,soil_moist,field-02,keepalive,,94,ok +2025-09-01T08:15:00Z,temp-A1,temperature,field-01,keepalive,,154,ok +2025-09-01T08:15:00Z,temp-A1,temperature,field-01,reading,27.479,16,ok +2025-09-01T08:16:00Z,hum-A2,humidity,field-01,reading,57.56,17,ok +2025-09-01T08:16:00Z,soil-A3,soil_moist,field-02,reading,35.716,9,ok +2025-09-01T08:16:00Z,temp-A1,temperature,field-01,reading,27.929,17,ok +2025-09-01T08:17:00Z,hum-A2,humidity,field-01,reading,59.837,18,ok +2025-09-01T08:17:00Z,temp-A1,temperature,field-01,reading,27.89,18,ok +2025-09-01T08:18:00Z,hum-A2,humidity,field-01,reading,60.314,19,ok +2025-09-01T08:18:00Z,soil-A3,soil_moist,field-02,reading,35.009,10,ok +2025-09-01T08:18:00Z,temp-A1,temperature,field-01,reading,27.968,19,ok +2025-09-01T08:19:00Z,hum-A2,humidity,field-01,reading,59.914,20,ok +2025-09-01T08:19:00Z,temp-A1,temperature,field-01,reading,27.654,20,ok +2025-09-01T08:20:00Z,hum-A2,humidity,field-01,keepalive,,185,ok +2025-09-01T08:20:00Z,hum-A2,humidity,field-01,reading,58.679,21,ok +2025-09-01T08:20:00Z,soil-A3,soil_moist,field-02,keepalive,,95,ok +2025-09-01T08:20:00Z,soil-A3,soil_moist,field-02,reading,130.0,11,corrupted +2025-09-01T08:20:00Z,temp-A1,temperature,field-01,keepalive,,155,ok +2025-09-01T08:20:00Z,temp-A1,temperature,field-01,reading,28.335,21,ok +2025-09-01T08:21:00Z,hum-A2,humidity,field-01,reading,61.063,22,ok +2025-09-01T08:21:00Z,temp-A1,temperature,field-01,reading,28.081,22,ok +2025-09-01T08:22:00Z,hum-A2,humidity,field-01,reading,59.32,23,ok +2025-09-01T08:22:00Z,soil-A3,soil_moist,field-02,reading,35.55,12,ok +2025-09-01T08:22:00Z,temp-A1,temperature,field-01,reading,28.017,23,ok +2025-09-01T08:23:00Z,hum-A2,humidity,field-01,reading,59.617,24,ok +2025-09-01T08:23:00Z,temp-A1,temperature,field-01,reading,28.544,24,ok +2025-09-01T08:24:00Z,hum-A2,humidity,field-01,reading,60.617,25,ok +2025-09-01T08:24:00Z,soil-A3,soil_moist,field-02,reading,36.521,13,ok +2025-09-01T08:24:00Z,temp-A1,temperature,field-01,reading,28.172,25,ok +2025-09-01T08:25:00Z,hum-A2,humidity,field-01,keepalive,,186,ok +2025-09-01T08:25:00Z,hum-A2,humidity,field-01,reading,61.747,26,ok +2025-09-01T08:25:00Z,soil-A3,soil_moist,field-02,keepalive,,96,ok +2025-09-01T08:25:00Z,temp-A1,temperature,field-01,keepalive,,156,ok +2025-09-01T08:25:00Z,temp-A1,temperature,field-01,reading,27.933,26,ok +2025-09-01T08:26:00Z,hum-A2,humidity,field-01,reading,61.563,27,ok +2025-09-01T08:26:00Z,soil-A3,soil_moist,field-02,reading,37.459,14,ok +2025-09-01T08:26:00Z,temp-A1,temperature,field-01,reading,28.183,27,ok +2025-09-01T08:27:00Z,hum-A2,humidity,field-01,reading,62.173,28,ok +2025-09-01T08:27:00Z,temp-A1,temperature,field-01,reading,27.847,28,ok +2025-09-01T08:28:00Z,hum-A2,humidity,field-01,reading,61.367,29,ok +2025-09-01T08:28:00Z,soil-A3,soil_moist,field-02,reading,36.668,15,ok +2025-09-01T08:28:00Z,temp-A1,temperature,field-01,reading,28.554,29,ok +2025-09-01T08:29:00Z,hum-A2,humidity,field-01,reading,61.777,30,ok +2025-09-01T08:29:00Z,temp-A1,temperature,field-01,reading,28.3,30,ok +2025-09-01T08:30:00Z,hum-A2,humidity,field-01,keepalive,,187,ok +2025-09-01T08:30:00Z,hum-A2,humidity,field-01,reading,61.192,31,ok +2025-09-01T08:30:00Z,soil-A3,soil_moist,field-02,keepalive,,97,ok +2025-09-01T08:30:00Z,soil-A3,soil_moist,field-02,reading,36.186,16,ok +2025-09-01T08:30:00Z,temp-A1,temperature,field-01,keepalive,,157,ok +2025-09-01T08:30:00Z,temp-A1,temperature,field-01,reading,28.272,31,ok +2025-09-01T08:31:00Z,hum-A2,humidity,field-01,reading,62.031,32,ok +2025-09-01T08:31:00Z,temp-A1,temperature,field-01,reading,28.671,32,ok +2025-09-01T08:32:00Z,hum-A2,humidity,field-01,reading,62.937,33,ok +2025-09-01T08:32:00Z,soil-A3,soil_moist,field-02,reading,36.151,17,ok +2025-09-01T08:32:00Z,temp-A1,temperature,field-01,reading,28.162,33,ok +2025-09-01T08:33:00Z,hum-A2,humidity,field-01,reading,62.395,34,ok +2025-09-01T08:33:00Z,temp-A1,temperature,field-01,reading,28.634,34,ok +2025-09-01T08:34:00Z,hum-A2,humidity,field-01,reading,63.183,35,ok +2025-09-01T08:34:00Z,soil-A3,soil_moist,field-02,reading,37.564,18,ok +2025-09-01T08:34:00Z,temp-A1,temperature,field-01,reading,28.148,35,ok +2025-09-01T08:35:00Z,hum-A2,humidity,field-01,keepalive,,188,ok +2025-09-01T08:35:00Z,hum-A2,humidity,field-01,reading,62.506,36,ok +2025-09-01T08:35:00Z,soil-A3,soil_moist,field-02,keepalive,,98,ok +2025-09-01T08:35:00Z,temp-A1,temperature,field-01,keepalive,,158,ok +2025-09-01T08:35:00Z,temp-A1,temperature,field-01,reading,28.46,36,ok +2025-09-01T08:36:00Z,hum-A2,humidity,field-01,reading,62.647,37,ok +2025-09-01T08:36:00Z,soil-A3,soil_moist,field-02,reading,37.064,19,ok +2025-09-01T08:36:00Z,temp-A1,temperature,field-01,reading,28.383,37,ok +2025-09-01T08:37:00Z,hum-A2,humidity,field-01,reading,63.303,38,ok +2025-09-01T08:37:00Z,temp-A1,temperature,field-01,reading,28.947,38,ok +2025-09-01T08:38:00Z,hum-A2,humidity,field-01,reading,63.261,39,ok +2025-09-01T08:38:00Z,soil-A3,soil_moist,field-02,reading,37.432,20,ok +2025-09-01T08:38:00Z,temp-A1,temperature,field-01,reading,29.037,39,ok +2025-09-01T08:39:00Z,hum-A2,humidity,field-01,reading,64.03,40,ok +2025-09-01T08:39:00Z,temp-A1,temperature,field-01,reading,28.645,40,ok +2025-09-01T08:40:00Z,hum-A2,humidity,field-01,keepalive,,189,ok +2025-09-01T08:40:00Z,hum-A2,humidity,field-01,reading,63.832,41,ok +2025-09-01T08:40:00Z,soil-A3,soil_moist,field-02,keepalive,,99,ok +2025-09-01T08:40:00Z,soil-A3,soil_moist,field-02,reading,37.711,21,ok +2025-09-01T08:40:00Z,temp-A1,temperature,field-01,keepalive,,159,ok +2025-09-01T08:40:00Z,temp-A1,temperature,field-01,reading,28.906,41,ok +2025-09-01T08:41:00Z,hum-A2,humidity,field-01,reading,65.209,42,ok +2025-09-01T08:41:00Z,temp-A1,temperature,field-01,reading,28.727,42,ok +2025-09-01T08:42:00Z,hum-A2,humidity,field-01,reading,63.762,43,ok +2025-09-01T08:42:00Z,soil-A3,soil_moist,field-02,reading,37.306,22,ok +2025-09-01T08:42:00Z,temp-A1,temperature,field-01,reading,28.901,43,ok +2025-09-01T08:43:00Z,hum-A2,humidity,field-01,reading,63.871,44,ok +2025-09-01T08:43:00Z,temp-A1,temperature,field-01,reading,28.66,44,ok +2025-09-01T08:44:00Z,hum-A2,humidity,field-01,reading,63.329,45,ok +2025-09-01T08:44:00Z,soil-A3,soil_moist,field-02,reading,37.254,23,ok +2025-09-01T08:44:00Z,temp-A1,temperature,field-01,reading,28.491,45,ok +2025-09-01T08:45:00Z,hum-A2,humidity,field-01,keepalive,,190,ok +2025-09-01T08:45:00Z,hum-A2,humidity,field-01,reading,64.282,46,ok +2025-09-01T08:45:00Z,soil-A3,soil_moist,field-02,keepalive,,100,ok +2025-09-01T08:45:00Z,temp-A1,temperature,field-01,keepalive,,160,ok +2025-09-01T08:45:00Z,temp-A1,temperature,field-01,reading,28.492,46,ok +2025-09-01T08:46:00Z,hum-A2,humidity,field-01,reading,64.906,47,ok +2025-09-01T08:46:00Z,soil-A3,soil_moist,field-02,reading,37.008,24,ok +2025-09-01T08:46:00Z,temp-A1,temperature,field-01,reading,28.949,47,ok +2025-09-01T08:47:00Z,hum-A2,humidity,field-01,reading,63.857,48,ok +2025-09-01T08:47:00Z,temp-A1,temperature,field-01,reading,29.339,48,ok +2025-09-01T08:48:00Z,hum-A2,humidity,field-01,reading,64.492,49,ok +2025-09-01T08:48:00Z,soil-A3,soil_moist,field-02,reading,36.01,25,ok +2025-09-01T08:48:00Z,temp-A1,temperature,field-01,reading,28.96,49,ok +2025-09-01T08:49:00Z,hum-A2,humidity,field-01,reading,65.183,50,ok +2025-09-01T08:49:00Z,temp-A1,temperature,field-01,reading,28.817,50,ok +2025-09-01T08:50:00Z,hum-A2,humidity,field-01,keepalive,,191,ok +2025-09-01T08:50:00Z,hum-A2,humidity,field-01,reading,64.576,51,ok +2025-09-01T08:50:00Z,soil-A3,soil_moist,field-02,keepalive,,101,ok +2025-09-01T08:50:00Z,soil-A3,soil_moist,field-02,reading,36.857,26,ok +2025-09-01T08:50:00Z,temp-A1,temperature,field-01,keepalive,,161,ok +2025-09-01T08:50:00Z,temp-A1,temperature,field-01,reading,29.318,51,ok +2025-09-01T08:51:00Z,hum-A2,humidity,field-01,reading,64.326,52,ok +2025-09-01T08:51:00Z,temp-A1,temperature,field-01,reading,28.996,52,ok +2025-09-01T08:52:00Z,hum-A2,humidity,field-01,reading,65.575,53,ok +2025-09-01T08:52:00Z,soil-A3,soil_moist,field-02,reading,37.641,27,ok +2025-09-01T08:52:00Z,temp-A1,temperature,field-01,reading,28.98,53,ok +2025-09-01T08:53:00Z,hum-A2,humidity,field-01,reading,64.924,54,ok +2025-09-01T08:53:00Z,temp-A1,temperature,field-01,reading,29.02,54,ok +2025-09-01T08:54:00Z,hum-A2,humidity,field-01,reading,65.206,55,ok +2025-09-01T08:54:00Z,soil-A3,soil_moist,field-02,reading,37.643,28,ok +2025-09-01T08:54:00Z,temp-A1,temperature,field-01,reading,28.951,55,ok +2025-09-01T08:55:00Z,hum-A2,humidity,field-01,keepalive,,192,ok +2025-09-01T08:55:00Z,hum-A2,humidity,field-01,reading,65.862,56,ok +2025-09-01T08:55:00Z,soil-A3,soil_moist,field-02,keepalive,,102,ok +2025-09-01T08:55:00Z,temp-A1,temperature,field-01,keepalive,,162,ok +2025-09-01T08:55:00Z,temp-A1,temperature,field-01,reading,28.923,56,ok +2025-09-01T08:56:00Z,hum-A2,humidity,field-01,reading,65.87,57,ok +2025-09-01T08:56:00Z,soil-A3,soil_moist,field-02,reading,37.982,29,ok +2025-09-01T08:56:00Z,temp-A1,temperature,field-01,reading,28.704,57,ok +2025-09-01T08:57:00Z,hum-A2,humidity,field-01,reading,64.698,58,ok +2025-09-01T08:57:00Z,temp-A1,temperature,field-01,reading,29.095,58,ok +2025-09-01T08:58:00Z,hum-A2,humidity,field-01,reading,64.57,59,ok +2025-09-01T08:58:00Z,soil-A3,soil_moist,field-02,reading,37.763,30,ok +2025-09-01T08:58:00Z,temp-A1,temperature,field-01,reading,28.979,59,ok +2025-09-01T08:59:00Z,hum-A2,humidity,field-01,reading,65.226,60,ok +2025-09-01T08:59:00Z,temp-A1,temperature,field-01,reading,29.238,60,ok +2025-09-01T09:00:00Z,hum-A2,humidity,field-01,keepalive,,193,ok +2025-09-01T09:00:00Z,hum-A2,humidity,field-01,reading,64.893,61,ok +2025-09-01T09:00:00Z,soil-A3,soil_moist,field-02,keepalive,,103,ok +2025-09-01T09:00:00Z,soil-A3,soil_moist,field-02,reading,37.946,31,ok +2025-09-01T09:01:00Z,hum-A2,humidity,field-01,reading,63.474,62,ok +2025-09-01T09:02:00Z,hum-A2,humidity,field-01,reading,66.001,63,ok +2025-09-01T09:02:00Z,soil-A3,soil_moist,field-02,reading,38.295,32,ok +2025-09-01T09:03:00Z,hum-A2,humidity,field-01,reading,64.977,64,ok +2025-09-01T09:04:00Z,hum-A2,humidity,field-01,reading,65.524,65,ok +2025-09-01T09:04:00Z,soil-A3,soil_moist,field-02,reading,37.488,33,ok +2025-09-01T09:05:00Z,hum-A2,humidity,field-01,keepalive,,194,ok +2025-09-01T09:05:00Z,hum-A2,humidity,field-01,reading,63.963,66,ok +2025-09-01T09:05:00Z,soil-A3,soil_moist,field-02,keepalive,,104,ok +2025-09-01T09:06:00Z,hum-A2,humidity,field-01,reading,65.892,67,ok +2025-09-01T09:06:00Z,soil-A3,soil_moist,field-02,reading,37.812,34,ok +2025-09-01T09:07:00Z,hum-A2,humidity,field-01,reading,64.344,68,ok +2025-09-01T09:08:00Z,hum-A2,humidity,field-01,reading,64.674,69,ok +2025-09-01T09:08:00Z,soil-A3,soil_moist,field-02,reading,37.171,35,ok +2025-09-01T09:09:00Z,hum-A2,humidity,field-01,reading,63.579,70,ok +2025-09-01T09:10:00Z,hum-A2,humidity,field-01,keepalive,,195,ok +2025-09-01T09:10:00Z,hum-A2,humidity,field-01,reading,64.499,71,ok +2025-09-01T09:10:00Z,soil-A3,soil_moist,field-02,keepalive,,105,ok +2025-09-01T09:10:00Z,soil-A3,soil_moist,field-02,reading,38.475,36,ok +2025-09-01T09:11:00Z,hum-A2,humidity,field-01,reading,64.374,72,ok +2025-09-01T09:12:00Z,hum-A2,humidity,field-01,reading,64.329,73,ok +2025-09-01T09:12:00Z,soil-A3,soil_moist,field-02,reading,38.016,37,ok +2025-09-01T09:13:00Z,hum-A2,humidity,field-01,reading,64.741,74,ok +2025-09-01T09:14:00Z,hum-A2,humidity,field-01,reading,64.345,75,ok +2025-09-01T09:14:00Z,soil-A3,soil_moist,field-02,reading,38.245,38,ok +2025-09-01T09:15:00Z,hum-A2,humidity,field-01,keepalive,,196,ok +2025-09-01T09:15:00Z,hum-A2,humidity,field-01,reading,64.977,76,ok +2025-09-01T09:15:00Z,soil-A3,soil_moist,field-02,keepalive,,106,ok +2025-09-01T09:16:00Z,hum-A2,humidity,field-01,reading,63.715,77,ok +2025-09-01T09:16:00Z,soil-A3,soil_moist,field-02,reading,38.769,39,ok +2025-09-01T09:17:00Z,hum-A2,humidity,field-01,reading,63.18,78,ok +2025-09-01T09:18:00Z,hum-A2,humidity,field-01,reading,63.842,79,ok +2025-09-01T09:18:00Z,soil-A3,soil_moist,field-02,reading,38.53,40,ok +2025-09-01T09:19:00Z,hum-A2,humidity,field-01,reading,64.235,80,ok +2025-09-01T09:20:00Z,hum-A2,humidity,field-01,keepalive,,197,ok +2025-09-01T09:20:00Z,hum-A2,humidity,field-01,reading,64.597,81,ok +2025-09-01T09:20:00Z,soil-A3,soil_moist,field-02,keepalive,,107,ok +2025-09-01T09:20:00Z,soil-A3,soil_moist,field-02,reading,37.947,41,ok +2025-09-01T09:21:00Z,hum-A2,humidity,field-01,reading,64.597,82,ok +2025-09-01T09:22:00Z,hum-A2,humidity,field-01,reading,64.597,83,ok +2025-09-01T09:22:00Z,soil-A3,soil_moist,field-02,reading,38.353,42,ok +2025-09-01T09:23:00Z,hum-A2,humidity,field-01,reading,64.597,84,ok +2025-09-01T09:24:00Z,hum-A2,humidity,field-01,reading,64.597,85,ok +2025-09-01T09:24:00Z,soil-A3,soil_moist,field-02,reading,37.219,43,ok +2025-09-01T09:25:00Z,hum-A2,humidity,field-01,keepalive,,198,ok +2025-09-01T09:25:00Z,hum-A2,humidity,field-01,reading,64.597,86,ok +2025-09-01T09:25:00Z,soil-A3,soil_moist,field-02,keepalive,,108,ok +2025-09-01T09:26:00Z,hum-A2,humidity,field-01,reading,64.597,87,ok +2025-09-01T09:26:00Z,soil-A3,soil_moist,field-02,reading,38.123,44,ok +2025-09-01T09:27:00Z,hum-A2,humidity,field-01,reading,64.597,88,ok +2025-09-01T09:28:00Z,hum-A2,humidity,field-01,reading,64.597,89,ok +2025-09-01T09:28:00Z,soil-A3,soil_moist,field-02,reading,37.942,45,ok +2025-09-01T09:29:00Z,hum-A2,humidity,field-01,reading,64.597,90,ok +2025-09-01T09:30:00Z,hum-A2,humidity,field-01,keepalive,,199,ok +2025-09-01T09:30:00Z,hum-A2,humidity,field-01,reading,64.597,91,ok +2025-09-01T09:30:00Z,soil-A3,soil_moist,field-02,keepalive,,109,ok +2025-09-01T09:30:00Z,soil-A3,soil_moist,field-02,reading,37.024,46,ok +2025-09-01T09:30:00Z,temp-A1,temperature,field-01,keepalive,,163,ok +2025-09-01T09:30:00Z,temp-A1,temperature,field-01,reading,28.505,61,ok +2025-09-01T09:31:00Z,hum-A2,humidity,field-01,reading,64.597,92,ok +2025-09-01T09:31:00Z,temp-A1,temperature,field-01,reading,28.493,62,ok +2025-09-01T09:32:00Z,hum-A2,humidity,field-01,reading,64.597,93,ok +2025-09-01T09:32:00Z,soil-A3,soil_moist,field-02,reading,38.457,47,ok +2025-09-01T09:32:00Z,temp-A1,temperature,field-01,reading,28.316,63,ok +2025-09-01T09:33:00Z,hum-A2,humidity,field-01,reading,64.597,94,ok +2025-09-01T09:33:00Z,temp-A1,temperature,field-01,reading,28.263,64,ok +2025-09-01T09:34:00Z,hum-A2,humidity,field-01,reading,64.597,95,ok +2025-09-01T09:34:00Z,soil-A3,soil_moist,field-02,reading,37.387,48,ok +2025-09-01T09:34:00Z,temp-A1,temperature,field-01,reading,28.102,65,ok +2025-09-01T09:35:00Z,hum-A2,humidity,field-01,keepalive,,200,ok +2025-09-01T09:35:00Z,hum-A2,humidity,field-01,reading,64.597,96,ok +2025-09-01T09:35:00Z,soil-A3,soil_moist,field-02,keepalive,,110,ok +2025-09-01T09:35:00Z,temp-A1,temperature,field-01,keepalive,,164,ok +2025-09-01T09:35:00Z,temp-A1,temperature,field-01,reading,28.184,66,ok +2025-09-01T09:36:00Z,hum-A2,humidity,field-01,reading,64.597,97,ok +2025-09-01T09:36:00Z,soil-A3,soil_moist,field-02,reading,38.027,49,ok +2025-09-01T09:36:00Z,temp-A1,temperature,field-01,reading,28.605,67,ok +2025-09-01T09:37:00Z,hum-A2,humidity,field-01,reading,64.597,98,ok +2025-09-01T09:37:00Z,temp-A1,temperature,field-01,reading,28.283,68,ok +2025-09-01T09:38:00Z,hum-A2,humidity,field-01,reading,64.597,99,ok +2025-09-01T09:38:00Z,soil-A3,soil_moist,field-02,reading,37.416,50,ok +2025-09-01T09:38:00Z,temp-A1,temperature,field-01,reading,27.997,69,ok +2025-09-01T09:39:00Z,hum-A2,humidity,field-01,reading,64.597,100,ok +2025-09-01T09:39:00Z,temp-A1,temperature,field-01,reading,27.926,70,ok +2025-09-01T09:40:00Z,hum-A2,humidity,field-01,keepalive,,201,ok +2025-09-01T09:40:00Z,hum-A2,humidity,field-01,reading,64.597,101,ok +2025-09-01T09:40:00Z,soil-A3,soil_moist,field-02,keepalive,,111,ok +2025-09-01T09:40:00Z,soil-A3,soil_moist,field-02,reading,-10.0,51,corrupted +2025-09-01T09:40:00Z,temp-A1,temperature,field-01,keepalive,,165,ok +2025-09-01T09:40:00Z,temp-A1,temperature,field-01,reading,27.802,71,ok +2025-09-01T09:41:00Z,hum-A2,humidity,field-01,reading,64.597,102,ok +2025-09-01T09:41:00Z,temp-A1,temperature,field-01,reading,28.003,72,ok +2025-09-01T09:42:00Z,hum-A2,humidity,field-01,reading,64.597,103,ok +2025-09-01T09:42:00Z,soil-A3,soil_moist,field-02,reading,38.15,52,ok +2025-09-01T09:42:00Z,temp-A1,temperature,field-01,reading,27.96,73,ok +2025-09-01T09:43:00Z,hum-A2,humidity,field-01,reading,64.597,104,ok +2025-09-01T09:43:00Z,temp-A1,temperature,field-01,reading,27.545,74,ok +2025-09-01T09:44:00Z,hum-A2,humidity,field-01,reading,64.597,105,ok +2025-09-01T09:44:00Z,soil-A3,soil_moist,field-02,reading,37.006,53,ok +2025-09-01T09:44:00Z,temp-A1,temperature,field-01,reading,27.883,75,ok +2025-09-01T09:45:00Z,hum-A2,humidity,field-01,keepalive,,202,ok +2025-09-01T09:45:00Z,hum-A2,humidity,field-01,reading,64.597,106,ok +2025-09-01T09:45:00Z,soil-A3,soil_moist,field-02,keepalive,,112,ok +2025-09-01T09:45:00Z,temp-A1,temperature,field-01,keepalive,,166,ok +2025-09-01T09:45:00Z,temp-A1,temperature,field-01,reading,27.623,76,ok +2025-09-01T09:46:00Z,hum-A2,humidity,field-01,reading,64.597,107,ok +2025-09-01T09:46:00Z,soil-A3,soil_moist,field-02,reading,36.578,54,ok +2025-09-01T09:46:00Z,temp-A1,temperature,field-01,reading,27.973,77,ok +2025-09-01T09:47:00Z,hum-A2,humidity,field-01,reading,64.597,108,ok +2025-09-01T09:47:00Z,temp-A1,temperature,field-01,reading,27.669,78,ok +2025-09-01T09:48:00Z,hum-A2,humidity,field-01,reading,64.597,109,ok +2025-09-01T09:48:00Z,soil-A3,soil_moist,field-02,reading,37.196,55,ok +2025-09-01T09:48:00Z,temp-A1,temperature,field-01,reading,27.931,79,ok +2025-09-01T09:49:00Z,hum-A2,humidity,field-01,reading,64.597,110,ok +2025-09-01T09:49:00Z,temp-A1,temperature,field-01,reading,27.446,80,ok +2025-09-01T09:50:00Z,hum-A2,humidity,field-01,keepalive,,203,ok +2025-09-01T09:50:00Z,hum-A2,humidity,field-01,reading,64.597,111,ok +2025-09-01T09:50:00Z,soil-A3,soil_moist,field-02,keepalive,,113,ok +2025-09-01T09:50:00Z,soil-A3,soil_moist,field-02,reading,37.601,56,ok +2025-09-01T09:50:00Z,temp-A1,temperature,field-01,keepalive,,167,ok +2025-09-01T09:50:00Z,temp-A1,temperature,field-01,reading,27.429,81,ok +2025-09-01T09:51:00Z,hum-A2,humidity,field-01,reading,64.597,112,ok +2025-09-01T09:51:00Z,temp-A1,temperature,field-01,reading,27.495,82,ok +2025-09-01T09:52:00Z,hum-A2,humidity,field-01,reading,64.597,113,ok +2025-09-01T09:52:00Z,soil-A3,soil_moist,field-02,reading,36.902,57,ok +2025-09-01T09:52:00Z,temp-A1,temperature,field-01,reading,27.595,83,ok +2025-09-01T09:53:00Z,hum-A2,humidity,field-01,reading,64.597,114,ok +2025-09-01T09:53:00Z,temp-A1,temperature,field-01,reading,27.445,84,ok +2025-09-01T09:54:00Z,hum-A2,humidity,field-01,reading,64.597,115,ok +2025-09-01T09:54:00Z,soil-A3,soil_moist,field-02,reading,36.687,58,ok +2025-09-01T09:54:00Z,temp-A1,temperature,field-01,reading,27.033,85,ok +2025-09-01T09:55:00Z,hum-A2,humidity,field-01,keepalive,,204,ok +2025-09-01T09:55:00Z,hum-A2,humidity,field-01,reading,64.597,116,ok +2025-09-01T09:55:00Z,soil-A3,soil_moist,field-02,keepalive,,114,ok +2025-09-01T09:55:00Z,temp-A1,temperature,field-01,keepalive,,168,ok +2025-09-01T09:55:00Z,temp-A1,temperature,field-01,reading,27.264,86,ok +2025-09-01T09:56:00Z,hum-A2,humidity,field-01,reading,64.597,117,ok +2025-09-01T09:56:00Z,soil-A3,soil_moist,field-02,reading,37.142,59,ok +2025-09-01T09:56:00Z,temp-A1,temperature,field-01,reading,27.18,87,ok +2025-09-01T09:57:00Z,hum-A2,humidity,field-01,reading,64.597,118,ok +2025-09-01T09:57:00Z,temp-A1,temperature,field-01,reading,27.037,88,ok +2025-09-01T09:58:00Z,hum-A2,humidity,field-01,reading,64.597,119,ok +2025-09-01T09:58:00Z,soil-A3,soil_moist,field-02,reading,35.417,60,ok +2025-09-01T09:58:00Z,temp-A1,temperature,field-01,reading,26.941,89,ok +2025-09-01T09:59:00Z,hum-A2,humidity,field-01,reading,64.597,120,ok +2025-09-01T09:59:00Z,temp-A1,temperature,field-01,reading,27.367,90,ok +2025-09-01T10:00:00Z,hum-A2,humidity,field-01,keepalive,,205,ok +2025-09-01T10:00:00Z,hum-A2,humidity,field-01,reading,55.088,121,ok +2025-09-01T10:00:00Z,soil-A3,soil_moist,field-02,keepalive,,115,ok +2025-09-01T10:00:00Z,soil-A3,soil_moist,field-02,reading,36.362,61,ok +2025-09-01T10:00:00Z,temp-A1,temperature,field-01,keepalive,,169,ok +2025-09-01T10:00:00Z,temp-A1,temperature,field-01,reading,26.887,91,ok +2025-09-01T10:01:00Z,hum-A2,humidity,field-01,reading,54.422,122,ok +2025-09-01T10:01:00Z,temp-A1,temperature,field-01,reading,26.743,92,ok +2025-09-01T10:02:00Z,hum-A2,humidity,field-01,reading,53.028,123,ok +2025-09-01T10:02:00Z,soil-A3,soil_moist,field-02,reading,35.896,62,ok +2025-09-01T10:02:00Z,temp-A1,temperature,field-01,reading,26.987,93,ok +2025-09-01T10:03:00Z,hum-A2,humidity,field-01,reading,54.243,124,ok +2025-09-01T10:03:00Z,temp-A1,temperature,field-01,reading,26.833,94,ok +2025-09-01T10:04:00Z,hum-A2,humidity,field-01,reading,54.521,125,ok +2025-09-01T10:04:00Z,soil-A3,soil_moist,field-02,reading,36.626,63,ok +2025-09-01T10:04:00Z,temp-A1,temperature,field-01,reading,26.74,95,ok +2025-09-01T10:05:00Z,hum-A2,humidity,field-01,keepalive,,206,ok +2025-09-01T10:05:00Z,hum-A2,humidity,field-01,reading,53.395,126,ok +2025-09-01T10:05:00Z,soil-A3,soil_moist,field-02,keepalive,,116,ok +2025-09-01T10:05:00Z,temp-A1,temperature,field-01,keepalive,,170,ok +2025-09-01T10:05:00Z,temp-A1,temperature,field-01,reading,26.859,96,ok +2025-09-01T10:06:00Z,hum-A2,humidity,field-01,reading,53.198,127,ok +2025-09-01T10:06:00Z,soil-A3,soil_moist,field-02,reading,35.999,64,ok +2025-09-01T10:06:00Z,temp-A1,temperature,field-01,reading,26.749,97,ok +2025-09-01T10:07:00Z,hum-A2,humidity,field-01,reading,54.11,128,ok +2025-09-01T10:07:00Z,temp-A1,temperature,field-01,reading,26.672,98,ok +2025-09-01T10:08:00Z,hum-A2,humidity,field-01,reading,51.738,129,ok +2025-09-01T10:08:00Z,soil-A3,soil_moist,field-02,reading,35.695,65,ok +2025-09-01T10:08:00Z,temp-A1,temperature,field-01,reading,26.74,99,ok +2025-09-01T10:09:00Z,hum-A2,humidity,field-01,reading,51.284,130,ok +2025-09-01T10:09:00Z,temp-A1,temperature,field-01,reading,26.553,100,ok +2025-09-01T10:10:00Z,hum-A2,humidity,field-01,keepalive,,207,ok +2025-09-01T10:10:00Z,hum-A2,humidity,field-01,reading,51.705,131,ok +2025-09-01T10:10:00Z,soil-A3,soil_moist,field-02,keepalive,,117,ok +2025-09-01T10:10:00Z,soil-A3,soil_moist,field-02,reading,35.959,66,ok +2025-09-01T10:10:00Z,temp-A1,temperature,field-01,keepalive,,171,ok +2025-09-01T10:10:00Z,temp-A1,temperature,field-01,reading,26.322,101,ok +2025-09-01T10:11:00Z,hum-A2,humidity,field-01,reading,51.019,132,ok +2025-09-01T10:11:00Z,temp-A1,temperature,field-01,reading,26.323,102,ok +2025-09-01T10:12:00Z,hum-A2,humidity,field-01,reading,52.804,133,ok +2025-09-01T10:12:00Z,soil-A3,soil_moist,field-02,reading,35.283,67,ok +2025-09-01T10:12:00Z,temp-A1,temperature,field-01,reading,26.241,103,ok +2025-09-01T10:13:00Z,hum-A2,humidity,field-01,reading,51.727,134,ok +2025-09-01T10:13:00Z,temp-A1,temperature,field-01,reading,26.338,104,ok +2025-09-01T10:14:00Z,hum-A2,humidity,field-01,reading,50.543,135,ok +2025-09-01T10:14:00Z,soil-A3,soil_moist,field-02,reading,35.89,68,ok +2025-09-01T10:14:00Z,temp-A1,temperature,field-01,reading,26.031,105,ok +2025-09-01T10:15:00Z,hum-A2,humidity,field-01,keepalive,,208,ok +2025-09-01T10:15:00Z,hum-A2,humidity,field-01,reading,50.5,136,ok +2025-09-01T10:15:00Z,soil-A3,soil_moist,field-02,keepalive,,118,ok +2025-09-01T10:15:00Z,temp-A1,temperature,field-01,keepalive,,172,ok +2025-09-01T10:15:00Z,temp-A1,temperature,field-01,reading,25.832,106,ok +2025-09-01T10:16:00Z,hum-A2,humidity,field-01,reading,53.041,137,ok +2025-09-01T10:16:00Z,soil-A3,soil_moist,field-02,reading,35.434,69,ok +2025-09-01T10:16:00Z,temp-A1,temperature,field-01,reading,26.168,107,ok +2025-09-01T10:17:00Z,hum-A2,humidity,field-01,reading,50.027,138,ok +2025-09-01T10:17:00Z,temp-A1,temperature,field-01,reading,25.836,108,ok +2025-09-01T10:18:00Z,hum-A2,humidity,field-01,reading,49.672,139,ok +2025-09-01T10:18:00Z,soil-A3,soil_moist,field-02,reading,35.463,70,ok +2025-09-01T10:18:00Z,temp-A1,temperature,field-01,reading,25.666,109,ok +2025-09-01T10:19:00Z,hum-A2,humidity,field-01,reading,50.294,140,ok +2025-09-01T10:19:00Z,temp-A1,temperature,field-01,reading,26.085,110,ok +2025-09-01T10:20:00Z,hum-A2,humidity,field-01,keepalive,,209,ok +2025-09-01T10:20:00Z,hum-A2,humidity,field-01,reading,50.334,141,ok +2025-09-01T10:20:00Z,soil-A3,soil_moist,field-02,keepalive,,119,ok +2025-09-01T10:20:00Z,soil-A3,soil_moist,field-02,reading,35.168,71,ok +2025-09-01T10:20:00Z,temp-A1,temperature,field-01,keepalive,,173,ok +2025-09-01T10:20:00Z,temp-A1,temperature,field-01,reading,25.823,111,ok +2025-09-01T10:21:00Z,hum-A2,humidity,field-01,reading,49.778,142,ok +2025-09-01T10:21:00Z,temp-A1,temperature,field-01,reading,26.019,112,ok +2025-09-01T10:22:00Z,hum-A2,humidity,field-01,reading,48.654,143,ok +2025-09-01T10:22:00Z,soil-A3,soil_moist,field-02,reading,35.857,72,ok +2025-09-01T10:22:00Z,temp-A1,temperature,field-01,reading,25.77,113,ok +2025-09-01T10:23:00Z,hum-A2,humidity,field-01,reading,48.237,144,ok +2025-09-01T10:23:00Z,temp-A1,temperature,field-01,reading,25.609,114,ok +2025-09-01T10:24:00Z,hum-A2,humidity,field-01,reading,49.43,145,ok +2025-09-01T10:24:00Z,soil-A3,soil_moist,field-02,reading,35.221,73,ok +2025-09-01T10:24:00Z,temp-A1,temperature,field-01,reading,25.542,115,ok +2025-09-01T10:25:00Z,hum-A2,humidity,field-01,keepalive,,210,ok +2025-09-01T10:25:00Z,hum-A2,humidity,field-01,reading,48.702,146,ok +2025-09-01T10:25:00Z,soil-A3,soil_moist,field-02,keepalive,,120,ok +2025-09-01T10:25:00Z,temp-A1,temperature,field-01,keepalive,,174,ok +2025-09-01T10:25:00Z,temp-A1,temperature,field-01,reading,25.646,116,ok +2025-09-01T10:26:00Z,hum-A2,humidity,field-01,reading,47.229,147,ok +2025-09-01T10:26:00Z,soil-A3,soil_moist,field-02,reading,35.41,74,ok +2025-09-01T10:26:00Z,temp-A1,temperature,field-01,reading,25.654,117,ok +2025-09-01T10:27:00Z,hum-A2,humidity,field-01,reading,49.28,148,ok +2025-09-01T10:27:00Z,temp-A1,temperature,field-01,reading,25.504,118,ok +2025-09-01T10:28:00Z,hum-A2,humidity,field-01,reading,48.77,149,ok +2025-09-01T10:28:00Z,soil-A3,soil_moist,field-02,reading,35.261,75,ok +2025-09-01T10:28:00Z,temp-A1,temperature,field-01,reading,25.574,119,ok +2025-09-01T10:29:00Z,hum-A2,humidity,field-01,reading,47.767,150,ok +2025-09-01T10:29:00Z,temp-A1,temperature,field-01,reading,25.285,120,ok +2025-09-01T10:30:00Z,hum-A2,humidity,field-01,keepalive,,211,ok +2025-09-01T10:30:00Z,hum-A2,humidity,field-01,reading,47.567,151,ok +2025-09-01T10:30:00Z,soil-A3,soil_moist,field-02,keepalive,,121,ok +2025-09-01T10:30:00Z,soil-A3,soil_moist,field-02,reading,34.491,76,ok +2025-09-01T10:30:00Z,temp-A1,temperature,field-01,keepalive,,175,ok +2025-09-01T10:30:00Z,temp-A1,temperature,field-01,reading,25.558,121,ok +2025-09-01T10:31:00Z,hum-A2,humidity,field-01,reading,47.781,152,ok +2025-09-01T10:31:00Z,temp-A1,temperature,field-01,reading,25.16,122,ok +2025-09-01T10:32:00Z,hum-A2,humidity,field-01,reading,46.872,153,ok +2025-09-01T10:32:00Z,soil-A3,soil_moist,field-02,reading,35.052,77,ok +2025-09-01T10:32:00Z,temp-A1,temperature,field-01,reading,25.446,123,ok +2025-09-01T10:33:00Z,hum-A2,humidity,field-01,reading,46.174,154,ok +2025-09-01T10:33:00Z,temp-A1,temperature,field-01,reading,25.59,124,ok +2025-09-01T10:34:00Z,hum-A2,humidity,field-01,reading,47.347,155,ok +2025-09-01T10:34:00Z,soil-A3,soil_moist,field-02,reading,34.815,78,ok +2025-09-01T10:34:00Z,temp-A1,temperature,field-01,reading,25.636,125,ok +2025-09-01T10:35:00Z,hum-A2,humidity,field-01,keepalive,,212,ok +2025-09-01T10:35:00Z,hum-A2,humidity,field-01,reading,45.779,156,ok +2025-09-01T10:35:00Z,soil-A3,soil_moist,field-02,keepalive,,122,ok +2025-09-01T10:35:00Z,temp-A1,temperature,field-01,keepalive,,176,ok +2025-09-01T10:35:00Z,temp-A1,temperature,field-01,reading,25.729,126,ok +2025-09-01T10:36:00Z,hum-A2,humidity,field-01,reading,47.09,157,ok +2025-09-01T10:36:00Z,soil-A3,soil_moist,field-02,reading,34.672,79,ok +2025-09-01T10:36:00Z,temp-A1,temperature,field-01,reading,25.044,127,ok +2025-09-01T10:37:00Z,hum-A2,humidity,field-01,reading,45.478,158,ok +2025-09-01T10:37:00Z,temp-A1,temperature,field-01,reading,25.478,128,ok +2025-09-01T10:38:00Z,hum-A2,humidity,field-01,reading,46.41,159,ok +2025-09-01T10:38:00Z,soil-A3,soil_moist,field-02,reading,34.191,80,ok +2025-09-01T10:38:00Z,temp-A1,temperature,field-01,reading,25.539,129,ok +2025-09-01T10:39:00Z,hum-A2,humidity,field-01,reading,46.246,160,ok +2025-09-01T10:39:00Z,temp-A1,temperature,field-01,reading,25.467,130,ok +2025-09-01T10:40:00Z,hum-A2,humidity,field-01,keepalive,,213,ok +2025-09-01T10:40:00Z,hum-A2,humidity,field-01,reading,47.651,161,ok +2025-09-01T10:40:00Z,soil-A3,soil_moist,field-02,keepalive,,123,ok +2025-09-01T10:40:00Z,soil-A3,soil_moist,field-02,reading,34.053,81,ok +2025-09-01T10:40:00Z,temp-A1,temperature,field-01,keepalive,,177,ok +2025-09-01T10:40:00Z,temp-A1,temperature,field-01,reading,25.059,131,ok +2025-09-01T10:41:00Z,hum-A2,humidity,field-01,reading,45.25,162,ok +2025-09-01T10:41:00Z,temp-A1,temperature,field-01,reading,25.303,132,ok +2025-09-01T10:42:00Z,hum-A2,humidity,field-01,reading,44.853,163,ok +2025-09-01T10:42:00Z,soil-A3,soil_moist,field-02,reading,34.537,82,ok +2025-09-01T10:42:00Z,temp-A1,temperature,field-01,reading,25.569,133,ok +2025-09-01T10:43:00Z,hum-A2,humidity,field-01,reading,45.357,164,ok +2025-09-01T10:43:00Z,temp-A1,temperature,field-01,reading,24.875,134,ok +2025-09-01T10:44:00Z,hum-A2,humidity,field-01,reading,44.871,165,ok +2025-09-01T10:44:00Z,soil-A3,soil_moist,field-02,reading,35.114,83,ok +2025-09-01T10:44:00Z,temp-A1,temperature,field-01,reading,25.249,135,ok +2025-09-01T10:45:00Z,hum-A2,humidity,field-01,keepalive,,214,ok +2025-09-01T10:45:00Z,hum-A2,humidity,field-01,reading,45.2,166,ok +2025-09-01T10:45:00Z,soil-A3,soil_moist,field-02,keepalive,,124,ok +2025-09-01T10:45:00Z,temp-A1,temperature,field-01,keepalive,,178,ok +2025-09-01T10:45:00Z,temp-A1,temperature,field-01,reading,25.357,136,ok +2025-09-01T10:46:00Z,hum-A2,humidity,field-01,reading,45.917,167,ok +2025-09-01T10:46:00Z,soil-A3,soil_moist,field-02,reading,33.488,84,ok +2025-09-01T10:46:00Z,temp-A1,temperature,field-01,reading,24.977,137,ok +2025-09-01T10:47:00Z,hum-A2,humidity,field-01,reading,46.112,168,ok +2025-09-01T10:47:00Z,temp-A1,temperature,field-01,reading,25.216,138,ok +2025-09-01T10:48:00Z,hum-A2,humidity,field-01,reading,46.744,169,ok +2025-09-01T10:48:00Z,soil-A3,soil_moist,field-02,reading,34.053,85,ok +2025-09-01T10:48:00Z,temp-A1,temperature,field-01,reading,24.869,139,ok +2025-09-01T10:49:00Z,hum-A2,humidity,field-01,reading,44.755,170,ok +2025-09-01T10:49:00Z,temp-A1,temperature,field-01,reading,24.906,140,ok +2025-09-01T10:50:00Z,hum-A2,humidity,field-01,keepalive,,215,ok +2025-09-01T10:50:00Z,hum-A2,humidity,field-01,reading,47.39,171,ok +2025-09-01T10:50:00Z,soil-A3,soil_moist,field-02,keepalive,,125,ok +2025-09-01T10:50:00Z,soil-A3,soil_moist,field-02,reading,33.488,86,ok +2025-09-01T10:50:00Z,temp-A1,temperature,field-01,keepalive,,179,ok +2025-09-01T10:50:00Z,temp-A1,temperature,field-01,reading,24.892,141,ok +2025-09-01T10:51:00Z,hum-A2,humidity,field-01,reading,46.807,172,ok +2025-09-01T10:51:00Z,temp-A1,temperature,field-01,reading,25.083,142,ok +2025-09-01T10:52:00Z,hum-A2,humidity,field-01,reading,45.007,173,ok +2025-09-01T10:52:00Z,soil-A3,soil_moist,field-02,reading,33.951,87,ok +2025-09-01T10:52:00Z,temp-A1,temperature,field-01,reading,25.016,143,ok +2025-09-01T10:53:00Z,hum-A2,humidity,field-01,reading,43.291,174,ok +2025-09-01T10:53:00Z,temp-A1,temperature,field-01,reading,25.216,144,ok +2025-09-01T10:54:00Z,hum-A2,humidity,field-01,reading,45.02,175,ok +2025-09-01T10:54:00Z,soil-A3,soil_moist,field-02,reading,33.667,88,ok +2025-09-01T10:54:00Z,temp-A1,temperature,field-01,reading,24.829,145,ok +2025-09-01T10:55:00Z,hum-A2,humidity,field-01,keepalive,,216,ok +2025-09-01T10:55:00Z,hum-A2,humidity,field-01,reading,45.113,176,ok +2025-09-01T10:55:00Z,soil-A3,soil_moist,field-02,keepalive,,126,ok +2025-09-01T10:55:00Z,temp-A1,temperature,field-01,keepalive,,180,ok +2025-09-01T10:55:00Z,temp-A1,temperature,field-01,reading,24.985,146,ok +2025-09-01T10:56:00Z,hum-A2,humidity,field-01,reading,43.192,177,ok +2025-09-01T10:56:00Z,soil-A3,soil_moist,field-02,reading,33.871,89,ok +2025-09-01T10:56:00Z,temp-A1,temperature,field-01,reading,25.198,147,ok +2025-09-01T10:57:00Z,hum-A2,humidity,field-01,reading,45.073,178,ok +2025-09-01T10:57:00Z,temp-A1,temperature,field-01,reading,25.115,148,ok +2025-09-01T10:58:00Z,hum-A2,humidity,field-01,reading,44.843,179,ok +2025-09-01T10:58:00Z,soil-A3,soil_moist,field-02,reading,31.839,90,ok +2025-09-01T10:58:00Z,temp-A1,temperature,field-01,reading,24.736,149,ok +2025-09-01T10:59:00Z,hum-A2,humidity,field-01,reading,44.371,180,ok +2025-09-01T10:59:00Z,temp-A1,temperature,field-01,reading,25.133,150,ok diff --git a/AgCloud/services/sensorGuard/sensorGuard/pytest.ini b/AgCloud/services/sensorGuard/sensorGuard/pytest.ini new file mode 100644 index 000000000..678c989ca --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/pytest.ini @@ -0,0 +1,16 @@ +[tool:pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + --verbose + --tb=short + --cov=flink_app + --cov-report=html:coverage_html + --cov-report=term-missing + --cov-fail-under=70 +markers = + unit: Unit tests + integration: Integration tests + slow: Slow tests \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/requirements.txt b/AgCloud/services/sensorGuard/sensorGuard/requirements.txt new file mode 100644 index 000000000..e70d76a76 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/requirements.txt @@ -0,0 +1,7 @@ +pyyaml +kafka-python>=2.0.2 +requests +urllib3 +grpcio +avro-python3==1.10.2 +protobuf==3.20.3 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/test-requirements.txt b/AgCloud/services/sensorGuard/sensorGuard/test-requirements.txt new file mode 100644 index 000000000..59cd202e4 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/test-requirements.txt @@ -0,0 +1,3 @@ +pytest>=7.0.0 +pytest-cov>=4.0.0 +pytest-mock>=3.10.0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/__init__.py b/AgCloud/services/sensorGuard/sensorGuard/tests/__init__.py new file mode 100644 index 000000000..1d342c1a8 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/__init__.py @@ -0,0 +1 @@ +# Test package init \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_engine.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_engine.py new file mode 100644 index 000000000..ba34aaaab --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_engine.py @@ -0,0 +1,195 @@ +import pytest +from unittest.mock import Mock, patch +from datetime import datetime, timezone +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.engine import Engine +from core.types import Event, Alert +from core.state import StateStore + + +@pytest.fixture(autouse=True) +def mock_engine_dependencies(): + """Mock external dependencies for Engine class""" + with patch('core.engine.get_access_token', return_value='test_token'), \ + patch('core.engine.update_device_last_seen'): + yield + + +class TestEngine: + """Test Engine class public methods""" + + def setup_method(self): + """Arrange - Set up test fixtures""" + self.mock_writer = Mock() + self.mock_cfg = { + 'features': {'out_of_range': True, 'stuck_sensor': True}, + 'ranges': {'temperature': {'min': 0, 'max': 50}}, + 'stuck': {'min_duration_seconds': 1800} + } + self.state_store = StateStore() + self.engine = Engine(self.mock_cfg, self.mock_writer, self.state_store) + + def test_engine_initialization(self): + """Test engine initializes correctly""" + # Assert + assert self.engine.cfg == self.mock_cfg + assert len(self.engine.writers) == 1 + assert self.engine.state is self.state_store + + def test_process_event_unknown_device(self): + """Test processing event for unknown device""" + # Arrange + event = Event( + ts=datetime.now(timezone.utc), + device_id="unknown_device", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=25.0, + seq=None, + quality="ok" + ) + + # Act + self.engine.process_event(event) + + # Assert - no alerts should be emitted for unknown devices + self.mock_writer.write.assert_not_called() + + def test_process_event_known_device_normal_value(self): + """Test processing normal value for known device""" + # Arrange + self.state_store.add_device("device_1", "temperature") + event = Event( + ts=datetime.now(timezone.utc), + device_id="device_1", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=25.0, + seq=None, + quality="ok" + ) + + # Act + self.engine.process_event(event) + + # Assert - device state should be updated + device_state = self.state_store.get("device_1") + assert device_state.last_seen_ts == event.ts + assert device_state.last_value == 25.0 + + def test_emit_alert_calls_all_writers(self): + """Test _emit calls write on all writers""" + # Arrange + writer1 = Mock() + writer2 = Mock() + engine = Engine(self.mock_cfg, [writer1, writer2], self.state_store) + alert = Alert( + device_id="device_1", + issue_type="test_alert", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act + engine._emit(alert) + + # Assert + writer1.write.assert_called_once_with(alert) + writer2.write.assert_called_once_with(alert) + + def test_open_once_new_alert(self): + """Test opening new alert when none exists""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act + self.engine._open_once(device_state, alert) + + # Assert + assert "out_of_range" in device_state.open_alerts + self.mock_writer.write.assert_called_once_with(alert) + + def test_open_once_existing_alert(self): + """Test not opening duplicate alert""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + # Pre-existing alert + existing_alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + device_state.open_alerts["out_of_range"] = existing_alert + + new_alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act + self.engine._open_once(device_state, new_alert) + + # Assert - should not emit new alert + self.mock_writer.write.assert_not_called() + assert device_state.open_alerts["out_of_range"] is existing_alert + + def test_close_if_open_existing_alert(self): + """Test closing existing alert""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + device_state.open_alerts["out_of_range"] = alert + close_ts = datetime.now(timezone.utc) + + # Act + self.engine._close_if_open(device_state, "out_of_range", close_ts) + + # Assert + assert "out_of_range" not in device_state.open_alerts + assert alert.end_ts == close_ts + self.mock_writer.write.assert_called_once_with(alert) + + def test_close_if_open_nonexistent_alert(self): + """Test closing non-existent alert does nothing""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + close_ts = datetime.now(timezone.utc) + + # Act + self.engine._close_if_open(device_state, "out_of_range", close_ts) + + # Assert - nothing should happen + self.mock_writer.write.assert_not_called() + assert len(device_state.open_alerts) == 0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_engine_quality.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_engine_quality.py new file mode 100644 index 000000000..6f6c7e9e1 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_engine_quality.py @@ -0,0 +1,683 @@ +""" +Professional, comprehensive tests for the Engine class. +These tests verify REAL business logic, not just code coverage. +""" +import pytest +import time +from unittest.mock import Mock, MagicMock, patch +from datetime import datetime, timezone, timedelta + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.engine import Engine +from core.types import Event, Alert, DeviceState +from core.state import StateStore + + +@pytest.fixture(autouse=True) +def mock_engine_dependencies(): + """Mock external dependencies for Engine class""" + with patch('core.engine.get_access_token', return_value='test_token'), \ + patch('core.engine.update_device_last_seen'), \ + patch('core.engine.get_sensors_last_seen', return_value=[]): + yield + + +class TestEngineBusinessLogic: + """Tests that verify the actual sensor monitoring business rules.""" + + def setup_method(self): + """Set up test environment with realistic configuration.""" + self.mock_writer = Mock() + self.cfg = { + "features": { + "corrupted": True, + "out_of_range": True, + "stuck_sensor": True, + "silence": True + }, + "prolonged_silence_seconds": 300, # 5 minutes + "ranges": { + "temperature": {"min": -40, "max": 85}, + "humidity": {"min": 0, "max": 100} + }, + "stuck": { + "temperature": {"tolerance": 0.1, "count": 5} + } + } + self.state_store = StateStore() + self.engine = Engine(self.cfg, self.mock_writer, self.state_store) + + def create_real_sensor_message(self, device_id, sensor_type, value, msg_type="telemetry", **extra): + """Create a message in the EXACT format that comes from real sensors.""" + return { + "sid": f"sensor-{device_id}", + "id": int(device_id), + "timestamp": datetime.now(timezone.utc).isoformat(), + "msg_type": msg_type, + "value": value, + "sensor": sensor_type, + "plant_id": 123, + "temperature": value if sensor_type == "temperature" else 25.0, + "humidity": value if sensor_type == "humidity" else 65.0, + "ph": 7.0, + "n": 50.0, + "p": 30.0, + "k": 40.0, + **extra + } + + def to_event(self, sensor_message): + """Convert sensor message to Event using EXACT logic from main.py.""" + if not isinstance(sensor_message, dict): + return None + ts = datetime.now(timezone.utc) + device_id = sensor_message.get("id") + sensor_type = sensor_message.get("sensor_type") or sensor_message.get("sensor", "unknown_sensor") + if not device_id: + device_id = "unknown_device" + else: + device_id = str(device_id) + + return Event( + ts=ts, + device_id=device_id, + sensor_type=sensor_type, + site_id=sensor_message.get("site_id"), + msg_type=sensor_message.get("msg_type", "reading"), + value=sensor_message.get("value"), + seq=sensor_message.get("seq"), + quality=sensor_message.get("quality"), + ) + + def test_new_unknown_device_is_ignored_completely(self): + """CRITICAL: Unknown devices should be completely ignored - no alerts, no state changes.""" + # Arrange: Create REAL sensor message from unknown device (not in state_store) + unknown_message = self.create_real_sensor_message( + device_id="999", # Device not added to state store + sensor_type="temperature", + value=25.0 + ) + + # Act: Convert and process like real system does + event = self.to_event(unknown_message) + assert event is not None, "Valid message should convert to event" + self.engine.process_event(event) + + # Assert: No alerts should be generated for unknown devices + assert self.mock_writer.write.call_count == 0 + + # Assert: Device should not be considered known + assert not self.state_store.is_known_device("999") + + # Assert: No state should exist for this device + result = self.state_store.get("999") + assert result is None, "Unknown device should return None from state store" + + def test_sensor_comeback_closes_keepalive_alerts_immediately(self): + """CRITICAL: When sensor sends data after being silent, missing_keepalive alert must close. + NOTE: prolonged_silence alerts are NOT closed by process_event - only by sweep_silence.""" + # Arrange: Add device and simulate missing keepalive alert + device_id = "5" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Manually create open missing_keepalive alert (simulating sweep_silence) + now = datetime.now(timezone.utc) + missing_alert = Alert( + device_id=device_id, + issue_type="missing_keepalive", + start_ts=now - timedelta(minutes=10), + end_ts=None, + severity="error", + sensor_type="temperature", + site_id=None, + details={"reason": "no data received"} + ) + + device_state.open_alerts["missing_keepalive"] = missing_alert + + # Act: Sensor sends REAL comeback message + comeback_message = self.create_real_sensor_message( + device_id="5", + sensor_type="temperature", + value=23.5 + ) + + comeback_event = self.to_event(comeback_message) + assert comeback_event is not None + self.engine.process_event(comeback_event) + + # Assert: missing_keepalive alert should be closed (emitted with end_ts) + assert self.mock_writer.write.call_count >= 1, "Expected at least 1 alert emission" + + # Verify the closed alert has end_ts set + calls = self.mock_writer.write.call_args_list + closed_alerts = [call[0][0] for call in calls] + + keepalive_closed = any(alert.issue_type == "missing_keepalive" and alert.end_ts is not None + for alert in closed_alerts) + + assert keepalive_closed, "missing_keepalive alert should be closed with end_ts" + + # Assert: missing_keepalive alert should no longer be open + assert "missing_keepalive" not in device_state.open_alerts + + def test_out_of_range_alert_opens_and_closes_correctly(self): + """CRITICAL: Out-of-range alerts must open when value is invalid, close when valid.""" + # Arrange: Add device + device_id = "5" + self.state_store.add_device(device_id, "temperature") + + # Act 1: Send out-of-range value using REAL message format + bad_message = self.create_real_sensor_message( + device_id="5", + sensor_type="temperature", + value=150.0 # Way above max of 85 + ) + + bad_event = self.to_event(bad_message) + assert bad_event is not None, "Valid message should convert to event" + + self.engine.process_event(bad_event) + + # Assert: Out-of-range alert should be opened + device_state = self.state_store.get(device_id) + assert "out_of_range" in device_state.open_alerts + + # Find the out-of-range alert that was emitted + calls = self.mock_writer.write.call_args_list + out_of_range_alerts = [call[0][0] for call in calls if call[0][0].issue_type == "out_of_range"] + assert len(out_of_range_alerts) == 1 + assert out_of_range_alerts[0].end_ts is None # Should be open + + # Reset mock for next assertion + self.mock_writer.reset_mock() + + # Act 2: Send valid value using REAL message format + good_message = self.create_real_sensor_message( + device_id="5", + sensor_type="temperature", + value=22.0 # Within valid range + ) + + good_event = self.to_event(good_message) + assert good_event is not None, "Valid message should convert to event" + + self.engine.process_event(good_event) + + # Assert: Out-of-range alert should be closed + assert "out_of_range" not in device_state.open_alerts + + # Verify alert was closed (emitted with end_ts) + closing_calls = self.mock_writer.write.call_args_list + if closing_calls: # Alert closure generates emission + closed_alert = closing_calls[0][0][0] + assert closed_alert.issue_type == "out_of_range" + assert closed_alert.end_ts is not None + + def test_complete_alert_lifecycle_with_multiple_bad_then_good_messages(self): + """COMPREHENSIVE: Test complete alert lifecycle - multiple bad messages, then recovery.""" + # Arrange: Add device + device_id = "10" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Act 1: Send first bad message (should open alert) + bad_message_1 = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=200.0 # Way out of range + ) + + self.engine.process_event(self.to_event(bad_message_1)) + + # Assert: Alert should be open + assert "out_of_range" in device_state.open_alerts + assert self.mock_writer.write.call_count == 1 # One alert opened + + # Act 2: Send second bad message (should NOT open duplicate alert) + self.mock_writer.reset_mock() + bad_message_2 = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=250.0 # Still out of range + ) + + self.engine.process_event(self.to_event(bad_message_2)) + + # Assert: Still same alert, no new alert created + assert "out_of_range" in device_state.open_alerts + assert self.mock_writer.write.call_count == 0 # No new alerts + + # Act 3: Send good message (should close alert) + self.mock_writer.reset_mock() + good_message = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=25.0 # Back to normal + ) + + self.engine.process_event(self.to_event(good_message)) + + # Assert: Alert should be closed completely + assert "out_of_range" not in device_state.open_alerts + assert self.mock_writer.write.call_count == 1 # Alert closure emitted + + # Verify the closed alert has proper end_ts + closed_alert = self.mock_writer.write.call_args[0][0] + assert closed_alert.issue_type == "out_of_range" + assert closed_alert.device_id == device_id + assert closed_alert.end_ts is not None + assert closed_alert.end_ts > closed_alert.start_ts # End after start + + # Act 4: Send another good message (should NOT close anything) + self.mock_writer.reset_mock() + another_good = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=30.0 + ) + + self.engine.process_event(self.to_event(another_good)) + + # Assert: No alert activity (no open alerts to close) + assert len(device_state.open_alerts) == 0 + assert self.mock_writer.write.call_count == 0 + + def test_multiple_alert_types_independence(self): + """CRITICAL: Different alert types should open/close independently.""" + # Arrange + device_id = "11" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Act 1: Trigger out_of_range alert + bad_temp_message = self.create_real_sensor_message( + device_id="11", + sensor_type="temperature", + value=200.0 + ) + self.engine.process_event(self.to_event(bad_temp_message)) + + # Act 2: Manually add a missing_keepalive alert (simulating silence) + keepalive_alert = Alert( + device_id=device_id, + issue_type="missing_keepalive", + start_ts=datetime.now(timezone.utc), + end_ts=None, + severity="error", + sensor_type="temperature" + ) + device_state.open_alerts["missing_keepalive"] = keepalive_alert + + # Assert: Both alerts should be open + assert "out_of_range" in device_state.open_alerts + assert "missing_keepalive" in device_state.open_alerts + assert len(device_state.open_alerts) == 2 + + # Act 3: Send good temperature (should close out_of_range but not missing_keepalive) + self.mock_writer.reset_mock() + good_temp_message = self.create_real_sensor_message( + device_id="11", + sensor_type="temperature", + value=25.0 + ) + self.engine.process_event(self.to_event(good_temp_message)) + + # Assert: out_of_range closed, missing_keepalive should be closed by _close_all_keepalive_alerts + assert "out_of_range" not in device_state.open_alerts + assert "missing_keepalive" not in device_state.open_alerts # This should be closed by comeback + + # Should have emitted 2 alerts: out_of_range closure + missing_keepalive closure + assert self.mock_writer.write.call_count == 2 + + def test_corrupted_message_overrides_out_of_range_alerts(self): + """CRITICAL: Corrupted message should close out_of_range and stuck_sensor alerts.""" + # Arrange + device_id = "12" + self.state_store.add_device(device_id, "humidity") + device_state = self.state_store.get(device_id) + + # Act 1: Create out_of_range alert first + out_of_range_message = self.create_real_sensor_message( + device_id="12", + sensor_type="humidity", + value=150.0 # Above max of 100 + ) + self.engine.process_event(self.to_event(out_of_range_message)) + + assert "out_of_range" in device_state.open_alerts + first_alert_count = self.mock_writer.write.call_count + + # Act 2: Send corrupted message (None value) + self.mock_writer.reset_mock() + corrupted_message = self.create_real_sensor_message( + device_id="12", + sensor_type="humidity", + value=None # This triggers corrupted alert + ) + self.engine.process_event(self.to_event(corrupted_message)) + + # Assert: Corrupted should open, out_of_range should be closed + assert "corrupted" in device_state.open_alerts + assert "out_of_range" not in device_state.open_alerts + + # Should emit: 1 corrupted open + 1 out_of_range close + assert self.mock_writer.write.call_count == 2 + + def test_sensor_value_oscillation_alert_behavior(self): + """COMPLEX: Test alert behavior when sensor oscillates between good/bad values.""" + # Arrange + device_id = "13" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Scenario: bad -> good -> bad -> good (should open/close/open/close) + test_sequence = [ + (100.0, True), # Bad (should open alert) + (25.0, False), # Good (should close alert) + (200.0, True), # Bad again (should open new alert) + (30.0, False) # Good again (should close alert) + ] + + total_opens = 0 + total_closes = 0 + + for i, (value, should_be_bad) in enumerate(test_sequence): + self.mock_writer.reset_mock() + + message = self.create_real_sensor_message( + device_id="13", + sensor_type="temperature", + value=value + ) + self.engine.process_event(self.to_event(message)) + + if should_be_bad: + # Should have alert open + assert "out_of_range" in device_state.open_alerts + if self.mock_writer.write.call_count > 0: + total_opens += 1 + else: + # Should NOT have alert open + assert "out_of_range" not in device_state.open_alerts + if self.mock_writer.write.call_count > 0: + total_closes += 1 + + # Should have opened 2 times and closed 2 times + assert total_opens == 2, f"Expected 2 opens, got {total_opens}" + assert total_closes == 2, f"Expected 2 closes, got {total_closes}" + + def test_sweep_silence_detects_missing_devices_correctly(self): + """CRITICAL: sweep_silence must detect devices that haven't been seen for too long. + This tests the REAL business logic: sweep_silence fetches from API and compares timestamps.""" + # Arrange: Add device to state store + device_id = "never_seen_device" + self.state_store.add_device(device_id, "humidity") + device_state = self.state_store.get(device_id) + + # Mock API to return this sensor with old last_seen timestamp + old_timestamp = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat() + mock_sensors_from_api = [ + { + "id": device_id, + "sensor_type": "humidity", + "last_seen": old_timestamp + } + ] + + # Act: Run silence sweep with mocked API response + with patch('core.engine.get_sensors_last_seen', return_value=mock_sensors_from_api): + now = datetime.now(timezone.utc) + self.engine.sweep_silence(now) + + # Assert: missing_keepalive alert should be created (gap > threshold) + assert "missing_keepalive" in device_state.open_alerts, \ + f"Expected missing_keepalive alert for device silent for 10 minutes (threshold is ~3 minutes)" + + # Verify alert was emitted + assert self.mock_writer.write.call_count >= 1, "Expected alert emission" + emitted_alert = self.mock_writer.write.call_args[0][0] + assert emitted_alert.issue_type == "missing_keepalive" + assert emitted_alert.device_id == device_id + assert emitted_alert.end_ts is None, "Alert should be open" + + def test_sweep_silence_detects_prolonged_silence_correctly(self): + """CRITICAL: sweep_silence must detect devices silent for too long and close alerts when they resume. + This tests REAL business logic: comparing API timestamps against threshold.""" + # Arrange: Add device to state store + device_id = "old_device" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Mock API to return sensor with old last_seen (10 minutes ago - exceeds 3 min threshold) + old_timestamp = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat() + mock_sensors_from_api = [ + { + "id": device_id, + "sensor_type": "temperature", + "last_seen": old_timestamp + } + ] + + # Act: Run silence sweep with mocked API - should open missing_keepalive alert + with patch('core.engine.get_sensors_last_seen', return_value=mock_sensors_from_api): + now = datetime.now(timezone.utc) + self.engine.sweep_silence(now) + + # Assert: missing_keepalive alert should be created (device silent > threshold) + assert "missing_keepalive" in device_state.open_alerts, \ + "Expected missing_keepalive alert for device silent for 10 minutes" + + # Verify alert was emitted + assert self.mock_writer.write.call_count >= 1 + first_alert = self.mock_writer.write.call_args[0][0] + assert first_alert.issue_type == "missing_keepalive" + assert first_alert.device_id == device_id + assert first_alert.end_ts is None, "Initial alert should be open" + + # Reset mock for next phase + self.mock_writer.reset_mock() + + # Act 2: Run sweep again with RECENT timestamp - should close the alert + recent_timestamp = (datetime.now(timezone.utc) - timedelta(seconds=30)).isoformat() + mock_sensors_recent = [ + { + "id": device_id, + "sensor_type": "temperature", + "last_seen": recent_timestamp + } + ] + + with patch('core.engine.get_sensors_last_seen', return_value=mock_sensors_recent): + now = datetime.now(timezone.utc) + self.engine.sweep_silence(now) + + # Assert: Alert should be closed (gap now < threshold) + assert "missing_keepalive" not in device_state.open_alerts, \ + "Alert should be closed when device is no longer silent" + + # Verify closing alert was emitted + assert self.mock_writer.write.call_count >= 1 + closing_alert = self.mock_writer.write.call_args[0][0] + assert closing_alert.issue_type == "missing_keepalive" + assert closing_alert.end_ts is not None, "Closing alert should have end_ts" + + def test_multiple_writers_receive_all_alerts(self): + """CRITICAL: When multiple writers are configured, all must receive every alert.""" + # Arrange: Setup engine with multiple writers + writer1 = Mock() + writer2 = Mock() + writer3 = Mock() + + engine = Engine(self.cfg, [writer1, writer2, writer3], self.state_store) + + device_id = "7" + self.state_store.add_device(device_id, "temperature") + + # Act: Generate an alert using REAL message format + bad_message = self.create_real_sensor_message( + device_id="7", + sensor_type="temperature", + value=-100.0 # Out of range (below min of -40) + ) + + bad_event = self.to_event(bad_message) + assert bad_event is not None + engine.process_event(bad_event) + + # Assert: All writers should receive the alert + assert writer1.write.call_count == 1 + assert writer2.write.call_count == 1 + assert writer3.write.call_count == 1 + + # Verify they all got the same alert + alert1 = writer1.write.call_args[0][0] + alert2 = writer2.write.call_args[0][0] + alert3 = writer3.write.call_args[0][0] + + assert alert1.issue_type == alert2.issue_type == alert3.issue_type == "out_of_range" + assert alert1.device_id == alert2.device_id == alert3.device_id == device_id + + def test_device_state_updates_correctly_during_processing(self): + """CRITICAL: Device state must be updated properly with each event.""" + # Arrange + device_id = "8" + self.state_store.add_device(device_id, "humidity") + + initial_state = self.state_store.get(device_id) + assert initial_state.last_seen_ts is None + assert initial_state.last_value is None + + # Act: Process first event using REAL message format + first_message = self.create_real_sensor_message( + device_id="8", + sensor_type="humidity", + value=45.2 + ) + + first_event = self.to_event(first_message) + assert first_event is not None + first_process_time = first_event.ts # Capture the actual timestamp used + + self.engine.process_event(first_event) + + # Assert: State should be updated + updated_state = self.state_store.get(device_id) + assert updated_state.last_seen_ts == first_process_time + assert updated_state.last_value == 45.2 + assert updated_state.sensor_type == "humidity" + + # Act: Process second event with different values + time.sleep(0.1) # Ensure different timestamp + second_message = self.create_real_sensor_message( + device_id="8", + sensor_type="humidity", + value=67.8 + ) + + second_event = self.to_event(second_message) + assert second_event is not None + second_process_time = second_event.ts + + self.engine.process_event(second_event) + + # Assert: State should reflect latest values + final_state = self.state_store.get(device_id) + assert final_state.last_seen_ts == second_process_time + assert final_state.last_value == 67.8 + assert final_state.sensor_type == "humidity" + + +class TestEngineEdgeCases: + """Tests for edge cases and error scenarios.""" + + def setup_method(self): + self.mock_writer = Mock() + self.cfg = { + "features": {"corrupted": True, "out_of_range": True, "stuck_sensor": True, "silence": True}, + "prolonged_silence_seconds": 300 + } + self.engine = Engine(self.cfg, self.mock_writer) + + def create_real_sensor_message(self, device_id, sensor_type, value, msg_type="telemetry", **extra): + """Create a message in the EXACT format that comes from real sensors.""" + return { + "sid": f"sensor-{device_id}", + "id": int(device_id), + "timestamp": datetime.now(timezone.utc).isoformat(), + "msg_type": msg_type, + "value": value, + "sensor": sensor_type, + "plant_id": 123, + "temperature": value if sensor_type == "temperature" else 25.0, + "humidity": value if sensor_type == "humidity" else 65.0, + "ph": 7.0, + "n": 50.0, + "p": 30.0, + "k": 40.0, + **extra + } + + def to_event(self, sensor_message): + """Convert sensor message to Event using EXACT logic from main.py.""" + if not isinstance(sensor_message, dict): + return None + ts = datetime.now(timezone.utc) + device_id = sensor_message.get("id") + sensor_type = sensor_message.get("sensor_type") or sensor_message.get("sensor", "unknown_sensor") + if not device_id: + device_id = "unknown_device" + else: + device_id = str(device_id) + + return Event( + ts=ts, + device_id=device_id, + sensor_type=sensor_type, + site_id=sensor_message.get("site_id"), + msg_type=sensor_message.get("msg_type", "reading"), + value=sensor_message.get("value"), + seq=sensor_message.get("seq"), + quality=sensor_message.get("quality"), + ) + + def test_engine_handles_none_values_gracefully(self): + """Edge case: Engine should handle None/null values without crashing.""" + # Arrange: Add device + device_id = "9" + self.engine.state.add_device(device_id, "temperature") + + # Act: Send REAL message with None value (like corrupted sensor reading) + null_message = self.create_real_sensor_message( + device_id="9", + sensor_type="temperature", + value=None # Corrupted/missing sensor reading + ) + + null_event = self.to_event(null_message) + assert null_event is not None + + # This should not raise an exception + try: + self.engine.process_event(null_event) + except Exception as e: + pytest.fail(f"Engine crashed with None value: {e}") + + def test_sweep_silence_on_empty_state_store(self): + """Edge case: sweep_silence should handle empty state store.""" + # Act: Run sweep on empty state (should not crash) + try: + self.engine.sweep_silence(datetime.now(timezone.utc)) + except Exception as e: + pytest.fail(f"sweep_silence crashed on empty state: {e}") + + # Assert: No alerts should be generated + assert self.mock_writer.write.call_count == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_state.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_state.py new file mode 100644 index 000000000..bb2e385fa --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_state.py @@ -0,0 +1,136 @@ +import pytest +from unittest.mock import Mock +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.state import StateStore +from core.types import DeviceState + + +class TestStateStore: + """Test StateStore class public methods""" + + def setup_method(self): + """Arrange - Set up test fixtures""" + self.state_store = StateStore() + + def test_add_device_new(self): + """Test adding new device to state store""" + # Act + self.state_store.add_device("device_1", "temperature") + + # Assert + assert self.state_store.is_known_device("device_1") + device_state = self.state_store.get("device_1") + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_add_device_existing(self): + """Test adding existing device doesn't overwrite""" + # Arrange + self.state_store.add_device("device_1", "temperature") + original_state = self.state_store.get("device_1") + + # Act - Try to add same device with different sensor type + self.state_store.add_device("device_1", "humidity") + + # Assert - Original state preserved + current_state = self.state_store.get("device_1") + assert current_state is original_state + assert current_state.sensor_type == "temperature" # Not changed + + def test_is_known_device_true(self): + """Test is_known_device returns True for existing device""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act & Assert + assert self.state_store.is_known_device("device_1") is True + + def test_is_known_device_false(self): + """Test is_known_device returns False for non-existing device""" + # Act & Assert + assert self.state_store.is_known_device("unknown_device") is False + + def test_get_device_existing(self): + """Test getting existing device state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act + device_state = self.state_store.get("device_1") + + # Assert + assert device_state is not None + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_get_device_nonexistent(self): + """Test getting non-existent device returns None""" + # Act + device_state = self.state_store.get("unknown_device") + + # Assert + assert device_state is None + + def test_all_states_empty(self): + """Test getting all devices when store is empty""" + # Act + all_devices = list(self.state_store.all_states()) + + # Assert + assert len(all_devices) == 0 + + def test_all_states_multiple(self): + """Test getting all devices with multiple devices""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + self.state_store.add_device("device_3", "pressure") + + # Act + all_states = list(self.state_store.all_states()) + + # Assert + assert len(all_states) == 3 + device_ids = [device_id for device_id, state in all_states] + assert "device_1" in device_ids + assert "device_2" in device_ids + assert "device_3" in device_ids + + def test_state_persistence(self): + """Test that device state persists across operations""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + # Act - Modify state + from datetime import datetime, timezone + ts = datetime.now(timezone.utc) + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert - Changes persist when retrieving again + retrieved_state = self.state_store.get("device_1") + assert retrieved_state.last_seen_ts == ts + assert retrieved_state.last_value == 25.0 + + def test_multiple_devices_independence(self): + """Test that multiple devices maintain independent state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + + device_1 = self.state_store.get("device_1") + device_2 = self.state_store.get("device_2") + + # Act - Modify only device_1 + device_1.last_value = 25.0 + + # Assert - device_2 not affected + assert device_1.last_value == 25.0 + assert device_2.last_value is None + assert device_1 is not device_2 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_state_fixed.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_state_fixed.py new file mode 100644 index 000000000..bb2e385fa --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_state_fixed.py @@ -0,0 +1,136 @@ +import pytest +from unittest.mock import Mock +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.state import StateStore +from core.types import DeviceState + + +class TestStateStore: + """Test StateStore class public methods""" + + def setup_method(self): + """Arrange - Set up test fixtures""" + self.state_store = StateStore() + + def test_add_device_new(self): + """Test adding new device to state store""" + # Act + self.state_store.add_device("device_1", "temperature") + + # Assert + assert self.state_store.is_known_device("device_1") + device_state = self.state_store.get("device_1") + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_add_device_existing(self): + """Test adding existing device doesn't overwrite""" + # Arrange + self.state_store.add_device("device_1", "temperature") + original_state = self.state_store.get("device_1") + + # Act - Try to add same device with different sensor type + self.state_store.add_device("device_1", "humidity") + + # Assert - Original state preserved + current_state = self.state_store.get("device_1") + assert current_state is original_state + assert current_state.sensor_type == "temperature" # Not changed + + def test_is_known_device_true(self): + """Test is_known_device returns True for existing device""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act & Assert + assert self.state_store.is_known_device("device_1") is True + + def test_is_known_device_false(self): + """Test is_known_device returns False for non-existing device""" + # Act & Assert + assert self.state_store.is_known_device("unknown_device") is False + + def test_get_device_existing(self): + """Test getting existing device state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act + device_state = self.state_store.get("device_1") + + # Assert + assert device_state is not None + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_get_device_nonexistent(self): + """Test getting non-existent device returns None""" + # Act + device_state = self.state_store.get("unknown_device") + + # Assert + assert device_state is None + + def test_all_states_empty(self): + """Test getting all devices when store is empty""" + # Act + all_devices = list(self.state_store.all_states()) + + # Assert + assert len(all_devices) == 0 + + def test_all_states_multiple(self): + """Test getting all devices with multiple devices""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + self.state_store.add_device("device_3", "pressure") + + # Act + all_states = list(self.state_store.all_states()) + + # Assert + assert len(all_states) == 3 + device_ids = [device_id for device_id, state in all_states] + assert "device_1" in device_ids + assert "device_2" in device_ids + assert "device_3" in device_ids + + def test_state_persistence(self): + """Test that device state persists across operations""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + # Act - Modify state + from datetime import datetime, timezone + ts = datetime.now(timezone.utc) + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert - Changes persist when retrieving again + retrieved_state = self.state_store.get("device_1") + assert retrieved_state.last_seen_ts == ts + assert retrieved_state.last_value == 25.0 + + def test_multiple_devices_independence(self): + """Test that multiple devices maintain independent state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + + device_1 = self.state_store.get("device_1") + device_2 = self.state_store.get("device_2") + + # Act - Modify only device_1 + device_1.last_value = 25.0 + + # Assert - device_2 not affected + assert device_1.last_value == 25.0 + assert device_2.last_value is None + assert device_1 is not device_2 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_state_quality.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_state_quality.py new file mode 100644 index 000000000..a068af528 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_state_quality.py @@ -0,0 +1,319 @@ +""" +Professional, comprehensive tests for StateStore class. +These tests verify data integrity and state management correctness. +""" +import pytest +from datetime import datetime, timezone, timedelta + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.state import StateStore +from core.types import DeviceState, Alert + + +class TestStateStoreDataIntegrity: + """Tests that verify data integrity and correct state management.""" + + def setup_method(self): + """Set up fresh StateStore for each test.""" + self.state_store = StateStore() + + def test_add_device_creates_proper_state_structure(self): + """CRITICAL: Adding device must create correct internal state structure.""" + # Arrange + device_id = "sensor_001" + sensor_type = "temperature" + + # Act: Add device + self.state_store.add_device(device_id, sensor_type) + + # Assert: Device should be in known devices + assert self.state_store.is_known_device(device_id) + + # Assert: Device state should be properly initialized + device_state = self.state_store.get(device_id) + assert device_state is not None + assert device_state.device_id == device_id + assert device_state.sensor_type == sensor_type + assert device_state.last_seen_ts is None # Should start as None + assert device_state.last_value is None # Should start as None + assert device_state.open_alerts == {} # Should start empty + + def test_get_unknown_device_returns_none_safely(self): + """CRITICAL: Getting unknown device must return None, not crash.""" + # Act: Try to get device that was never added + result = self.state_store.get("nonexistent_device") + + # Assert: Should return None safely + assert result is None + + # Assert: Should not be considered known + assert not self.state_store.is_known_device("nonexistent_device") + + def test_device_state_persistence_across_operations(self): + """CRITICAL: Device state changes must persist correctly.""" + # Arrange: Add device and get its state + device_id = "persistent_device" + self.state_store.add_device(device_id, "humidity") + device_state = self.state_store.get(device_id) + + # Act: Modify device state + test_time = datetime.now(timezone.utc) + test_value = 42.7 + test_alert = Alert( + issue_type="test_alert", + device_id=device_id, + sensor_type="humidity", + site_id="test_site", + severity="warning", + start_ts=test_time, + end_ts=None, + details={"test": "data"} + ) + + device_state.last_seen_ts = test_time + device_state.last_value = test_value + device_state.open_alerts["test_alert"] = test_alert + + # Assert: Changes should persist when retrieving again + retrieved_state = self.state_store.get(device_id) + assert retrieved_state.last_seen_ts == test_time + assert retrieved_state.last_value == test_value + assert "test_alert" in retrieved_state.open_alerts + assert retrieved_state.open_alerts["test_alert"].issue_type == "test_alert" + + def test_multiple_devices_maintain_separate_states(self): + """CRITICAL: Multiple devices must have completely separate states.""" + # Arrange: Add multiple devices + device1 = "temp_sensor_01" + device2 = "humid_sensor_02" + device3 = "pressure_sensor_03" + + self.state_store.add_device(device1, "temperature") + self.state_store.add_device(device2, "humidity") + self.state_store.add_device(device3, "pressure") + + # Act: Set different values for each device + time1 = datetime.now(timezone.utc) + time2 = time1 + timedelta(minutes=1) + time3 = time1 + timedelta(minutes=2) + + state1 = self.state_store.get(device1) + state2 = self.state_store.get(device2) + state3 = self.state_store.get(device3) + + state1.last_seen_ts = time1 + state1.last_value = 25.5 + + state2.last_seen_ts = time2 + state2.last_value = 65.8 + + state3.last_seen_ts = time3 + state3.last_value = 1013.2 + + # Assert: Each device maintains its own independent state + retrieved1 = self.state_store.get(device1) + retrieved2 = self.state_store.get(device2) + retrieved3 = self.state_store.get(device3) + + assert retrieved1.last_seen_ts == time1 + assert retrieved1.last_value == 25.5 + assert retrieved1.sensor_type == "temperature" + + assert retrieved2.last_seen_ts == time2 + assert retrieved2.last_value == 65.8 + assert retrieved2.sensor_type == "humidity" + + assert retrieved3.last_seen_ts == time3 + assert retrieved3.last_value == 1013.2 + assert retrieved3.sensor_type == "pressure" + + def test_all_states_returns_correct_iterator(self): + """CRITICAL: all_states() must return all devices and their current states.""" + # Arrange: Add several devices with different states + devices_data = [ + ("device_a", "temperature", 22.1), + ("device_b", "humidity", 58.3), + ("device_c", "pressure", 1015.7) + ] + + for device_id, sensor_type, value in devices_data: + self.state_store.add_device(device_id, sensor_type) + state = self.state_store.get(device_id) + state.last_value = value + + # Act: Get all states + all_states = dict(self.state_store.all_states()) + + # Assert: Should contain exactly the devices we added + assert len(all_states) == 3 + assert "device_a" in all_states + assert "device_b" in all_states + assert "device_c" in all_states + + # Assert: States should contain correct data + assert all_states["device_a"].sensor_type == "temperature" + assert all_states["device_a"].last_value == 22.1 + + assert all_states["device_b"].sensor_type == "humidity" + assert all_states["device_b"].last_value == 58.3 + + assert all_states["device_c"].sensor_type == "pressure" + assert all_states["device_c"].last_value == 1015.7 + + def test_device_id_type_conversion_consistency(self): + """CRITICAL: Device IDs must be consistently converted to strings.""" + # Act: Add devices with different ID types + self.state_store.add_device(123, "temperature") # Integer + self.state_store.add_device("456", "humidity") # String + self.state_store.add_device(789.0, "pressure") # Float + + # Assert: All should be accessible as strings + assert self.state_store.is_known_device("123") + assert self.state_store.is_known_device("456") + assert self.state_store.is_known_device("789.0") + + # Assert: States should be retrievable with string IDs + assert self.state_store.get("123") is not None + assert self.state_store.get("456") is not None + assert self.state_store.get("789.0") is not None + + # Assert: Original types should also work (converted internally) + assert self.state_store.is_known_device(123) + assert self.state_store.is_known_device(456) + assert self.state_store.is_known_device(789.0) + + def test_duplicate_device_addition_is_safe(self): + """CRITICAL: Adding same device multiple times must be safe.""" + # Arrange: Add device and modify its state + device_id = "duplicate_test_device" + self.state_store.add_device(device_id, "temperature") + + original_state = self.state_store.get(device_id) + test_time = datetime.now(timezone.utc) + original_state.last_seen_ts = test_time + original_state.last_value = 99.9 + + # Act: Add same device again (should not overwrite existing state) + self.state_store.add_device(device_id, "humidity") # Different sensor type + + # Assert: Original state should be preserved + current_state = self.state_store.get(device_id) + assert current_state.last_seen_ts == test_time + assert current_state.last_value == 99.9 + assert current_state.sensor_type == "temperature" # Should keep original + + +class TestStateStoreEdgeCases: + """Tests for edge cases and boundary conditions.""" + + def setup_method(self): + self.state_store = StateStore() + + def test_empty_state_store_operations(self): + """Edge case: Operations on empty state store should be safe.""" + # Assert: Empty state store behaves correctly + assert not self.state_store.is_known_device("any_device") + assert self.state_store.get("any_device") is None + + # all_states should return empty iterator + all_states = list(self.state_store.all_states()) + assert len(all_states) == 0 + + def test_none_and_empty_device_ids(self): + """Edge case: None/empty device IDs should be handled gracefully.""" + # Act/Assert: These should not crash + try: + result1 = self.state_store.is_known_device("") + result2 = self.state_store.get("") + # Empty string is valid, should return False/None + assert result1 is False + assert result2 is None + except Exception as e: + pytest.fail(f"Empty string device ID caused crash: {e}") + + def test_very_large_number_of_devices(self): + """Performance test: Should handle many devices efficiently.""" + # Arrange: Add many devices + num_devices = 1000 + for i in range(num_devices): + device_id = f"device_{i:04d}" + self.state_store.add_device(device_id, f"sensor_type_{i % 10}") + + # Act/Assert: Should be able to access all devices + for i in range(num_devices): + device_id = f"device_{i:04d}" + assert self.state_store.is_known_device(device_id) + state = self.state_store.get(device_id) + assert state is not None + assert state.device_id == device_id + + # Should return correct count + all_states = list(self.state_store.all_states()) + assert len(all_states) == num_devices + + +class TestStateStoreBusinessRules: + """Tests that verify business logic around state management.""" + + def setup_method(self): + self.state_store = StateStore() + + def test_alert_lifecycle_management(self): + """Business rule: Alert lifecycle should be managed correctly in device state.""" + # Arrange: Add device + device_id = "alert_lifecycle_device" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Act: Simulate alert lifecycle + now = datetime.now(timezone.utc) + + # 1. Open alert + alert1 = Alert( + issue_type="out_of_range", + device_id=device_id, + sensor_type="temperature", + site_id="site1", + severity="warning", + start_ts=now, + end_ts=None, + details={"value": 150.0, "max": 85.0} + ) + device_state.open_alerts["out_of_range"] = alert1 + + # 2. Open second alert + alert2 = Alert( + issue_type="stuck_sensor", + device_id=device_id, + sensor_type="temperature", + site_id="site1", + severity="error", + start_ts=now + timedelta(minutes=1), + end_ts=None, + details={"repeated_value": 150.0} + ) + device_state.open_alerts["stuck_sensor"] = alert2 + + # Assert: Both alerts should be tracked + assert len(device_state.open_alerts) == 2 + assert "out_of_range" in device_state.open_alerts + assert "stuck_sensor" in device_state.open_alerts + + # 3. Close first alert + closed_alert = device_state.open_alerts.pop("out_of_range") + closed_alert.end_ts = now + timedelta(minutes=2) + + # Assert: Only one alert should remain open + assert len(device_state.open_alerts) == 1 + assert "out_of_range" not in device_state.open_alerts + assert "stuck_sensor" in device_state.open_alerts + + # Assert: Closed alert should have end_ts + assert closed_alert.end_ts is not None + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_types.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_types.py new file mode 100644 index 000000000..d049f6da3 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_types.py @@ -0,0 +1,170 @@ +import pytest +from datetime import datetime, timezone +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.types import Event, Alert, DeviceState + + +class TestEvent: + """Test Event class""" + + def test_event_creation(self): + """Test creating Event with valid data""" + # Arrange + ts = datetime.now(timezone.utc) + + # Act + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="telemetry", + value=25.5, + seq=1, + quality="ok" + ) + + # Assert + assert event.ts == ts + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value == 25.5 + assert event.msg_type == "telemetry" + assert event.seq == 1 + assert event.quality == "ok" + + def test_event_with_minimal_data(self): + """Test creating Event with minimal required data""" + # Arrange & Act + ts = datetime.now(timezone.utc) + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=None, + seq=None, + quality=None + ) + + # Assert + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value is None + + +class TestAlert: + """Test Alert class""" + + def test_alert_creation(self): + """Test creating Alert with valid data""" + # Arrange + start_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=start_ts, + end_ts=None, + sensor_type="temperature" + ) + + # Assert + assert alert.issue_type == "out_of_range" + assert alert.device_id == "sensor_1" + assert alert.severity == "error" + assert alert.start_ts == start_ts + assert alert.end_ts is None + assert alert.sensor_type == "temperature" + + def test_alert_with_end_time(self): + """Test alert with both start and end times""" + # Arrange + start_ts = datetime.now(timezone.utc) + end_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_2", + issue_type="missing_keepalive", + severity="critical", + start_ts=start_ts, + end_ts=end_ts + ) + + # Assert + assert alert.start_ts == start_ts + assert alert.end_ts == end_ts + + +class TestDeviceState: + """Test DeviceState class""" + + def test_device_state_creation(self): + """Test creating DeviceState""" + # Arrange & Act + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + + # Assert + assert device_state.device_id == "sensor_1" + assert device_state.sensor_type == "temperature" + assert device_state.last_seen_ts is None + assert device_state.last_value is None + assert len(device_state.open_alerts) == 0 + + def test_device_state_update(self): + """Test updating device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + ts = datetime.now(timezone.utc) + + # Act + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert + assert device_state.last_seen_ts == ts + assert device_state.last_value == 25.0 + + def test_device_state_alerts_management(self): + """Test managing alerts in device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act - Add alert + device_state.open_alerts["out_of_range"] = alert + + # Assert + assert "out_of_range" in device_state.open_alerts + assert device_state.open_alerts["out_of_range"] == alert + + # Act - Remove alert + removed_alert = device_state.open_alerts.pop("out_of_range") + + # Assert + assert removed_alert == alert + assert len(device_state.open_alerts) == 0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_types_fixed.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_types_fixed.py new file mode 100644 index 000000000..d049f6da3 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_types_fixed.py @@ -0,0 +1,170 @@ +import pytest +from datetime import datetime, timezone +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.types import Event, Alert, DeviceState + + +class TestEvent: + """Test Event class""" + + def test_event_creation(self): + """Test creating Event with valid data""" + # Arrange + ts = datetime.now(timezone.utc) + + # Act + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="telemetry", + value=25.5, + seq=1, + quality="ok" + ) + + # Assert + assert event.ts == ts + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value == 25.5 + assert event.msg_type == "telemetry" + assert event.seq == 1 + assert event.quality == "ok" + + def test_event_with_minimal_data(self): + """Test creating Event with minimal required data""" + # Arrange & Act + ts = datetime.now(timezone.utc) + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=None, + seq=None, + quality=None + ) + + # Assert + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value is None + + +class TestAlert: + """Test Alert class""" + + def test_alert_creation(self): + """Test creating Alert with valid data""" + # Arrange + start_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=start_ts, + end_ts=None, + sensor_type="temperature" + ) + + # Assert + assert alert.issue_type == "out_of_range" + assert alert.device_id == "sensor_1" + assert alert.severity == "error" + assert alert.start_ts == start_ts + assert alert.end_ts is None + assert alert.sensor_type == "temperature" + + def test_alert_with_end_time(self): + """Test alert with both start and end times""" + # Arrange + start_ts = datetime.now(timezone.utc) + end_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_2", + issue_type="missing_keepalive", + severity="critical", + start_ts=start_ts, + end_ts=end_ts + ) + + # Assert + assert alert.start_ts == start_ts + assert alert.end_ts == end_ts + + +class TestDeviceState: + """Test DeviceState class""" + + def test_device_state_creation(self): + """Test creating DeviceState""" + # Arrange & Act + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + + # Assert + assert device_state.device_id == "sensor_1" + assert device_state.sensor_type == "temperature" + assert device_state.last_seen_ts is None + assert device_state.last_value is None + assert len(device_state.open_alerts) == 0 + + def test_device_state_update(self): + """Test updating device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + ts = datetime.now(timezone.utc) + + # Act + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert + assert device_state.last_seen_ts == ts + assert device_state.last_value == 25.0 + + def test_device_state_alerts_management(self): + """Test managing alerts in device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act - Add alert + device_state.open_alerts["out_of_range"] = alert + + # Assert + assert "out_of_range" in device_state.open_alerts + assert device_state.open_alerts["out_of_range"] == alert + + # Act - Remove alert + removed_alert = device_state.open_alerts.pop("out_of_range") + + # Assert + assert removed_alert == alert + assert len(device_state.open_alerts) == 0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/sensorGuard/tests/test_types_quality.py b/AgCloud/services/sensorGuard/sensorGuard/tests/test_types_quality.py new file mode 100644 index 000000000..d72e23ce8 --- /dev/null +++ b/AgCloud/services/sensorGuard/sensorGuard/tests/test_types_quality.py @@ -0,0 +1,439 @@ +""" +Professional, comprehensive tests for Types (dataclasses). +These tests verify data validation, business rules, and type safety. +""" +import pytest +from datetime import datetime, timezone, timedelta +from copy import deepcopy + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.types import Event, Alert, DeviceState + + +class TestEventDataIntegrity: + """Tests that verify Event dataclass behavior and business rules.""" + + def test_event_creation_with_all_required_fields(self): + """CRITICAL: Event must be creatable with all required fields.""" + # Arrange + test_time = datetime.now(timezone.utc) + + # Act: Create event with all fields + event = Event( + ts=test_time, + device_id="sensor_123", + sensor_type="temperature", + site_id="greenhouse_a", + msg_type="reading", + value=24.7, + seq=42, + quality="ok" + ) + + # Assert: All fields should be set correctly + assert event.ts == test_time + assert event.device_id == "sensor_123" + assert event.sensor_type == "temperature" + assert event.site_id == "greenhouse_a" + assert event.msg_type == "reading" + assert event.value == 24.7 + assert event.seq == 42 + assert event.quality == "ok" + + def test_event_with_none_optional_fields(self): + """Business rule: Event should handle None values for optional fields.""" + # Act: Create event with minimal required fields + event = Event( + ts=datetime.now(timezone.utc), + device_id="minimal_device", + sensor_type="humidity", + site_id=None, # Optional + msg_type="keepalive", + value=None, # Optional (e.g., keepalive messages) + seq=None, # Optional + quality=None # Optional + ) + + # Assert: Should handle None values gracefully + assert event.site_id is None + assert event.value is None + assert event.seq is None + assert event.quality is None + + def test_event_immutability_after_creation(self): + """Data integrity: Event fields should be modifiable after creation.""" + # Arrange: Create event + original_time = datetime.now(timezone.utc) + event = Event( + ts=original_time, + device_id="test_device", + sensor_type="temperature", + site_id="site1", + msg_type="reading", + value=25.0, + seq=1, + quality="ok" + ) + + # Act: Modify fields (should be allowed for dataclass) + new_time = original_time + timedelta(seconds=30) + event.ts = new_time + event.value = 26.5 + event.quality = "corrupted" + + # Assert: Changes should be reflected + assert event.ts == new_time + assert event.value == 26.5 + assert event.quality == "corrupted" + + def test_event_different_msg_types_validation(self): + """Business rule: Different msg_type values should be handled correctly.""" + base_params = { + "ts": datetime.now(timezone.utc), + "device_id": "msg_type_test", + "sensor_type": "temperature", + "site_id": "site1" + } + + # Test reading message + reading_event = Event( + **base_params, + msg_type="reading", + value=23.4, + seq=1, + quality="ok" + ) + assert reading_event.msg_type == "reading" + assert reading_event.value is not None + + # Test keepalive message (typically no sensor value) + keepalive_event = Event( + **base_params, + msg_type="keepalive", + value=None, # Keepalives usually don't have sensor values + seq=2, + quality=None + ) + assert keepalive_event.msg_type == "keepalive" + assert keepalive_event.value is None + + +class TestAlertDataIntegrity: + """Tests that verify Alert dataclass behavior and lifecycle management.""" + + def test_alert_creation_with_required_fields(self): + """CRITICAL: Alert must be creatable with all required fields.""" + # Arrange + start_time = datetime.now(timezone.utc) + + # Act: Create alert + alert = Alert( + device_id="alert_test_device", + issue_type="out_of_range", + start_ts=start_time, + end_ts=None, # Open alert + severity="warning", + sensor_type="temperature", + site_id="greenhouse_b", + details={"value": 150.0, "max_allowed": 85.0} + ) + + # Assert: All fields should be set correctly + assert alert.device_id == "alert_test_device" + assert alert.issue_type == "out_of_range" + assert alert.start_ts == start_time + assert alert.end_ts is None + assert alert.severity == "warning" + assert alert.sensor_type == "temperature" + assert alert.site_id == "greenhouse_b" + assert alert.details["value"] == 150.0 + assert alert.details["max_allowed"] == 85.0 + + def test_alert_lifecycle_open_to_closed(self): + """Business rule: Alert should properly transition from open to closed state.""" + # Arrange: Create open alert + start_time = datetime.now(timezone.utc) + alert = Alert( + device_id="lifecycle_test", + issue_type="stuck_sensor", + start_ts=start_time, + end_ts=None, # Initially open + severity="error" + ) + + # Verify initially open + assert alert.end_ts is None + + # Act: Close the alert + end_time = start_time + timedelta(minutes=5) + alert.end_ts = end_time + + # Assert: Should be properly closed + assert alert.end_ts == end_time + duration = alert.end_ts - alert.start_ts + assert duration.total_seconds() == 300 # 5 minutes + + def test_alert_different_severities(self): + """Business rule: Different severity levels should be supported.""" + base_params = { + "device_id": "severity_test", + "issue_type": "test_issue", + "start_ts": datetime.now(timezone.utc), + "end_ts": None + } + + # Test different severity levels + warning_alert = Alert(**base_params, severity="warning") + error_alert = Alert(**base_params, severity="error") + critical_alert = Alert(**base_params, severity="critical") + + assert warning_alert.severity == "warning" + assert error_alert.severity == "error" + assert critical_alert.severity == "critical" + + def test_alert_details_dictionary_flexibility(self): + """Business rule: Alert details should support flexible data structures.""" + # Act: Create alert with complex details + alert = Alert( + device_id="details_test", + issue_type="complex_issue", + start_ts=datetime.now(timezone.utc), + end_ts=None, + severity="warning", + details={ + "sensor_readings": [22.1, 22.1, 22.1, 22.1], + "threshold": 0.1, + "consecutive_count": 4, + "metadata": { + "location": "field_section_3", + "operator": "automated_system" + }, + "numeric_value": 42.7, + "boolean_flag": True + } + ) + + # Assert: Complex details should be preserved + assert len(alert.details["sensor_readings"]) == 4 + assert alert.details["threshold"] == 0.1 + assert alert.details["metadata"]["location"] == "field_section_3" + assert alert.details["numeric_value"] == 42.7 + assert alert.details["boolean_flag"] is True + + +class TestDeviceStateDataIntegrity: + """Tests that verify DeviceState dataclass behavior and state management.""" + + def test_device_state_creation_with_minimal_params(self): + """CRITICAL: DeviceState should initialize with sensible defaults.""" + # Act: Create device state with minimal parameters + device_state = DeviceState(device_id="minimal_device") + + # Assert: Should have proper defaults + assert device_state.device_id == "minimal_device" + assert device_state.sensor_type is None + assert device_state.last_seen_ts is None + assert device_state.last_value is None + assert device_state.run_length == 0 + assert device_state.stuck_since_ts is None + assert device_state.open_alerts == {} + + def test_device_state_alert_management(self): + """Business rule: DeviceState should properly manage multiple open alerts.""" + # Arrange: Create device state + device_state = DeviceState( + device_id="multi_alert_device", + sensor_type="temperature" + ) + + # Act: Add multiple alerts + now = datetime.now(timezone.utc) + + alert1 = Alert( + device_id="multi_alert_device", + issue_type="out_of_range", + start_ts=now, + end_ts=None, + severity="warning" + ) + + alert2 = Alert( + device_id="multi_alert_device", + issue_type="stuck_sensor", + start_ts=now + timedelta(minutes=1), + end_ts=None, + severity="error" + ) + + device_state.open_alerts["out_of_range"] = alert1 + device_state.open_alerts["stuck_sensor"] = alert2 + + # Assert: Both alerts should be tracked + assert len(device_state.open_alerts) == 2 + assert "out_of_range" in device_state.open_alerts + assert "stuck_sensor" in device_state.open_alerts + + # Assert: Alerts should maintain their properties + assert device_state.open_alerts["out_of_range"].severity == "warning" + assert device_state.open_alerts["stuck_sensor"].severity == "error" + + def test_device_state_evolution_over_time(self): + """Business rule: DeviceState should track sensor data evolution correctly.""" + # Arrange: Create device state + device_state = DeviceState( + device_id="evolution_test", + sensor_type="humidity" + ) + + # Act: Simulate sensor data evolution + time1 = datetime.now(timezone.utc) + time2 = time1 + timedelta(minutes=1) + time3 = time1 + timedelta(minutes=2) + + # First reading + device_state.last_seen_ts = time1 + device_state.last_value = 45.2 + device_state.run_length = 1 + + # Second reading (same value - potential stuck sensor) + device_state.last_seen_ts = time2 + device_state.last_value = 45.2 # Same value + device_state.run_length = 2 + device_state.stuck_since_ts = time1 # Started being stuck from first occurrence + + # Third reading (different value - unstuck) + device_state.last_seen_ts = time3 + device_state.last_value = 46.8 # Different value + device_state.run_length = 1 # Reset + device_state.stuck_since_ts = None # No longer stuck + + # Assert: State evolution should be properly tracked + assert device_state.last_seen_ts == time3 + assert device_state.last_value == 46.8 + assert device_state.run_length == 1 + assert device_state.stuck_since_ts is None + + def test_device_state_deep_copy_independence(self): + """Data integrity: DeviceState copies should be independent.""" + # Arrange: Create device state with complex data + original_time = datetime.now(timezone.utc) + original_state = DeviceState( + device_id="copy_test", + sensor_type="temperature", + last_seen_ts=original_time, + last_value=25.0, + run_length=3 + ) + + # Add an alert + alert = Alert( + device_id="copy_test", + issue_type="test_alert", + start_ts=original_time, + end_ts=None, + severity="warning" + ) + original_state.open_alerts["test_alert"] = alert + + # Act: Create deep copy + copied_state = deepcopy(original_state) + + # Modify original + new_time = original_time + timedelta(minutes=1) + original_state.last_seen_ts = new_time + original_state.last_value = 26.0 + original_state.open_alerts["test_alert"].severity = "error" + + # Assert: Copy should remain unchanged + assert copied_state.last_seen_ts == original_time + assert copied_state.last_value == 25.0 + assert copied_state.open_alerts["test_alert"].severity == "warning" + + +class TestTypesBusinessRules: + """Tests that verify business rules across all types.""" + + def test_event_to_alert_data_consistency(self): + """Business rule: Alerts generated from Events should maintain data consistency.""" + # Arrange: Create event + event_time = datetime.now(timezone.utc) + event = Event( + ts=event_time, + device_id="consistency_test", + sensor_type="pressure", + site_id="factory_floor_2", + msg_type="reading", + value=999.9, # Out of range value + seq=15, + quality="ok" + ) + + # Act: Create alert based on event (simulating engine behavior) + alert = Alert( + device_id=event.device_id, # Must match + issue_type="out_of_range", + start_ts=event.ts, # Must use event timestamp + end_ts=None, + severity="error", + sensor_type=event.sensor_type, # Must match + site_id=event.site_id, # Must match + details={ + "trigger_value": event.value, # Include event data + "trigger_seq": event.seq, + "trigger_quality": event.quality + } + ) + + # Assert: Data consistency between event and alert + assert alert.device_id == event.device_id + assert alert.start_ts == event.ts + assert alert.sensor_type == event.sensor_type + assert alert.site_id == event.site_id + assert alert.details["trigger_value"] == event.value + assert alert.details["trigger_seq"] == event.seq + assert alert.details["trigger_quality"] == event.quality + + def test_timestamp_ordering_consistency(self): + """Business rule: Timestamps should maintain logical ordering.""" + # Arrange: Create time sequence + base_time = datetime.now(timezone.utc) + event_time = base_time + alert_start = base_time + timedelta(seconds=1) + alert_end = base_time + timedelta(minutes=5) + + # Act: Create objects with time sequence + event = Event( + ts=event_time, + device_id="timing_test", + sensor_type="temperature", + site_id="test_site", + msg_type="reading", + value=25.0, + seq=1, + quality="ok" + ) + + alert = Alert( + device_id="timing_test", + issue_type="test_issue", + start_ts=alert_start, + end_ts=alert_end, + severity="warning" + ) + + device_state = DeviceState( + device_id="timing_test", + last_seen_ts=event_time + ) + + # Assert: Timestamp relationships should be logical + assert event.ts <= alert.start_ts # Event should trigger alert + assert alert.start_ts < alert.end_ts # Alert start before end + assert device_state.last_seen_ts == event.ts # State reflects event time + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/test-requirements.txt b/AgCloud/services/sensorGuard/test-requirements.txt new file mode 100644 index 000000000..59cd202e4 --- /dev/null +++ b/AgCloud/services/sensorGuard/test-requirements.txt @@ -0,0 +1,3 @@ +pytest>=7.0.0 +pytest-cov>=4.0.0 +pytest-mock>=3.10.0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/__init__.py b/AgCloud/services/sensorGuard/tests/__init__.py new file mode 100644 index 000000000..1d342c1a8 --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/__init__.py @@ -0,0 +1 @@ +# Test package init \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_engine.py b/AgCloud/services/sensorGuard/tests/test_engine.py new file mode 100644 index 000000000..ba34aaaab --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_engine.py @@ -0,0 +1,195 @@ +import pytest +from unittest.mock import Mock, patch +from datetime import datetime, timezone +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.engine import Engine +from core.types import Event, Alert +from core.state import StateStore + + +@pytest.fixture(autouse=True) +def mock_engine_dependencies(): + """Mock external dependencies for Engine class""" + with patch('core.engine.get_access_token', return_value='test_token'), \ + patch('core.engine.update_device_last_seen'): + yield + + +class TestEngine: + """Test Engine class public methods""" + + def setup_method(self): + """Arrange - Set up test fixtures""" + self.mock_writer = Mock() + self.mock_cfg = { + 'features': {'out_of_range': True, 'stuck_sensor': True}, + 'ranges': {'temperature': {'min': 0, 'max': 50}}, + 'stuck': {'min_duration_seconds': 1800} + } + self.state_store = StateStore() + self.engine = Engine(self.mock_cfg, self.mock_writer, self.state_store) + + def test_engine_initialization(self): + """Test engine initializes correctly""" + # Assert + assert self.engine.cfg == self.mock_cfg + assert len(self.engine.writers) == 1 + assert self.engine.state is self.state_store + + def test_process_event_unknown_device(self): + """Test processing event for unknown device""" + # Arrange + event = Event( + ts=datetime.now(timezone.utc), + device_id="unknown_device", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=25.0, + seq=None, + quality="ok" + ) + + # Act + self.engine.process_event(event) + + # Assert - no alerts should be emitted for unknown devices + self.mock_writer.write.assert_not_called() + + def test_process_event_known_device_normal_value(self): + """Test processing normal value for known device""" + # Arrange + self.state_store.add_device("device_1", "temperature") + event = Event( + ts=datetime.now(timezone.utc), + device_id="device_1", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=25.0, + seq=None, + quality="ok" + ) + + # Act + self.engine.process_event(event) + + # Assert - device state should be updated + device_state = self.state_store.get("device_1") + assert device_state.last_seen_ts == event.ts + assert device_state.last_value == 25.0 + + def test_emit_alert_calls_all_writers(self): + """Test _emit calls write on all writers""" + # Arrange + writer1 = Mock() + writer2 = Mock() + engine = Engine(self.mock_cfg, [writer1, writer2], self.state_store) + alert = Alert( + device_id="device_1", + issue_type="test_alert", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act + engine._emit(alert) + + # Assert + writer1.write.assert_called_once_with(alert) + writer2.write.assert_called_once_with(alert) + + def test_open_once_new_alert(self): + """Test opening new alert when none exists""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act + self.engine._open_once(device_state, alert) + + # Assert + assert "out_of_range" in device_state.open_alerts + self.mock_writer.write.assert_called_once_with(alert) + + def test_open_once_existing_alert(self): + """Test not opening duplicate alert""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + # Pre-existing alert + existing_alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + device_state.open_alerts["out_of_range"] = existing_alert + + new_alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act + self.engine._open_once(device_state, new_alert) + + # Assert - should not emit new alert + self.mock_writer.write.assert_not_called() + assert device_state.open_alerts["out_of_range"] is existing_alert + + def test_close_if_open_existing_alert(self): + """Test closing existing alert""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + alert = Alert( + device_id="device_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + device_state.open_alerts["out_of_range"] = alert + close_ts = datetime.now(timezone.utc) + + # Act + self.engine._close_if_open(device_state, "out_of_range", close_ts) + + # Assert + assert "out_of_range" not in device_state.open_alerts + assert alert.end_ts == close_ts + self.mock_writer.write.assert_called_once_with(alert) + + def test_close_if_open_nonexistent_alert(self): + """Test closing non-existent alert does nothing""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + close_ts = datetime.now(timezone.utc) + + # Act + self.engine._close_if_open(device_state, "out_of_range", close_ts) + + # Assert - nothing should happen + self.mock_writer.write.assert_not_called() + assert len(device_state.open_alerts) == 0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_engine_quality.py b/AgCloud/services/sensorGuard/tests/test_engine_quality.py new file mode 100644 index 000000000..6f6c7e9e1 --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_engine_quality.py @@ -0,0 +1,683 @@ +""" +Professional, comprehensive tests for the Engine class. +These tests verify REAL business logic, not just code coverage. +""" +import pytest +import time +from unittest.mock import Mock, MagicMock, patch +from datetime import datetime, timezone, timedelta + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.engine import Engine +from core.types import Event, Alert, DeviceState +from core.state import StateStore + + +@pytest.fixture(autouse=True) +def mock_engine_dependencies(): + """Mock external dependencies for Engine class""" + with patch('core.engine.get_access_token', return_value='test_token'), \ + patch('core.engine.update_device_last_seen'), \ + patch('core.engine.get_sensors_last_seen', return_value=[]): + yield + + +class TestEngineBusinessLogic: + """Tests that verify the actual sensor monitoring business rules.""" + + def setup_method(self): + """Set up test environment with realistic configuration.""" + self.mock_writer = Mock() + self.cfg = { + "features": { + "corrupted": True, + "out_of_range": True, + "stuck_sensor": True, + "silence": True + }, + "prolonged_silence_seconds": 300, # 5 minutes + "ranges": { + "temperature": {"min": -40, "max": 85}, + "humidity": {"min": 0, "max": 100} + }, + "stuck": { + "temperature": {"tolerance": 0.1, "count": 5} + } + } + self.state_store = StateStore() + self.engine = Engine(self.cfg, self.mock_writer, self.state_store) + + def create_real_sensor_message(self, device_id, sensor_type, value, msg_type="telemetry", **extra): + """Create a message in the EXACT format that comes from real sensors.""" + return { + "sid": f"sensor-{device_id}", + "id": int(device_id), + "timestamp": datetime.now(timezone.utc).isoformat(), + "msg_type": msg_type, + "value": value, + "sensor": sensor_type, + "plant_id": 123, + "temperature": value if sensor_type == "temperature" else 25.0, + "humidity": value if sensor_type == "humidity" else 65.0, + "ph": 7.0, + "n": 50.0, + "p": 30.0, + "k": 40.0, + **extra + } + + def to_event(self, sensor_message): + """Convert sensor message to Event using EXACT logic from main.py.""" + if not isinstance(sensor_message, dict): + return None + ts = datetime.now(timezone.utc) + device_id = sensor_message.get("id") + sensor_type = sensor_message.get("sensor_type") or sensor_message.get("sensor", "unknown_sensor") + if not device_id: + device_id = "unknown_device" + else: + device_id = str(device_id) + + return Event( + ts=ts, + device_id=device_id, + sensor_type=sensor_type, + site_id=sensor_message.get("site_id"), + msg_type=sensor_message.get("msg_type", "reading"), + value=sensor_message.get("value"), + seq=sensor_message.get("seq"), + quality=sensor_message.get("quality"), + ) + + def test_new_unknown_device_is_ignored_completely(self): + """CRITICAL: Unknown devices should be completely ignored - no alerts, no state changes.""" + # Arrange: Create REAL sensor message from unknown device (not in state_store) + unknown_message = self.create_real_sensor_message( + device_id="999", # Device not added to state store + sensor_type="temperature", + value=25.0 + ) + + # Act: Convert and process like real system does + event = self.to_event(unknown_message) + assert event is not None, "Valid message should convert to event" + self.engine.process_event(event) + + # Assert: No alerts should be generated for unknown devices + assert self.mock_writer.write.call_count == 0 + + # Assert: Device should not be considered known + assert not self.state_store.is_known_device("999") + + # Assert: No state should exist for this device + result = self.state_store.get("999") + assert result is None, "Unknown device should return None from state store" + + def test_sensor_comeback_closes_keepalive_alerts_immediately(self): + """CRITICAL: When sensor sends data after being silent, missing_keepalive alert must close. + NOTE: prolonged_silence alerts are NOT closed by process_event - only by sweep_silence.""" + # Arrange: Add device and simulate missing keepalive alert + device_id = "5" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Manually create open missing_keepalive alert (simulating sweep_silence) + now = datetime.now(timezone.utc) + missing_alert = Alert( + device_id=device_id, + issue_type="missing_keepalive", + start_ts=now - timedelta(minutes=10), + end_ts=None, + severity="error", + sensor_type="temperature", + site_id=None, + details={"reason": "no data received"} + ) + + device_state.open_alerts["missing_keepalive"] = missing_alert + + # Act: Sensor sends REAL comeback message + comeback_message = self.create_real_sensor_message( + device_id="5", + sensor_type="temperature", + value=23.5 + ) + + comeback_event = self.to_event(comeback_message) + assert comeback_event is not None + self.engine.process_event(comeback_event) + + # Assert: missing_keepalive alert should be closed (emitted with end_ts) + assert self.mock_writer.write.call_count >= 1, "Expected at least 1 alert emission" + + # Verify the closed alert has end_ts set + calls = self.mock_writer.write.call_args_list + closed_alerts = [call[0][0] for call in calls] + + keepalive_closed = any(alert.issue_type == "missing_keepalive" and alert.end_ts is not None + for alert in closed_alerts) + + assert keepalive_closed, "missing_keepalive alert should be closed with end_ts" + + # Assert: missing_keepalive alert should no longer be open + assert "missing_keepalive" not in device_state.open_alerts + + def test_out_of_range_alert_opens_and_closes_correctly(self): + """CRITICAL: Out-of-range alerts must open when value is invalid, close when valid.""" + # Arrange: Add device + device_id = "5" + self.state_store.add_device(device_id, "temperature") + + # Act 1: Send out-of-range value using REAL message format + bad_message = self.create_real_sensor_message( + device_id="5", + sensor_type="temperature", + value=150.0 # Way above max of 85 + ) + + bad_event = self.to_event(bad_message) + assert bad_event is not None, "Valid message should convert to event" + + self.engine.process_event(bad_event) + + # Assert: Out-of-range alert should be opened + device_state = self.state_store.get(device_id) + assert "out_of_range" in device_state.open_alerts + + # Find the out-of-range alert that was emitted + calls = self.mock_writer.write.call_args_list + out_of_range_alerts = [call[0][0] for call in calls if call[0][0].issue_type == "out_of_range"] + assert len(out_of_range_alerts) == 1 + assert out_of_range_alerts[0].end_ts is None # Should be open + + # Reset mock for next assertion + self.mock_writer.reset_mock() + + # Act 2: Send valid value using REAL message format + good_message = self.create_real_sensor_message( + device_id="5", + sensor_type="temperature", + value=22.0 # Within valid range + ) + + good_event = self.to_event(good_message) + assert good_event is not None, "Valid message should convert to event" + + self.engine.process_event(good_event) + + # Assert: Out-of-range alert should be closed + assert "out_of_range" not in device_state.open_alerts + + # Verify alert was closed (emitted with end_ts) + closing_calls = self.mock_writer.write.call_args_list + if closing_calls: # Alert closure generates emission + closed_alert = closing_calls[0][0][0] + assert closed_alert.issue_type == "out_of_range" + assert closed_alert.end_ts is not None + + def test_complete_alert_lifecycle_with_multiple_bad_then_good_messages(self): + """COMPREHENSIVE: Test complete alert lifecycle - multiple bad messages, then recovery.""" + # Arrange: Add device + device_id = "10" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Act 1: Send first bad message (should open alert) + bad_message_1 = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=200.0 # Way out of range + ) + + self.engine.process_event(self.to_event(bad_message_1)) + + # Assert: Alert should be open + assert "out_of_range" in device_state.open_alerts + assert self.mock_writer.write.call_count == 1 # One alert opened + + # Act 2: Send second bad message (should NOT open duplicate alert) + self.mock_writer.reset_mock() + bad_message_2 = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=250.0 # Still out of range + ) + + self.engine.process_event(self.to_event(bad_message_2)) + + # Assert: Still same alert, no new alert created + assert "out_of_range" in device_state.open_alerts + assert self.mock_writer.write.call_count == 0 # No new alerts + + # Act 3: Send good message (should close alert) + self.mock_writer.reset_mock() + good_message = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=25.0 # Back to normal + ) + + self.engine.process_event(self.to_event(good_message)) + + # Assert: Alert should be closed completely + assert "out_of_range" not in device_state.open_alerts + assert self.mock_writer.write.call_count == 1 # Alert closure emitted + + # Verify the closed alert has proper end_ts + closed_alert = self.mock_writer.write.call_args[0][0] + assert closed_alert.issue_type == "out_of_range" + assert closed_alert.device_id == device_id + assert closed_alert.end_ts is not None + assert closed_alert.end_ts > closed_alert.start_ts # End after start + + # Act 4: Send another good message (should NOT close anything) + self.mock_writer.reset_mock() + another_good = self.create_real_sensor_message( + device_id="10", + sensor_type="temperature", + value=30.0 + ) + + self.engine.process_event(self.to_event(another_good)) + + # Assert: No alert activity (no open alerts to close) + assert len(device_state.open_alerts) == 0 + assert self.mock_writer.write.call_count == 0 + + def test_multiple_alert_types_independence(self): + """CRITICAL: Different alert types should open/close independently.""" + # Arrange + device_id = "11" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Act 1: Trigger out_of_range alert + bad_temp_message = self.create_real_sensor_message( + device_id="11", + sensor_type="temperature", + value=200.0 + ) + self.engine.process_event(self.to_event(bad_temp_message)) + + # Act 2: Manually add a missing_keepalive alert (simulating silence) + keepalive_alert = Alert( + device_id=device_id, + issue_type="missing_keepalive", + start_ts=datetime.now(timezone.utc), + end_ts=None, + severity="error", + sensor_type="temperature" + ) + device_state.open_alerts["missing_keepalive"] = keepalive_alert + + # Assert: Both alerts should be open + assert "out_of_range" in device_state.open_alerts + assert "missing_keepalive" in device_state.open_alerts + assert len(device_state.open_alerts) == 2 + + # Act 3: Send good temperature (should close out_of_range but not missing_keepalive) + self.mock_writer.reset_mock() + good_temp_message = self.create_real_sensor_message( + device_id="11", + sensor_type="temperature", + value=25.0 + ) + self.engine.process_event(self.to_event(good_temp_message)) + + # Assert: out_of_range closed, missing_keepalive should be closed by _close_all_keepalive_alerts + assert "out_of_range" not in device_state.open_alerts + assert "missing_keepalive" not in device_state.open_alerts # This should be closed by comeback + + # Should have emitted 2 alerts: out_of_range closure + missing_keepalive closure + assert self.mock_writer.write.call_count == 2 + + def test_corrupted_message_overrides_out_of_range_alerts(self): + """CRITICAL: Corrupted message should close out_of_range and stuck_sensor alerts.""" + # Arrange + device_id = "12" + self.state_store.add_device(device_id, "humidity") + device_state = self.state_store.get(device_id) + + # Act 1: Create out_of_range alert first + out_of_range_message = self.create_real_sensor_message( + device_id="12", + sensor_type="humidity", + value=150.0 # Above max of 100 + ) + self.engine.process_event(self.to_event(out_of_range_message)) + + assert "out_of_range" in device_state.open_alerts + first_alert_count = self.mock_writer.write.call_count + + # Act 2: Send corrupted message (None value) + self.mock_writer.reset_mock() + corrupted_message = self.create_real_sensor_message( + device_id="12", + sensor_type="humidity", + value=None # This triggers corrupted alert + ) + self.engine.process_event(self.to_event(corrupted_message)) + + # Assert: Corrupted should open, out_of_range should be closed + assert "corrupted" in device_state.open_alerts + assert "out_of_range" not in device_state.open_alerts + + # Should emit: 1 corrupted open + 1 out_of_range close + assert self.mock_writer.write.call_count == 2 + + def test_sensor_value_oscillation_alert_behavior(self): + """COMPLEX: Test alert behavior when sensor oscillates between good/bad values.""" + # Arrange + device_id = "13" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Scenario: bad -> good -> bad -> good (should open/close/open/close) + test_sequence = [ + (100.0, True), # Bad (should open alert) + (25.0, False), # Good (should close alert) + (200.0, True), # Bad again (should open new alert) + (30.0, False) # Good again (should close alert) + ] + + total_opens = 0 + total_closes = 0 + + for i, (value, should_be_bad) in enumerate(test_sequence): + self.mock_writer.reset_mock() + + message = self.create_real_sensor_message( + device_id="13", + sensor_type="temperature", + value=value + ) + self.engine.process_event(self.to_event(message)) + + if should_be_bad: + # Should have alert open + assert "out_of_range" in device_state.open_alerts + if self.mock_writer.write.call_count > 0: + total_opens += 1 + else: + # Should NOT have alert open + assert "out_of_range" not in device_state.open_alerts + if self.mock_writer.write.call_count > 0: + total_closes += 1 + + # Should have opened 2 times and closed 2 times + assert total_opens == 2, f"Expected 2 opens, got {total_opens}" + assert total_closes == 2, f"Expected 2 closes, got {total_closes}" + + def test_sweep_silence_detects_missing_devices_correctly(self): + """CRITICAL: sweep_silence must detect devices that haven't been seen for too long. + This tests the REAL business logic: sweep_silence fetches from API and compares timestamps.""" + # Arrange: Add device to state store + device_id = "never_seen_device" + self.state_store.add_device(device_id, "humidity") + device_state = self.state_store.get(device_id) + + # Mock API to return this sensor with old last_seen timestamp + old_timestamp = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat() + mock_sensors_from_api = [ + { + "id": device_id, + "sensor_type": "humidity", + "last_seen": old_timestamp + } + ] + + # Act: Run silence sweep with mocked API response + with patch('core.engine.get_sensors_last_seen', return_value=mock_sensors_from_api): + now = datetime.now(timezone.utc) + self.engine.sweep_silence(now) + + # Assert: missing_keepalive alert should be created (gap > threshold) + assert "missing_keepalive" in device_state.open_alerts, \ + f"Expected missing_keepalive alert for device silent for 10 minutes (threshold is ~3 minutes)" + + # Verify alert was emitted + assert self.mock_writer.write.call_count >= 1, "Expected alert emission" + emitted_alert = self.mock_writer.write.call_args[0][0] + assert emitted_alert.issue_type == "missing_keepalive" + assert emitted_alert.device_id == device_id + assert emitted_alert.end_ts is None, "Alert should be open" + + def test_sweep_silence_detects_prolonged_silence_correctly(self): + """CRITICAL: sweep_silence must detect devices silent for too long and close alerts when they resume. + This tests REAL business logic: comparing API timestamps against threshold.""" + # Arrange: Add device to state store + device_id = "old_device" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Mock API to return sensor with old last_seen (10 minutes ago - exceeds 3 min threshold) + old_timestamp = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat() + mock_sensors_from_api = [ + { + "id": device_id, + "sensor_type": "temperature", + "last_seen": old_timestamp + } + ] + + # Act: Run silence sweep with mocked API - should open missing_keepalive alert + with patch('core.engine.get_sensors_last_seen', return_value=mock_sensors_from_api): + now = datetime.now(timezone.utc) + self.engine.sweep_silence(now) + + # Assert: missing_keepalive alert should be created (device silent > threshold) + assert "missing_keepalive" in device_state.open_alerts, \ + "Expected missing_keepalive alert for device silent for 10 minutes" + + # Verify alert was emitted + assert self.mock_writer.write.call_count >= 1 + first_alert = self.mock_writer.write.call_args[0][0] + assert first_alert.issue_type == "missing_keepalive" + assert first_alert.device_id == device_id + assert first_alert.end_ts is None, "Initial alert should be open" + + # Reset mock for next phase + self.mock_writer.reset_mock() + + # Act 2: Run sweep again with RECENT timestamp - should close the alert + recent_timestamp = (datetime.now(timezone.utc) - timedelta(seconds=30)).isoformat() + mock_sensors_recent = [ + { + "id": device_id, + "sensor_type": "temperature", + "last_seen": recent_timestamp + } + ] + + with patch('core.engine.get_sensors_last_seen', return_value=mock_sensors_recent): + now = datetime.now(timezone.utc) + self.engine.sweep_silence(now) + + # Assert: Alert should be closed (gap now < threshold) + assert "missing_keepalive" not in device_state.open_alerts, \ + "Alert should be closed when device is no longer silent" + + # Verify closing alert was emitted + assert self.mock_writer.write.call_count >= 1 + closing_alert = self.mock_writer.write.call_args[0][0] + assert closing_alert.issue_type == "missing_keepalive" + assert closing_alert.end_ts is not None, "Closing alert should have end_ts" + + def test_multiple_writers_receive_all_alerts(self): + """CRITICAL: When multiple writers are configured, all must receive every alert.""" + # Arrange: Setup engine with multiple writers + writer1 = Mock() + writer2 = Mock() + writer3 = Mock() + + engine = Engine(self.cfg, [writer1, writer2, writer3], self.state_store) + + device_id = "7" + self.state_store.add_device(device_id, "temperature") + + # Act: Generate an alert using REAL message format + bad_message = self.create_real_sensor_message( + device_id="7", + sensor_type="temperature", + value=-100.0 # Out of range (below min of -40) + ) + + bad_event = self.to_event(bad_message) + assert bad_event is not None + engine.process_event(bad_event) + + # Assert: All writers should receive the alert + assert writer1.write.call_count == 1 + assert writer2.write.call_count == 1 + assert writer3.write.call_count == 1 + + # Verify they all got the same alert + alert1 = writer1.write.call_args[0][0] + alert2 = writer2.write.call_args[0][0] + alert3 = writer3.write.call_args[0][0] + + assert alert1.issue_type == alert2.issue_type == alert3.issue_type == "out_of_range" + assert alert1.device_id == alert2.device_id == alert3.device_id == device_id + + def test_device_state_updates_correctly_during_processing(self): + """CRITICAL: Device state must be updated properly with each event.""" + # Arrange + device_id = "8" + self.state_store.add_device(device_id, "humidity") + + initial_state = self.state_store.get(device_id) + assert initial_state.last_seen_ts is None + assert initial_state.last_value is None + + # Act: Process first event using REAL message format + first_message = self.create_real_sensor_message( + device_id="8", + sensor_type="humidity", + value=45.2 + ) + + first_event = self.to_event(first_message) + assert first_event is not None + first_process_time = first_event.ts # Capture the actual timestamp used + + self.engine.process_event(first_event) + + # Assert: State should be updated + updated_state = self.state_store.get(device_id) + assert updated_state.last_seen_ts == first_process_time + assert updated_state.last_value == 45.2 + assert updated_state.sensor_type == "humidity" + + # Act: Process second event with different values + time.sleep(0.1) # Ensure different timestamp + second_message = self.create_real_sensor_message( + device_id="8", + sensor_type="humidity", + value=67.8 + ) + + second_event = self.to_event(second_message) + assert second_event is not None + second_process_time = second_event.ts + + self.engine.process_event(second_event) + + # Assert: State should reflect latest values + final_state = self.state_store.get(device_id) + assert final_state.last_seen_ts == second_process_time + assert final_state.last_value == 67.8 + assert final_state.sensor_type == "humidity" + + +class TestEngineEdgeCases: + """Tests for edge cases and error scenarios.""" + + def setup_method(self): + self.mock_writer = Mock() + self.cfg = { + "features": {"corrupted": True, "out_of_range": True, "stuck_sensor": True, "silence": True}, + "prolonged_silence_seconds": 300 + } + self.engine = Engine(self.cfg, self.mock_writer) + + def create_real_sensor_message(self, device_id, sensor_type, value, msg_type="telemetry", **extra): + """Create a message in the EXACT format that comes from real sensors.""" + return { + "sid": f"sensor-{device_id}", + "id": int(device_id), + "timestamp": datetime.now(timezone.utc).isoformat(), + "msg_type": msg_type, + "value": value, + "sensor": sensor_type, + "plant_id": 123, + "temperature": value if sensor_type == "temperature" else 25.0, + "humidity": value if sensor_type == "humidity" else 65.0, + "ph": 7.0, + "n": 50.0, + "p": 30.0, + "k": 40.0, + **extra + } + + def to_event(self, sensor_message): + """Convert sensor message to Event using EXACT logic from main.py.""" + if not isinstance(sensor_message, dict): + return None + ts = datetime.now(timezone.utc) + device_id = sensor_message.get("id") + sensor_type = sensor_message.get("sensor_type") or sensor_message.get("sensor", "unknown_sensor") + if not device_id: + device_id = "unknown_device" + else: + device_id = str(device_id) + + return Event( + ts=ts, + device_id=device_id, + sensor_type=sensor_type, + site_id=sensor_message.get("site_id"), + msg_type=sensor_message.get("msg_type", "reading"), + value=sensor_message.get("value"), + seq=sensor_message.get("seq"), + quality=sensor_message.get("quality"), + ) + + def test_engine_handles_none_values_gracefully(self): + """Edge case: Engine should handle None/null values without crashing.""" + # Arrange: Add device + device_id = "9" + self.engine.state.add_device(device_id, "temperature") + + # Act: Send REAL message with None value (like corrupted sensor reading) + null_message = self.create_real_sensor_message( + device_id="9", + sensor_type="temperature", + value=None # Corrupted/missing sensor reading + ) + + null_event = self.to_event(null_message) + assert null_event is not None + + # This should not raise an exception + try: + self.engine.process_event(null_event) + except Exception as e: + pytest.fail(f"Engine crashed with None value: {e}") + + def test_sweep_silence_on_empty_state_store(self): + """Edge case: sweep_silence should handle empty state store.""" + # Act: Run sweep on empty state (should not crash) + try: + self.engine.sweep_silence(datetime.now(timezone.utc)) + except Exception as e: + pytest.fail(f"sweep_silence crashed on empty state: {e}") + + # Assert: No alerts should be generated + assert self.mock_writer.write.call_count == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_state.py b/AgCloud/services/sensorGuard/tests/test_state.py new file mode 100644 index 000000000..bb2e385fa --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_state.py @@ -0,0 +1,136 @@ +import pytest +from unittest.mock import Mock +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.state import StateStore +from core.types import DeviceState + + +class TestStateStore: + """Test StateStore class public methods""" + + def setup_method(self): + """Arrange - Set up test fixtures""" + self.state_store = StateStore() + + def test_add_device_new(self): + """Test adding new device to state store""" + # Act + self.state_store.add_device("device_1", "temperature") + + # Assert + assert self.state_store.is_known_device("device_1") + device_state = self.state_store.get("device_1") + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_add_device_existing(self): + """Test adding existing device doesn't overwrite""" + # Arrange + self.state_store.add_device("device_1", "temperature") + original_state = self.state_store.get("device_1") + + # Act - Try to add same device with different sensor type + self.state_store.add_device("device_1", "humidity") + + # Assert - Original state preserved + current_state = self.state_store.get("device_1") + assert current_state is original_state + assert current_state.sensor_type == "temperature" # Not changed + + def test_is_known_device_true(self): + """Test is_known_device returns True for existing device""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act & Assert + assert self.state_store.is_known_device("device_1") is True + + def test_is_known_device_false(self): + """Test is_known_device returns False for non-existing device""" + # Act & Assert + assert self.state_store.is_known_device("unknown_device") is False + + def test_get_device_existing(self): + """Test getting existing device state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act + device_state = self.state_store.get("device_1") + + # Assert + assert device_state is not None + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_get_device_nonexistent(self): + """Test getting non-existent device returns None""" + # Act + device_state = self.state_store.get("unknown_device") + + # Assert + assert device_state is None + + def test_all_states_empty(self): + """Test getting all devices when store is empty""" + # Act + all_devices = list(self.state_store.all_states()) + + # Assert + assert len(all_devices) == 0 + + def test_all_states_multiple(self): + """Test getting all devices with multiple devices""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + self.state_store.add_device("device_3", "pressure") + + # Act + all_states = list(self.state_store.all_states()) + + # Assert + assert len(all_states) == 3 + device_ids = [device_id for device_id, state in all_states] + assert "device_1" in device_ids + assert "device_2" in device_ids + assert "device_3" in device_ids + + def test_state_persistence(self): + """Test that device state persists across operations""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + # Act - Modify state + from datetime import datetime, timezone + ts = datetime.now(timezone.utc) + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert - Changes persist when retrieving again + retrieved_state = self.state_store.get("device_1") + assert retrieved_state.last_seen_ts == ts + assert retrieved_state.last_value == 25.0 + + def test_multiple_devices_independence(self): + """Test that multiple devices maintain independent state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + + device_1 = self.state_store.get("device_1") + device_2 = self.state_store.get("device_2") + + # Act - Modify only device_1 + device_1.last_value = 25.0 + + # Assert - device_2 not affected + assert device_1.last_value == 25.0 + assert device_2.last_value is None + assert device_1 is not device_2 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_state_fixed.py b/AgCloud/services/sensorGuard/tests/test_state_fixed.py new file mode 100644 index 000000000..bb2e385fa --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_state_fixed.py @@ -0,0 +1,136 @@ +import pytest +from unittest.mock import Mock +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.state import StateStore +from core.types import DeviceState + + +class TestStateStore: + """Test StateStore class public methods""" + + def setup_method(self): + """Arrange - Set up test fixtures""" + self.state_store = StateStore() + + def test_add_device_new(self): + """Test adding new device to state store""" + # Act + self.state_store.add_device("device_1", "temperature") + + # Assert + assert self.state_store.is_known_device("device_1") + device_state = self.state_store.get("device_1") + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_add_device_existing(self): + """Test adding existing device doesn't overwrite""" + # Arrange + self.state_store.add_device("device_1", "temperature") + original_state = self.state_store.get("device_1") + + # Act - Try to add same device with different sensor type + self.state_store.add_device("device_1", "humidity") + + # Assert - Original state preserved + current_state = self.state_store.get("device_1") + assert current_state is original_state + assert current_state.sensor_type == "temperature" # Not changed + + def test_is_known_device_true(self): + """Test is_known_device returns True for existing device""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act & Assert + assert self.state_store.is_known_device("device_1") is True + + def test_is_known_device_false(self): + """Test is_known_device returns False for non-existing device""" + # Act & Assert + assert self.state_store.is_known_device("unknown_device") is False + + def test_get_device_existing(self): + """Test getting existing device state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + + # Act + device_state = self.state_store.get("device_1") + + # Assert + assert device_state is not None + assert device_state.device_id == "device_1" + assert device_state.sensor_type == "temperature" + + def test_get_device_nonexistent(self): + """Test getting non-existent device returns None""" + # Act + device_state = self.state_store.get("unknown_device") + + # Assert + assert device_state is None + + def test_all_states_empty(self): + """Test getting all devices when store is empty""" + # Act + all_devices = list(self.state_store.all_states()) + + # Assert + assert len(all_devices) == 0 + + def test_all_states_multiple(self): + """Test getting all devices with multiple devices""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + self.state_store.add_device("device_3", "pressure") + + # Act + all_states = list(self.state_store.all_states()) + + # Assert + assert len(all_states) == 3 + device_ids = [device_id for device_id, state in all_states] + assert "device_1" in device_ids + assert "device_2" in device_ids + assert "device_3" in device_ids + + def test_state_persistence(self): + """Test that device state persists across operations""" + # Arrange + self.state_store.add_device("device_1", "temperature") + device_state = self.state_store.get("device_1") + + # Act - Modify state + from datetime import datetime, timezone + ts = datetime.now(timezone.utc) + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert - Changes persist when retrieving again + retrieved_state = self.state_store.get("device_1") + assert retrieved_state.last_seen_ts == ts + assert retrieved_state.last_value == 25.0 + + def test_multiple_devices_independence(self): + """Test that multiple devices maintain independent state""" + # Arrange + self.state_store.add_device("device_1", "temperature") + self.state_store.add_device("device_2", "humidity") + + device_1 = self.state_store.get("device_1") + device_2 = self.state_store.get("device_2") + + # Act - Modify only device_1 + device_1.last_value = 25.0 + + # Assert - device_2 not affected + assert device_1.last_value == 25.0 + assert device_2.last_value is None + assert device_1 is not device_2 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_state_quality.py b/AgCloud/services/sensorGuard/tests/test_state_quality.py new file mode 100644 index 000000000..a068af528 --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_state_quality.py @@ -0,0 +1,319 @@ +""" +Professional, comprehensive tests for StateStore class. +These tests verify data integrity and state management correctness. +""" +import pytest +from datetime import datetime, timezone, timedelta + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.state import StateStore +from core.types import DeviceState, Alert + + +class TestStateStoreDataIntegrity: + """Tests that verify data integrity and correct state management.""" + + def setup_method(self): + """Set up fresh StateStore for each test.""" + self.state_store = StateStore() + + def test_add_device_creates_proper_state_structure(self): + """CRITICAL: Adding device must create correct internal state structure.""" + # Arrange + device_id = "sensor_001" + sensor_type = "temperature" + + # Act: Add device + self.state_store.add_device(device_id, sensor_type) + + # Assert: Device should be in known devices + assert self.state_store.is_known_device(device_id) + + # Assert: Device state should be properly initialized + device_state = self.state_store.get(device_id) + assert device_state is not None + assert device_state.device_id == device_id + assert device_state.sensor_type == sensor_type + assert device_state.last_seen_ts is None # Should start as None + assert device_state.last_value is None # Should start as None + assert device_state.open_alerts == {} # Should start empty + + def test_get_unknown_device_returns_none_safely(self): + """CRITICAL: Getting unknown device must return None, not crash.""" + # Act: Try to get device that was never added + result = self.state_store.get("nonexistent_device") + + # Assert: Should return None safely + assert result is None + + # Assert: Should not be considered known + assert not self.state_store.is_known_device("nonexistent_device") + + def test_device_state_persistence_across_operations(self): + """CRITICAL: Device state changes must persist correctly.""" + # Arrange: Add device and get its state + device_id = "persistent_device" + self.state_store.add_device(device_id, "humidity") + device_state = self.state_store.get(device_id) + + # Act: Modify device state + test_time = datetime.now(timezone.utc) + test_value = 42.7 + test_alert = Alert( + issue_type="test_alert", + device_id=device_id, + sensor_type="humidity", + site_id="test_site", + severity="warning", + start_ts=test_time, + end_ts=None, + details={"test": "data"} + ) + + device_state.last_seen_ts = test_time + device_state.last_value = test_value + device_state.open_alerts["test_alert"] = test_alert + + # Assert: Changes should persist when retrieving again + retrieved_state = self.state_store.get(device_id) + assert retrieved_state.last_seen_ts == test_time + assert retrieved_state.last_value == test_value + assert "test_alert" in retrieved_state.open_alerts + assert retrieved_state.open_alerts["test_alert"].issue_type == "test_alert" + + def test_multiple_devices_maintain_separate_states(self): + """CRITICAL: Multiple devices must have completely separate states.""" + # Arrange: Add multiple devices + device1 = "temp_sensor_01" + device2 = "humid_sensor_02" + device3 = "pressure_sensor_03" + + self.state_store.add_device(device1, "temperature") + self.state_store.add_device(device2, "humidity") + self.state_store.add_device(device3, "pressure") + + # Act: Set different values for each device + time1 = datetime.now(timezone.utc) + time2 = time1 + timedelta(minutes=1) + time3 = time1 + timedelta(minutes=2) + + state1 = self.state_store.get(device1) + state2 = self.state_store.get(device2) + state3 = self.state_store.get(device3) + + state1.last_seen_ts = time1 + state1.last_value = 25.5 + + state2.last_seen_ts = time2 + state2.last_value = 65.8 + + state3.last_seen_ts = time3 + state3.last_value = 1013.2 + + # Assert: Each device maintains its own independent state + retrieved1 = self.state_store.get(device1) + retrieved2 = self.state_store.get(device2) + retrieved3 = self.state_store.get(device3) + + assert retrieved1.last_seen_ts == time1 + assert retrieved1.last_value == 25.5 + assert retrieved1.sensor_type == "temperature" + + assert retrieved2.last_seen_ts == time2 + assert retrieved2.last_value == 65.8 + assert retrieved2.sensor_type == "humidity" + + assert retrieved3.last_seen_ts == time3 + assert retrieved3.last_value == 1013.2 + assert retrieved3.sensor_type == "pressure" + + def test_all_states_returns_correct_iterator(self): + """CRITICAL: all_states() must return all devices and their current states.""" + # Arrange: Add several devices with different states + devices_data = [ + ("device_a", "temperature", 22.1), + ("device_b", "humidity", 58.3), + ("device_c", "pressure", 1015.7) + ] + + for device_id, sensor_type, value in devices_data: + self.state_store.add_device(device_id, sensor_type) + state = self.state_store.get(device_id) + state.last_value = value + + # Act: Get all states + all_states = dict(self.state_store.all_states()) + + # Assert: Should contain exactly the devices we added + assert len(all_states) == 3 + assert "device_a" in all_states + assert "device_b" in all_states + assert "device_c" in all_states + + # Assert: States should contain correct data + assert all_states["device_a"].sensor_type == "temperature" + assert all_states["device_a"].last_value == 22.1 + + assert all_states["device_b"].sensor_type == "humidity" + assert all_states["device_b"].last_value == 58.3 + + assert all_states["device_c"].sensor_type == "pressure" + assert all_states["device_c"].last_value == 1015.7 + + def test_device_id_type_conversion_consistency(self): + """CRITICAL: Device IDs must be consistently converted to strings.""" + # Act: Add devices with different ID types + self.state_store.add_device(123, "temperature") # Integer + self.state_store.add_device("456", "humidity") # String + self.state_store.add_device(789.0, "pressure") # Float + + # Assert: All should be accessible as strings + assert self.state_store.is_known_device("123") + assert self.state_store.is_known_device("456") + assert self.state_store.is_known_device("789.0") + + # Assert: States should be retrievable with string IDs + assert self.state_store.get("123") is not None + assert self.state_store.get("456") is not None + assert self.state_store.get("789.0") is not None + + # Assert: Original types should also work (converted internally) + assert self.state_store.is_known_device(123) + assert self.state_store.is_known_device(456) + assert self.state_store.is_known_device(789.0) + + def test_duplicate_device_addition_is_safe(self): + """CRITICAL: Adding same device multiple times must be safe.""" + # Arrange: Add device and modify its state + device_id = "duplicate_test_device" + self.state_store.add_device(device_id, "temperature") + + original_state = self.state_store.get(device_id) + test_time = datetime.now(timezone.utc) + original_state.last_seen_ts = test_time + original_state.last_value = 99.9 + + # Act: Add same device again (should not overwrite existing state) + self.state_store.add_device(device_id, "humidity") # Different sensor type + + # Assert: Original state should be preserved + current_state = self.state_store.get(device_id) + assert current_state.last_seen_ts == test_time + assert current_state.last_value == 99.9 + assert current_state.sensor_type == "temperature" # Should keep original + + +class TestStateStoreEdgeCases: + """Tests for edge cases and boundary conditions.""" + + def setup_method(self): + self.state_store = StateStore() + + def test_empty_state_store_operations(self): + """Edge case: Operations on empty state store should be safe.""" + # Assert: Empty state store behaves correctly + assert not self.state_store.is_known_device("any_device") + assert self.state_store.get("any_device") is None + + # all_states should return empty iterator + all_states = list(self.state_store.all_states()) + assert len(all_states) == 0 + + def test_none_and_empty_device_ids(self): + """Edge case: None/empty device IDs should be handled gracefully.""" + # Act/Assert: These should not crash + try: + result1 = self.state_store.is_known_device("") + result2 = self.state_store.get("") + # Empty string is valid, should return False/None + assert result1 is False + assert result2 is None + except Exception as e: + pytest.fail(f"Empty string device ID caused crash: {e}") + + def test_very_large_number_of_devices(self): + """Performance test: Should handle many devices efficiently.""" + # Arrange: Add many devices + num_devices = 1000 + for i in range(num_devices): + device_id = f"device_{i:04d}" + self.state_store.add_device(device_id, f"sensor_type_{i % 10}") + + # Act/Assert: Should be able to access all devices + for i in range(num_devices): + device_id = f"device_{i:04d}" + assert self.state_store.is_known_device(device_id) + state = self.state_store.get(device_id) + assert state is not None + assert state.device_id == device_id + + # Should return correct count + all_states = list(self.state_store.all_states()) + assert len(all_states) == num_devices + + +class TestStateStoreBusinessRules: + """Tests that verify business logic around state management.""" + + def setup_method(self): + self.state_store = StateStore() + + def test_alert_lifecycle_management(self): + """Business rule: Alert lifecycle should be managed correctly in device state.""" + # Arrange: Add device + device_id = "alert_lifecycle_device" + self.state_store.add_device(device_id, "temperature") + device_state = self.state_store.get(device_id) + + # Act: Simulate alert lifecycle + now = datetime.now(timezone.utc) + + # 1. Open alert + alert1 = Alert( + issue_type="out_of_range", + device_id=device_id, + sensor_type="temperature", + site_id="site1", + severity="warning", + start_ts=now, + end_ts=None, + details={"value": 150.0, "max": 85.0} + ) + device_state.open_alerts["out_of_range"] = alert1 + + # 2. Open second alert + alert2 = Alert( + issue_type="stuck_sensor", + device_id=device_id, + sensor_type="temperature", + site_id="site1", + severity="error", + start_ts=now + timedelta(minutes=1), + end_ts=None, + details={"repeated_value": 150.0} + ) + device_state.open_alerts["stuck_sensor"] = alert2 + + # Assert: Both alerts should be tracked + assert len(device_state.open_alerts) == 2 + assert "out_of_range" in device_state.open_alerts + assert "stuck_sensor" in device_state.open_alerts + + # 3. Close first alert + closed_alert = device_state.open_alerts.pop("out_of_range") + closed_alert.end_ts = now + timedelta(minutes=2) + + # Assert: Only one alert should remain open + assert len(device_state.open_alerts) == 1 + assert "out_of_range" not in device_state.open_alerts + assert "stuck_sensor" in device_state.open_alerts + + # Assert: Closed alert should have end_ts + assert closed_alert.end_ts is not None + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_types.py b/AgCloud/services/sensorGuard/tests/test_types.py new file mode 100644 index 000000000..d049f6da3 --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_types.py @@ -0,0 +1,170 @@ +import pytest +from datetime import datetime, timezone +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.types import Event, Alert, DeviceState + + +class TestEvent: + """Test Event class""" + + def test_event_creation(self): + """Test creating Event with valid data""" + # Arrange + ts = datetime.now(timezone.utc) + + # Act + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="telemetry", + value=25.5, + seq=1, + quality="ok" + ) + + # Assert + assert event.ts == ts + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value == 25.5 + assert event.msg_type == "telemetry" + assert event.seq == 1 + assert event.quality == "ok" + + def test_event_with_minimal_data(self): + """Test creating Event with minimal required data""" + # Arrange & Act + ts = datetime.now(timezone.utc) + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=None, + seq=None, + quality=None + ) + + # Assert + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value is None + + +class TestAlert: + """Test Alert class""" + + def test_alert_creation(self): + """Test creating Alert with valid data""" + # Arrange + start_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=start_ts, + end_ts=None, + sensor_type="temperature" + ) + + # Assert + assert alert.issue_type == "out_of_range" + assert alert.device_id == "sensor_1" + assert alert.severity == "error" + assert alert.start_ts == start_ts + assert alert.end_ts is None + assert alert.sensor_type == "temperature" + + def test_alert_with_end_time(self): + """Test alert with both start and end times""" + # Arrange + start_ts = datetime.now(timezone.utc) + end_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_2", + issue_type="missing_keepalive", + severity="critical", + start_ts=start_ts, + end_ts=end_ts + ) + + # Assert + assert alert.start_ts == start_ts + assert alert.end_ts == end_ts + + +class TestDeviceState: + """Test DeviceState class""" + + def test_device_state_creation(self): + """Test creating DeviceState""" + # Arrange & Act + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + + # Assert + assert device_state.device_id == "sensor_1" + assert device_state.sensor_type == "temperature" + assert device_state.last_seen_ts is None + assert device_state.last_value is None + assert len(device_state.open_alerts) == 0 + + def test_device_state_update(self): + """Test updating device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + ts = datetime.now(timezone.utc) + + # Act + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert + assert device_state.last_seen_ts == ts + assert device_state.last_value == 25.0 + + def test_device_state_alerts_management(self): + """Test managing alerts in device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act - Add alert + device_state.open_alerts["out_of_range"] = alert + + # Assert + assert "out_of_range" in device_state.open_alerts + assert device_state.open_alerts["out_of_range"] == alert + + # Act - Remove alert + removed_alert = device_state.open_alerts.pop("out_of_range") + + # Assert + assert removed_alert == alert + assert len(device_state.open_alerts) == 0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_types_fixed.py b/AgCloud/services/sensorGuard/tests/test_types_fixed.py new file mode 100644 index 000000000..d049f6da3 --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_types_fixed.py @@ -0,0 +1,170 @@ +import pytest +from datetime import datetime, timezone +import sys +import os + +# Add the flink_app to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.types import Event, Alert, DeviceState + + +class TestEvent: + """Test Event class""" + + def test_event_creation(self): + """Test creating Event with valid data""" + # Arrange + ts = datetime.now(timezone.utc) + + # Act + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="telemetry", + value=25.5, + seq=1, + quality="ok" + ) + + # Assert + assert event.ts == ts + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value == 25.5 + assert event.msg_type == "telemetry" + assert event.seq == 1 + assert event.quality == "ok" + + def test_event_with_minimal_data(self): + """Test creating Event with minimal required data""" + # Arrange & Act + ts = datetime.now(timezone.utc) + event = Event( + ts=ts, + device_id="sensor_1", + sensor_type="temperature", + site_id=None, + msg_type="reading", + value=None, + seq=None, + quality=None + ) + + # Assert + assert event.device_id == "sensor_1" + assert event.sensor_type == "temperature" + assert event.value is None + + +class TestAlert: + """Test Alert class""" + + def test_alert_creation(self): + """Test creating Alert with valid data""" + # Arrange + start_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=start_ts, + end_ts=None, + sensor_type="temperature" + ) + + # Assert + assert alert.issue_type == "out_of_range" + assert alert.device_id == "sensor_1" + assert alert.severity == "error" + assert alert.start_ts == start_ts + assert alert.end_ts is None + assert alert.sensor_type == "temperature" + + def test_alert_with_end_time(self): + """Test alert with both start and end times""" + # Arrange + start_ts = datetime.now(timezone.utc) + end_ts = datetime.now(timezone.utc) + + # Act + alert = Alert( + device_id="sensor_2", + issue_type="missing_keepalive", + severity="critical", + start_ts=start_ts, + end_ts=end_ts + ) + + # Assert + assert alert.start_ts == start_ts + assert alert.end_ts == end_ts + + +class TestDeviceState: + """Test DeviceState class""" + + def test_device_state_creation(self): + """Test creating DeviceState""" + # Arrange & Act + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + + # Assert + assert device_state.device_id == "sensor_1" + assert device_state.sensor_type == "temperature" + assert device_state.last_seen_ts is None + assert device_state.last_value is None + assert len(device_state.open_alerts) == 0 + + def test_device_state_update(self): + """Test updating device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + ts = datetime.now(timezone.utc) + + # Act + device_state.last_seen_ts = ts + device_state.last_value = 25.0 + + # Assert + assert device_state.last_seen_ts == ts + assert device_state.last_value == 25.0 + + def test_device_state_alerts_management(self): + """Test managing alerts in device state""" + # Arrange + device_state = DeviceState( + device_id="sensor_1", + sensor_type="temperature" + ) + alert = Alert( + device_id="sensor_1", + issue_type="out_of_range", + severity="error", + start_ts=datetime.now(timezone.utc), + end_ts=None + ) + + # Act - Add alert + device_state.open_alerts["out_of_range"] = alert + + # Assert + assert "out_of_range" in device_state.open_alerts + assert device_state.open_alerts["out_of_range"] == alert + + # Act - Remove alert + removed_alert = device_state.open_alerts.pop("out_of_range") + + # Assert + assert removed_alert == alert + assert len(device_state.open_alerts) == 0 \ No newline at end of file diff --git a/AgCloud/services/sensorGuard/tests/test_types_quality.py b/AgCloud/services/sensorGuard/tests/test_types_quality.py new file mode 100644 index 000000000..d72e23ce8 --- /dev/null +++ b/AgCloud/services/sensorGuard/tests/test_types_quality.py @@ -0,0 +1,439 @@ +""" +Professional, comprehensive tests for Types (dataclasses). +These tests verify data validation, business rules, and type safety. +""" +import pytest +from datetime import datetime, timezone, timedelta +from copy import deepcopy + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'flink_app')) + +from core.types import Event, Alert, DeviceState + + +class TestEventDataIntegrity: + """Tests that verify Event dataclass behavior and business rules.""" + + def test_event_creation_with_all_required_fields(self): + """CRITICAL: Event must be creatable with all required fields.""" + # Arrange + test_time = datetime.now(timezone.utc) + + # Act: Create event with all fields + event = Event( + ts=test_time, + device_id="sensor_123", + sensor_type="temperature", + site_id="greenhouse_a", + msg_type="reading", + value=24.7, + seq=42, + quality="ok" + ) + + # Assert: All fields should be set correctly + assert event.ts == test_time + assert event.device_id == "sensor_123" + assert event.sensor_type == "temperature" + assert event.site_id == "greenhouse_a" + assert event.msg_type == "reading" + assert event.value == 24.7 + assert event.seq == 42 + assert event.quality == "ok" + + def test_event_with_none_optional_fields(self): + """Business rule: Event should handle None values for optional fields.""" + # Act: Create event with minimal required fields + event = Event( + ts=datetime.now(timezone.utc), + device_id="minimal_device", + sensor_type="humidity", + site_id=None, # Optional + msg_type="keepalive", + value=None, # Optional (e.g., keepalive messages) + seq=None, # Optional + quality=None # Optional + ) + + # Assert: Should handle None values gracefully + assert event.site_id is None + assert event.value is None + assert event.seq is None + assert event.quality is None + + def test_event_immutability_after_creation(self): + """Data integrity: Event fields should be modifiable after creation.""" + # Arrange: Create event + original_time = datetime.now(timezone.utc) + event = Event( + ts=original_time, + device_id="test_device", + sensor_type="temperature", + site_id="site1", + msg_type="reading", + value=25.0, + seq=1, + quality="ok" + ) + + # Act: Modify fields (should be allowed for dataclass) + new_time = original_time + timedelta(seconds=30) + event.ts = new_time + event.value = 26.5 + event.quality = "corrupted" + + # Assert: Changes should be reflected + assert event.ts == new_time + assert event.value == 26.5 + assert event.quality == "corrupted" + + def test_event_different_msg_types_validation(self): + """Business rule: Different msg_type values should be handled correctly.""" + base_params = { + "ts": datetime.now(timezone.utc), + "device_id": "msg_type_test", + "sensor_type": "temperature", + "site_id": "site1" + } + + # Test reading message + reading_event = Event( + **base_params, + msg_type="reading", + value=23.4, + seq=1, + quality="ok" + ) + assert reading_event.msg_type == "reading" + assert reading_event.value is not None + + # Test keepalive message (typically no sensor value) + keepalive_event = Event( + **base_params, + msg_type="keepalive", + value=None, # Keepalives usually don't have sensor values + seq=2, + quality=None + ) + assert keepalive_event.msg_type == "keepalive" + assert keepalive_event.value is None + + +class TestAlertDataIntegrity: + """Tests that verify Alert dataclass behavior and lifecycle management.""" + + def test_alert_creation_with_required_fields(self): + """CRITICAL: Alert must be creatable with all required fields.""" + # Arrange + start_time = datetime.now(timezone.utc) + + # Act: Create alert + alert = Alert( + device_id="alert_test_device", + issue_type="out_of_range", + start_ts=start_time, + end_ts=None, # Open alert + severity="warning", + sensor_type="temperature", + site_id="greenhouse_b", + details={"value": 150.0, "max_allowed": 85.0} + ) + + # Assert: All fields should be set correctly + assert alert.device_id == "alert_test_device" + assert alert.issue_type == "out_of_range" + assert alert.start_ts == start_time + assert alert.end_ts is None + assert alert.severity == "warning" + assert alert.sensor_type == "temperature" + assert alert.site_id == "greenhouse_b" + assert alert.details["value"] == 150.0 + assert alert.details["max_allowed"] == 85.0 + + def test_alert_lifecycle_open_to_closed(self): + """Business rule: Alert should properly transition from open to closed state.""" + # Arrange: Create open alert + start_time = datetime.now(timezone.utc) + alert = Alert( + device_id="lifecycle_test", + issue_type="stuck_sensor", + start_ts=start_time, + end_ts=None, # Initially open + severity="error" + ) + + # Verify initially open + assert alert.end_ts is None + + # Act: Close the alert + end_time = start_time + timedelta(minutes=5) + alert.end_ts = end_time + + # Assert: Should be properly closed + assert alert.end_ts == end_time + duration = alert.end_ts - alert.start_ts + assert duration.total_seconds() == 300 # 5 minutes + + def test_alert_different_severities(self): + """Business rule: Different severity levels should be supported.""" + base_params = { + "device_id": "severity_test", + "issue_type": "test_issue", + "start_ts": datetime.now(timezone.utc), + "end_ts": None + } + + # Test different severity levels + warning_alert = Alert(**base_params, severity="warning") + error_alert = Alert(**base_params, severity="error") + critical_alert = Alert(**base_params, severity="critical") + + assert warning_alert.severity == "warning" + assert error_alert.severity == "error" + assert critical_alert.severity == "critical" + + def test_alert_details_dictionary_flexibility(self): + """Business rule: Alert details should support flexible data structures.""" + # Act: Create alert with complex details + alert = Alert( + device_id="details_test", + issue_type="complex_issue", + start_ts=datetime.now(timezone.utc), + end_ts=None, + severity="warning", + details={ + "sensor_readings": [22.1, 22.1, 22.1, 22.1], + "threshold": 0.1, + "consecutive_count": 4, + "metadata": { + "location": "field_section_3", + "operator": "automated_system" + }, + "numeric_value": 42.7, + "boolean_flag": True + } + ) + + # Assert: Complex details should be preserved + assert len(alert.details["sensor_readings"]) == 4 + assert alert.details["threshold"] == 0.1 + assert alert.details["metadata"]["location"] == "field_section_3" + assert alert.details["numeric_value"] == 42.7 + assert alert.details["boolean_flag"] is True + + +class TestDeviceStateDataIntegrity: + """Tests that verify DeviceState dataclass behavior and state management.""" + + def test_device_state_creation_with_minimal_params(self): + """CRITICAL: DeviceState should initialize with sensible defaults.""" + # Act: Create device state with minimal parameters + device_state = DeviceState(device_id="minimal_device") + + # Assert: Should have proper defaults + assert device_state.device_id == "minimal_device" + assert device_state.sensor_type is None + assert device_state.last_seen_ts is None + assert device_state.last_value is None + assert device_state.run_length == 0 + assert device_state.stuck_since_ts is None + assert device_state.open_alerts == {} + + def test_device_state_alert_management(self): + """Business rule: DeviceState should properly manage multiple open alerts.""" + # Arrange: Create device state + device_state = DeviceState( + device_id="multi_alert_device", + sensor_type="temperature" + ) + + # Act: Add multiple alerts + now = datetime.now(timezone.utc) + + alert1 = Alert( + device_id="multi_alert_device", + issue_type="out_of_range", + start_ts=now, + end_ts=None, + severity="warning" + ) + + alert2 = Alert( + device_id="multi_alert_device", + issue_type="stuck_sensor", + start_ts=now + timedelta(minutes=1), + end_ts=None, + severity="error" + ) + + device_state.open_alerts["out_of_range"] = alert1 + device_state.open_alerts["stuck_sensor"] = alert2 + + # Assert: Both alerts should be tracked + assert len(device_state.open_alerts) == 2 + assert "out_of_range" in device_state.open_alerts + assert "stuck_sensor" in device_state.open_alerts + + # Assert: Alerts should maintain their properties + assert device_state.open_alerts["out_of_range"].severity == "warning" + assert device_state.open_alerts["stuck_sensor"].severity == "error" + + def test_device_state_evolution_over_time(self): + """Business rule: DeviceState should track sensor data evolution correctly.""" + # Arrange: Create device state + device_state = DeviceState( + device_id="evolution_test", + sensor_type="humidity" + ) + + # Act: Simulate sensor data evolution + time1 = datetime.now(timezone.utc) + time2 = time1 + timedelta(minutes=1) + time3 = time1 + timedelta(minutes=2) + + # First reading + device_state.last_seen_ts = time1 + device_state.last_value = 45.2 + device_state.run_length = 1 + + # Second reading (same value - potential stuck sensor) + device_state.last_seen_ts = time2 + device_state.last_value = 45.2 # Same value + device_state.run_length = 2 + device_state.stuck_since_ts = time1 # Started being stuck from first occurrence + + # Third reading (different value - unstuck) + device_state.last_seen_ts = time3 + device_state.last_value = 46.8 # Different value + device_state.run_length = 1 # Reset + device_state.stuck_since_ts = None # No longer stuck + + # Assert: State evolution should be properly tracked + assert device_state.last_seen_ts == time3 + assert device_state.last_value == 46.8 + assert device_state.run_length == 1 + assert device_state.stuck_since_ts is None + + def test_device_state_deep_copy_independence(self): + """Data integrity: DeviceState copies should be independent.""" + # Arrange: Create device state with complex data + original_time = datetime.now(timezone.utc) + original_state = DeviceState( + device_id="copy_test", + sensor_type="temperature", + last_seen_ts=original_time, + last_value=25.0, + run_length=3 + ) + + # Add an alert + alert = Alert( + device_id="copy_test", + issue_type="test_alert", + start_ts=original_time, + end_ts=None, + severity="warning" + ) + original_state.open_alerts["test_alert"] = alert + + # Act: Create deep copy + copied_state = deepcopy(original_state) + + # Modify original + new_time = original_time + timedelta(minutes=1) + original_state.last_seen_ts = new_time + original_state.last_value = 26.0 + original_state.open_alerts["test_alert"].severity = "error" + + # Assert: Copy should remain unchanged + assert copied_state.last_seen_ts == original_time + assert copied_state.last_value == 25.0 + assert copied_state.open_alerts["test_alert"].severity == "warning" + + +class TestTypesBusinessRules: + """Tests that verify business rules across all types.""" + + def test_event_to_alert_data_consistency(self): + """Business rule: Alerts generated from Events should maintain data consistency.""" + # Arrange: Create event + event_time = datetime.now(timezone.utc) + event = Event( + ts=event_time, + device_id="consistency_test", + sensor_type="pressure", + site_id="factory_floor_2", + msg_type="reading", + value=999.9, # Out of range value + seq=15, + quality="ok" + ) + + # Act: Create alert based on event (simulating engine behavior) + alert = Alert( + device_id=event.device_id, # Must match + issue_type="out_of_range", + start_ts=event.ts, # Must use event timestamp + end_ts=None, + severity="error", + sensor_type=event.sensor_type, # Must match + site_id=event.site_id, # Must match + details={ + "trigger_value": event.value, # Include event data + "trigger_seq": event.seq, + "trigger_quality": event.quality + } + ) + + # Assert: Data consistency between event and alert + assert alert.device_id == event.device_id + assert alert.start_ts == event.ts + assert alert.sensor_type == event.sensor_type + assert alert.site_id == event.site_id + assert alert.details["trigger_value"] == event.value + assert alert.details["trigger_seq"] == event.seq + assert alert.details["trigger_quality"] == event.quality + + def test_timestamp_ordering_consistency(self): + """Business rule: Timestamps should maintain logical ordering.""" + # Arrange: Create time sequence + base_time = datetime.now(timezone.utc) + event_time = base_time + alert_start = base_time + timedelta(seconds=1) + alert_end = base_time + timedelta(minutes=5) + + # Act: Create objects with time sequence + event = Event( + ts=event_time, + device_id="timing_test", + sensor_type="temperature", + site_id="test_site", + msg_type="reading", + value=25.0, + seq=1, + quality="ok" + ) + + alert = Alert( + device_id="timing_test", + issue_type="test_issue", + start_ts=alert_start, + end_ts=alert_end, + severity="warning" + ) + + device_state = DeviceState( + device_id="timing_test", + last_seen_ts=event_time + ) + + # Assert: Timestamp relationships should be logical + assert event.ts <= alert.start_ts # Event should trigger alert + assert alert.start_ts < alert.end_ts # Alert start before end + assert device_state.last_seen_ts == event.ts # State reflects event time + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/AgCloud/services/sound_metrics/Dockerfile b/AgCloud/services/sound_metrics/Dockerfile new file mode 100644 index 000000000..efedc1eb6 --- /dev/null +++ b/AgCloud/services/sound_metrics/Dockerfile @@ -0,0 +1,52 @@ +FROM python:3.11-slim + +# System deps +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Bring in app files (including optional ./certs) +COPY . . + +# Install any *.crt in ./certs (if present), regardless of file names +RUN if [ -d /app/certs ] && [ -n "$(find /app/certs -type f -name '*.crt' -print -quit)" ]; then \ + find /app/certs -type f -name '*.crt' -exec cp {} /usr/local/share/ca-certificates/ \; && \ + update-ca-certificates; \ + else \ + echo "No extra CA certs found. Skipping CA update."; \ + fi + +# Ensure pip/requests use the system CA bundle (includes optional NetFree certs) +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Persist pip setting as fallback +RUN printf "[global]\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf + +# Python deps (now benefit from the updated CA store if certs exist) +RUN pip install --no-cache-dir -r requirements.txt + +# App code already copied above +ENV PYTHONUNBUFFERED=1 \ + ADDR=0.0.0.0 \ + PORT=8005 \ + WINDOW_MIN=5 \ + FRAME_SEC=0.1 \ + THRESHOLD=0.01 \ + USE_UTC=false \ + MINIO_ENDPOINT=minio:9000 \ + MINIO_ACCESS_KEY=minioadmin \ + MINIO_SECRET_KEY=minioadmin123 \ + MINIO_BUCKET=sound \ + MINIO_PREFIX=sounds/ + +EXPOSE 8005 + +HEALTHCHECK --interval=30s --timeout=5s --retries=5 \ + CMD curl -fsS http://localhost:8005/metrics >/dev/null || exit 1 + +CMD ["python", "-u", "src/metrics.py"] diff --git a/AgCloud/services/sound_metrics/requirements.txt b/AgCloud/services/sound_metrics/requirements.txt new file mode 100644 index 000000000..fb09ba3d6 --- /dev/null +++ b/AgCloud/services/sound_metrics/requirements.txt @@ -0,0 +1,5 @@ +numpy +soundfile +prometheus_client +pydub # MP3/M4A/AAC +minio \ No newline at end of file diff --git a/AgCloud/services/sound_metrics/src/metrics.py b/AgCloud/services/sound_metrics/src/metrics.py new file mode 100644 index 000000000..126ef5fe9 --- /dev/null +++ b/AgCloud/services/sound_metrics/src/metrics.py @@ -0,0 +1,252 @@ +# services/sound_metrics/src/metrics.py +import os, re, time, math +from pathlib import Path +from datetime import datetime, timedelta +import numpy as np, soundfile as sf +from prometheus_client import Gauge, Counter, start_http_server +from minio import Minio +from io import BytesIO + +try: + from pydub import AudioSegment + HAVE_PYDUB = True +except Exception: + HAVE_PYDUB = False + +# === Environment === +ADDR = os.getenv("ADDR", "0.0.0.0") +PORT = int(os.getenv("PORT", "8005")) +WINDOW_MIN = int(os.getenv("WINDOW_MIN", 5)) +FRAME_SEC = float(os.getenv("FRAME_SEC", 0.1)) +THRESHOLD = float(os.getenv("THRESHOLD", 0.01)) +USE_UTC = os.getenv("USE_UTC", "false").lower() == "true" +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "localhost:9000") +MINIO_ACCESS = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET = os.getenv("MINIO_SECRET_KEY", "minioadmin123") +MINIO_BUCKET = os.getenv("MINIO_BUCKET", "sound") +MINIO_PREFIX = os.getenv("MINIO_PREFIX", "sounds/") +MINIO_PREFIXES = [p.strip() for p in os.getenv("MINIO_PREFIXES", "").split(",") if p.strip()] +if not MINIO_PREFIXES: + MINIO_PREFIXES = [MINIO_PREFIX] + +ALLOWED_EXTS = {".wav", ".flac", ".ogg", ".aiff", ".aif", ".au", ".mp3", ".m4a", ".aac", ".opus"} +FFMPEG_EXTS = {".mp3", ".m4a", ".aac"} + +def now_time(): + return datetime.utcnow() if USE_UTC else datetime.now() + +# === Prometheus metrics (REAL metrics only) === +g_avg_rms = Gauge("sound_avg_volume", "5m avg RMS", ["mic_id"]) +g_std_rms = Gauge("sound_std_volume", "5m std of RMS", ["mic_id"]) +g_uptime = Gauge("sound_mic_uptime_ratio", "5m uptime ratio", ["mic_id"]) +g_volume_db = Gauge("sound_volume_db", "5m average volume (as dB from RMS)", ["mic_id"]) +g_mic_uptime_seconds = Gauge("mic_uptime_seconds", "Uptime seconds (in current window)", ["mic_id"]) + +# Debug / visibility counters +files_scanned_total = Counter("agcloud_files_scanned_total", "Total objects seen in MinIO", ["prefix"]) +files_parsed_total = Counter("agcloud_files_parsed_total", "Total audio files parsed", ["prefix", "mic_id"]) +files_skipped_total = Counter("agcloud_files_skipped_total", "Skipped objects", ["prefix", "reason"]) + +# Filename pattern: _.ext e.g., MIC-01_20251109T232907Z.wav +# FNAME_RE = re.compile(r"^(?P[^_]+)_(?P\d{8}T\d{6}Z)$", re.IGNORECASE) + +# Filename patterns (flexible): +# 1) _ +# 2) - +# 3) _ (no Z) +PATTERNS = [ + re.compile(r"^(?P[^_]+)_(?P\d{8}T\d{6}Z)$", re.IGNORECASE), + re.compile(r"^(?P[^-]+)-(?P\d{8}T\d{6}Z)$", re.IGNORECASE), + re.compile(r"^(?P[^_]+)_(?P\d{8}T\d{6})$", re.IGNORECASE), +] + +def parse_name(fname: str): + p = Path(fname).stem + for rx in PATTERNS: + m = rx.match(p) + if m: + mic = m.group("mic") + ts_str = m.group("ts") + # allow both with/without 'Z' + fmt = "%Y%m%dT%H%M%SZ" if ts_str.endswith("Z") else "%Y%m%dT%H%M%S" + ts = datetime.strptime(ts_str, fmt) + return mic, ts + return None, None + +def window_start_for(ts: datetime) -> datetime: + bucket_min = (ts.minute // WINDOW_MIN) * WINDOW_MIN + return ts.replace(minute=bucket_min, second=0, microsecond=0) + +def load_audio_bytes(data: bytes, ext: str): + if ext in FFMPEG_EXTS: + if not HAVE_PYDUB: + raise RuntimeError("pydub (with FFmpeg) required for compressed audio") + audio = AudioSegment.from_file(BytesIO(data)) + arr = np.array(audio.get_array_of_samples()) + if audio.channels > 1: + arr = arr.reshape((-1, audio.channels)).mean(axis=1) + max_int = float(1 << (8 * audio.sample_width - 1)) + samples = arr.astype(np.float32) / max_int + return samples, audio.frame_rate + else: + with sf.SoundFile(BytesIO(data)) as f: + samples = f.read(always_2d=False) + sr = f.samplerate + if samples.ndim == 2: + samples = samples.mean(axis=1) + return samples, sr + +def process_audio_bytes(data: bytes, ext: str): + samples, sr = load_audio_bytes(data, ext) + if not samples.size: + return 0.0, 0.0, 0, 0 + rms_all = float(np.sqrt(np.mean(samples**2))) + std_all = float(np.std(samples)) + frame = max(1, int(sr * FRAME_SEC)) + active = total = 0 + for i in range(0, len(samples), frame): + seg = samples[i:i+frame] + if len(seg) == 0: + continue + r = float(np.sqrt(np.mean(seg**2))) + if r >= THRESHOLD: + active += 1 + total += 1 + return rms_all, std_all, active, total + +def apply_delta(slot, delta, sign): + slot["sum"] += sign * delta["sum"] + slot["sum_sq"] += sign * delta["sum_sq"] + slot["n"] += sign * delta["n"] + slot["active_frames"] += sign * delta["active_frames"] + slot["total_frames"] += sign * delta["total_frames"] + +def flush_finished_windows(now: datetime, agg): + finished = [] + for (mic, wstart), s in list(agg.items()): + if (wstart + timedelta(minutes=WINDOW_MIN)) <= now: + n = s["n"] + if n: + mean = s["sum"] / n + var = (s["sum_sq"] / n) - mean**2 + std = math.sqrt(max(var, 0.0)) + uptime_ratio = s["active_frames"] / s["total_frames"] if s["total_frames"] else 0.0 + + # Set REAL metrics + g_avg_rms.labels(mic).set(mean) + g_std_rms.labels(mic).set(std) + g_uptime.labels(mic).set(uptime_ratio) + + # Derived REAL metrics for dashboard + db = 20.0 * math.log10(max(mean, 1e-12)) + g_volume_db.labels(mic).set(db) + uptime_seconds = s["active_frames"] * FRAME_SEC + g_mic_uptime_seconds.labels(mic).set(uptime_seconds) + + print(f"[FLUSH] mic={mic} window={wstart:%Y-%m-%d %H:%M} " + f"RMS={mean:.5f} STD={std:.5f} uptime_ratio={uptime_ratio:.3f} " + f"db={db:.2f} uptime_sec={uptime_seconds:.2f}") + + finished.append((mic, wstart)) + for key in finished: + del agg[key] + +def main(): + start_http_server(PORT, addr=ADDR) + print(f"✔ MinIO sound metrics at: http://{ADDR}:{PORT}/metrics") + client = Minio(MINIO_ENDPOINT, MINIO_ACCESS, MINIO_SECRET, secure=False) + + seen = {} # fname -> etag + agg = {} # (mic, wstart) -> accumulators + contrib = {} # fname -> {"wstart": dt, "delta": {...}} + + while True: + now = now_time() + # try: + # objects = client.list_objects(MINIO_BUCKET, prefix=MINIO_PREFIX, recursive=True) + # except Exception as e: + # print(f"[WARN] list_objects failed: {e}") + # time.sleep(10) + # continue + + # Iterate all configured prefixes + for prefix in MINIO_PREFIXES: + try: + objects = client.list_objects(MINIO_BUCKET, prefix=prefix, recursive=True) + except Exception as e: + print(f"[WARN] list_objects failed for prefix={prefix}: {e}") + time.sleep(5) + continue + + for obj in objects: + fname_full = obj.object_name + fname = Path(fname_full).name + ext = Path(fname).suffix.lower() + files_scanned_total.labels(prefix=prefix).inc() + if ext not in ALLOWED_EXTS: + files_skipped_total.labels(prefix=prefix, reason="ext").inc() + continue + + etag = getattr(obj, "etag", None) + if fname in seen and seen[fname] == etag: + continue + + mic, ts = parse_name(fname) + if not mic: + files_skipped_total.labels(prefix=prefix, reason="name").inc() + # Helpful log once in a while + print(f"[SKIP:name] {fname_full} (pattern mismatch)") + continue + + resp = None + try: + resp = client.get_object(MINIO_BUCKET, fname_full) + data = resp.read() + except Exception as e: + print(f"[ERROR] get_object {fname}: {e}") + if resp: + try: resp.close(); resp.release_conn() + except Exception: pass + continue + finally: + if resp: + try: resp.close(); resp.release_conn() + except Exception: pass + + try: + rms, _std, active, total = process_audio_bytes(data, ext) + except Exception as e: + print(f"[ERROR] process_audio {fname}: {e}") + continue + + new_wstart = window_start_for(ts) + new_delta = { + "sum": float(rms), + "sum_sq": float(rms * rms), + "n": 1, + "active_frames": int(active), + "total_frames": int(total), + } + files_parsed_total.labels(prefix=prefix, mic_id=mic).inc() + + if fname in contrib: + prev = contrib[fname] + prev_key = (mic, prev["wstart"]) + if prev_key in agg: + apply_delta(agg[prev_key], prev["delta"], sign=-1) + + key = (mic, new_wstart) + slot = agg.setdefault(key, {"sum": 0.0, "sum_sq": 0.0, "n": 0, + "active_frames": 0, "total_frames": 0}) + apply_delta(slot, new_delta, sign=+1) + + seen[fname] = etag + contrib[fname] = {"wstart": new_wstart, "delta": new_delta} + print(f"[OK] {fname} → mic={mic} RMS={rms:.4f} active={active}/{total} " + f"window={new_wstart:%Y-%m-%d %H:%M}") + + flush_finished_windows(now, agg) + time.sleep(15) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/sounds_classifier/Dockerfile.classifier-svc b/AgCloud/services/sounds_classifier/Dockerfile.classifier-svc new file mode 100644 index 000000000..14fc3ff43 --- /dev/null +++ b/AgCloud/services/sounds_classifier/Dockerfile.classifier-svc @@ -0,0 +1,58 @@ +FROM python:3.12-slim + +# System deps + codecs + CA + Kafka/DB native libs (librdkafka, libpq) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libsndfile1 \ + ffmpeg \ + ca-certificates \ + wget curl \ + librdkafka1 \ + libpq5 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# ---- Corporate CAs ---- +# Place your CA files under classify/certs/*.crt before build +COPY certs/*.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PYTHONUNBUFFERED=1 + + +COPY requirements.txt /app/requirements.txt +RUN python -m pip install --upgrade pip \ + && pip install --no-cache-dir -r /app/requirements.txt + +# Install PyTorch CPU wheels from official index (kept separate for clearer errors/caching) +RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cpu torch==2.5.1+cpu + +# ---- Checkpoint bootstrap (download once at build if missing) ---- +# Configure via build-args or ENV: +ARG CHECKPOINT_URL="https://example.com/path/to/Cnn14_mAP=0.431.pth" +ARG CHECKPOINT_PATH="/app/classification/models/panns_data/Cnn14_mAP=0.431.pth" +ENV CHECKPOINT_URL=${CHECKPOINT_URL} \ + CHECKPOINT=${CHECKPOINT_PATH} + +# Create target folder and download checkpoint WITHOUT importing project code +RUN set -eux; \ + p="${CHECKPOINT}"; \ + url="${CHECKPOINT_URL}"; \ + mkdir -p "$(dirname "$p")"; \ + if [ ! -f "$p" ]; then \ + [ -n "$url" ]; curl -L -o "$p" "$url"; \ + fi; \ + echo "Checkpoint ready at: $p" + +RUN mkdir -p /root/panns_data && \ + curl -L -o /root/panns_data/class_labels_indices.csv \ + https://storage.googleapis.com/us_audioset/youtube_corpus/v1/csv/class_labels_indices.csv + +COPY src/classification /app/classification +RUN touch /app/classification/__init__.py + +EXPOSE 8088 +CMD ["uvicorn", "classification.app:app", "--host", "0.0.0.0", "--port", "8088", "--log-level", "info"] \ No newline at end of file diff --git a/AgCloud/services/sounds_classifier/README.md b/AgCloud/services/sounds_classifier/README.md new file mode 100644 index 000000000..aa7dda159 --- /dev/null +++ b/AgCloud/services/sounds_classifier/README.md @@ -0,0 +1,59 @@ +# 🎧 Sound Classifier Service (CNN14-based) + +Service that classifies audio files using CNN14 model. It: +1. Receives S3 object location (bucket+key) +2. Classifies the sound +3. Stores result in PostgreSQL (optional) +4. Sends alert to Kafka topic if specific sounds detected (optional) +Built with **FastAPI**, **PANNs (CNN14)**, **PostgreSQL**, and optional **Kafka alerts** for real-time monitoring. + +## Quick Start +```bash +docker compose up -d sounds_classifier +``` +Service runs on **http://localhost:8088** (see `docker-compose.yml`, port 8088). + +## API Usage +```json +POST /classify +{ + "s3_bucket": "your-bucket", + "s3_key": "path/to/audio.wav" +} +``` + +### Example Response +```json +{ + "label": "vehicle", + "probs": { + "vehicle": 0.93, + "animal": 0.05, + "shotgun": 0.02 + } +} +``` + +## Supported Audio Formats +- WAV, MP3, FLAC, OGG +- M4A, AAC, WMA, OPUS + + +## Health & Docs +- `GET /health` → basic readiness and model load status +- Swagger UI: [http://localhost:8088/docs](http://localhost:8088/docs) + +## 🧪 Testing +Run all tests (unit + integration): +```bash +pytest -v --cov=src --cov-report=term-missing +``` + +## System Requirements +- Docker and Docker Compose +- MinIO instance with access credentials + +## Notes + • First startup may take ~30s to load the CNN14 model into memory. + • Kafka alerts are optional; see `KAFKA_BROKERS` and `ALERTS_TOPIC`. + • Database writes are handled through `classification.core.db_io_pg`. \ No newline at end of file diff --git a/AgCloud/services/sounds_classifier/requirements.txt b/AgCloud/services/sounds_classifier/requirements.txt new file mode 100644 index 000000000..cb7054ecc --- /dev/null +++ b/AgCloud/services/sounds_classifier/requirements.txt @@ -0,0 +1,20 @@ +# Web API +fastapi==0.115.5 +uvicorn[standard]==0.30.6 +pydantic==2.9.2 + +# Core scientific / audio +numpy==1.26.4 +scipy==1.11.4 +librosa==0.10.2.post1 +soundfile==0.12.1 +joblib==1.4.2 +scikit-learn==1.5.2 + +# Storage / messaging / DB +minio==7.2.7 +confluent-kafka==2.6.0 +psycopg2-binary==2.9.9 + +# PANNs helper (optional but referenced by imports) +panns-inference==0.1.1 diff --git a/AgCloud/services/sounds_classifier/src/classification/__init__.py b/AgCloud/services/sounds_classifier/src/classification/__init__.py new file mode 100644 index 000000000..d163ffe0c --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/__init__.py @@ -0,0 +1,7 @@ +""" +Top-level package for the project. +Avoid importing heavy submodules at package import time to keep imports safe for tests. +""" + +__all__ = ["__version__"] +__version__ = "0.0.0" \ No newline at end of file diff --git a/AgCloud/services/sounds_classifier/src/classification/app.py b/AgCloud/services/sounds_classifier/src/classification/app.py new file mode 100644 index 000000000..a2b526747 --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/app.py @@ -0,0 +1,209 @@ +import logging +import time +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel +from typing import Dict, Optional +import os +import numpy as np +import joblib +from psycopg2 import extensions + +from panns_inference import AudioTagging +from classification.core.model_io import SAMPLE_RATE +from classification.scripts import classify as cls_script +from classification.core.db_utils import ensure_run, open_db, resolve_file_id +from classification.core.db_io_pg import finish_run, upsert_file_aggregate + +app = FastAPI(title="Audio Classifier API", version="2.0.0") + +# --- Globals (singletons) --- +PANN_MODEL: Optional[AudioTagging] = None +SK_PIPELINE = None +DB_CONN = None +DB_RUN_ID = os.getenv("DB_RUN_ID", "api-default") +DB_SCHEMA = os.getenv("DB_SCHEMA", "agcloud_audio") + +CHECKPOINT_PATH = os.getenv( + "CHECKPOINT", + "/app/classification/models/panns_data/Cnn14_mAP=0.431.pth" +) +HEAD_PATH = os.getenv( + "HEAD", + "/app/classification/models/head/head_cnn14_rf.joblib" # adapt if different +) + +class ClassifyIn(BaseModel): + s3_bucket: str + s3_key: str + return_probs: bool = True + +class ClassifyOut(BaseModel): + label: str + probs: Dict[str, float] + sent_alert: bool = True + alert_topic: Optional[str] = None + alert_skip_reason: Optional[str] = None + +def _ensure_conn_clean(conn): + """Rollback any non-idle transaction so the connection is usable.""" + try: + if conn is not None and conn.get_transaction_status() != extensions.TRANSACTION_STATUS_IDLE: + conn.rollback() + except Exception: + # swallow – if rollback itself fails, next ops will raise and be handled + pass + +@app.on_event("startup") +def load_models_on_startup() -> None: + """ + Load heavy models once and perform a short warm-up to avoid cold-start. + Also open DB connection and ensure run row exists. """ + global PANN_MODEL, SK_PIPELINE, DB_CONN + logger = logging.getLogger("uvicorn.error") + + logger.info("Loading models into memory...") + PANN_MODEL = AudioTagging(checkpoint_path=CHECKPOINT_PATH) + SK_PIPELINE = None + try: + if os.path.exists(HEAD_PATH): + SK_PIPELINE = joblib.load(HEAD_PATH) + logger.info("✅ SK pipeline loaded from HEAD.") + else: + logger.warning(f"HEAD pipeline not found at {HEAD_PATH}; using built-in head.") + except Exception as e: + logger.warning(f"HEAD pipeline load failed ({e}); using built-in head.") + + # 3) Warm-up forward pass with 1 second of silence + dummy = np.zeros((1, SAMPLE_RATE * 10), dtype=np.float32) # add batch dim + try: + _ = PANN_MODEL.inference(dummy) + logger.info("✅ PANN model warm-up complete.") + except Exception as e: + logger.warning(f"PANN warm-up skipped ({e})") + + # DB connect + ensure run + try: + DB_CONN = open_db() + ensure_run(DB_CONN, DB_RUN_ID) + logger.info(f"✅ DB connected; run '{DB_RUN_ID}' ensured in schema '{DB_SCHEMA}'.") + except Exception as e: + logger.error(f"DB init failed: {e}") + raise + + logger.info("✅ All models loaded and ready.") + +@app.on_event("shutdown") +def close_db_on_shutdown() -> None: + """ + Cleanly close the global DB connection on shutdown. + """ + global DB_CONN + try: + if DB_CONN is not None: + try: + finish_run(DB_CONN, DB_RUN_ID) + except Exception: + pass + DB_CONN.close() + except Exception: + pass + finally: + DB_CONN = None + +# dedicated API perf logger +api_logger = logging.getLogger("audio_cls.api") +api_logger.setLevel(logging.INFO) +if not api_logger.handlers: + h = logging.StreamHandler() + h.setFormatter(logging.Formatter("[%(asctime)s] [API] %(message)s", "%Y-%m-%d %H:%M:%S")) + api_logger.addHandler(h) + +@app.post("/classify", response_model=ClassifyOut) +def classify(body: ClassifyIn): + """ + Run the full classification pipeline: + - Download from MinIO (s3_bucket + s3_key) + - Model inference with open-set threshold + - DB upsert into agcloud_audio.file_aggregates + """ + start = time.perf_counter() + status_code = 200 + _ensure_conn_clean(DB_CONN) + try: + # 1) Require the file to already exist in public.sound_new_sounds_connections → else 404 + try: + file_id = resolve_file_id(DB_CONN, bucket=body.s3_bucket, object_key=body.s3_key) + except ValueError as e: + DB_CONN.rollback() + # file not found in public.sound_new_sounds_connections → return 404 (do NOT create) + raise HTTPException(status_code=404, detail=str(e)) + + # 2) Run classification + result = cls_script.run_classification_job( + s3_bucket=body.s3_bucket, + s3_key=body.s3_key, + pann_model=PANN_MODEL, + sk_pipeline=SK_PIPELINE + ) + + # 3) Upsert aggregate to DB (JSONB) + upsert_file_aggregate(DB_CONN, { + "run_id": DB_RUN_ID, + "file_id": file_id, + "head_probs_json": result.get("probs", {}), + "head_pred_label": result.get("label"), + "head_pred_prob": result.get("pred_prob"), + "head_unknown_threshold": result.get("unknown_threshold"), + "head_is_another": result.get("is_another"), + "num_windows": result.get("num_windows"), + "agg_mode": result.get("agg_mode"), + "processing_ms": result.get("processing_ms"), + }) + + # 4) Build API response (include alert status if exists) + out = {"label": result.get("label", ""), + "probs": result.get("probs", {}), + "sent_alert": bool(result.get("sent_alert", False)), + "alert_topic": result.get("alert_topic"), + "alert_skip_reason": result.get("alert_skip_reason"), + } + if not body.return_probs: + out["probs"] = {} + return ClassifyOut(**out) + + except HTTPException as e: + status_code = e.status_code + raise + except Exception as e: + try: + DB_CONN.rollback() + except Exception: + pass + status_code = 500 + raise HTTPException(status_code=500, detail=str(e)) + finally: + elapsed_ms = (time.perf_counter() - start) * 1000.0 + api_logger.info( + f"path=/classify bucket={body.s3_bucket} key={body.s3_key} " + f"latency_ms={elapsed_ms:.2f} status={status_code}" + ) + +@app.get("/health") +def health(): + return { + "ok": True, + "pann_loaded": PANN_MODEL is not None, + "sk_pipeline_loaded": SK_PIPELINE is not None + } + +@app.middleware("http") +async def timing_middleware(request, call_next): + t0 = time.perf_counter() + response = await call_next(request) + elapsed_ms = (time.perf_counter() - t0) * 1000.0 + + # log only interesting routes (keep or adjust as you like) + if request.url.path in ("/classify", "/health"): + api_logger.info(f"path={request.url.path} status={response.status_code} latency_ms={elapsed_ms:.2f}") + + return response diff --git a/AgCloud/services/sounds_classifier/src/classification/backbones/__init__.py b/AgCloud/services/sounds_classifier/src/classification/backbones/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sounds_classifier/src/classification/backbones/cnn14.py b/AgCloud/services/sounds_classifier/src/classification/backbones/cnn14.py new file mode 100644 index 000000000..afc7e327e --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/backbones/cnn14.py @@ -0,0 +1,80 @@ +from __future__ import annotations +from typing import Tuple, List, Optional +import numpy as np +from panns_inference import AudioTagging +from classification.core.model_io import _to_numpy, ensure_checkpoint + +def load_cnn14_model( + checkpoint_path: Optional[str] = None, + checkpoint_url: Optional[str] = None, + device: str = "cpu" +) -> AudioTagging: + """ + Load a CNN14 AudioTagging model. + Either checkpoint_path or checkpoint_url must be provided. + Always resolves to a local path via ensure_checkpoint. + """ + if not (checkpoint_path or checkpoint_url): + raise FileNotFoundError("Either checkpoint_path or checkpoint_url must be provided.") + + ckpt = ensure_checkpoint(checkpoint_path, checkpoint_url) # returns a local path + return AudioTagging(checkpoint_path=ckpt, device=device) + + +def run_embedding(at: AudioTagging, wav: np.ndarray) -> np.ndarray: + try: + res = at.inference(wav) + except Exception: + res = at.inference(wav[None, :]) + + emb = None + if isinstance(res, dict): + emb = res.get("embedding", None) + elif isinstance(res, tuple) and len(res) >= 2: + emb = res[1] + if emb is None: + raise RuntimeError("No embedding returned by panns_inference.") + return _to_numpy(emb).reshape(-1) + + +def run_cnn14_embedding(model: AudioTagging, wav: np.ndarray) -> np.ndarray: + """ + Run embedding extraction; validate input waveform. + Raises ValueError if wav is empty. + """ + wav = np.asarray(wav) + if wav.size == 0: + raise ValueError("waveform must not be empty") + if wav.dtype != np.float32: + wav = wav.astype(np.float32, copy=False) + return run_embedding(model, wav) + + +def run_cnn14_embeddings_batch(model: AudioTagging, windows: np.ndarray, batch_size: int = 32) -> np.ndarray: + """ + Compute embeddings for a batch of windows in shape (N, samples). + Returns array (N, emb_dim) float32. + """ + if windows.ndim != 2: + raise ValueError("windows must be 2D (N, samples)") + n = windows.shape[0] + embs = [] + i = 0 + while i < n: + j = min(i + batch_size, n) + chunk = np.array(windows[i:j], dtype=np.float32, copy=True, order="C") + # panns_inference supports batched input (N, samples) + res = model.inference(chunk) + if isinstance(res, dict): + emb = res.get("embedding") + elif isinstance(res, tuple) and len(res) >= 2: + emb = res[1] + else: + raise RuntimeError("Unexpected inference output") + e = _to_numpy(emb).astype(np.float32, copy=False) + if e.ndim == 1: + e = e[None, :] + embs.append(e) + i = j + E = np.concatenate(embs, axis=0).astype(np.float32, copy=False) + return E diff --git a/AgCloud/services/sounds_classifier/src/classification/core/__init__.py b/AgCloud/services/sounds_classifier/src/classification/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sounds_classifier/src/classification/core/db_gis_helpers.py b/AgCloud/services/sounds_classifier/src/classification/core/db_gis_helpers.py new file mode 100644 index 000000000..4389d2a91 --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/core/db_gis_helpers.py @@ -0,0 +1,87 @@ +# db_gis_helpers.py +from __future__ import annotations +from typing import Optional, Tuple +from datetime import datetime, timezone + +def _to_dt_utc(value): + """Accepts datetime or ISO string (possibly ending with 'Z') and returns aware datetime in UTC.""" + if isinstance(value, datetime): + return value.astimezone(timezone.utc) if value.tzinfo else value.replace(tzinfo=timezone.utc) + s = str(value) + if s.endswith("Z"): + s = s[:-1] # drop Z for strptime + dt = datetime.fromisoformat(s) if "T" in s else datetime.strptime(s, "%Y-%m-%dT%H:%M:%S") + return dt.replace(tzinfo=timezone.utc) + try: + # try full ISO first + dt = datetime.fromisoformat(s) + return dt.astimezone(timezone.utc) if dt.tzinfo else dt.replace(tzinfo=timezone.utc) + except Exception: + # last resort: YYYY-MM-DDTHH:MM:SS + dt = datetime.strptime(s.replace(" ", "T"), "%Y-%m-%dT%H:%M:%S") + return dt.replace(tzinfo=timezone.utc) + +def fetch_gis_by_device_and_time(conn, device_id: str, started_at) -> Tuple[Optional[float], Optional[float], Optional[str]]: + started_dt = _to_dt_utc(started_at) + try: + with conn.cursor() as cur: + cur.execute( + """ + SELECT + (gis_origin->>'latitude')::double precision AS lat, + (gis_origin->>'longitude')::double precision AS lon, + (gis_origin->>'area') AS area + FROM public.sounds_metadata + WHERE device_id = %s + AND capture_time = %s + LIMIT 1 + """, + (device_id, started_dt), + ) + row = cur.fetchone() + if row: + return row[0], row[1], row[2] + + cur.execute( + """ + SELECT + (gis_origin->>'latitude')::double precision AS lat, + (gis_origin->>'longitude')::double precision AS lon, + (gis_origin->>'area') AS area + FROM public.sounds_metadata + WHERE device_id = %s + AND capture_time BETWEEN %s - interval '30 seconds' AND %s + interval '30 seconds' + ORDER BY ABS(EXTRACT(EPOCH FROM (capture_time - %s))) ASC + LIMIT 1 + """, + (device_id, started_dt, started_dt, started_dt), + ) + row = cur.fetchone() + if row: + return row[0], row[1], row[2] + except Exception: + pass + return None, None, None + +def fetch_gis_by_filename(conn, file_name: str) -> Tuple[Optional[float], Optional[float], Optional[str]]: + try: + with conn.cursor() as cur: + cur.execute( + """ + SELECT + (gis_origin->>'latitude')::double precision AS lat, + (gis_origin->>'longitude')::double precision AS lon, + (gis_origin->>'area') AS area + FROM public.sounds_metadata + WHERE file_name = %s + ORDER BY created_at DESC + LIMIT 1 + """, + (file_name,), + ) + row = cur.fetchone() + if row: + return row[0], row[1], row[2] + except Exception: + pass + return None, None, None diff --git a/AgCloud/services/sounds_classifier/src/classification/core/db_io_pg.py b/AgCloud/services/sounds_classifier/src/classification/core/db_io_pg.py new file mode 100644 index 000000000..5a1789e4c --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/core/db_io_pg.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import json +import re +from typing import Any, Dict, Optional + +import psycopg2 +import psycopg2.extras +from psycopg2.extensions import connection as PGConnection +from psycopg2 import sql +import logging + +LOGGER = logging.getLogger(__name__) +_SCHEMA_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + +def open_db(db_url: str, schema: str = "audio_cls") -> PGConnection: + if not db_url: + raise ValueError("db_url is required (e.g., postgresql://user:pass@host:port/db)") + if not _SCHEMA_RE.match(schema): + raise ValueError(f"invalid schema name: {schema}") + + conn = psycopg2.connect(db_url) + conn.autocommit = False + try: + with conn.cursor() as cur: + cur.execute(sql.SQL("CREATE SCHEMA IF NOT EXISTS {}").format(sql.Identifier(schema))) + cur.execute(sql.SQL("SET search_path TO {}, public").format(sql.Identifier(schema))) + conn.commit() + LOGGER.info("DB connected; schema=%s", schema) + except Exception: + conn.rollback() + LOGGER.exception("failed to init schema/search_path") + raise + return conn + +def upsert_run(conn: PGConnection, meta: Dict[str, Any]) -> None: + try: + with conn.cursor() as cur: + cur.execute( + """ + INSERT INTO runs + (run_id, model_name, checkpoint, head_path, labels_csv, + window_sec, hop_sec, pad_last, agg, topk, device, code_version, notes) + VALUES + (%(run_id)s, %(model_name)s, %(checkpoint)s, %(head_path)s, %(labels_csv)s, + %(window_sec)s, %(hop_sec)s, %(pad_last)s, %(agg)s, %(topk)s, %(device)s, %(code_version)s, %(notes)s) + ON CONFLICT (run_id) DO NOTHING + """, + meta, + ) + conn.commit() + LOGGER.debug("upsert_run: %s", meta.get("run_id")) + except Exception: + conn.rollback() + LOGGER.exception("upsert_run failed") + raise + +def finish_run(conn: PGConnection, run_id: str) -> None: + try: + with conn.cursor() as cur: + cur.execute("UPDATE runs SET finished_at = now() WHERE run_id = %s", (run_id,)) + conn.commit() + LOGGER.info("finish_run: %s", run_id) + except Exception: + conn.rollback() + LOGGER.exception("finish_run failed: %s", run_id) + raise + +def _jsonify(v: Any) -> psycopg2.extras.Json: + if isinstance(v, str): + try: + v = json.loads(v) + except Exception: + v = {"raw": v} + return psycopg2.extras.Json(v) + +def upsert_file_aggregate(conn: PGConnection, row: Dict[str, Any]) -> None: + data = dict(row) + if "head_probs_json" in data and data["head_probs_json"] is not None: + data["head_probs_json"] = _jsonify(data.get("head_probs_json")) + + try: + with conn.cursor() as cur: + cur.execute( + """ + INSERT INTO file_aggregates + (run_id, file_id, + head_probs_json, head_pred_label, head_pred_prob, head_unknown_threshold, head_is_another, + num_windows, agg_mode, processing_ms) + VALUES + (%(run_id)s, %(file_id)s, + %(head_probs_json)s, %(head_pred_label)s, %(head_pred_prob)s, %(head_unknown_threshold)s, %(head_is_another)s, + %(num_windows)s, %(agg_mode)s, %(processing_ms)s) + ON CONFLICT (run_id, file_id) DO UPDATE SET + head_probs_json = EXCLUDED.head_probs_json, + head_pred_label = EXCLUDED.head_pred_label, + head_pred_prob = EXCLUDED.head_pred_prob, + head_unknown_threshold = EXCLUDED.head_unknown_threshold, + head_is_another = EXCLUDED.head_is_another, + num_windows = EXCLUDED.num_windows, + agg_mode = EXCLUDED.agg_mode, + processing_ms = EXCLUDED.processing_ms + """, + data, + ) + conn.commit() + LOGGER.debug("upsert_file_aggregate: run=%s file=%s", data.get("run_id"), data.get("file_id")) + except Exception: + conn.rollback() + LOGGER.exception("upsert_file_aggregate failed") + raise + diff --git a/AgCloud/services/sounds_classifier/src/classification/core/db_utils.py b/AgCloud/services/sounds_classifier/src/classification/core/db_utils.py new file mode 100644 index 000000000..f2b7f139e --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/core/db_utils.py @@ -0,0 +1,126 @@ +import os +import psycopg2 +from psycopg2 import sql +from typing import Optional + +FILES_SCHEMA = os.getenv("FILES_SCHEMA", "public") +FILES_TABLE = os.getenv("FILES_TABLE", "sound_new_sounds_connections") + +def _files_table_ql() -> sql.SQL: + return sql.SQL("{}.{}").format(sql.Identifier(FILES_SCHEMA), sql.Identifier(FILES_TABLE)) + +_KEY_COL = sql.Identifier("key") + +def open_db(): + host = os.getenv("DB_HOST", "postgres") + port = int(os.getenv("DB_PORT", "5432")) + db = os.getenv("DB_NAME", "missions_db") + user = os.getenv("DB_USER", "missions_user") + pwd = os.getenv("DB_PASSWORD", "pg123") + schema = os.getenv("DB_SCHEMA", "agcloud_audio") + + conn = psycopg2.connect(host=host, port=port, dbname=db, user=user, password=pwd) + conn.autocommit = False + with conn.cursor() as cur: + cur.execute(sql.SQL("SET search_path TO {}, public;").format(sql.Identifier(schema))) + return conn + +def ensure_file(conn, *, bucket: str, object_key: str, + size_bytes: Optional[int] = None, + sample_rate: Optional[int] = None, + duration_s: Optional[float] = None) -> int: + """Idempotent ensure in public.sound_new_sounds_connections by (bucket, object_key).""" + combined_key = f"{bucket}/{object_key}".lstrip("/") + try: + with conn.cursor() as cur: + cur.execute( + sql.SQL("SELECT id FROM {} WHERE {} = %s") + .format(_files_table_ql(),_KEY_COL), + (combined_key,), + ) + row = cur.fetchone() + if row: + return int(row[0]) + + cur.execute( + sql.SQL(""" + INSERT INTO {} ({}, size_bytes, sample_rate, duration_s) + VALUES (%s, %s, %s, %s) + RETURNING id + """).format(_files_table_ql(), _KEY_COL), + (combined_key, size_bytes, sample_rate, duration_s), + ) + new_id = cur.fetchone()[0] + + conn.commit() + return int(new_id) + except Exception: + conn.rollback() + raise + +def ensure_run(conn, run_id: str): + """ + Ensure there is a row in agcloud_audio.runs for FK constraints. + This will INSERT ... ON CONFLICT DO NOTHING with reasonable defaults. + """ + import os + window_sec = float(os.getenv("WINDOW_SEC", "2.0")) + hop_sec = float(os.getenv("HOP_SEC", "0.5")) + pad_last = os.getenv("PAD_LAST", "true").lower() in ("1", "true", "yes", "on") + + with conn.cursor() as cur: + cur.execute(""" + INSERT INTO runs ( + run_id, model_name, checkpoint, head_path, labels_csv, + window_sec, hop_sec, pad_last, agg, topk, device, code_version, notes + ) + VALUES ( + %(run_id)s, %(model_name)s, %(checkpoint)s, %(head_path)s, %(labels_csv)s, + %(window_sec)s, %(hop_sec)s, %(pad_last)s, %(agg)s, %(topk)s, %(device)s, + %(code_version)s, %(notes)s + ) + ON CONFLICT (run_id) DO NOTHING + """, { + "run_id": run_id, + "model_name": os.getenv("MODEL_NAME", "panns_cnn14"), + "checkpoint": os.getenv("CHECKPOINT", "panns_cnn14.pth"), + "head_path": os.getenv("HEAD", ""), + "labels_csv": os.getenv("LABELS_CSV", ""), + "window_sec": float(os.getenv("WINDOW_SEC", "10")), + "hop_sec": float(os.getenv("HOP_SEC", "10")), + "pad_last": os.getenv("PAD_LAST", "false").lower() == "true", + "agg": os.getenv("AGG", "mean"), + "topk": int(os.getenv("TOPK", "3")), + "device": os.getenv("DEVICE", "cpu"), + "code_version": os.getenv("CODE_VERSION", ""), + "notes": os.getenv("RUN_NOTES", "created by API ensure_run") + }) + conn.commit() + +def resolve_file_id(conn, *, file_id: Optional[int] = None, + bucket: Optional[str] = None, object_key: Optional[str] = None) -> int: + """Select-only (NO insert). Raises ValueError if not found.""" + with conn.cursor() as cur: + if file_id is not None: + cur.execute( + sql.SQL("SELECT id FROM {} WHERE id = %s").format(_files_table_ql()), + (file_id,), + ) + row = cur.fetchone() + if row: + return int(row[0]) + raise ValueError(f"id {file_id} not found in {FILES_SCHEMA}.{FILES_TABLE}") + + if bucket is not None and object_key is not None: + combined_key = f"{bucket}/{object_key}".lstrip("/") + cur.execute( + sql.SQL("SELECT id FROM {} WHERE {} = %s") + .format(_files_table_ql(), _KEY_COL), + (combined_key,), + ) + row = cur.fetchone() + if row: + return int(row[0]) + raise ValueError(f"File s3://{bucket}/{object_key} not found in {FILES_SCHEMA}.{FILES_TABLE}") + + raise ValueError("Must provide file_id or (bucket, object_key)") diff --git a/AgCloud/services/sounds_classifier/src/classification/core/model_io.py b/AgCloud/services/sounds_classifier/src/classification/core/model_io.py new file mode 100644 index 000000000..fcfa3a0f6 --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/core/model_io.py @@ -0,0 +1,248 @@ +from __future__ import annotations + +import pathlib +import shutil +import subprocess +from typing import Any, List, Optional, Tuple, Literal + +import numpy as np +import soundfile as sf +import librosa +import logging +import os +from numpy.lib.stride_tricks import sliding_window_view + +try: + import torch +except Exception: + torch = None + +LOGGER = logging.getLogger(__name__) + +SAMPLE_RATE = 32000 +MIN_SAMPLES = 16000 +HARD_EXTS = {".mp3", ".opus", ".m4a", ".aac", ".wma"} +SUPPORTED_EXTS = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"} + +def ensure_numpy_1d(x): + """ + Force input to be numpy float32 vector (1-D). + Accepts numpy, torch.Tensor, TF tensors, and torch-like wrappers (duck-typed). + """ + # Torch-like (duck typing): has detach/cpu/numpy + if not isinstance(x, np.ndarray): + has_detach = hasattr(x, "detach") + has_cpu = hasattr(x, "cpu") + has_numpy = hasattr(x, "numpy") + if has_detach and has_cpu and has_numpy: + try: + x = x.detach().cpu().numpy() + except Exception: + pass + # Generic tensors (e.g., TF), expose .numpy() without detach/cpu + elif has_numpy and callable(getattr(x, "numpy", None)): + try: + x = x.numpy() + except Exception: + pass + + # Final conversion to numpy float32 + x = np.asarray(x, dtype=np.float32) + + # Flatten to 1-D + if x.ndim > 1: + x = x.reshape(-1) + return x + + +def has_ffmpeg() -> bool: + return shutil.which("ffmpeg") is not None + + +def decode_with_ffmpeg_to_float32_mono(path: str, target_sr: int = SAMPLE_RATE) -> np.ndarray: + cmd = ["ffmpeg", "-v", "error", "-i", path, "-vn", "-ac", "1", "-ar", str(target_sr), "-f", "f32le", "pipe:1"] + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) + y = np.frombuffer(proc.stdout, dtype=np.float32) + if y.size < MIN_SAMPLES: + y = np.concatenate([y, np.zeros(MIN_SAMPLES - y.size, dtype=np.float32)], axis=0) + return y + + +def ensure_checkpoint(checkpoint_path: str, checkpoint_url: Optional[str]) -> str: + import urllib.request + p = pathlib.Path(checkpoint_path) + p.parent.mkdir(parents=True, exist_ok=True) + if p.exists(): + return str(p) + if not checkpoint_url: + raise FileNotFoundError(f"No checkpoint at {p}. Provide --checkpoint or --checkpoint-url.") + urllib.request.urlretrieve(checkpoint_url, p) + LOGGER.info("downloaded checkpoint to %s", p) + return str(p) + + +def load_audio(path: str, target_sr: int = SAMPLE_RATE) -> np.ndarray: + ext = pathlib.Path(path).suffix.lower() + + def _pad(y: np.ndarray) -> np.ndarray: + if y.size < MIN_SAMPLES: + y = np.concatenate([y, np.zeros(MIN_SAMPLES - y.size, dtype=np.float32)]) + return y + + # Compressed/streaming formats first (e.g., mp3, m4a, etc.) + if ext in HARD_EXTS: + try: + y, _ = librosa.load(path, sr=target_sr, mono=True) + y = ensure_numpy_1d(y) + return _pad(y) + except Exception: + if has_ffmpeg(): + LOGGER.warning("librosa failed; using ffmpeg fallback for %s", path) + y = decode_with_ffmpeg_to_float32_mono(path, target_sr) + y = ensure_numpy_1d(y) + return _pad(y) + LOGGER.exception("failed to load compressed audio: %s", path) + raise + + # Uncompressed / common wavs + try: + y, sr = sf.read(path, always_2d=False) + if hasattr(y, "ndim") and y.ndim > 1: + y = np.mean(y, axis=1) + + y = ensure_numpy_1d(y) + if int(sr) != int(target_sr): + y = librosa.resample(y, orig_sr=int(sr), target_sr=int(target_sr)) + y = ensure_numpy_1d(y) + + return _pad(y) + + except Exception: + try: + y, _ = librosa.load(path, sr=target_sr, mono=True) + y = ensure_numpy_1d(y) + return _pad(y) + except Exception: + if has_ffmpeg(): + LOGGER.warning("soundfile/librosa failed; using ffmpeg fallback for %s", path) + y = decode_with_ffmpeg_to_float32_mono(path, target_sr) + y = ensure_numpy_1d(y) + return _pad(y) + LOGGER.exception("failed to load audio: %s", path) + raise + + +def _to_numpy(x: Any) -> np.ndarray: + if (torch is not None) and hasattr(torch, "Tensor") and isinstance(x, torch.Tensor): # type: ignore + x = x.detach().cpu().numpy() + arr = np.asarray(x, dtype=np.float32) + if arr.ndim == 2: + if arr.shape[0] == 1: + arr = arr[0] + elif arr.shape[1] == 1: + arr = arr[:, 0] + else: + arr = arr.reshape(-1) + elif arr.ndim != 1: + arr = arr.reshape(-1) + return arr + + +def segment_waveform( + wav: np.ndarray, + sr: int = SAMPLE_RATE, + window_sec: float = 2.0, + hop_sec: float = 0.5, + pad_last: bool = True, +) -> List[np.ndarray]: + """ + Splits waveform into overlapping fixed-size windows. + Returns list of 1D numpy arrays (segments), each of length window_sec * sr. + """ + wav = np.asarray(wav, dtype=np.float32).reshape(-1) + win = max(1, int(round(window_sec * sr))) + hop = max(1, int(round(hop_sec * sr))) + n = wav.size + + segments: List[np.ndarray] = [] + if n == 0: + return segments + + i = 0 + while i + win <= n: + seg = wav[i: i + win].astype(np.float32) + segments.append(seg) + i += hop + + if pad_last and (i < n): + tail = wav[i:] + pad = np.zeros(win - tail.size, dtype=np.float32) + seg = np.concatenate([tail, pad], axis=0) + segments.append(seg) + elif not segments and pad_last: + pad = np.zeros(win - n, dtype=np.float32) + seg = np.concatenate([wav, pad], axis=0) + segments.append(seg) + + # ensure all are 1D np.float32 arrays + return [np.asarray(seg, dtype=np.float32).flatten() for seg in segments] + + +def segment_waveform_2d_view( + wav: np.ndarray, + sr: int = SAMPLE_RATE, + window_sec: float = 2.0, + hop_sec: float = 0.5, + pad_last: bool = True, +) -> np.ndarray: + """ + Return a 2D view of windows with shape (N, win) float32, minimizing copies. + The last window is padded if pad_last=True and needed (that one will copy). + """ + wav = np.asarray(wav, dtype=np.float32).reshape(-1) + win = max(1, int(round(window_sec * sr))) + hop = max(1, int(round(hop_sec * sr))) + n = wav.size + if n == 0: + return np.zeros((0, win), dtype=np.float32) + + if n >= win: + # sliding view for all full windows (no copy) + sw = sliding_window_view(wav, win)[::hop] # shape (N_full, win), view + if pad_last and ((n - win) % hop != 0): + tail_start = (sw.shape[0] * hop) + tail = wav[tail_start:] + pad = np.zeros(win - tail.size, dtype=np.float32) + last = np.concatenate([tail, pad], axis=0)[None, :] + return np.vstack([sw.astype(np.float32, copy=False), last.astype(np.float32, copy=False)]) + return sw.astype(np.float32, copy=False) + + # n < win + if pad_last: + pad = np.zeros(win - n, dtype=np.float32) + return np.concatenate([wav, pad], axis=0)[None, :] + return np.zeros((0, win), dtype=np.float32) + + +def aggregate_matrix(mat: np.ndarray, mode: Literal["mean", "max"] = "mean") -> np.ndarray: + if not isinstance(mat, np.ndarray): + raise TypeError("mat must be a numpy.ndarray") + if mat.ndim != 2: + raise ValueError("expected shape (num_windows, num_classes)") + if mat.shape[0] == 0: + raise ValueError("cannot aggregate an empty window matrix (num_windows == 0)") + if mat.shape[1] == 0: + raise ValueError("expected num_classes > 0") + if mode == "mean": + # Ignore NaNs when computing per-class means + v = np.nanmean(mat.astype(np.float32, copy=False), axis=0) + elif mode == "max": + # Ignore NaNs when computing per-class max + v = np.nanmax(mat.astype(np.float32, copy=False), axis=0) + else: + raise ValueError(f"Unsupported aggregation mode: {mode}") + + # Ensure finite float32 output; all-NaN columns become 0.0 + v = np.nan_to_num(v, nan=0.0, posinf=np.finfo(np.float32).max, neginf=np.finfo(np.float32).min) + return v.astype(np.float32, copy=False) + diff --git a/AgCloud/services/sounds_classifier/src/classification/models/custom_labels.csv b/AgCloud/services/sounds_classifier/src/classification/models/custom_labels.csv new file mode 100644 index 000000000..028674305 --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/models/custom_labels.csv @@ -0,0 +1,12 @@ +index,display_name +0,predatory_animals +1,non_predatory_animals +2,birds +3,fire +4,footsteps +5,insects +6,screaming +7,shotgun +8,stormy_weather +9,streaming_water +10,vehicle diff --git a/AgCloud/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib b/AgCloud/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib new file mode 100644 index 000000000..535fa1218 Binary files /dev/null and b/AgCloud/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib differ diff --git a/AgCloud/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib.meta.json b/AgCloud/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib.meta.json new file mode 100644 index 000000000..fc380654f --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib.meta.json @@ -0,0 +1,23 @@ +{ + "class_order": [ + "predatory_animals", + "non_predatory_animals", + "birds", + "fire", + "footsteps", + "insects", + "screaming", + "shotgun", + "stormy_weather", + "streaming_water", + "vehicle" + ], + "seed": 42, + "test_size": 0.2, + "train_dir": "C:\\Users\\user1\\Desktop\\programming\\kamatech\\AgCloud\\AgCloud\\classification\\data\\train", + "checkpoint": "C:\\Users\\user1\\panns_data\\Cnn14_mAP=0.431.pth", + "device": "cpu", + "backbone": "cnn14", + "embedding_dim": 2048, + "head_type": "rf" +} \ No newline at end of file diff --git a/AgCloud/services/sounds_classifier/src/classification/scripts/__init__.py b/AgCloud/services/sounds_classifier/src/classification/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/sounds_classifier/src/classification/scripts/alerts.py b/AgCloud/services/sounds_classifier/src/classification/scripts/alerts.py new file mode 100644 index 000000000..bccf69048 --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/scripts/alerts.py @@ -0,0 +1,183 @@ +import json +import time +import logging +from typing import Optional, Dict, Any +from confluent_kafka import Producer, KafkaException +import uuid +from datetime import datetime, timezone + +LOGGER = logging.getLogger("audio_cls.alerts") +if not LOGGER.handlers: + # Minimal console handler if none configured by the app + h = logging.StreamHandler() + fmt = logging.Formatter("[%(levelname)s] %(name)s: %(message)s") + h.setFormatter(fmt) + LOGGER.addHandler(h) +LOGGER.setLevel(logging.INFO) + +# Cache one Producer per brokers string +_producer_cache: dict[str, Producer] = {} + +def _get_producer(brokers: str) -> Producer: + """ + Lazily create and cache a Kafka Producer for the given brokers string. + Do NOT load env here; configuration is passed by the caller (service). + """ + p = _producer_cache.get(brokers) + if p is not None: + return p + + conf = { + "bootstrap.servers": brokers, + "queue.buffering.max.ms": 5, # small batching for low latency + "message.timeout.ms": 5000, # fail fast + "socket.keepalive.enable": True, + "api.version.request": True, + } + try: + p = Producer(conf) + except KafkaException as e: + LOGGER.error("Failed to initialize Kafka producer (brokers=%s): %s", brokers, e) + raise + _producer_cache[brokers] = p + return p + +def _delivery_report(err, msg): + if err is not None: + LOGGER.warning("Kafka delivery failed: %s", err) + else: + LOGGER.info( + "Kafka delivered: topic=%s partition=%s offset=%s", + msg.topic(), msg.partition(), msg.offset() + ) + +def _iso_utc(dt: datetime) -> str: + return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + +def send_alert( + *, + brokers: str, + topic: str, + label: str, + probs: Dict[str, float], + meta: Optional[Dict[str, Any]] = None, +) -> bool: + """ + Send a JSON alert to Kafka. Returns True if enqueued+delivered (within flush timeout), + False on immediate failure. Delivery problems are logged via _delivery_report. + """ + payload = { + "label": label, + "probs": probs, + "meta": meta or {}, + "ts": int(time.time() * 1000), + } + try: + p = _get_producer(brokers) + p.produce(topic=topic, value=json.dumps(payload).encode("utf-8"), callback=_delivery_report) + # Serve delivery callbacks; flush returns number of undelivered messages (0 == success) + p.poll(0) + # undelivered = p.flush(5) + # if undelivered != 0: + # LOGGER.warning("Kafka flush returned %s undelivered message(s)", undelivered) + # return False + return True + except KafkaException as e: + LOGGER.error("Kafka exception while producing: %s", e) + return False + except BufferError as e: + LOGGER.error("Kafka local queue full: %s", e) + return False + except Exception as e: + LOGGER.error("Kafka produce error: %s", e) + return False + +# ---- Backwards compatibility shim ---- +def send_kafka_alert(file_path: str, label: str, prob: float) -> bool: + """ + Legacy helper kept for backward compatibility. Reads brokers/topic from env + ONLY if caller insists on using this function. Prefer send_alert(...). + """ + import os # local import to avoid env dependency on module load + brokers = os.getenv("KAFKA_BROKERS") or os.getenv("KAFKA_BROKER", "localhost:9092") + topic = os.getenv("ALERTS_TOPIC") or os.getenv("KAFKA_ALERTS_TOPIC", "alerts") + + payload_probs = {label: float(prob)} + meta = {"file_path": file_path, "source": "legacy_send_kafka_alert"} + + return send_alert( + brokers=brokers, + topic=topic, + label=label, + probs=payload_probs, + meta=meta, + ) + +# ---- Structured alert with strict required fields ---- +REQUIRED_FIELDS = ("alert_id", "alert_type", "device_id", "started_at") + +def send_structured_alert( + *, + brokers: str, + topic: str = "alerts", + alert_type: str, + device_id: str, + started_at: str, + ended_at: Optional[str] = None, + confidence: Optional[float] = None, + severity: Optional[int] = None, + area: Optional[str] = None, + lat: Optional[float] = None, + lon: Optional[float] = None, + image_url: Optional[str] = None, + vod: Optional[str] = None, + hls: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, + alert_id: Optional[str] = None, + message_key: Optional[str] = None, +) -> bool: + """ + Send alert JSON to Kafka in the required schema. + Required: alert_id, alert_type, device_id, started_at (ISO-8601 Z). + Optional fields are included ONLY if explicitly provided (no defaults/guesses). + """ + payload: Dict[str, Any] = { + "alert_id": alert_id or str(uuid.uuid4()), + "alert_type": alert_type, + "device_id": device_id, + "started_at": started_at, + } + + # Append optional fields IFF provided (no guessing) + if ended_at: payload["ended_at"] = ended_at + if confidence is not None: payload["confidence"] = float(confidence) + if severity is not None: payload["severity"] = int(severity) + if area: payload["area"] = area + if lat is not None: payload["lat"] = float(lat) + if lon is not None: payload["lon"] = float(lon) + if image_url: payload["image_url"] = image_url + if vod: payload["vod"] = vod + if hls: payload["hls"] = hls + if meta is not None: payload["meta"] = meta + + missing = [f for f in REQUIRED_FIELDS if f not in payload or payload[f] in (None, "")] + if missing: + LOGGER.error("Structured alert missing required fields: %s", missing) + return False + + try: + p = _get_producer(brokers) + p.produce( + topic=topic, + value=json.dumps(payload).encode("utf-8"), + key=(message_key.encode("utf-8") if isinstance(message_key, str) else None), + callback=_delivery_report + ) + p.poll(0) + return True + except KafkaException as e: + LOGGER.error("Kafka exception while producing structured alert: %s", e) + return False + except Exception as e: + LOGGER.error("Kafka produce error (structured alert): %s", e) + return False \ No newline at end of file diff --git a/AgCloud/services/sounds_classifier/src/classification/scripts/classify.py b/AgCloud/services/sounds_classifier/src/classification/scripts/classify.py new file mode 100644 index 000000000..81a829837 --- /dev/null +++ b/AgCloud/services/sounds_classifier/src/classification/scripts/classify.py @@ -0,0 +1,456 @@ +from __future__ import annotations + +import logging +import os +import tempfile +from pathlib import Path +import time +from typing import Dict, List, Optional, Tuple, Any +from panns_inference import AudioTagging +import numpy as np +import joblib + +from minio import Minio +from minio.error import S3Error +import re +from datetime import datetime, timezone +import uuid + +from classification.core.model_io import ( + SAMPLE_RATE, + _to_numpy, + load_audio, # returns 1-D float32 mono @ SAMPLE_RATE + segment_waveform_2d_view, + aggregate_matrix, +) +from classification.backbones.cnn14 import load_cnn14_model, run_cnn14_embeddings_batch +from classification.scripts import alerts +from classification.core.db_utils import open_db +from classification.core.db_gis_helpers import ( + fetch_gis_by_device_and_time, + fetch_gis_by_filename, +) + +# ----------------------------- +# Environment configuration +# ----------------------------- +DEVICE = os.getenv("DEVICE", "cpu").strip().lower() +BACKBONE = os.getenv("BACKBONE", "cnn14").strip().lower() + +CHECKPOINT = os.getenv("CHECKPOINT") or "" +CHECKPOINT_URL = os.getenv("CHECKPOINT_URL") or "" + +HEAD_PATH = os.getenv("HEAD") or "" # joblib path +LABELS_CSV = os.getenv("LABELS_CSV") or "" # optional (if head has classes_, not needed) + +WINDOW_SEC = float(os.getenv("WINDOW_SEC", "2.0")) +HOP_SEC = float(os.getenv("HOP_SEC", "0.5")) +PAD_LAST = os.getenv("PAD_LAST", "true").strip().lower() in ("1", "true", "yes", "on") +AGG = os.getenv("AGG", "mean").strip().lower() # "mean" | "max" + +UNKNOWN_THRESHOLD = float(os.getenv("UNKNOWN_THRESHOLD", "0.55")) + +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "minio:9000") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minio") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minio123") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false").strip().lower() in ("1", "true", "yes", "on") + +ALLOWED_BUCKETS: List[str] = [b.strip() for b in os.getenv("ALLOWED_BUCKETS", "sound").split(",") if b.strip()] +ALLOWED_CONTENT_TYPES: List[str] = [t.strip() for t in os.getenv( + "ALLOWED_CONTENT_TYPES", + "audio/wav,audio/x-wav,audio/mpeg,audio/flac,audio/ogg,audio/mp4" +).split(",") if t.strip()] +MAX_BYTES = int(os.getenv("MAX_BYTES", str(50 * 1024 * 1024))) + +KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") +ALERTS_TOPIC = os.getenv("ALERTS_TOPIC", "alerts") + +# ----------------------------- +# Lazy runtime (model/head/labels) +# ----------------------------- +class _Runtime: + model = None # CNN14 backbone + head = None # sklearn pipeline with predict_proba + classes: List[str] = [] # class names aligned to head output + +R = _Runtime() + +_MINIO_CLIENT = None +_DB_CONN = None + +def _get_minio(): + global _MINIO_CLIENT + if _MINIO_CLIENT is None: + _MINIO_CLIENT = Minio( + MINIO_ENDPOINT, access_key=MINIO_ACCESS_KEY, secret_key=MINIO_SECRET_KEY, secure=MINIO_SECURE + ) + return _MINIO_CLIENT + +def _get_db_conn(): + """ + Lazy-open and cache a DB connection using open_db(). + """ + global _DB_CONN + if _DB_CONN is None: + _DB_CONN = open_db() + return _DB_CONN + +_TS_PATTERNS = ( + # ISO-like with Z or without Z, with or without 'T' + r"(?P\d{4}-?\d{2}-?\d{2}[T ]?\d{2}:?\d{2}:?\d{2}Z?)", + # Compact: YYYYMMDDTHHMMSSZ or YYYYMMDDHHMMSS + r"(?P\d{8}T?\d{6}Z?)", + # Epoch seconds or millis + r"(?P\d{10}|\d{13})", +) + +def _parse_started_at_from_token(token: str) -> Optional[str]: + """Return ISO8601 UTC Z string if token looks like a timestamp; else None.""" + t = token.strip() + # epoch + if re.fullmatch(r"\d{13}", t): + dt = datetime.fromtimestamp(int(t)/1000.0, tz=timezone.utc) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + if re.fullmatch(r"\d{10}", t): + dt = datetime.fromtimestamp(int(t), tz=timezone.utc) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + # compact YYYYMMDD[ T]?HHMMSS[Z]? + m = re.fullmatch(r"(\d{8})T?(\d{6})Z?", t) + if m: + d, h = m.groups() + dt = datetime.strptime(d + h, "%Y%m%d%H%M%S").replace(tzinfo=timezone.utc) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + # ISO-ish (allow missing separators) + # normalize: keep only digits and Z, then rebuild + if re.fullmatch(r"\d{4}-?\d{2}-?\d{2}[T ]?\d{2}:?\d{2}:?\d{2}Z?", t): + # try a few formats + for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%d %H:%M:%SZ", + "%Y-%m-%dT%H:%M:%S", "%Y%m%dT%H%M%SZ", "%Y%m%d%H%M%S"): + try: + if t.endswith("Z") and fmt.endswith("Z"): + dt = datetime.strptime(t.replace(" ", "T"), fmt).replace(tzinfo=timezone.utc) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + if not t.endswith("Z") and not fmt.endswith("Z"): + dt = datetime.strptime(t.replace(" ", "T").replace("-", "").replace(":", ""), "%Y%m%dT%H%M%S").replace(tzinfo=timezone.utc) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + except Exception: + pass + return None + +def _extract_device_and_started_at_from_key(s3_key: str) -> Tuple[Optional[str], Optional[str]]: + """ + Expect filenames like 'sensorId_timestamp.*' (timestamp token can be in a few formats). + Return (device_id, started_at_isoZ) or (None, None) if not confident. + """ + name = Path(s3_key).name + m = re.match(r"(?P[^_/]+)_(?P[^_.]+)", name) + if not m: + return None, None + device_id = m.group("dev").strip() + ts_token = m.group("ts").strip() + started_at = _parse_started_at_from_token(ts_token) + if not device_id or not started_at: + return None, None + return device_id, started_at + +def _load_backbone_once() -> None: + if R.model is not None: + return + if BACKBONE != "cnn14": + raise RuntimeError(f"Only BACKBONE=cnn14 is supported in this service, got {BACKBONE}") + # load_cnn14_model internally handles checkpoint/url (your impl) + R.model = load_cnn14_model(CHECKPOINT or None, device=DEVICE) + +def _load_head_once() -> None: + if R.head is not None: + return + if not HEAD_PATH: + raise RuntimeError("HEAD env var is required (path to joblib head)") + R.head = joblib.load(HEAD_PATH) + if not hasattr(R.head, "predict_proba"): + raise RuntimeError("HEAD must expose predict_proba(X) and classes_)") + + # 1) try labels from CSV if provided (most robust for production) + labels_csv = os.getenv("LABELS_CSV") or "" + if labels_csv: + from classification.core.model_io import load_labels_from_csv + labels = load_labels_from_csv(labels_csv) + if not labels: + raise RuntimeError(f"Labels CSV is empty or unreadable: {labels_csv}") + R.classes = labels + return + + # 2) else, try meta.json next to HEAD (HEAD_META env or HEAD+'.meta.json') + head_meta = os.getenv("HEAD_META") or (HEAD_PATH + ".meta.json") + labels_from_meta = [] + try: + if os.path.exists(head_meta): + import json + with open(head_meta, "r", encoding="utf-8") as f: + meta = json.load(f) + if isinstance(meta.get("class_order"), list) and len(meta["class_order"]) > 0: + labels_from_meta = [str(x) for x in meta["class_order"]] + except Exception as e: + print(f"⚠️ Warning: failed to parse HEAD meta: {e}") + + # 3) reconcile with head.classes_ + head_classes = list(getattr(R.head, "classes_", [])) + if labels_from_meta: + # if head.classes_ are [0..N-1], we map by index + if all(isinstance(c, (int, np.integer)) for c in head_classes): + if len(head_classes) != len(labels_from_meta): + raise RuntimeError( + f"Meta class_order length ({len(labels_from_meta)}) != head.classes_ length ({len(head_classes)})" + ) + R.classes = labels_from_meta + return + # else: if head.classes_ already hold real names, prefer them + R.classes = [str(c) for c in head_classes] if head_classes else labels_from_meta + return + + # 4) fallback to head.classes_ as strings + if head_classes: + R.classes = [str(c) for c in head_classes] + return + + # 5) no labels source found + raise RuntimeError( + "No labels source found. Provide LABELS_CSV, or HEAD_META with class_order, " + "or ensure the head exposes string class names via classes_." + ) + +# ----------------------------- +# Embedding/inference helpers +# ----------------------------- + +def _aggregate_probs(per_window_probs: np.ndarray) -> np.ndarray: + """ + Aggregate per-window class probabilities to a single clip-level vector. + Supports mean|max; returns 1-D float32. + """ + if per_window_probs.ndim != 2: + raise ValueError("expected shape (num_windows, num_classes)") + if per_window_probs.shape[0] == 0: + return np.zeros((per_window_probs.shape[1],), dtype=np.float32) + v = aggregate_matrix(per_window_probs, mode=AGG) + # When AGG=max, v might be logits-like — but we trained on probs, so it is already probabilities. + # If needed: apply softmax here. For a calibrated head (sklearn) it's already in [0,1]. + return v.astype(np.float32, copy=False) + +# ----------------------------- +# Public API for service +# ----------------------------- + +# Create a dedicated logger for performance metrics +perf_logger = logging.getLogger("audio_cls.perf") +perf_logger.setLevel(logging.INFO) +if not perf_logger.handlers: + h = logging.StreamHandler() + fmt = logging.Formatter("[%(asctime)s] [PERF] %(message)s", "%Y-%m-%d %H:%M:%S") + h.setFormatter(fmt) + perf_logger.addHandler(h) + +def classify_file( + path: str, + pann_model: Optional[AudioTagging] = None, + sk_pipeline: Optional[Any] = None +) -> Dict[str, object]: + t0 = time.perf_counter() + if sk_pipeline is None: + _load_head_once() + if pann_model is None: + _load_backbone_once() + + wav = np.array(load_audio(path, SAMPLE_RATE), dtype=np.float32, copy=True, order="C") + windows_2d = segment_waveform_2d_view( + wav, SAMPLE_RATE, window_sec=WINDOW_SEC, hop_sec=HOP_SEC, pad_last=PAD_LAST + ) + + num_windows = int(windows_2d.shape[0]) + if num_windows == 0: + result = { + "label": "another", + "probs": {c: 0.0 for c in R.classes}, + "pred_prob": 0.0, + "unknown_threshold": UNKNOWN_THRESHOLD, + "is_another": True, + "num_windows": 0, + "agg_mode": AGG, + "processing_ms": int((time.perf_counter() - t0) * 1000.0), + } + return result + + # Batch embeddings + if pann_model is not None: + win = np.array(windows_2d, dtype=np.float32, copy=True, order="C") + seg = pann_model.inference(win) + if isinstance(seg, dict): + seg = seg.get("embedding") + elif isinstance(seg, tuple) and len(seg) >= 2: + seg = seg[1] + seg = np.asarray(seg, dtype=np.float32) + if seg.ndim == 1: + seg = seg[None, :] + else: + win = np.array(windows_2d, dtype=np.float32, copy=True, order="C") + seg = run_cnn14_embeddings_batch(R.model, win, batch_size=32) + + # Head predict_proba + clf = sk_pipeline if sk_pipeline is not None else R.head + per_window_probs = np.asarray(clf.predict_proba(seg), dtype=np.float32) + + # Aggregate and finalize + agg_vec = _aggregate_probs(per_window_probs) + k = int(np.argmax(agg_vec)) + top_prob = float(agg_vec[k]) + top_label = R.classes[k] + final_label = top_label if top_prob >= UNKNOWN_THRESHOLD else "another" + probs = {cls: float(p) for cls, p in zip(R.classes, agg_vec)} + + processing_ms = int((time.perf_counter() - t0) * 1000.0) + + return { + "label": final_label, + "probs": probs, + "pred_prob": top_prob, + "unknown_threshold": UNKNOWN_THRESHOLD, + "is_another": (final_label == "another"), + "num_windows": num_windows, + "agg_mode": AGG, + "processing_ms": processing_ms, + } + +def run_classification_job( + *, + s3_bucket: str, + s3_key: str, + pann_model: Optional[AudioTagging] = None, + sk_pipeline: Optional[Any] = None +) -> Dict[str, object]: + """ + Download from MinIO → classify_file → (optional) Kafka alert. + Returns a dict with 'label', 'probs, and alert send status. + """ + _load_head_once() + if ALLOWED_BUCKETS and s3_bucket not in ALLOWED_BUCKETS: + raise RuntimeError(f"Bucket '{s3_bucket}' is not allowed") + + client = _get_minio() + + # stat & validate + try: + stat = client.stat_object(s3_bucket, s3_key) + except S3Error as e: + raise RuntimeError(f"S3 stat failed: {e}") from e + size = getattr(stat, "size", None) + ctype = getattr(stat, "content_type", "") or "" + if size and size > MAX_BYTES: + raise RuntimeError(f"Object too large: {size} > {MAX_BYTES}") + if ctype and ALLOWED_CONTENT_TYPES and ctype not in ALLOWED_CONTENT_TYPES: + raise RuntimeError(f"Unsupported content-type: {ctype}") + + # download to temp + suffix = Path(s3_key).suffix or ".wav" + fd, tmp_path = tempfile.mkstemp(prefix="audio_", suffix=suffix) + os.close(fd) + try: + client.fget_object(s3_bucket, s3_key, tmp_path) + result = classify_file(tmp_path, pann_model=pann_model, sk_pipeline=sk_pipeline) + # default alert flags + result.setdefault("sent_alert", False) + result.setdefault("alert_topic", None) + result.setdefault("alert_skip_reason", None) + if result.get("processing_ms") is not None: + try: + result["processing_ms"] = int(result["processing_ms"]) + except Exception: + pass + + if result["label"] != "another" and KAFKA_BROKERS and ALERTS_TOPIC: + device_id, started_at = _extract_device_and_started_at_from_key(s3_key) + if device_id and started_at: + lat = lon = area = None + try: + conn = _get_db_conn() + lat, lon, area = fetch_gis_by_device_and_time(conn, device_id, started_at) + if lat is None or lon is None: + file_name = Path(s3_key).name + fl, fn, fa = fetch_gis_by_filename(conn, file_name) + lat = lat if lat is not None else fl + lon = lon if lon is not None else fn + area = area or fa + except Exception: + pass + + try: + label = str(result["label"]) + alert_type = f"suspicious_sound-{label}" + + severity = None + sev_map_env = os.getenv("ALERT_SEVERITY_MAP", "").strip() + if sev_map_env: + try: + _sev_map = __import__("json").loads(sev_map_env) + if isinstance(_sev_map, dict) and label in _sev_map: + _s = _sev_map[label] + if isinstance(_s, (int, np.integer)): + severity = int(_s) + except Exception: + pass + + confidence = float(result.get("pred_prob") or 0.0) + + meta = { + "bucket": s3_bucket, + "key": s3_key, + "processing_ms": result.get("processing_ms"), + } + if meta["processing_ms"] is None: + meta.pop("processing_ms") + message_key = f"{device_id}|{started_at}" + alert_id = str(uuid.uuid4()) + + perf_logger.info( + "About to send structured alert: topic=%s key=%s type=%s lat=%s lon=%s area=%s", + ALERTS_TOPIC, message_key, alert_type, lat, lon, area + ) + + ok = alerts.send_structured_alert( + brokers=KAFKA_BROKERS, + topic=ALERTS_TOPIC, + alert_type=alert_type, + device_id=device_id, + started_at=started_at, + confidence=confidence, + severity=severity, + area=area, + lat=lat, + lon=lon, + meta=meta, + alert_id=alert_id, + message_key=message_key, + ) + if ok: + result["sent_alert"] = True + result["alert_topic"] = ALERTS_TOPIC + else: + perf_logger.warning("Alert send returned False (topic=%s key=%s)", ALERTS_TOPIC, s3_key) + result["alert_skip_reason"] = "kafka_produce_returned_false" + except Exception as e: + perf_logger.warning("Alert send failed: %s (key=%s)", e, s3_key) + result["alert_skip_reason"] = "kafka_exception" + else: + perf_logger.warning( + "Skip alert (missing device_id/started_at) for key=%s", s3_key + ) + result["alert_skip_reason"] = "missing_device_or_started_at" + elif result["label"] == "another": + result["alert_skip_reason"] = "label_is_another" + elif not KAFKA_BROKERS or not ALERTS_TOPIC: + result["alert_skip_reason"] = "missing_env_brokers_or_topic" + return result + finally: + try: + os.remove(tmp_path) + except Exception: + pass diff --git a/AgCloud/services/sounds_classifier/tests/conftest.py b/AgCloud/services/sounds_classifier/tests/conftest.py new file mode 100644 index 000000000..afb6a527e --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/conftest.py @@ -0,0 +1,56 @@ +import sys +import pathlib +import os +import pytest + +# 1) Ensure "src" is on sys.path so `import classification...` works +# This walks up from tests/ to repo root and prepends /src +HERE = pathlib.Path(__file__).resolve() +ROOT = HERE +for _ in range(6): # walk up a few levels just in case + if (ROOT / "src").exists(): + sys.path.insert(0, str(ROOT / "src")) + break + ROOT = ROOT.parent + +# 2) Provide minimal, isolated env defaults for tests +@pytest.fixture(autouse=True) +def _isolate_env(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): + # Core runtime defaults + monkeypatch.setenv("DEVICE", "cpu") + monkeypatch.setenv("BACKBONE", "cnn14") + + # Windowing / aggregation + monkeypatch.setenv("WINDOW_SEC", "2.0") + monkeypatch.setenv("HOP_SEC", "0.5") + monkeypatch.setenv("PAD_LAST", "true") + monkeypatch.setenv("AGG", "mean") + monkeypatch.setenv("UNKNOWN_THRESHOLD", "0.55") + + # Disable optional integrations by default + monkeypatch.setenv("KAFKA_BROKERS", "") + monkeypatch.delenv("WRITE_DB", raising=False) + monkeypatch.delenv("DB_URL", raising=False) + + # HEAD path (tests can override with real/mocked head if needed) + head_dir = tmp_path / "head" + head_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setenv("HEAD", str(head_dir / "dummy.joblib")) + # Let tests decide labels source; default to none + monkeypatch.setenv("LABELS_CSV", "") + monkeypatch.delenv("HEAD_META", raising=False) + + # MinIO defaults (won't be used unless explicitly mocked) + monkeypatch.setenv("MINIO_ENDPOINT", "minio:9000") + monkeypatch.setenv("MINIO_ACCESS_KEY", "minio") + monkeypatch.setenv("MINIO_SECRET_KEY", "minio123") + monkeypatch.setenv("MINIO_SECURE", "false") + + # Kafka alerts defaults + monkeypatch.setenv("ALERTS_TOPIC", "dev-robot-alerts") + + # Checkpoint defaults (tests typically mock loading; real file not needed) + monkeypatch.setenv("CHECKPOINT", str(tmp_path / "models" / "panns_data" / "Cnn14_mAP=0.431.pth")) + monkeypatch.delenv("CHECKPOINT_URL", raising=False) + + yield diff --git a/AgCloud/services/sounds_classifier/tests/test_alerts.py b/AgCloud/services/sounds_classifier/tests/test_alerts.py new file mode 100644 index 000000000..f2dd89850 --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_alerts.py @@ -0,0 +1,161 @@ +import json +import types +import os +import pytest + +import classification.scripts.alerts as alerts + + +# ---------- test helpers ---------- +class DummyMsg: + def __init__(self, topic, partition, offset): + self._t = topic + self._p = partition + self._o = offset + def topic(self): return self._t + def partition(self): return self._p + def offset(self): return self._o + + +class DummyProducer: + def __init__(self, *a, **kw): + self._queue = [] + self._flushed = 0 + def produce(self, *, topic, value, callback=None): + self._queue.append((topic, value)) + if callback: + callback(None, DummyMsg(topic, 0, len(self._queue) - 1)) + def poll(self, _): # api compatibility + pass + def flush(self, timeout): + self._flushed += 1 + return 0 # 0 undelivered → success + + +@pytest.fixture(autouse=True) +def _clear_cache(): + alerts._producer_cache.clear() + yield + alerts._producer_cache.clear() + + +# ---------- tests ---------- +def test_send_alert_success_and_payload(monkeypatch): + dp = DummyProducer() + monkeypatch.setattr(alerts, "Producer", lambda *_a, **_k: dp, raising=True) + + ok = alerts.send_alert( + brokers="kafka:9092", + topic="dev-robot-alerts", + label="car", + probs={"car": 0.9, "dog": 0.1}, + meta={"bucket": "b", "key": "k.wav"}, + ) + assert ok is True + # Verify one message with proper JSON payload + assert len(dp._queue) == 1 + topic, raw = dp._queue[0] + assert topic == "dev-robot-alerts" + payload = json.loads(raw.decode("utf-8")) + assert payload["label"] == "car" + assert payload["probs"]["car"] == pytest.approx(0.9) + assert isinstance(payload["ts"], int) + + +def test_producer_cache_reuse(monkeypatch): + calls = {"made": 0} + def mk(*_a, **_k): + calls["made"] += 1 + return DummyProducer() + monkeypatch.setattr(alerts, "Producer", mk, raising=True) + + # First call creates a producer + assert alerts.send_alert(brokers="kafka:9092", topic="t", label="x", probs={"x": 1.0}) + # Second call should reuse cache → no extra Producer() + assert alerts.send_alert(brokers="kafka:9092", topic="t", label="y", probs={"y": 1.0}) + assert calls["made"] == 1 + assert len(alerts._producer_cache) == 1 + + +def test_send_alert_is_non_blocking_and_returns_true(monkeypatch, caplog): + class ProducerNoFlush(DummyProducer): + def flush(self, timeout): + return 1 # would indicate undelivered if we used it, but we don't block now + + monkeypatch.setattr(alerts, "Producer", lambda *_a, **_k: ProducerNoFlush(), raising=True) + + ok = alerts.send_alert(brokers="b:9092", topic="t", label="x", probs={"x": 1.0}) + assert ok is True + assert any("Kafka delivered" in rec.message or "Kafka" in rec.message for rec in caplog.records) + + +def test_send_alert_kafka_exception_on_init(monkeypatch, caplog): + class DummyKafkaEx(alerts.KafkaException): # use the module's class + pass + def boom(*_a, **_k): + raise DummyKafkaEx("init failed") + monkeypatch.setattr(alerts, "Producer", boom, raising=True) + + ok = alerts.send_alert(brokers="bad:9092", topic="t", label="x", probs={"x": 1.0}) + assert ok is False + assert any("exception while producing" in rec.message or "Failed to initialize Kafka" in rec.message + for rec in caplog.records) + + +def test_send_alert_buffer_error(monkeypatch, caplog): + class BufErrProducer(DummyProducer): + def produce(self, *a, **k): + raise BufferError("local queue full") + monkeypatch.setattr(alerts, "Producer", lambda *_a, **_k: BufErrProducer(), raising=True) + + ok = alerts.send_alert(brokers="b:9092", topic="t", label="x", probs={"x": 1.0}) + assert ok is False + assert any("local queue full" in rec.message for rec in caplog.records) + + +def test_send_alert_runtime_error(monkeypatch, caplog): + class BoomProducer(DummyProducer): + def produce(self, *a, **k): + raise RuntimeError("produce exploded") + monkeypatch.setattr(alerts, "Producer", lambda *_a, **_k: BoomProducer(), raising=True) + + ok = alerts.send_alert(brokers="b:9092", topic="t", label="x", probs={"x": 1.0}) + assert ok is False + assert any("produce error" in rec.message for rec in caplog.records) + + +def test_legacy_send_kafka_alert_reads_env(monkeypatch): + # Check that env is read and forwarded into send_alert + captured = {} + def fake_send_alert(*, brokers, topic, label, probs, meta): + captured["brokers"] = brokers + captured["topic"] = topic + captured["label"] = label + captured["probs"] = probs + captured["meta"] = meta + return True + + monkeypatch.setenv("KAFKA_BROKERS", "env-b:9092") + monkeypatch.setenv("ALERTS_TOPIC", "env-topic") + monkeypatch.setattr(alerts, "send_alert", fake_send_alert, raising=True) + + ok = alerts.send_kafka_alert(file_path="/tmp/a.wav", label="car", prob=0.7) + assert ok is True + assert captured["brokers"] == "env-b:9092" + assert captured["topic"] == "env-topic" + assert captured["label"] == "car" + assert captured["probs"] == {"car": 0.7} + assert captured["meta"]["file_path"] == "/tmp/a.wav" + + +def test__delivery_report_logs_ok_and_err(caplog): + # OK case + alerts._delivery_report(None, DummyMsg("t", 0, 1)) + # Error case + class Err: + def __str__(self): return "boom" + alerts._delivery_report(Err(), DummyMsg("t", 0, 2)) + + # We don't assert exact wording, just that both paths logged + assert any("Kafka delivered" in r.message for r in caplog.records) + assert any("Kafka delivery failed" in r.message for r in caplog.records) diff --git a/AgCloud/services/sounds_classifier/tests/test_app.py b/AgCloud/services/sounds_classifier/tests/test_app.py new file mode 100644 index 000000000..6056cf4ef --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_app.py @@ -0,0 +1,215 @@ +from fastapi.testclient import TestClient +import classification.app as app_mod + +# ----------------------------- +# Helpers / Fakes +# ----------------------------- +class DummyAT: + """Lightweight fake for AudioTagging to let startup warm-up pass.""" + def __init__(self, *a, **k): + pass + def inference(self, x): + # Return something with "embedding" to match warm-up expectations + return {"embedding": [0.0, 0.0]} + +class DummyConn: + """Fake psycopg2 connection used by open_db().""" + def __init__(self): + self.closed = False + self._executed = [] + def cursor(self): + class C: + def __init__(self, outer): + self.outer = outer + def __enter__(self): + return self + def __exit__(self, *a): + return False + def execute(self, *a, **k): + self.outer._executed.append(("EXEC", a, k)) + return C(self) + def commit(self): + self._executed.append(("COMMIT", None, None)) + def rollback(self): + self._executed.append(("ROLLBACK", None, None)) + def close(self): + self.closed = True + +def _patch_startup_and_db(monkeypatch, *, capture): + """ + Patch all heavy/IO dependencies used by app startup and endpoints. + 'capture' is a dict used to collect calls for assertions. + """ + # Avoid heavy model loading + monkeypatch.setattr(app_mod, "AudioTagging", DummyAT, raising=True) + # Pretend pipeline file does not exist -> skip joblib.load + monkeypatch.setattr(app_mod.os.path, "exists", lambda p: False, raising=True) + + # open_db returns DummyConn and we keep the instance in capture + def _open_db(): + conn = DummyConn() + capture["conn"] = conn + return conn + monkeypatch.setattr(app_mod, "open_db", _open_db, raising=True) + + # No-op ensure_run + monkeypatch.setattr(app_mod, "ensure_run", lambda conn, run_id: None, raising=True) + + # Safe defaults for resolve/upsert; specific tests will override if needed + monkeypatch.setattr(app_mod, "resolve_file_id", lambda *a, **k: 123, raising=True) + + def _upsert(conn, payload): + capture.setdefault("upserts", []).append({"conn": conn, "payload": payload}) + monkeypatch.setattr(app_mod, "upsert_file_aggregate", _upsert, raising=True) + + +# ----------------------------- +# Tests +# ----------------------------- +def test_health_ok(monkeypatch): + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + # Use context manager so startup/shutdown definitely run + with TestClient(app_mod.app) as client: + r = client.get("/health") + assert r.status_code == 200 + data = r.json() + assert isinstance(data, dict) + assert data.get("ok") is True + # Assert values that reflect startup side effects + assert data.get("pann_loaded") is True + assert data.get("sk_pipeline_loaded") in (True, False) + # DB connection was created + assert cap.get("conn") is not None + +def test_startup_and_shutdown_close_db(monkeypatch): + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + # Run inside context to trigger startup+shutdown + with TestClient(app_mod.app) as client: + r = client.get("/health") + assert r.status_code == 200 + # During runtime, connection object is set + assert cap.get("conn") is not None + assert cap["conn"].closed is False + # After shutdown hook ran, global DB_CONN should be None and fake conn closed + assert app_mod.DB_CONN is None + assert cap["conn"].closed is True + +def test_classify_200_success(monkeypatch): + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + + # resolve_file_id -> a fixed file_id + monkeypatch.setattr(app_mod, "resolve_file_id", lambda *a, **k: 42, raising=True) + + # classification core returns a rich dict (with extra fields) + import classification.scripts.classify as cls + monkeypatch.setattr( + cls, + "run_classification_job", + lambda **k: { + "label": "car", + "probs": {"car": 0.9, "dog": 0.1}, + "pred_prob": 0.9, + "unknown_threshold": 0.55, + "is_another": False, + "num_windows": 5, + "agg_mode": "mean", + "processing_ms": 123.0, + }, + raising=True + ) + + with TestClient(app_mod.app) as client: + r = client.post("/classify", json={"s3_bucket": "ok", "s3_key": "file.wav", "return_porbs": True}) + assert r.status_code == 200 + body = r.json() + assert body["label"] == "car" + # default return_probs is False -> probs stripped + assert body["probs"] == {"car": 0.9, "dog": 0.1} + + # Verify upsert called with our collected payload + assert len(cap.get("upserts", [])) == 1 + payload = cap["upserts"][0]["payload"] + assert payload["file_id"] == 42 + assert payload["head_pred_label"] == "car" + assert payload["num_windows"] == 5 + assert payload["agg_mode"] == "mean" + assert payload["processing_ms"] == 123.0 + +def test_classify_200_with_return_probs_true(monkeypatch): + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + monkeypatch.setattr(app_mod, "resolve_file_id", lambda *a, **k: 88, raising=True) + + import classification.scripts.classify as cls + monkeypatch.setattr( + cls, + "run_classification_job", + lambda **k: {"label": "dog", "probs": {"car": 0.2, "dog": 0.8}}, + raising=True + ) + + with TestClient(app_mod.app) as client: + r = client.post("/classify", json={"s3_bucket": "b", "s3_key": "key.wav", "return_probs": True}) + assert r.status_code == 200 + body = r.json() + assert body["label"] == "dog" + assert body["probs"] == {"car": 0.2, "dog": 0.8} + # ensure upsert invoked + assert len(cap.get("upserts", [])) == 1 + assert cap["upserts"][0]["payload"]["file_id"] == 88 + +def test_classify_404_when_file_missing(monkeypatch): + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + # Simulate resolve_file_id failure (file not in public.files) + def _resolve(*a, **k): + raise ValueError("not found") + monkeypatch.setattr(app_mod, "resolve_file_id", _resolve, raising=True) + + with TestClient(app_mod.app) as client: + r = client.post("/classify", json={"s3_bucket": "b", "s3_key": "missing.wav"}) + assert r.status_code == 404 + # No upsert on failure + assert cap.get("upserts", []) == [] + +def test_classify_500_when_core_raises(monkeypatch): + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + monkeypatch.setattr(app_mod, "resolve_file_id", lambda *a, **k: 55, raising=True) + + import classification.scripts.classify as cls + def _raiser(**k): + raise RuntimeError("boom") + monkeypatch.setattr(cls, "run_classification_job", _raiser, raising=True) + + with TestClient(app_mod.app) as client: + r = client.post("/classify", json={"s3_bucket": "b", "s3_key": "crash.wav"}) + assert r.status_code == 500 + # No upsert on failure + assert cap.get("upserts", []) == [] + +def test_middleware_executes(monkeypatch): + """ + Hitting endpoints implicitly passes through the timing middleware. + This test mainly ensures middleware path executes without errors + (coverage); assertions are on status only. + """ + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + # First pass: just exercise /health + with TestClient(app_mod.app) as client: + r1 = client.get("/health") + assert r1.status_code == 200 + + # Second pass: exercise /classify with a minimal classifier mock + cap = {} + _patch_startup_and_db(monkeypatch, capture=cap) + import classification.scripts.classify as cls + monkeypatch.setattr(cls, "run_classification_job", + lambda **k: {"label": "ok", "probs": {}}, raising=True) + with TestClient(app_mod.app) as client: + r = client.post("/classify", json={"s3_bucket": "b", "s3_key": "k.wav"}) + assert r.status_code == 200 diff --git a/AgCloud/services/sounds_classifier/tests/test_classify_core.py b/AgCloud/services/sounds_classifier/tests/test_classify_core.py new file mode 100644 index 000000000..8c9423af3 --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_classify_core.py @@ -0,0 +1,262 @@ +import numpy as np +from pathlib import Path +import classification.scripts.classify as c + + +# ---- Helpers ---- +class DummyHead: + def __init__(self, classes): + self.classes_ = classes + self._probas = None + def set_out(self, arr): + self._probas = arr + def predict_proba(self, X): + n = X.shape[0] + return np.tile(self._probas, (n, 1)) + +def _reset_runtime(): + c.R = c._Runtime() + +def _to_dict_result(res): + """Normalize classifier outputs to a dict {label, probs} for tests.""" + if isinstance(res, tuple): + label, probs = res + return {"label": label, "probs": probs} + return res + + +# ---- Core classification flow & validations ---- +def test_classify_file_unknown_threshold(monkeypatch): + _reset_runtime() + c.R.model = object() + head = DummyHead(classes=["car", "dog"]) + head.set_out(np.array([0.51, 0.49], dtype=np.float32)) + c.R.head = head + c.R.classes = head.classes_ + + monkeypatch.setattr(c, "load_audio", lambda p, sr: np.ones(16000, dtype=np.float32), raising=True) + # one window: shape (1, 16000) + monkeypatch.setattr(c, "segment_waveform_2d_view", + lambda *a, **k: np.ones((1, 16000), dtype=np.float32), + raising=True) + monkeypatch.setattr( + c, + "run_cnn14_embeddings_batch", + lambda _model, windows, batch_size=32: np.tile(np.array([[1, 2, 3, 4]], dtype=np.float32), (windows.shape[0], 1)), + raising=True, + ) + result = _to_dict_result(c.classify_file("dummy.wav")) + assert result["label"] in ("car", "another") + assert set(result["probs"].keys()) == {"car", "dog"} + + +def test__aggregate_probs_rejects_bad_ndim(): + x = np.array([0.1, 0.9], dtype=np.float32) # 1-D + try: + c._aggregate_probs(x) + assert False, "Expected ValueError for 1-D array" + except ValueError as e: + assert "expected shape" in str(e) + + +def test__aggregate_probs_empty_windows_returns_zeros(): + x = np.zeros((0, 3), dtype=np.float32) + out = c._aggregate_probs(x) + assert out.shape == (3,) + assert np.allclose(out, 0.0) + + +def test_classify_file_returns_another_when_no_segments(monkeypatch): + _reset_runtime() + c.R.model = object() + + class Head: + def __init__(self): self.classes_ = ["car", "dog"] + def predict_proba(self, X): return np.zeros((X.shape[0], 2), dtype=np.float32) + + c.R.head = Head() + c.R.classes = ["car", "dog"] + + monkeypatch.setattr(c, "load_audio", lambda p, sr: np.ones(16000, dtype=np.float32), raising=True) + # zero windows: shape (0, 16000) + monkeypatch.setattr(c, "segment_waveform_2d_view", + lambda *a, **k: np.zeros((0, 16000), dtype=np.float32), + raising=True) + + result = _to_dict_result(c.classify_file("dummy.wav")) + assert result["label"] == "another" + assert set(result["probs"].keys()) == {"car", "dog"} + + +def test_classify_file_with_agg_max(monkeypatch): + _reset_runtime() + old_agg = c.AGG + c.AGG = "max" + try: + c.R.model = object() + + class Head: + def __init__(self): self.classes_ = ["a", "b"] + def predict_proba(self, X): + # pretend we got two windows → choose element-wise max + return np.array([[0.2, 0.8], [0.6, 0.4]], dtype=np.float32) + + c.R.head = Head() + c.R.classes = ["a", "b"] + + monkeypatch.setattr(c, "load_audio", lambda p, sr: np.ones(2 * 16000, dtype=np.float32), raising=True) + # two windows: shape (2, 16000) + monkeypatch.setattr(c, "segment_waveform_2d_view", + lambda *a, **k: np.ones((2, 16000), dtype=np.float32), + raising=True) + monkeypatch.setattr( + c, + "run_cnn14_embeddings_batch", + lambda _model, windows, batch_size=32: np.tile(np.array([[1, 2, 3, 4]], dtype=np.float32), (windows.shape[0], 1)), + raising=True, + ) + result = _to_dict_result(c.classify_file("x.wav")) + assert set(result["probs"].keys()) == {"a", "b"} + assert np.isclose(result["probs"]["a"], 0.6) or np.isclose(result["probs"]["b"], 0.8) + finally: + c.AGG = old_agg + + +def test_run_classification_job_happy_path(monkeypatch, tmp_path): + _reset_runtime() + + class Stat: size = 10; content_type = "audio/wav" + class Client: + def stat_object(self, b, k): return Stat() + def fget_object(self, b, k, dst): Path(dst).write_bytes(b"RIFF") + + monkeypatch.setattr(c, "_get_minio", lambda: Client(), raising=True) + + c.KAFKA_BROKERS, c.ALERTS_TOPIC = "", "dev-robot-alerts" + monkeypatch.setattr(c.alerts, "send_alert", lambda **kw: None, raising=True) + + c.R.model = object() + head = DummyHead(classes=["car", "dog"]); head.set_out(np.array([0.6, 0.4], dtype=np.float32)) + c.R.head = head; c.R.classes = head.classes_ + + monkeypatch.setattr(c, "load_audio", lambda p, sr: np.ones(16000, dtype=np.float32), raising=True) + monkeypatch.setattr(c, "segment_waveform_2d_view", + lambda *a, **k: np.ones((1, 16000), dtype=np.float32), + raising=True) + monkeypatch.setattr( + c, + "run_cnn14_embeddings_batch", + lambda _model, windows, batch_size=32: np.tile(np.array([[1, 2, 3, 4]], dtype=np.float32), (windows.shape[0], 1)), + raising=True, + ) + + out = _to_dict_result(c.run_classification_job(s3_bucket="b", s3_key="k.wav")) + assert out["label"] in ("car", "another") + assert "probs" in out and isinstance(out["probs"], dict) + + +def test_run_classification_job_bucket_not_allowed(): + _reset_runtime() + c.R.head = object() + old = c.ALLOWED_BUCKETS + c.ALLOWED_BUCKETS = ["only-this-bucket"] + try: + try: + _ = c.run_classification_job(s3_bucket="not-allowed", s3_key="a.wav") + assert False, "Expected RuntimeError for disallowed bucket" + except RuntimeError as e: + assert "not allowed" in str(e) + finally: + c.ALLOWED_BUCKETS = old + + +def test_run_classification_job_rejects_content_type(monkeypatch): + _reset_runtime() + c.R.head = object() + c.ALLOWED_BUCKETS = [] + old_types = c.ALLOWED_CONTENT_TYPES + c.ALLOWED_CONTENT_TYPES = ["audio/wav"] + + class Stat: size = 1024; content_type = "text/plain" + class Client: + def stat_object(self, b, k): return Stat() + def fget_object(self, b, k, dst): Path(dst).write_bytes(b"RIFF") + + monkeypatch.setattr(c, "_get_minio", lambda: Client(), raising=True) + + try: + _ = c.run_classification_job(s3_bucket="ok", s3_key="a.wav") + assert False, "Expected RuntimeError for unsupported content-type" + except RuntimeError as e: + assert "Unsupported content-type" in str(e) + finally: + c.ALLOWED_CONTENT_TYPES = old_types + + +def test_run_classification_job_rejects_size(monkeypatch): + _reset_runtime() + c.R.head = object() + c.ALLOWED_BUCKETS = [] + old_max = c.MAX_BYTES; c.MAX_BYTES = 10 + + class Stat: size = 11; content_type = "audio/wav" + class Client: + def stat_object(self, b, k): return Stat() + def fget_object(self, b, k, dst): Path(dst).write_bytes(b"RIFF") + + monkeypatch.setattr(c, "_get_minio", lambda: Client(), raising=True) + + try: + _ = c.run_classification_job(s3_bucket="ok", s3_key="a.wav") + assert False, "Expected RuntimeError for object too large" + except RuntimeError as e: + assert "Object too large" in str(e) + finally: + c.MAX_BYTES = old_max + + +def test_run_classification_job_s3error_fails_fast(monkeypatch): + _reset_runtime() + c.R.head = object() + c.ALLOWED_BUCKETS = [] + + class S3Err(Exception): pass + monkeypatch.setattr(c, "S3Error", S3Err, raising=True) + + class Client: + def stat_object(self, b, k): raise S3Err("boom") + def fget_object(self, b, k, dst): raise AssertionError("should not be called") + + monkeypatch.setattr(c, "_get_minio", lambda: Client(), raising=True) + + try: + _ = c.run_classification_job(s3_bucket="ok", s3_key="a.wav") + assert False, "Expected RuntimeError wrapping S3 failure" + except RuntimeError as e: + assert "S3 stat failed" in str(e) + + +def test_run_classification_job_adds_wav_suffix_when_missing(monkeypatch): + _reset_runtime() + c.R.head = object() + c.R.classes = ["car", "dog"] + c.ALLOWED_BUCKETS = [] + + class Stat: size = 100; content_type = "audio/wav" + observed = {"ext": None} + + class Client: + def stat_object(self, b, k): return Stat() + def fget_object(self, b, k, dst): + observed["ext"] = Path(dst).suffix + Path(dst).write_bytes(b"RIFF") + + monkeypatch.setattr(c, "_get_minio", lambda: Client(), raising=True) + monkeypatch.setattr(c, "classify_file", + lambda p, **_: {"label": "another", "probs": {"car": 0.0, "dog": 0.0}}, + raising=True) + monkeypatch.setattr(c.os, "remove", lambda p: None, raising=True) + + out = _to_dict_result(c.run_classification_job(s3_bucket="ok", s3_key="noext")) + assert out["label"] in ("another", "car", "dog") + assert observed["ext"] == ".wav" diff --git a/AgCloud/services/sounds_classifier/tests/test_classify_head_loading.py b/AgCloud/services/sounds_classifier/tests/test_classify_head_loading.py new file mode 100644 index 000000000..60064879c --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_classify_head_loading.py @@ -0,0 +1,142 @@ +import json +import numpy as np +import classification.scripts.classify as c + + +def _reset_runtime(): + c.R = c._Runtime() + + +def test__load_backbone_once_calls_loader(monkeypatch): + _reset_runtime() + old = c.BACKBONE + try: + c.BACKBONE = "cnn14" + called = {"ok": False} + def fake_loader(checkpoint_path=None, device="cpu", checkpoint_url=None): + called["ok"] = True + return object() + monkeypatch.setattr(c, "load_cnn14_model", fake_loader, raising=True) + c._load_backbone_once() + assert called["ok"] is True and c.R.model is not None + # Second call should be a no-op + called["ok"] = False + c._load_backbone_once() + assert called["ok"] is False + finally: + c.BACKBONE = old + + +def test__load_backbone_once_rejects_non_cnn14(): + _reset_runtime() + old = c.BACKBONE + try: + c.BACKBONE = "something-else" + try: + c._load_backbone_once() + assert False, "Expected RuntimeError for unsupported backbone" + except RuntimeError as e: + assert "Only BACKBONE=cnn14" in str(e) + finally: + c.BACKBONE = old + + +def test__load_head_once_requires_head(): + _reset_runtime() + old = c.HEAD_PATH + try: + c.HEAD_PATH = "" + try: + c._load_head_once() + assert False, "Expected RuntimeError when HEAD env/path is missing" + except RuntimeError as e: + assert "HEAD env var is required" in str(e) + finally: + c.HEAD_PATH = old + + +def test__load_head_once_uses_labels_csv(monkeypatch): + _reset_runtime() + + class DummyHead: + def __init__(self): + self.classes_ = ["x", "y"] + def predict_proba(self, X): + return np.zeros((X.shape[0], 2), dtype=np.float32) + + c.HEAD_PATH = "dummy.joblib" + monkeypatch.setattr(c.joblib, "load", lambda _: DummyHead(), raising=True) + + monkeypatch.setenv("LABELS_CSV", "labels.csv") + import classification.core.model_io as mio + # Important: raising=False because the attribute doesn't exist in the real module + monkeypatch.setattr(mio, "load_labels_from_csv", lambda _: ["car", "dog"], raising=False) + + c._load_head_once() + assert c.R.head is not None + assert c.R.classes == ["car", "dog"] + monkeypatch.delenv("LABELS_CSV", raising=False) + + +def test__load_head_once_fallback_to_head_classes(monkeypatch): + _reset_runtime() + + class DummyHead: + def __init__(self): + self.classes_ = ["cat", "plane"] + def predict_proba(self, X): + return np.zeros((X.shape[0], 2), dtype=np.float32) + + c.HEAD_PATH = "dummy.joblib" + monkeypatch.setattr(c.joblib, "load", lambda _: DummyHead(), raising=True) + monkeypatch.delenv("LABELS_CSV", raising=False) + monkeypatch.setenv("HEAD_META", "does_not_exist.json") + + c._load_head_once() + assert c.R.head is not None + assert c.R.classes == ["cat", "plane"] + + +def test__load_head_once_uses_meta_with_indexed_classes(monkeypatch, tmp_path): + _reset_runtime() + + class DummyHeadInt: + def __init__(self): + self.classes_ = [0, 1] + def predict_proba(self, X): + return np.zeros((X.shape[0], 2), dtype=np.float32) + + c.HEAD_PATH = "dummy.joblib" + monkeypatch.setattr(c.joblib, "load", lambda _: DummyHeadInt(), raising=True) + + meta_path = tmp_path / "head_meta.json" + meta_path.write_text(json.dumps({"class_order": ["engine", "bird"]}), encoding="utf-8") + monkeypatch.setenv("HEAD_META", str(meta_path)) + monkeypatch.delenv("LABELS_CSV", raising=False) + + c._load_head_once() + assert c.R.classes == ["engine", "bird"] + + +def test__load_head_once_meta_length_mismatch_raises(monkeypatch, tmp_path): + _reset_runtime() + + class DummyHeadInt: + def __init__(self): + self.classes_ = [0, 1] + def predict_proba(self, X): + return np.zeros((X.shape[0], 2), dtype=np.float32) + + c.HEAD_PATH = "dummy.joblib" + monkeypatch.setattr(c.joblib, "load", lambda _: DummyHeadInt(), raising=True) + + meta_path = tmp_path / "bad_meta.json" + meta_path.write_text(json.dumps({"class_order": ["only-one"]}), encoding="utf-8") + monkeypatch.setenv("HEAD_META", str(meta_path)) + monkeypatch.delenv("LABELS_CSV", raising=False) + + try: + c._load_head_once() + assert False, "Expected RuntimeError for meta length mismatch" + except RuntimeError as e: + assert "class_order length" in str(e) diff --git a/AgCloud/services/sounds_classifier/tests/test_cnn14.py b/AgCloud/services/sounds_classifier/tests/test_cnn14.py new file mode 100644 index 000000000..44330e533 --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_cnn14.py @@ -0,0 +1,111 @@ +import numpy as np +import types +import builtins +import pytest +import classification.backbones.cnn14 as cnn14 +from classification.backbones.cnn14 import run_cnn14_embeddings_batch +from classification.core.model_io import segment_waveform_2d_view, SAMPLE_RATE + +class DummyAT: + def __init__(self, checkpoint_path: str, device: str = "cpu"): + self.checkpoint_path = checkpoint_path + self.device = device + def inference(self, wav): + # Return (dummy_probs, dummy_embedding) + emb = np.ones((1, 2048), dtype=np.float32) + return (None, emb) + +class DummyATTuple: + def inference(self, x): + # x: (N, samples) + N = x.shape[0] + emb_dim = 8 + scores = np.zeros((N, 10), dtype=np.float32) # unused + embs = np.arange(N * emb_dim, dtype=np.float32).reshape(N, emb_dim) + return (scores, embs) + +class DummyATDict: + def inference(self, x): + N = x.shape[0] + emb_dim = 16 + return {"embedding": np.ones((N, emb_dim), dtype=np.float32)} + +def test_load_cnn14_model_uses_ensure_checkpoint(monkeypatch, tmp_path): + ckpt = tmp_path / "m.pth" + def fake_ensure(path, url): + ckpt.write_bytes(b"dummy") + return str(ckpt) + monkeypatch.setattr(cnn14, "ensure_checkpoint", fake_ensure) + # Mock panns_inference.AudioTagging + monkeypatch.setattr(cnn14, "AudioTagging", DummyAT, raising=True) + + m = cnn14.load_cnn14_model(checkpoint_path=str(ckpt), device="cpu") + assert isinstance(m, DummyAT) + assert m.checkpoint_path == str(ckpt) + +def test_run_cnn14_embedding_happy_path(monkeypatch): + dummy = DummyAT("x") + x = np.random.randn(32000).astype(np.float32) + e = cnn14.run_cnn14_embedding(dummy, x) + assert e.shape == (2048,) + +def test_run_cnn14_embedding_empty_raises(monkeypatch): + dummy = DummyAT("x") + with pytest.raises(ValueError): + cnn14.run_cnn14_embedding(dummy, np.array([], dtype=np.float32)) + + +def test_run_cnn14_embeddings_batch_tuple_output(monkeypatch): + model = DummyATTuple() + # Prevent flattening of (N, D) to (N*D,) + monkeypatch.setattr(cnn14, "_to_numpy", + lambda x: np.asarray(x, dtype=np.float32), + raising=True) + + windows = np.ones((5, 32000), dtype=np.float32) + E = run_cnn14_embeddings_batch(model, windows, batch_size=2) + assert E.shape == (5, 8) + assert np.allclose(E[1], np.arange(8, 16, dtype=np.float32)) + +def test_run_cnn14_embeddings_batch_dict_output(monkeypatch): + model = DummyATDict() + # Prevent flattening of (N, D) to (N*D,) + monkeypatch.setattr(cnn14, "_to_numpy", + lambda x: np.asarray(x, dtype=np.float32), + raising=True) + windows = np.ones((3, 16000), dtype=np.float32) + E = run_cnn14_embeddings_batch(model, windows, batch_size=32) + assert E.shape == (3, 16) + assert np.allclose(E, 1.0) + +def test_segment_waveform_2d_basic_exact_fit(): + sr = SAMPLE_RATE + win_s = 1.0 + hop_s = 0.75 + wav = np.ones(int(sr * 1.6), dtype=np.float32) + segs = segment_waveform_2d_view(wav, sr=sr, window_sec=win_s, hop_sec=hop_s, pad_last=False) + # Expect exactly one full window: [0..1.0] + assert segs.shape[0] == 1 + assert segs.shape[1] == int(sr * win_s) + # No padding is expected when pad_last=False + +def test_segment_waveform_2d_pad_last(): + sr = SAMPLE_RATE + win_s = 1.0 + hop_s = 0.75 + wav = np.ones(int(sr * 1.6), dtype=np.float32) + segs = segment_waveform_2d_view(wav, sr=sr, window_sec=win_s, hop_sec=hop_s, pad_last=True) + # Expect one full window + one padded tail window → total 2 + assert segs.shape[0] == 2 + assert segs.shape[1] == int(sr * win_s) + assert np.any(segs[-1] == 0.0) + +def test_segment_waveform_2d_empty_input_returns_empty_or_padded(): + sr = SAMPLE_RATE + segs_no_pad = segment_waveform_2d_view(np.zeros(0, dtype=np.float32), sr=sr, + window_sec=1.0, hop_sec=0.5, pad_last=False) + assert segs_no_pad.shape == (0, int(sr * 1.0)) + segs_pad = segment_waveform_2d_view(np.zeros(0, dtype=np.float32), sr=sr, + window_sec=1.0, hop_sec=0.5, pad_last=True) + # Current impl returns 0 windows even with pad_last for empty input + assert segs_pad.shape == (0, int(sr * 1.0)) diff --git a/AgCloud/services/sounds_classifier/tests/test_db_io_pg.py b/AgCloud/services/sounds_classifier/tests/test_db_io_pg.py new file mode 100644 index 000000000..235f5a5e5 --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_db_io_pg.py @@ -0,0 +1,167 @@ +import json +import types +import pytest +import classification.core.db_io_pg as dbpg + + +# ------------------------- +# Dummy connection helpers +# ------------------------- + +class DummyCursor: + def __init__(self, rec=None, raise_on_execute=False): + self.queries = [] + self._rec = rec + self._raise = raise_on_execute + def execute(self, q, p=None): + if self._raise: + raise RuntimeError("boom-exec") + self.queries.append((q, p)) + def fetchone(self): + return (123,) + def __enter__(self): + return self + def __exit__(self, *a): + return False + +class DummyConn: + def __init__(self, raise_on_execute=False): + self.cursors = [] + self.autocommit = False + self._commits = 0 + self._rollbacks = 0 + self._raise = raise_on_execute + def cursor(self): + c = DummyCursor(raise_on_execute=self._raise) + self.cursors.append(c) + return c + def commit(self): + self._commits += 1 + def rollback(self): + self._rollbacks += 1 + + +# ------------------------- +# open_db +# ------------------------- + +def test_open_db_validates_and_initializes_schema(monkeypatch): + # make psycopg2.connect return our dummy connection + monkeypatch.setattr(dbpg.psycopg2, "connect", lambda url: DummyConn()) + conn = dbpg.open_db("postgresql://u:p@h:5432/db", schema="audio_cls") + assert isinstance(conn, DummyConn) + # schema init should have committed once + assert conn._commits >= 1 + +def test_open_db_rejects_bad_schema(): + with pytest.raises(ValueError): + dbpg.open_db("postgresql://u:p@h:5432/db", schema="bad-dash") + +def test_open_db_rollback_on_failure(monkeypatch): + # first cursor.execute will raise + monkeypatch.setattr(dbpg.psycopg2, "connect", lambda url: DummyConn(raise_on_execute=True)) + with pytest.raises(RuntimeError): + dbpg.open_db("postgresql://u:p@h:5432/db", schema="audio_cls") + + +# ------------------------- +# upsert_run +# ------------------------- + +def test_upsert_run_success(monkeypatch): + conn = DummyConn() + dbpg.upsert_run(conn, { + "run_id": "r1", "model_name": "CNN14", "checkpoint": "ckpt", + "head_path": "h", "labels_csv": "l", "window_sec": 2.0, "hop_sec": 0.5, + "pad_last": True, "agg": "mean", "topk": 10, "device": "cpu", + "code_version": "v", "notes": "n" + }) + assert conn._commits == 1 + assert conn._rollbacks == 0 + # Ensure at least one execute has been issued + assert conn.cursors and conn.cursors[0].queries + +def test_upsert_run_rollback_on_exception(monkeypatch): + conn = DummyConn(raise_on_execute=True) + with pytest.raises(RuntimeError): + dbpg.upsert_run(conn, { + "run_id":"r1","model_name":"CNN14","checkpoint":"ckpt","head_path":"h","labels_csv":"l", + "window_sec":2.0,"hop_sec":0.5,"pad_last":True,"agg":"mean","topk":10,"device":"cpu", + "code_version":"v","notes":"n" + }) + assert conn._rollbacks == 1 + assert conn._commits == 0 + + +# ------------------------- +# finish_run +# ------------------------- + +def test_finish_run_success(): + conn = DummyConn() + dbpg.finish_run(conn, "r1") + assert conn._commits == 1 + assert conn._rollbacks == 0 + assert conn.cursors[0].queries # UPDATE executed + +def test_finish_run_rollback_on_exception(): + conn = DummyConn(raise_on_execute=True) + with pytest.raises(RuntimeError): + dbpg.finish_run(conn, "r1") + assert conn._rollbacks == 1 + assert conn._commits == 0 + +def test__jsonify_variants(monkeypatch): + # Wrap psycopg2.extras.Json to observe value passed in + captured = {"value": None} + def fake_json(v): + captured["value"] = v + return ("JsonWrapped", v) + + monkeypatch.setattr(dbpg.psycopg2.extras, "Json", fake_json, raising=True) + + # string with valid JSON → parsed dict + j = dbpg._jsonify('{"a":1}') + assert j == ("JsonWrapped", {"a": 1}) + assert captured["value"] == {"a": 1} + + # plain string → {"raw": "..."} + j2 = dbpg._jsonify("hello") + assert j2 == ("JsonWrapped", {"raw": "hello"}) + assert captured["value"] == {"raw": "hello"} + + # dict passes through + j3 = dbpg._jsonify({"k": 3}) + assert j3 == ("JsonWrapped", {"k": 3}) + assert captured["value"] == {"k": 3} + +def test_upsert_file_aggregate_success(monkeypatch): + # Make Json a pass-through so psycopg2.extras.Json(v) -> v + monkeypatch.setattr(dbpg.psycopg2.extras, "Json", lambda x: x, raising=True) + + conn = DummyConn() + dbpg.upsert_file_aggregate(conn, { + "run_id":"r1","file_id":123, + "head_probs_json":{"car":0.9},"head_pred_label":"car","head_pred_prob":0.9, + "head_unknown_threshold":0.55,"head_is_another":False, + "num_windows":3,"agg_mode":"mean","processing_ms":123 + }) + assert conn._commits == 1 + assert conn._rollbacks == 0 + +def test_upsert_file_aggregate_accepts_string_json_and_rollback_on_exception(monkeypatch): + # Json wrapper + monkeypatch.setattr(dbpg.psycopg2.extras, "Json", lambda x: x, raising=True) + + # connection that will fail during execute + conn = DummyConn(raise_on_execute=True) + with pytest.raises(RuntimeError): + dbpg.upsert_file_aggregate(conn, { + "run_id":"r1","file_id":123, + "head_probs_json":'{"car":0.9}', # string json + "head_pred_label":"car","head_pred_prob":0.9, + "head_unknown_threshold":0.55,"head_is_another":False, + "num_windows":3,"agg_mode":"mean","processing_ms":123 + }) + assert conn._rollbacks == 1 + assert conn._commits == 0 diff --git a/AgCloud/services/sounds_classifier/tests/test_db_utils.py b/AgCloud/services/sounds_classifier/tests/test_db_utils.py new file mode 100644 index 000000000..bbe6e8f43 --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_db_utils.py @@ -0,0 +1,165 @@ +import os +import re +import pytest + +import classification.core.db_utils as dbu + +# ----------------------------- +# Fake psycopg2 connection/cursor +# ----------------------------- +class FakeCursor: + def __init__(self, script_recorder): + self.script_recorder = script_recorder + self._fetchone = None # single value returned by fetchone() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + def execute(self, query, params=None): + # Record statements and params for later assertions + self.script_recorder.append(("EXEC", str(query), params)) + + def fetchone(self): + return self._fetchone + + # helper for tests to set what fetchone should return + def set_fetchone(self, value): + self._fetchone = value + + +class FakeConn: + def __init__(self): + self.autocommit = False + self.closed = False + self._script = [] + self._cursor = FakeCursor(self._script) + + def cursor(self): + return self._cursor + + def commit(self): + self._script.append(("COMMIT", None, None)) + + def rollback(self): + self._script.append(("ROLLBACK", None, None)) + + def close(self): + self.closed = True + + # test helper + def script(self): + return list(self._script) + + +# ----------------------------- +# Fixture +# ----------------------------- +@pytest.fixture +def fake_conn(monkeypatch): + """ + Patch psycopg2.connect to return our FakeConn. + Also ensure env vars exist with harmless defaults. + """ + fc = FakeConn() + + def fake_connect(**kwargs): + return fc + + # Patch psycopg2.connect inside db_utils module + monkeypatch.setattr(dbu.psycopg2, "connect", fake_connect) + + # Minimal env for db_utils.open_db() + monkeypatch.setenv("DB_HOST", "postgres") + monkeypatch.setenv("DB_PORT", "5432") + monkeypatch.setenv("DB_NAME", "missions_db") + monkeypatch.setenv("DB_USER", "missions_user") + monkeypatch.setenv("DB_PASSWORD", "pg123") + monkeypatch.setenv("DB_SCHEMA", "agcloud_audio") + + return fc + + +# ----------------------------- +# Tests for open_db() +# ----------------------------- +def test_open_db_sets_search_path(fake_conn): + conn = dbu.open_db() + assert conn is fake_conn + # We expect one SET search_path statement with our schema + script = conn.script() + execs = [s for s in script if s[0] == "EXEC"] + assert any( + ("SET search_path TO" in q) and ("agcloud_audio" in q) + for _, q, _ in execs + ), f"Expected SET search_path to agcloud_audio, got: {execs}" + # autocommit should remain False + assert conn.autocommit is False + + +# ----------------------------- +# Tests for ensure_run() +# ----------------------------- +def test_ensure_run_inserts_and_commits(fake_conn, monkeypatch): + # Provide env to fill NOT NULL columns + monkeypatch.setenv("MODEL_NAME", "panns_cnn14") + monkeypatch.setenv("CHECKPOINT", "ckpt.pth") + monkeypatch.setenv("HEAD", "/tmp/head.joblib") + monkeypatch.setenv("LABELS_CSV", "") + monkeypatch.setenv("WINDOW_SEC", "10") + monkeypatch.setenv("HOP_SEC", "10") + monkeypatch.setenv("PAD_LAST", "true") + monkeypatch.setenv("AGG", "mean") + monkeypatch.setenv("TOPK", "3") + monkeypatch.setenv("DEVICE", "cpu") + monkeypatch.setenv("CODE_VERSION", "test") + monkeypatch.setenv("RUN_NOTES", "unit-test") + + dbu.ensure_run(fake_conn, run_id="run-123") + # We expect one INSERT and one COMMIT + script = fake_conn.script() + insert_calls = [s for s in script if s[0] == "EXEC" and "INSERT INTO runs" in s[1]] + assert len(insert_calls) == 1, f"expected single INSERT, got: {insert_calls}" + assert ("COMMIT", None, None) in script + + +# ----------------------------- +# Tests for resolve_file_id() +# ----------------------------- +def test_resolve_file_id_by_file_id_ok(fake_conn): + # Simulate existing file_id + fake_conn._cursor.set_fetchone((42,)) + file_id = dbu.resolve_file_id(fake_conn, file_id=42) + assert file_id == 42 + # Verify the WHERE clause by id appeared + execs = [s for s in fake_conn.script() if s[0] == "EXEC"] + assert any("WHERE file_id = %s" in q for _, q, _ in execs) + +def test_resolve_file_id_by_file_id_not_found(fake_conn): + fake_conn._cursor.set_fetchone(None) + with pytest.raises(ValueError) as ex: + dbu.resolve_file_id(fake_conn, file_id=999) + assert "file_id 999 not found" in str(ex.value) + +def test_resolve_file_id_by_bucket_key_ok(fake_conn): + # Simulate a row found by (bucket, object_key) + fake_conn._cursor.set_fetchone((321,)) + out = dbu.resolve_file_id(fake_conn, bucket="b", object_key="k") + assert out == 321 + execs = [s for s in fake_conn.script() if s[0] == "EXEC"] + assert any("WHERE bucket = %s AND object_key = %s" in q for _, q, _ in execs) + +def test_resolve_file_id_by_bucket_key_not_found(fake_conn): + fake_conn._cursor.set_fetchone(None) + with pytest.raises(ValueError) as ex: + dbu.resolve_file_id(fake_conn, bucket="b", object_key="k") + msg = str(ex.value) + # Be flexible about formatting: just assert key parts of the message exist + assert "not found in public.files" in msg + assert "s3://b/k" in msg or ("bucket" in msg and "object_key" in msg and "b" in msg and "k" in msg) + +def test_resolve_file_id_requires_params(fake_conn): + with pytest.raises(ValueError): + dbu.resolve_file_id(fake_conn) # neither file_id nor (bucket,key) diff --git a/AgCloud/services/sounds_classifier/tests/test_model_io.py b/AgCloud/services/sounds_classifier/tests/test_model_io.py new file mode 100644 index 000000000..4634a9804 --- /dev/null +++ b/AgCloud/services/sounds_classifier/tests/test_model_io.py @@ -0,0 +1,258 @@ +import numpy as np +from pathlib import Path +import types +import pytest +import soundfile as sf + +import classification.core.model_io as mio +from classification.core.model_io import ( + aggregate_matrix, + segment_waveform, + load_audio, + SAMPLE_RATE, + ensure_checkpoint, + ensure_numpy_1d, + _to_numpy, +) + +# ---------- aggregate_matrix: happy paths and error branches ---------- + +def test_aggregate_matrix_mean_and_max_non_empty(): + X = np.array([[0.1, 0.9], [0.7, 0.3], [0.2, 0.8]], dtype=np.float32) + m = aggregate_matrix(X, mode="mean") + M = aggregate_matrix(X, mode="max") + assert np.allclose(m, X.mean(0)) + assert np.allclose(M, X.max(0)) + +def test_aggregate_matrix_empty_raises(): + X = np.zeros((0, 2), dtype=np.float32) + with pytest.raises(ValueError) as e: + aggregate_matrix(X, mode="mean") + assert "empty window matrix" in str(e.value) + +def test_aggregate_matrix_zero_classes_raises(): + X = np.zeros((3, 0), dtype=np.float32) + with pytest.raises(ValueError) as e: + aggregate_matrix(X, mode="mean") + assert "num_classes > 0" in str(e.value) + +def test_aggregate_matrix_wrong_type_and_ndim(): + with pytest.raises(TypeError): + aggregate_matrix([[1, 2], [3, 4]], mode="mean") # not np.ndarray + with pytest.raises(ValueError): + aggregate_matrix(np.array([1, 2, 3], dtype=np.float32), mode="mean") # 1D + with pytest.raises(ValueError): + aggregate_matrix(np.zeros((2, 2, 2), dtype=np.float32), mode="mean") # 3D + +def test_aggregate_matrix_unsupported_mode(): + X = np.ones((2, 2), dtype=np.float32) + with pytest.raises(ValueError): + aggregate_matrix(X, mode="median") + +def test_aggregate_matrix_nan_and_infs_are_handled(): + X = np.array([[np.nan, 1.0, np.inf, -np.inf], + [2.0, np.nan, 3.0, -5.0 ]], dtype=np.float32) + out_mean = aggregate_matrix(X, mode="mean") + out_max = aggregate_matrix(X, mode="max") + assert out_mean.dtype == np.float32 + assert out_max.dtype == np.float32 + # NaNs should be treated as missing, and inf/-inf should be clamped via nan_to_num. + assert np.isfinite(out_mean).all() + assert np.isfinite(out_max).all() + +# ---------- ensure_numpy_1d & _to_numpy ---------- + +def test_ensure_numpy_1d_duck_typed_torch_like(): + class DuckTensor: + def __init__(self, arr): self._arr = np.asarray(arr, dtype=np.float32) + def detach(self): return self + def cpu(self): return self + def numpy(self): return self._arr + x = DuckTensor([[1.0], [2.0], [3.0]]) + y = ensure_numpy_1d(x) + assert isinstance(y, np.ndarray) + assert y.shape == (3,) + assert y.dtype == np.float32 + +def test_ensure_numpy_1d_object_with_numpy_method_only(): + class OnlyNumpy: + def __init__(self, arr): self._arr = np.asarray(arr, dtype=np.float32) + def numpy(self): return self._arr + x = OnlyNumpy([[1.0, 2.0, 3.0]]) + y = ensure_numpy_1d(x) + assert y.shape == (3,) + assert y.dtype == np.float32 + +def test__to_numpy_handles_2d_shapes_and_casts_to_float32(): + x = np.array([[1.0], [2.0], [3.0]], dtype=np.float64) + y = _to_numpy(x) + assert y.dtype == np.float32 + assert y.shape == (3,) + +@pytest.mark.skipif(mio.torch is None, reason="torch is not available") +def test__to_numpy_with_real_torch_tensor_when_available(): + import torch + x = torch.tensor([[1.0, 2.0, 3.0]], dtype=torch.float32) + try: + y = _to_numpy(x) + except RuntimeError as e: + # Some builds of torch raise when calling .numpy() if NumPy is mismatched/not present. + assert "Numpy is not available" in str(e) + else: + assert isinstance(y, np.ndarray) + assert y.dtype == np.float32 + assert y.shape == (3,) + +# ---------- segment_waveform edge cases ---------- + +def test_segment_waveform_empty_and_padding_logic(): + # empty wav returns [] + assert segment_waveform(np.array([], dtype=np.float32), sr=SAMPLE_RATE) == [] + + # shorter than one window, pad_last=True adds one padded segment + short = np.ones(100, dtype=np.float32) + segs = segment_waveform(short, sr=SAMPLE_RATE, window_sec=0.01, hop_sec=0.005, pad_last=True) + assert len(segs) >= 1 + for s in segs: + assert s.ndim == 1 and s.dtype == np.float32 + + # shorter than one window, pad_last=False should not add segment + segs2 = segment_waveform(short, sr=SAMPLE_RATE, window_sec=0.01, hop_sec=0.005, pad_last=False) + # Depending on rounding, there may be 0 or >0 windows; enforce that no tail padding is added: + # if the first while loop didn't fit any full window, with pad_last=False we expect 0 segments. + assert len(segs2) in (0,) + +def test_segment_waveform_overlap_and_types(): + wav = np.arange(1000, dtype=np.int16) # non-float input should be casted + segs = segment_waveform(wav, sr=1000, window_sec=0.1, hop_sec=0.05, pad_last=True) + assert len(segs) >= 1 + assert all(s.dtype == np.float32 for s in segs) + +# ---------- load_audio: WAV roundtrip & padding ---------- + +def _sine(sr: int, seconds: float) -> np.ndarray: + t = np.linspace(0, seconds, int(sr * seconds), endpoint=False, dtype=np.float32) + return np.sin(2 * np.pi * 440.0 * t).astype(np.float32) + +def test_load_audio_roundtrip_and_padding(tmp_path: Path): + wav = _sine(SAMPLE_RATE, 0.25) # shorter than MIN_SAMPLES (16000) + p = tmp_path / "a.wav" + sf.write(p, wav, samplerate=SAMPLE_RATE, subtype="PCM_16") + out = load_audio(str(p), SAMPLE_RATE) + assert out.dtype == np.float32 + assert out.ndim == 1 + assert out.size >= mio.MIN_SAMPLES # padded + +def test_load_audio_resample_path(monkeypatch, tmp_path: Path): + # Write at sr=16000; target is 32000. Force librosa.resample path. + wav16 = _sine(16000, 0.2).astype(np.float32) + p = tmp_path / "b.wav" + sf.write(p, wav16, samplerate=16000, subtype="PCM_16") + + called = {"resample": False} + def fake_resample(y, orig_sr, target_sr): + called["resample"] = True + # return float64 to test cast back to float32 + return np.asarray(np.repeat(y, 2), dtype=np.float64) + + monkeypatch.setattr(mio.librosa, "resample", fake_resample, raising=True) + out = load_audio(str(p), SAMPLE_RATE) + assert called["resample"] is True + assert out.dtype == np.float32 + assert out.ndim == 1 + +# ---------- load_audio: HARD_EXTS (mp3/m4a/...) branch + ffmpeg fallback ---------- + +def test_load_audio_hard_ext_uses_librosa_success(monkeypatch, tmp_path: Path): + # Fake an mp3 file; we'll mock librosa.load to return data, so the bytes don't matter. + p = tmp_path / "x.mp3" + p.write_bytes(b"ID3") + + def fake_librosa_load(path, sr, mono): + assert str(path) == str(p) + return np.ones(100, dtype=np.float32), sr + + monkeypatch.setattr(mio.librosa, "load", fake_librosa_load, raising=True) + out = load_audio(str(p), SAMPLE_RATE) + assert out.dtype == np.float32 + assert out.ndim == 1 + assert out.size >= mio.MIN_SAMPLES + +def test_load_audio_hard_ext_librosa_fail_ffmpeg_fallback(monkeypatch, tmp_path: Path): + p = tmp_path / "y.m4a" + p.write_bytes(b"\x00\x00\x00\x20ftypM4A ") # header-ish; not used + + def fake_librosa_fail(*a, **k): raise RuntimeError("boom") + monkeypatch.setattr(mio.librosa, "load", fake_librosa_fail, raising=True) + monkeypatch.setattr(mio, "has_ffmpeg", lambda: True, raising=True) + # make ffmpeg path return short buffer to test padding + monkeypatch.setattr(mio, "decode_with_ffmpeg_to_float32_mono", + lambda path, target_sr: np.ones(10, dtype=np.float32), raising=True) + out = load_audio(str(p), SAMPLE_RATE) + assert out.size >= mio.MIN_SAMPLES + assert out.dtype == np.float32 + +def test_load_audio_all_fail_then_ffmpeg(monkeypatch, tmp_path: Path): + # For non-HARD ext: raise in soundfile.read, then raise in librosa.load, then use ffmpeg + p = tmp_path / "z.ogg" + p.write_bytes(b"OggS...") + + def fake_sf_read(*a, **k): raise RuntimeError("sf fail") + def fake_librosa_load(*a, **k): raise RuntimeError("librosa fail") + + monkeypatch.setattr(sf, "read", fake_sf_read, raising=True) + monkeypatch.setattr(mio.librosa, "load", fake_librosa_load, raising=True) + monkeypatch.setattr(mio, "has_ffmpeg", lambda: True, raising=True) + monkeypatch.setattr(mio, "decode_with_ffmpeg_to_float32_mono", + lambda path, target_sr: np.ones(33, dtype=np.float32), raising=True) + out = load_audio(str(p), SAMPLE_RATE) + assert out.ndim == 1 and out.dtype == np.float32 + +# ---------- ffmpeg helpers ---------- + +def test_has_ffmpeg_uses_shutil_which(monkeypatch): + # Ensure function depends on shutil.which + monkeypatch.setattr(mio.shutil, "which", lambda _: "/usr/bin/ffmpeg") + assert mio.has_ffmpeg() is True + monkeypatch.setattr(mio.shutil, "which", lambda _: None) + assert mio.has_ffmpeg() is False + +def test_decode_with_ffmpeg_to_float32_mono_monkeypatched(monkeypatch, tmp_path: Path): + # We won't call real ffmpeg; we mock subprocess.run to return a small f32 buffer. + p = tmp_path / "q.ogg" + p.write_bytes(b"OggS...") + + class DummyProc: + def __init__(self, data): self.stdout = data; self.stderr = b"" + # 4 float32 numbers -> 16 bytes; MIN_SAMPLES will trigger padding. + raw = (np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32)).tobytes() + + monkeypatch.setattr( + mio.subprocess, "run", + lambda cmd, stdout, stderr, check: DummyProc(raw), + raising=True + ) + + out = mio.decode_with_ffmpeg_to_float32_mono(str(p), mio.SAMPLE_RATE) + assert out.dtype == np.float32 + assert out.ndim == 1 + assert out.size >= mio.MIN_SAMPLES + +# ---------- ensure_checkpoint (patch urllib at global module level) ---------- + +def test_ensure_checkpoint_downloads_when_missing(monkeypatch, tmp_path): + target = tmp_path / "models" / "panns_data" / "m.pth" + called = {"ok": False} + def fake_urlretrieve(url, dst): + called["ok"] = True + Path(dst).write_bytes(b"ok") + monkeypatch.setattr("urllib.request.urlretrieve", fake_urlretrieve, raising=True) + path = ensure_checkpoint(str(target), "http://example.com/m.pth") + assert called["ok"] is True + assert Path(path).exists() + +def test_ensure_numpy_and_to_numpy_helpers(): + x = np.array([[1.0], [2.0], [3.0]], dtype=np.float32) + assert ensure_numpy_1d(x).shape == (3,) + y = _to_numpy(x.reshape(1, -1)) + assert y.ndim == 1 diff --git a/AgCloud/services/sounds_flink/Dockerfile b/AgCloud/services/sounds_flink/Dockerfile new file mode 100644 index 000000000..9fc4a5a5c --- /dev/null +++ b/AgCloud/services/sounds_flink/Dockerfile @@ -0,0 +1,155 @@ +# syntax=docker/dockerfile:1 +FROM flink:1.19.3-scala_2.12-java11 + +USER root +WORKDIR /opt/app + +# ----------------------------- +# System CA & Python toolchain +# ----------------------------- +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates wget curl python3 python3-venv python3-pip jq gnupg \ + && update-ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# ----------------------------- +# Optional NetFree CAs (empty dir is OK) +# If you have custom CAs, put *.crt in ./certs and they will be added. +# ----------------------------- +COPY certs/ /usr/local/share/ca-certificates/ +RUN update-ca-certificates || true + +# ----------------------------- +# SSL env for pip/requests (works with/without NetFree) +# ----------------------------- +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# ----------------------------- +# Optional PyPI mirror (for NetFree or internal mirror) +# Pass --build-arg PIP_INDEX_URL=https:///simple +# ----------------------------- +ARG PIP_INDEX_URL="" +RUN set -eux; \ + mkdir -p /etc/pip; \ + { \ + echo "[global]"; \ + echo "cert = /etc/ssl/certs/ca-certificates.crt"; \ + echo "trusted-host = pypi.org"; \ + echo "trusted-host = files.pythonhosted.org"; \ + } > /etc/pip/pip.conf; \ + if [ -n "$PIP_INDEX_URL" ]; then \ + echo "index-url = $PIP_INDEX_URL" >> /etc/pip/pip.conf; \ + fi + +# ----------------------------- +# Python venv (expose system packages as fallback) +# ----------------------------- +RUN python3 -m venv /opt/venv --system-site-packages +ENV PATH="/opt/venv/bin:${PATH}" + +# ----------------------------- +# Copy requirements and install (pip first, apt fallback for some libs) +# ----------------------------- +COPY requirements.txt /opt/app/requirements.txt + +# Optional: allow passing a pre-downloaded PyFlink wheel (for NetFree) +ARG PYFLINK_WHEEL_URL="" +# Make sure shell sees a defined variable even with `set -u` +ENV PYFLINK_WHEEL_URL="${PYFLINK_WHEEL_URL}" + +RUN set -eux; \ + python -m pip install --no-cache-dir --upgrade \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + --cert /etc/ssl/certs/ca-certificates.crt \ + pip setuptools wheel; \ + echo ">>> Installing Python deps via pip (requirements.txt)"; \ + if ! python -m pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + --cert /etc/ssl/certs/ca-certificates.crt \ + -r /opt/app/requirements.txt; then \ + echo "WARN: pip install failed or blocked; trying apt fallback for core libs"; \ + apt-get update && apt-get install -y --no-install-recommends \ + python3-yaml python3-protobuf python3-grpcio \ + && rm -rf /var/lib/apt/lists/*; \ + fi; \ + echo '>>> Ensuring requests is installed (pip first, apt fallback)'; \ + if ! python -m pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + --cert /etc/ssl/certs/ca-certificates.crt \ + requests==2.32.3; then \ + echo 'WARN: pip blocked for requests; falling back to apt'; \ + apt-get update && apt-get install -y --no-install-recommends python3-requests || true; \ + rm -rf /var/lib/apt/lists/*; \ + fi; \ + echo ">>> Enforcing PyFlink presence (wheel or pip)"; \ + if [ -n "${PYFLINK_WHEEL_URL:-}" ]; then \ + python -m pip install --no-cache-dir \ + --cert /etc/ssl/certs/ca-certificates.crt "${PYFLINK_WHEEL_URL}" \ + || (echo 'FATAL: PyFlink wheel install failed' && exit 1); \ + else \ + python -m pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + --cert /etc/ssl/certs/ca-certificates.crt \ + apache-flink==1.19.3 \ + || (echo 'FATAL: apache-flink install failed' && exit 1); \ + fi; \ + echo ">>> Forcing critical runtime libs into venv (pip first, apt fallback)"; \ + if ! python -m pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + --cert /etc/ssl/certs/ca-certificates.crt \ + protobuf==4.25.3 googleapis-common-protos==1.63.0 grpcio==1.60.0; then \ + echo "WARN: pip blocked for key libs; using apt fallback"; \ + apt-get update && apt-get install -y --no-install-recommends \ + python3-protobuf python3-grpcio || true; \ + rm -rf /var/lib/apt/lists/*; \ + fi; \ + python - <<'PY' +import sys +print("Python:", sys.version) +# hard fail if imports are not available inside the venv +for mod in ("requests", "urllib3", "google", "google.protobuf", "grpc", "pyflink"): + try: + __import__(mod) + print(mod, "OK") + except Exception as e: + print("FATAL import check:", mod, "->", e) + raise SystemExit(1) +PY + + +# ----------------------------- +# Flink Kafka connector jars (REQUIRED for Kafka in cluster mode) +# Version aligned to Flink 1.19 +# ----------------------------- +RUN mkdir -p /opt/flink/lib && \ + wget -qO /opt/flink/lib/flink-connector-kafka-3.2.0-1.19.jar \ + https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.2.0-1.19/flink-connector-kafka-3.2.0-1.19.jar && \ + wget -qO /opt/flink/lib/kafka-clients-3.7.0.jar \ + https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.7.0/kafka-clients-3.7.0.jar + +# ----------------------------- +# Copy app code (keep small, configurable by ENV) +# ----------------------------- +COPY config.py /opt/app/config.py +COPY processor.py /opt/app/processor.py +COPY flink_job.py /opt/app/flink_job.py + +# ----------------------------- +# Runtime ENV defaults (can be overridden in docker-compose) +# ----------------------------- +ENV PYTHONPATH=/opt/app \ + PYFLINK_CLIENT_EXECUTABLE=python \ + PYFLINK_PYTHON=python \ + KAFKA_BROKERS=kafka:9092 \ + SOURCE_TOPIC=sound.new.sounds \ + SINK_TOPIC=classified.sounds \ + GROUP_ID=flink-classifier-sounds \ + KAFKA_START=earliest + +# ----------------------------- +# Default CMD is noop; compose sets jobmanager/taskmanager/submitter commands +# ----------------------------- +CMD ["bash", "-lc", "echo 'Flink image ready (NetFree/Non-NetFree compatible)'; tail -f /dev/null"] diff --git a/AgCloud/services/sounds_flink/config.py b/AgCloud/services/sounds_flink/config.py new file mode 100644 index 000000000..989f8677e --- /dev/null +++ b/AgCloud/services/sounds_flink/config.py @@ -0,0 +1,23 @@ +import os + +# Kafka / topics +KAFKA_BROKERS = os.getenv("KAFKA_BROKERS", "kafka:9092") +SOURCE_TOPIC = os.getenv("SOURCE_TOPIC", "sound_new_sounds_connections") +SINK_TOPIC = os.getenv("SINK_TOPIC", "") # empty = print to stdout only +GROUP_ID = os.getenv("GROUP_ID", "flink-classifier-sounds") +KAFKA_START = os.getenv("KAFKA_START", "earliest") # earliest|latest + +# HTTP classifier +CLASSIFIER_HTTP_URL = os.getenv("CLASSIFIER_HTTP_URL", "http://sounds_classifier:8088/classify") +REQUEST_TIMEOUT = float(os.getenv("REQUEST_TIMEOUT", "5.0")) +RETRIES_TOTAL = int(os.getenv("RETRIES_TOTAL", "3")) +BACKOFF_FACTOR = float(os.getenv("BACKOFF_FACTOR", "0.5")) + +# Flink +DEFAULT_PARALLELISM = int(os.getenv("DEFAULT_PARALLELISM", "1")) +CHECKPOINT_MS = int(os.getenv("CHECKPOINT_MS", "10000")) # 10s +DELIVERY_GUARANTEE = os.getenv("DELIVERY_GUARANTEE", "AT_LEAST_ONCE") # AT_LEAST_ONCE|NONE +TRANSACTION_TIMEOUT_MS = os.getenv("TRANSACTION_TIMEOUT_MS", "600000") # 10 min + +# Optional default bucket to use when input only carries an object key +DEFAULT_BUCKET = os.getenv("DEFAULT_BUCKET", "sound") \ No newline at end of file diff --git a/AgCloud/services/sounds_flink/flink_job.py b/AgCloud/services/sounds_flink/flink_job.py new file mode 100644 index 000000000..de2429cc9 --- /dev/null +++ b/AgCloud/services/sounds_flink/flink_job.py @@ -0,0 +1,83 @@ +""" +Flink Python DataStream job: +- Kafka source (JSON notifications) +- Per-record HTTP classification via pooled Session (processor.process_json_line) +- Optional Kafka sink; if SINK_TOPIC is empty -> print to stdout +""" + +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.datastream.connectors.kafka import ( + KafkaSource, KafkaSink, KafkaRecordSerializationSchema, DeliveryGuarantee +) +from pyflink.common.serialization import SimpleStringSchema +from pyflink.common.watermark_strategy import WatermarkStrategy +from pyflink.datastream.checkpointing_mode import CheckpointingMode +from pyflink.common import Types +from processor import process_json_line + +from config import ( + KAFKA_BROKERS, + SOURCE_TOPIC, + SINK_TOPIC, + GROUP_ID, + KAFKA_START, + DEFAULT_PARALLELISM, + CHECKPOINT_MS, + DELIVERY_GUARANTEE, + TRANSACTION_TIMEOUT_MS, +) + +def main(): + env = StreamExecutionEnvironment.get_execution_environment() + env.set_parallelism(DEFAULT_PARALLELISM) + env.enable_checkpointing(CHECKPOINT_MS, CheckpointingMode.EXACTLY_ONCE) + + source = ( + KafkaSource.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_topics(SOURCE_TOPIC) + .set_group_id(GROUP_ID) + .set_property("auto.offset.reset", KAFKA_START) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + + stream = env.from_source( + source, + WatermarkStrategy.no_watermarks(), + f"source-{SOURCE_TOPIC}", + ) + + mapped = stream.map(process_json_line, output_type=Types.STRING()) + filtered = mapped.filter(lambda s: bool(s and s.strip())) + + # Always print for quick debugging + filtered.name("stdout-preview").print() + + # Optional Kafka sink + if SINK_TOPIC: + guarantee = ( + DeliveryGuarantee.AT_LEAST_ONCE + if DELIVERY_GUARANTEE.upper() == "AT_LEAST_ONCE" + else DeliveryGuarantee.NONE + ) + sink = ( + KafkaSink.builder() + .set_bootstrap_servers(KAFKA_BROKERS) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(SINK_TOPIC) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .set_delivery_guarantee(guarantee) + .set_property("transaction.timeout.ms", TRANSACTION_TIMEOUT_MS) + .build() + ) + filtered.sink_to(sink).name(f"sink-{SINK_TOPIC}") + + env.execute("flink-http-classifier") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/sounds_flink/processor.py b/AgCloud/services/sounds_flink/processor.py new file mode 100644 index 000000000..1b4c77814 --- /dev/null +++ b/AgCloud/services/sounds_flink/processor.py @@ -0,0 +1,136 @@ +import json +import logging +from datetime import datetime +from typing import Tuple, Optional, Dict +from urllib.parse import unquote, unquote_plus + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +from config import ( + CLASSIFIER_HTTP_URL, + REQUEST_TIMEOUT, + RETRIES_TOTAL, + BACKOFF_FACTOR, + DEFAULT_BUCKET, +) + +# Reusable HTTP session with retries/backoff +_session = requests.Session() +_retries = Retry( + total=RETRIES_TOTAL, + backoff_factor=BACKOFF_FACTOR, + status_forcelist=(429, 500, 502, 503, 504), + allowed_methods=["GET", "POST"], + respect_retry_after_header=True, +) +_session.mount("http://", HTTPAdapter(max_retries=_retries)) +_session.mount("https://", HTTPAdapter(max_retries=_retries)) + + +def _try_json(raw: str) -> Optional[Dict]: + try: + return json.loads(raw) + except Exception: + return None + + +def _extract_bucket_key(event: Dict) -> Tuple[Optional[str], Optional[str]]: + """ + Extract (bucket, key) from multiple possible MinIO/S3 event shapes. + Supports: + - short link format: {"file_name": "...", "key": "/", "linked_time": "..."} + - flat: {"Bucket": "...", "Key": "..."} + - Records[0].s3.bucket.name / Records[0].s3.object.key + """ + bucket: Optional[str] = None + key: Optional[str] = None + + # 1) Short link format (from *_connections topics): key="/" + if isinstance(event.get("key"), str): + k = event["key"].strip() + k = unquote_plus(unquote(k)) + if "/" in k: + bucket, key = k.split("/", 1) + else: + key = k # no bucket provided here + + # 2) Flat shape + if (bucket is None or key is None) and event.get("Bucket") and event.get("Key"): + bucket = bucket or event.get("Bucket") + key = key or event.get("Key") + + # 3) Records[...] S3-style + if bucket is None or key is None: + records = event.get("Records") or [] + if records: + r0 = records[0] + s3 = r0.get("s3", {}) + b = s3.get("bucket", {}) + o = s3.get("object", {}) + bucket = bucket or b.get("name") + key = key or o.get("key") + + # Normalize/URL-decode + if isinstance(key, str) and key: + key = unquote_plus(unquote(key)) + + return bucket, key + + +def _classify(bucket: Optional[str], key: Optional[str]) -> Optional[Dict]: + """ + Call the classifier service with the resolved (bucket, key). + The classifier expects: + { "s3_bucket": "...", "s3_key": "..." } + """ + if not key: + return None + + # Prefer provided bucket, otherwise fallback to DEFAULT_BUCKET if configured + eff_bucket = bucket or (DEFAULT_BUCKET if DEFAULT_BUCKET else None) + if not eff_bucket: + # Without a bucket we cannot call the classifier + return None + + payload = { + "s3_bucket": eff_bucket, + "s3_key": key, + } + + try: + resp = _session.post(CLASSIFIER_HTTP_URL, json=payload, timeout=REQUEST_TIMEOUT) + if resp.status_code >= 400: + logging.warning("Classifier returned %s for key=%s", resp.status_code, key) + return None + return resp.json() + except Exception as e: + logging.warning("Classifier request failed for key=%s: %s", key, e) + return None + + +def process_json_line(raw: str) -> str: + """ + Map function: input raw JSON string -> output JSON string or "" to skip. + 1) Parse JSON + 2) Extract (bucket, key) + 3) Call classifier (payload: s3_bucket/s3_key) + 4) Return compact JSON result or "" to drop + """ + event = _try_json(raw) + if not event: + return "" + + bucket, key = _extract_bucket_key(event) + result = _classify(bucket, key) + if not result: + return "" + + out = { + "s3_bucket": bucket or DEFAULT_BUCKET or "", + "s3_key": key, + "result": result, + "received_at": datetime.utcnow().isoformat(timespec="seconds") + "Z", + } + return json.dumps(out, separators=(",", ":")) diff --git a/AgCloud/services/sounds_flink/requirements.txt b/AgCloud/services/sounds_flink/requirements.txt new file mode 100644 index 000000000..f5de43b68 --- /dev/null +++ b/AgCloud/services/sounds_flink/requirements.txt @@ -0,0 +1,6 @@ +apache-flink==1.19.3 +requests==2.32.3 +urllib3==2.2.3 +protobuf==4.25.3 +googleapis-common-protos==1.63.0 +grpcio==1.60.0 diff --git a/AgCloud/services/tests/test_db_upsert.py b/AgCloud/services/tests/test_db_upsert.py new file mode 100644 index 000000000..15d5ec27f --- /dev/null +++ b/AgCloud/services/tests/test_db_upsert.py @@ -0,0 +1,25 @@ +# services/rover_ingest/tests/test_db_upsert.py +import os +import pytest +from services.rover_ingest.db import upsert +from services.rover_ingest.schema import ImageMeta +from services.rover_ingest import storage_minio + +requires_db = pytest.mark.skipif( + not os.getenv("PG_DSN"), reason="PG_DSN not set" +) + +@requires_db +def test_upsert_idempotent(monkeypatch): + monkeypatch.setattr(storage_minio, "object_exists", lambda key: True) + meta = ImageMeta.model_validate({ + "schema_ver": 1, + "device_id": "rover-07", + "image_id": "20250910T101500Z-abc123", + "captured_at": "2025-09-10T10:15:00Z", + "gps": {"lat": 31.7, "lon": 35.2}, + "s3_key": "rover-07/2025/09/10/20250910T101500Z-abc123.jpg", + "meta_src": "manifest", + }) + upsert(meta) + upsert(meta) \ No newline at end of file diff --git a/AgCloud/services/tests/test_metrics.py b/AgCloud/services/tests/test_metrics.py new file mode 100644 index 000000000..864522304 --- /dev/null +++ b/AgCloud/services/tests/test_metrics.py @@ -0,0 +1,26 @@ +# services/rover_ingest/tests/test_metrics.py +from services.rover_ingest.app import handle_message, INGEST_OK, INGEST_FAIL +from services.rover_ingest import storage_minio +from services.rover_ingest.db import upsert + +def test_handle_message_increments_success(monkeypatch): + payload = { + "schema_ver": 1, + "device_id": "rover-07", + "image_id": "20250910T101500Z-abc123", + "captured_at": "2025-09-10T10:15:00Z", + "gps": {"lat": 31.7, "lon": 35.2}, + "s3_key": "rover-07/2025/09/10/20250910T101500Z-abc123.jpg", + "meta_src": "manifest", + } + monkeypatch.setattr(storage_minio, "object_exists", lambda key: True) + called = {"n": 0} + monkeypatch.setattr("services.rover_ingest.db.upsert", lambda meta: None) + + before_ok = INGEST_OK._value.get() + before_fail = INGEST_FAIL._value.get() + + handle_message(payload) + + assert INGEST_OK._value.get() == before_ok + 1 + assert INGEST_FAIL._value.get() == before_fail diff --git a/AgCloud/services/tests/test_schema.py b/AgCloud/services/tests/test_schema.py new file mode 100644 index 000000000..a1ae80750 --- /dev/null +++ b/AgCloud/services/tests/test_schema.py @@ -0,0 +1,48 @@ +# from services.rover_ingest.schema import ImageMeta + +# def test_parse_valid_minimal(): +# payload = { +# "schema_ver": 1, +# "device_id": "r1", +# "image_id": "img-1", +# "captured_at": "2025-01-01T10:00:00Z", +# "gps": {"lat": 31.0, "lon": 35.0}, +# "heading_deg": 370.0, +# "alt_m": 1.2, +# "s3_key": "rover-images/r1/2025/01/01/img-1.jpg", +# "meta_src": "manifest" +# } +# meta = ImageMeta.parse_obj(payload) +# assert 0.0 <= meta.heading_deg < 360.0 + +# def test_missing_required_raises(): +# bad = {"device_id": "r1"} +# try: +# ImageMeta.parse_obj(bad) +# assert False, "expected validation error" +# except Exception: +# assert True + +# services/rover_ingest/tests/test_schema.py +import datetime as dt +import pytest +from pydantic import BaseModel, field_validator +from services.rover_ingest.schema import ImageMeta + +def test_valid_payload_parses(): + payload = { + "schema_ver": 1, + "device_id": "rover-07", + "image_id": "20250910T101500Z-abc123", + "captured_at": "2025-09-10T10:15:00Z", + "gps": {"lat": 31.7767, "lon": 35.2345}, + "s3_key": "rover-07/2025/09/10/20250910T101500Z-abc123.jpg", + "meta_src": "manifest", + } + m = ImageMeta.model_validate(payload) + assert m.device_id == "rover-07" + assert m.captured_at.tzinfo is not None + +def test_missing_required_field_fails(): + with pytest.raises(Exception): + ImageMeta.model_validate({"schema_ver": 1}) diff --git a/AgCloud/services/tests/test_validators.py b/AgCloud/services/tests/test_validators.py new file mode 100644 index 000000000..726514d0d --- /dev/null +++ b/AgCloud/services/tests/test_validators.py @@ -0,0 +1,53 @@ +# from services.rover_ingest.schema import ImageMeta +# from services.rover_ingest.validators import validate_semantics + +# # Monkey-patch: avoid real MinIO in unit tests +# import services.rover_ingest.storage_minio as storage_minio +# storage_minio.object_exists = lambda key: True + +# def test_validate_semantics_ok(): +# meta = ImageMeta.parse_obj({ +# "schema_ver": 1, +# "device_id": "r1", +# "image_id": "img-2", +# "captured_at": "2025-01-01T10:00:00Z", +# "gps": {"lat": 31.0, "lon": 35.0}, +# "heading_deg": 10.0, +# "s3_key": "rover-images/r1/2025/01/01/img-2.jpg", +# "meta_src": "manifest" +# }) +# validate_semantics(meta) + +# services/rover_ingest/tests/test_validators.py +import pytest +from types import SimpleNamespace +from services.rover_ingest.validators import validate_semantics + +def test_validate_semantics_passes_when_object_exists(monkeypatch): + meta = SimpleNamespace( + device_id="rover-07", + s3_key="rover-07/2025/09/10/20250910T101500Z-abc123.jpg", + gps=SimpleNamespace(lat=31.7, lon=35.2), + captured_at=None + ) + monkeypatch.setenv("MINIO_BUCKET", "rover-images") + monkeypatch.setenv("MINIO_ENDPOINT", "dummy:9000") + + # מדמים שקובץ קיים + from services.rover_ingest import storage_minio + monkeypatch.setattr(storage_minio, "object_exists", lambda key: True) + + validate_semantics(meta) # לא אמור לזרוק + +def test_validate_semantics_raises_when_missing_object(monkeypatch): + meta = SimpleNamespace( + device_id="rover-07", + s3_key="missing.jpg", + gps=SimpleNamespace(lat=31.7, lon=35.2), + captured_at=None + ) + from services.rover_ingest import storage_minio + monkeypatch.setattr(storage_minio, "object_exists", lambda key: False) + + with pytest.raises(FileNotFoundError): + validate_semantics(meta) diff --git a/AgCloud/services/vector_service/Dockerfile b/AgCloud/services/vector_service/Dockerfile new file mode 100644 index 000000000..1ee96b19d --- /dev/null +++ b/AgCloud/services/vector_service/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . /app + +RUN pip install fastapi uvicorn asyncpg numpy + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/AgCloud/services/vector_service/README b/AgCloud/services/vector_service/README new file mode 100644 index 000000000..0ed58aca6 --- /dev/null +++ b/AgCloud/services/vector_service/README @@ -0,0 +1,184 @@ +========================================= +README – Sensor Embeddings Search System +========================================= + +📘 Overview +----------- +This project implements an AI-powered similarity search system +based on **sensor embeddings** stored in PostgreSQL with the `pgvector` extension. + +It allows: +- Creating embeddings for sensors from the `sensors` table. +- Storing embeddings in the `embeddings` table. +- Searching for similar sensors based on vector similarity. +- Running advanced, filtered similarity queries (by date, type, status). + +------------------------------------------ +📦 Database Schema +------------------------------------------ + +CREATE TABLE sensors ( + id SERIAL PRIMARY KEY, + sensor TEXT UNIQUE NOT NULL, + sensor_type TEXT NOT NULL, + owner_name TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + install_date TIMESTAMP DEFAULT NOW(), + status TEXT DEFAULT 'active', + description TEXT, + last_maintenance TIMESTAMP +); + +CREATE TABLE embeddings ( + id BIGSERIAL PRIMARY KEY, + sensor_id INT REFERENCES sensors(id) ON DELETE CASCADE, + vec vector(5) +); + +------------------------------------------ +🚀 How to Run +------------------------------------------ + +1️⃣ Start Docker containers: + docker compose up postgres vector_service + +2️⃣ Check that the API is live: + http://localhost:8005/docs + +3️⃣ Generate embeddings from sensors: + Invoke-RestMethod -Method Post -Uri "http://localhost:8005/generate_embeddings_from_sensors" + +4️⃣ Verify data: + docker exec -it postgres psql -U missions_user -d missions_db + SELECT COUNT(*) FROM embeddings; + +------------------------------------------ +🔍 Main API Endpoints with Examples +------------------------------------------ + +------------------------------------------ +1️⃣ Generate Embeddings +------------------------------------------ +Creates embeddings for all sensors in the database. + +PowerShell: +Invoke-RestMethod -Method Post -Uri "http://localhost:8005/generate_embeddings_from_sensors" + +------------------------------------------ +2️⃣ Search by Vector +------------------------------------------ +Finds embeddings most similar to the provided vector. + +PowerShell: +Invoke-RestMethod -Method Post -Uri "http://localhost:8005/search" ` + -Headers @{ "Content-Type" = "application/json" } ` + -Body '[31.7,34.8,9,5,1]' | ConvertTo-Json -Depth 5 + +------------------------------------------ +3️⃣ Find Similar Sensors by Sensor ID +------------------------------------------ +Returns sensors most similar to a specific sensor. + +PowerShell: +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors/2" | ConvertTo-Json -Depth 5 + +------------------------------------------ +4️⃣ Advanced Similarity Search +------------------------------------------ +Flexible endpoint supporting multiple filters. + +🔹 Base query (no filters) +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28" ` + | ConvertTo-Json -Depth 5 + +🔹 Same day only +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&same_day=true" ` + | ConvertTo-Json -Depth 5 + +🔹 Same day + same type +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&same_day=true&same_type=true" ` + | ConvertTo-Json -Depth 5 + +🔹 Same day + same type + same status +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&same_day=true&same_type=true&same_status=true" ` + | ConvertTo-Json -Depth 5 + +🔹sensors installed on Wednesday +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&date_filter=wednesday" ` + | ConvertTo-Json -Depth 5 + +🔹sensors installed on last Wednesday +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&date_filter=last_wednesday&same_type=true" ` + | ConvertTo-Json -Depth 5 +------------------------------------------ +📊 SQL Examples +------------------------------------------ + +-- Show all sensors: +SELECT id, sensor_name, install_date FROM sensors; + +-- Insert two sensors with the same installation day: +INSERT INTO sensors (sensor_name, sensor_type, owner_name, lat, lon, install_date, status) +VALUES +('TestSensor_A', 'temperature', 'Dana', 31.771, 34.790, '2025-10-26 09:00:00', 'active'), +('TestSensor_B', 'temperature', 'Dana', 31.772, 34.792, '2025-10-26 11:00:00', 'active'); + +-- Generate embeddings for the new sensors: +Invoke-RestMethod -Method Post -Uri "http://localhost:8005/generate_embeddings_from_sensors" + +-- Verify embeddings: +SELECT e.sensor_id, s.sensor_name, s.sensor_type, e.vec::text +FROM embeddings e +JOIN sensors s ON e.sensor_id = s.id; + +------------------------------------------ +🧠 Example JSON Output +------------------------------------------ + +{ + "base_sensor": { + "sensor_id": 28, + "sensor_name": "SoilTemp02", + "sensor_type": "temperature", + "install_date": "2025-10-25T09:00:00Z", + "status": "active" + }, + "criteria": { + "same_day": true, + "same_type": true, + "same_status": true + }, + "similar_sensors": [ + { + "sensor_id": 31, + "distance": 1.83, + "sensor_name": "SoilTemp05", + "sensor_type": "temperature", + "install_date": "2025-10-25T11:00:00Z", + "status": "active" + }, + { + "sensor_id": 33, + "distance": 3.44, + "sensor_name": "SoilTemp07", + "sensor_type": "temperature", + "install_date": "2025-10-25T08:00:00Z", + "status": "active" + } + ] +} + +------------------------------------------ +📌 Summary +------------------------------------------ +✔ End-to-end embedding pipeline for sensors +✔ PostgreSQL vector search using `pgvector` +✔ Smart similarity filtering by: + • Same day + • Same type + • Same status +✔ All queries available through one generic endpoint: + `/similar_sensors_advanced` + +========================================= diff --git a/AgCloud/services/vector_service/main.py b/AgCloud/services/vector_service/main.py new file mode 100644 index 000000000..b5122fd4c --- /dev/null +++ b/AgCloud/services/vector_service/main.py @@ -0,0 +1,249 @@ + +from fastapi import FastAPI +import asyncpg +import numpy as np +import os + +app = FastAPI() + +DB_HOST = os.getenv("DB_HOST", "postgres") +DB_PORT = int(os.getenv("DB_PORT", 5432)) +DB_USER = os.getenv("DB_USER", "missions_user") +DB_PASS = os.getenv("DB_PASS", "pg123") +DB_NAME = os.getenv("DB_NAME", "missions_db") + +@app.on_event("startup") +async def startup(): + import asyncio + max_retries = 10 + for attempt in range(max_retries): + try: + app.state.conn = await asyncpg.connect( + user=DB_USER, + password=DB_PASS, + database=DB_NAME, + host=DB_HOST, + port=DB_PORT + ) + print("✅ Connected to Postgres") + return + except Exception as e: + print(f"⏳ Waiting for Postgres... attempt {attempt + 1}/{max_retries} ({e})") + await asyncio.sleep(3) + raise RuntimeError("❌ Could not connect to Postgres after several attempts") + +@app.on_event("shutdown") +async def shutdown(): + await app.state.conn.close() + +@app.post("/add_embedding") +async def add_embedding(vector: list[float]): + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + await app.state.conn.execute("INSERT INTO embeddings (vec) VALUES ($1::vector)", vec_str) + return {"status": "ok"} + +@app.post("/search") +async def search(vector: list[float], limit: int = 5): + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + rows = await app.state.conn.fetch( + "SELECT id, vec <-> $1::vector AS distance FROM embeddings ORDER BY vec <-> $1::vector LIMIT $2;", + vec_str, limit + ) + return {"results": [{"id": r["id"], "distance": r["distance"]} for r in rows]} + +@app.post("/generate_embeddings_from_sensors") +async def generate_embeddings_from_sensors(): + """ + שולף נתונים מטבלת sensors, יוצר מהם embeddings, ושומר אותם ב-DB יחד עם sensor_id. + """ + rows = await app.state.conn.fetch("SELECT id, sensor_name, sensor_type, lat, lon, status FROM sensors;") + if not rows: + return {"message": "No sensors found."} + + inserted = 0 + for r in rows: + sensor_id = r["id"] + lat = r["lat"] or 0.0 + lon = r["lon"] or 0.0 + name_len = len(r["sensor_name"] or "") + type_len = len(r["sensor_type"] or "") + status_score = 1.0 if (r["status"] or "").lower() == "active" else 0.0 + + # יצירת embedding פשוט + vector = np.array([lat, lon, name_len, type_len, status_score], dtype=float) + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + + # שמירה ל-DB כולל sensor_id + await app.state.conn.execute( + "INSERT INTO embeddings (sensor_id, vec) VALUES ($1, $2::vector)", + sensor_id, vec_str + ) + inserted += 1 + + print(f"✅ {inserted} embeddings inserted (with sensor_id).") + return {"message": f"{inserted} embeddings generated from sensors (with sensor_id)."} +@app.get("/similar_sensors/{sensor_id}") +async def similar_sensors(sensor_id: int, limit: int = 5): + # שליפת ה-embedding של הסנסור שביקשנו + row = await app.state.conn.fetchrow( + "SELECT vec FROM embeddings WHERE sensor_id=$1;", sensor_id + ) + if not row: + return {"message": f"No embedding found for sensor_id {sensor_id}"} + + vec = row["vec"] + + # שליפת הסנסורים הכי דומים לפי המרחק הווקטורי + results = await app.state.conn.fetch( + """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.lat, s.lon, s.status + FROM embeddings e + JOIN sensors s ON e.sensor_id = s.id + WHERE e.sensor_id <> $2 + ORDER BY distance + LIMIT $3; + """, + vec, sensor_id, limit + ) + + # בניית התשובה + return { + "similar_sensors": [ + { + "sensor_id": r["sensor_id"], + "distance": r["distance"], + "sensor_name": r["sensor_name"], + "sensor_type": r["sensor_type"], + "lat": r["lat"], + "lon": r["lon"], + "status": r["status"] + } + for r in results + ] + } +from datetime import datetime, timedelta + +@app.get("/similar_sensors_advanced") +async def similar_sensors_advanced( + sensor_id: int, + same_day: bool = False, + same_type: bool = False, + same_status: bool = False, + date_filter: str = None, + limit: int = 5 +): + """ + Generic endpoint for flexible similarity queries. + Supports filters: + - same_day (bool) + - same_type (bool) + - same_status (bool) + - date_filter ('today', 'yesterday', 'monday', 'last_wednesday', etc.) + """ + + # Fetch base sensor + sensor = await app.state.conn.fetchrow("SELECT * FROM sensors WHERE id=$1;", sensor_id) + if not sensor: + return {"message": f"Sensor {sensor_id} not found."} + + base_date = sensor["install_date"].date() + sensor_type = sensor["sensor_type"] + status = sensor["status"] + + # Fetch embedding + row = await app.state.conn.fetchrow("SELECT vec FROM embeddings WHERE sensor_id=$1;", sensor_id) + if not row: + return {"message": f"No embedding found for sensor {sensor_id}."} + vec = row["vec"] + + # --- date_filter support --- + start_date, end_date = None, None + today = datetime.utcnow().date() + weekdays = { + "monday": 0, "tuesday": 1, "wednesday": 2, + "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6 + } + + if date_filter: + df = date_filter.lower() + if df == "today": + start_date, end_date = today, today + elif df == "yesterday": + start_date = today - timedelta(days=1) + end_date = start_date + elif df.startswith("last_"): + day = df.replace("last_", "") + if day in weekdays: + today_weekday = today.weekday() + days_back = (today_weekday - weekdays[day] + 7) % 7 + 7 + target_day = today - timedelta(days=days_back) + start_date, end_date = target_day, target_day + elif df in weekdays: + today_weekday = today.weekday() + days_back = (today_weekday - weekdays[df] + 7) % 7 + target_day = today - timedelta(days=days_back) + start_date, end_date = target_day, target_day + + # --- dynamic query --- + query = """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.install_date, s.status + FROM embeddings e + JOIN sensors s ON e.sensor_id = s.id + WHERE e.sensor_id <> $2 + """ + params = [vec, sensor_id] + param_idx = 3 + + if same_day: + query += f" AND DATE(s.install_date) = ${param_idx}" + params.append(base_date) + param_idx += 1 + + if same_type: + query += f" AND s.sensor_type = ${param_idx}" + params.append(sensor_type) + param_idx += 1 + + if same_status: + query += f" AND s.status = ${param_idx}" + params.append(status) + param_idx += 1 + + if start_date and end_date: + query += f" AND DATE(s.install_date) BETWEEN ${param_idx} AND ${param_idx + 1}" + params.extend([start_date, end_date]) + param_idx += 2 + + query += f" ORDER BY distance LIMIT ${param_idx};" + params.append(limit) + + results = await app.state.conn.fetch(query, *params) + + return { + "base_sensor": { + "sensor_id": sensor_id, + "sensor_name": sensor["sensor_name"], + "sensor_type": sensor_type, + "install_date": str(sensor["install_date"]), + "status": status + }, + "filters": { + "same_day": same_day, + "same_type": same_type, + "same_status": same_status, + "date_filter": date_filter + }, + "similar_sensors": [ + { + "sensor_id": r["sensor_id"], + "distance": r["distance"], + "sensor_name": r["sensor_name"], + "sensor_type": r["sensor_type"], + "install_date": str(r["install_date"]), + "status": r["status"] + } + for r in results + ] + } diff --git a/AgCloud/services/vector_service/vector_service/Dockerfile b/AgCloud/services/vector_service/vector_service/Dockerfile new file mode 100644 index 000000000..1ee96b19d --- /dev/null +++ b/AgCloud/services/vector_service/vector_service/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.11-slim + +WORKDIR /app +COPY . /app + +RUN pip install fastapi uvicorn asyncpg numpy + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/AgCloud/services/vector_service/vector_service/README b/AgCloud/services/vector_service/vector_service/README new file mode 100644 index 000000000..0ed58aca6 --- /dev/null +++ b/AgCloud/services/vector_service/vector_service/README @@ -0,0 +1,184 @@ +========================================= +README – Sensor Embeddings Search System +========================================= + +📘 Overview +----------- +This project implements an AI-powered similarity search system +based on **sensor embeddings** stored in PostgreSQL with the `pgvector` extension. + +It allows: +- Creating embeddings for sensors from the `sensors` table. +- Storing embeddings in the `embeddings` table. +- Searching for similar sensors based on vector similarity. +- Running advanced, filtered similarity queries (by date, type, status). + +------------------------------------------ +📦 Database Schema +------------------------------------------ + +CREATE TABLE sensors ( + id SERIAL PRIMARY KEY, + sensor TEXT UNIQUE NOT NULL, + sensor_type TEXT NOT NULL, + owner_name TEXT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + install_date TIMESTAMP DEFAULT NOW(), + status TEXT DEFAULT 'active', + description TEXT, + last_maintenance TIMESTAMP +); + +CREATE TABLE embeddings ( + id BIGSERIAL PRIMARY KEY, + sensor_id INT REFERENCES sensors(id) ON DELETE CASCADE, + vec vector(5) +); + +------------------------------------------ +🚀 How to Run +------------------------------------------ + +1️⃣ Start Docker containers: + docker compose up postgres vector_service + +2️⃣ Check that the API is live: + http://localhost:8005/docs + +3️⃣ Generate embeddings from sensors: + Invoke-RestMethod -Method Post -Uri "http://localhost:8005/generate_embeddings_from_sensors" + +4️⃣ Verify data: + docker exec -it postgres psql -U missions_user -d missions_db + SELECT COUNT(*) FROM embeddings; + +------------------------------------------ +🔍 Main API Endpoints with Examples +------------------------------------------ + +------------------------------------------ +1️⃣ Generate Embeddings +------------------------------------------ +Creates embeddings for all sensors in the database. + +PowerShell: +Invoke-RestMethod -Method Post -Uri "http://localhost:8005/generate_embeddings_from_sensors" + +------------------------------------------ +2️⃣ Search by Vector +------------------------------------------ +Finds embeddings most similar to the provided vector. + +PowerShell: +Invoke-RestMethod -Method Post -Uri "http://localhost:8005/search" ` + -Headers @{ "Content-Type" = "application/json" } ` + -Body '[31.7,34.8,9,5,1]' | ConvertTo-Json -Depth 5 + +------------------------------------------ +3️⃣ Find Similar Sensors by Sensor ID +------------------------------------------ +Returns sensors most similar to a specific sensor. + +PowerShell: +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors/2" | ConvertTo-Json -Depth 5 + +------------------------------------------ +4️⃣ Advanced Similarity Search +------------------------------------------ +Flexible endpoint supporting multiple filters. + +🔹 Base query (no filters) +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28" ` + | ConvertTo-Json -Depth 5 + +🔹 Same day only +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&same_day=true" ` + | ConvertTo-Json -Depth 5 + +🔹 Same day + same type +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&same_day=true&same_type=true" ` + | ConvertTo-Json -Depth 5 + +🔹 Same day + same type + same status +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&same_day=true&same_type=true&same_status=true" ` + | ConvertTo-Json -Depth 5 + +🔹sensors installed on Wednesday +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&date_filter=wednesday" ` + | ConvertTo-Json -Depth 5 + +🔹sensors installed on last Wednesday +Invoke-RestMethod -Uri "http://localhost:8005/similar_sensors_advanced?sensor_id=28&date_filter=last_wednesday&same_type=true" ` + | ConvertTo-Json -Depth 5 +------------------------------------------ +📊 SQL Examples +------------------------------------------ + +-- Show all sensors: +SELECT id, sensor_name, install_date FROM sensors; + +-- Insert two sensors with the same installation day: +INSERT INTO sensors (sensor_name, sensor_type, owner_name, lat, lon, install_date, status) +VALUES +('TestSensor_A', 'temperature', 'Dana', 31.771, 34.790, '2025-10-26 09:00:00', 'active'), +('TestSensor_B', 'temperature', 'Dana', 31.772, 34.792, '2025-10-26 11:00:00', 'active'); + +-- Generate embeddings for the new sensors: +Invoke-RestMethod -Method Post -Uri "http://localhost:8005/generate_embeddings_from_sensors" + +-- Verify embeddings: +SELECT e.sensor_id, s.sensor_name, s.sensor_type, e.vec::text +FROM embeddings e +JOIN sensors s ON e.sensor_id = s.id; + +------------------------------------------ +🧠 Example JSON Output +------------------------------------------ + +{ + "base_sensor": { + "sensor_id": 28, + "sensor_name": "SoilTemp02", + "sensor_type": "temperature", + "install_date": "2025-10-25T09:00:00Z", + "status": "active" + }, + "criteria": { + "same_day": true, + "same_type": true, + "same_status": true + }, + "similar_sensors": [ + { + "sensor_id": 31, + "distance": 1.83, + "sensor_name": "SoilTemp05", + "sensor_type": "temperature", + "install_date": "2025-10-25T11:00:00Z", + "status": "active" + }, + { + "sensor_id": 33, + "distance": 3.44, + "sensor_name": "SoilTemp07", + "sensor_type": "temperature", + "install_date": "2025-10-25T08:00:00Z", + "status": "active" + } + ] +} + +------------------------------------------ +📌 Summary +------------------------------------------ +✔ End-to-end embedding pipeline for sensors +✔ PostgreSQL vector search using `pgvector` +✔ Smart similarity filtering by: + • Same day + • Same type + • Same status +✔ All queries available through one generic endpoint: + `/similar_sensors_advanced` + +========================================= diff --git a/AgCloud/services/vector_service/vector_service/main.py b/AgCloud/services/vector_service/vector_service/main.py new file mode 100644 index 000000000..b5122fd4c --- /dev/null +++ b/AgCloud/services/vector_service/vector_service/main.py @@ -0,0 +1,249 @@ + +from fastapi import FastAPI +import asyncpg +import numpy as np +import os + +app = FastAPI() + +DB_HOST = os.getenv("DB_HOST", "postgres") +DB_PORT = int(os.getenv("DB_PORT", 5432)) +DB_USER = os.getenv("DB_USER", "missions_user") +DB_PASS = os.getenv("DB_PASS", "pg123") +DB_NAME = os.getenv("DB_NAME", "missions_db") + +@app.on_event("startup") +async def startup(): + import asyncio + max_retries = 10 + for attempt in range(max_retries): + try: + app.state.conn = await asyncpg.connect( + user=DB_USER, + password=DB_PASS, + database=DB_NAME, + host=DB_HOST, + port=DB_PORT + ) + print("✅ Connected to Postgres") + return + except Exception as e: + print(f"⏳ Waiting for Postgres... attempt {attempt + 1}/{max_retries} ({e})") + await asyncio.sleep(3) + raise RuntimeError("❌ Could not connect to Postgres after several attempts") + +@app.on_event("shutdown") +async def shutdown(): + await app.state.conn.close() + +@app.post("/add_embedding") +async def add_embedding(vector: list[float]): + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + await app.state.conn.execute("INSERT INTO embeddings (vec) VALUES ($1::vector)", vec_str) + return {"status": "ok"} + +@app.post("/search") +async def search(vector: list[float], limit: int = 5): + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + rows = await app.state.conn.fetch( + "SELECT id, vec <-> $1::vector AS distance FROM embeddings ORDER BY vec <-> $1::vector LIMIT $2;", + vec_str, limit + ) + return {"results": [{"id": r["id"], "distance": r["distance"]} for r in rows]} + +@app.post("/generate_embeddings_from_sensors") +async def generate_embeddings_from_sensors(): + """ + שולף נתונים מטבלת sensors, יוצר מהם embeddings, ושומר אותם ב-DB יחד עם sensor_id. + """ + rows = await app.state.conn.fetch("SELECT id, sensor_name, sensor_type, lat, lon, status FROM sensors;") + if not rows: + return {"message": "No sensors found."} + + inserted = 0 + for r in rows: + sensor_id = r["id"] + lat = r["lat"] or 0.0 + lon = r["lon"] or 0.0 + name_len = len(r["sensor_name"] or "") + type_len = len(r["sensor_type"] or "") + status_score = 1.0 if (r["status"] or "").lower() == "active" else 0.0 + + # יצירת embedding פשוט + vector = np.array([lat, lon, name_len, type_len, status_score], dtype=float) + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + + # שמירה ל-DB כולל sensor_id + await app.state.conn.execute( + "INSERT INTO embeddings (sensor_id, vec) VALUES ($1, $2::vector)", + sensor_id, vec_str + ) + inserted += 1 + + print(f"✅ {inserted} embeddings inserted (with sensor_id).") + return {"message": f"{inserted} embeddings generated from sensors (with sensor_id)."} +@app.get("/similar_sensors/{sensor_id}") +async def similar_sensors(sensor_id: int, limit: int = 5): + # שליפת ה-embedding של הסנסור שביקשנו + row = await app.state.conn.fetchrow( + "SELECT vec FROM embeddings WHERE sensor_id=$1;", sensor_id + ) + if not row: + return {"message": f"No embedding found for sensor_id {sensor_id}"} + + vec = row["vec"] + + # שליפת הסנסורים הכי דומים לפי המרחק הווקטורי + results = await app.state.conn.fetch( + """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.lat, s.lon, s.status + FROM embeddings e + JOIN sensors s ON e.sensor_id = s.id + WHERE e.sensor_id <> $2 + ORDER BY distance + LIMIT $3; + """, + vec, sensor_id, limit + ) + + # בניית התשובה + return { + "similar_sensors": [ + { + "sensor_id": r["sensor_id"], + "distance": r["distance"], + "sensor_name": r["sensor_name"], + "sensor_type": r["sensor_type"], + "lat": r["lat"], + "lon": r["lon"], + "status": r["status"] + } + for r in results + ] + } +from datetime import datetime, timedelta + +@app.get("/similar_sensors_advanced") +async def similar_sensors_advanced( + sensor_id: int, + same_day: bool = False, + same_type: bool = False, + same_status: bool = False, + date_filter: str = None, + limit: int = 5 +): + """ + Generic endpoint for flexible similarity queries. + Supports filters: + - same_day (bool) + - same_type (bool) + - same_status (bool) + - date_filter ('today', 'yesterday', 'monday', 'last_wednesday', etc.) + """ + + # Fetch base sensor + sensor = await app.state.conn.fetchrow("SELECT * FROM sensors WHERE id=$1;", sensor_id) + if not sensor: + return {"message": f"Sensor {sensor_id} not found."} + + base_date = sensor["install_date"].date() + sensor_type = sensor["sensor_type"] + status = sensor["status"] + + # Fetch embedding + row = await app.state.conn.fetchrow("SELECT vec FROM embeddings WHERE sensor_id=$1;", sensor_id) + if not row: + return {"message": f"No embedding found for sensor {sensor_id}."} + vec = row["vec"] + + # --- date_filter support --- + start_date, end_date = None, None + today = datetime.utcnow().date() + weekdays = { + "monday": 0, "tuesday": 1, "wednesday": 2, + "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6 + } + + if date_filter: + df = date_filter.lower() + if df == "today": + start_date, end_date = today, today + elif df == "yesterday": + start_date = today - timedelta(days=1) + end_date = start_date + elif df.startswith("last_"): + day = df.replace("last_", "") + if day in weekdays: + today_weekday = today.weekday() + days_back = (today_weekday - weekdays[day] + 7) % 7 + 7 + target_day = today - timedelta(days=days_back) + start_date, end_date = target_day, target_day + elif df in weekdays: + today_weekday = today.weekday() + days_back = (today_weekday - weekdays[df] + 7) % 7 + target_day = today - timedelta(days=days_back) + start_date, end_date = target_day, target_day + + # --- dynamic query --- + query = """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.install_date, s.status + FROM embeddings e + JOIN sensors s ON e.sensor_id = s.id + WHERE e.sensor_id <> $2 + """ + params = [vec, sensor_id] + param_idx = 3 + + if same_day: + query += f" AND DATE(s.install_date) = ${param_idx}" + params.append(base_date) + param_idx += 1 + + if same_type: + query += f" AND s.sensor_type = ${param_idx}" + params.append(sensor_type) + param_idx += 1 + + if same_status: + query += f" AND s.status = ${param_idx}" + params.append(status) + param_idx += 1 + + if start_date and end_date: + query += f" AND DATE(s.install_date) BETWEEN ${param_idx} AND ${param_idx + 1}" + params.extend([start_date, end_date]) + param_idx += 2 + + query += f" ORDER BY distance LIMIT ${param_idx};" + params.append(limit) + + results = await app.state.conn.fetch(query, *params) + + return { + "base_sensor": { + "sensor_id": sensor_id, + "sensor_name": sensor["sensor_name"], + "sensor_type": sensor_type, + "install_date": str(sensor["install_date"]), + "status": status + }, + "filters": { + "same_day": same_day, + "same_type": same_type, + "same_status": same_status, + "date_filter": date_filter + }, + "similar_sensors": [ + { + "sensor_id": r["sensor_id"], + "distance": r["distance"], + "sensor_name": r["sensor_name"], + "sensor_type": r["sensor_type"], + "install_date": str(r["install_date"]), + "status": r["status"] + } + for r in results + ] + } diff --git a/AgCloud/services/weed_detection/Dockerfile b/AgCloud/services/weed_detection/Dockerfile new file mode 100644 index 000000000..5821909cc --- /dev/null +++ b/AgCloud/services/weed_detection/Dockerfile @@ -0,0 +1,42 @@ +# ---- Base: lightweight Python + Torch CPU ---- +FROM python:3.10-slim + +ARG DEBIAN_FRONTEND=noninteractive +ENV PIP_NO_CACHE_DIR=1 PYTHONUNBUFFERED=1 + +# System and build tools +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl build-essential gcc libpq5 git && \ + rm -rf /var/lib/apt/lists/* + +# NetFree CA +COPY certs/netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +RUN update-ca-certificates + +# Make pip/requests use the system CA bundle +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +# NEW: install certifi and replace it with the system bundle +RUN python -m pip install --upgrade pip certifi && python - <<'PY' +import certifi, shutil, os +src = "/etc/ssl/certs/ca-certificates.crt" +dst = certifi.where() +os.makedirs(os.path.dirname(dst), exist_ok=True) +shutil.copyfile(src, dst) +print("certifi bundle replaced:", dst) +PY + +# Install PyTorch CPU (2.9.0) + torchvision (0.24.0) from the PyTorch index +RUN pip install "torch==2.9.0+cpu" "torchvision==0.24.0+cpu" --index-url https://download.pytorch.org/whl/cpu + +# Other dependencies — excluding torch/torchvision/torchaudio +WORKDIR /app +COPY requirements.txt /tmp/requirements.txt +RUN sed -i '/^torch/d;/^torchvision/d;/^torchaudio/d' /tmp/requirements.txt && \ + pip install --no-cache-dir -r /tmp/requirements.txt + +# Code and execution +COPY . . +CMD ["python", "-m", "scripts.run_detection", "--storage", "minio"] diff --git a/AgCloud/services/weed_detection/README.md b/AgCloud/services/weed_detection/README.md new file mode 100644 index 000000000..2d1de8659 --- /dev/null +++ b/AgCloud/services/weed_detection/README.md @@ -0,0 +1,140 @@ +# 🌱 Weed Detection Pipeline — MinIO → PostgreSQL + +## Overview +This project implements a **weed detection and analysis pipeline** that automatically: +1. Retrieves images from **MinIO** (S3-compatible object storage). +2. Runs **weed detection** using a combination of heuristic image analysis and machine learning (MobileNetV3). +3. Writes detection results and statistics into a **Relational Database (PostgreSQL)**. + +It’s designed for **automated weekly or on-demand runs** using Docker or local Python execution. + +--- + +## 🧠 Architecture + +**Flow:** +`MinIO (images) → Local cache → Weed Detection (Heuristic + ML) → PostgreSQL` + +### Main Steps +1. **Data Input** + - Images are loaded from MinIO using the credentials defined in `.env`. + - Supports both local and remote (S3-compatible) backends. + +2. **Processing** + - **Heuristic Detection** using Excess Green (ExG) and Otsu thresholding. + - **ML Refinement** with a small MobileNetV3 model (`ml_model.py`) to improve detection accuracy. + - Output: weed masks, bounding boxes, and anomaly scores. + +3. **Database Output** + - Results are inserted into PostgreSQL tables (`tile_stats`, `anomalies`, `qa_runs`) via SQLAlchemy. + - Geometry data is stored as WKT (PostGIS-compatible). + +--- + +## 🧩 Project Structure + +``` +project_root/ +├── scripts/ +│ └── run_detection.py # Main entry point for batch processing +├── src/ +│ ├── detectors/ # Weed and disease detection logic +│ ├── pipeline/ # Database and utility modules +│ └── models/ # ML models (e.g., MobileNetV3) +├── data/ # Local image cache +├── Dockerfile +├── docker-compose.yml +├── .env +└── run_weekly.ps1 # Windows PowerShell automation script +``` + +--- + +## ⚙️ Technologies Used + +| Component | Description | +|------------------|-------------| +| **Python 3.10+** | Core language | +| **PyTorch** | ML inference (MobileNetV3) | +| **OpenCV** | Image preprocessing and segmentation | +| **SQLAlchemy** | ORM and database connection | +| **MinIO SDK** | S3-compatible data access | +| **Docker Compose** | Service orchestration | +| **PostgreSQL + PostGIS** | Result storage and spatial data handling | + +--- + +## 🧾 Environment Configuration + +The `.env` file defines all key environment variables: +```ini +DB_URL=postgresql+psycopg2://user:password@db:5432/missions_db +STORAGE_BACKEND=minio +MINIO_ENDPOINT=minio-hot:9000 +MINIO_ACCESS_KEY=minioadmin +MINIO_SECRET_KEY=minioadmin123 +MINIO_BUCKET=ground +MINIO_SECURE=false +BATCH_SIZE=64 +MAX_WORKERS=4 +MIN_BBOX_AREA=150 +MIN_COMPONENT_AREA=200 +``` + +--- + +## 🚀 Running the Project + +### Option 1: Run via Docker +```bash +docker compose up -d --build +docker compose logs -f weed-detector +``` + +### Option 2: Run Locally (Python) +```bash +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows +pip install -r requirements.txt +python -m scripts.run_detection --storage minio +``` + +--- + +## 🕒 Scheduled Execution (Windows) + +The `run_weekly.ps1` script automates weekly runs using **Task Scheduler**. +It: +- Ensures Docker is running +- Executes `docker compose run` for the detector +- Logs output to `C:\logs\weed-weekly.log` +- Prevents concurrent runs using a lock mechanism + +--- + +## 🗄️ Database Schema (Simplified) + +| Table | Purpose | +|----------------|----------| +| **anomalies** | Stores detected weed events and metadata | +| **tile_stats** | Aggregated scores per image/tile | +| **qa_runs** | Logs of detection runs for debugging and QA | + +> Requires PostgreSQL with PostGIS enabled for geometry operations. + +--- + +## 🧰 Troubleshooting + +| Issue | Cause | Fix | +|-------|--------|-----| +| **Torch model not found** | blocked download or cache missing | Manually place model in `~/.cache/torch/hub/checkpoints` | +| **UniqueViolation on tile_stats** | duplicate tile_id/mission_id | Add `ON CONFLICT DO NOTHING` or adjust mission IDs | +| **Slow performance** | batch size too high | Lower `BATCH_SIZE` and `MAX_WORKERS` | +| **SSL errors** | missing CA certificate | Verify `CA_CERT_PATH` or disable `MINIO_SECURE` if local | + +--- + +## 🏁 Summary + +This project provides an **end-to-end pipeline** for automated weed detection — from image retrieval to database integration — built for scalable, repeatable, and containerized deployment. diff --git a/AgCloud/services/weed_detection/config/config.yaml b/AgCloud/services/weed_detection/config/config.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/weed_detection/docker-compose.yml b/AgCloud/services/weed_detection/docker-compose.yml new file mode 100644 index 000000000..4e8ced77f --- /dev/null +++ b/AgCloud/services/weed_detection/docker-compose.yml @@ -0,0 +1,13 @@ +services: + weed-detector: + build: . + container_name: weed-detector + restart: unless-stopped + env_file: + - .env + volumes: + - ./data_minio_cache:/app/data_minio_cache + command: ["python", "-m", "scripts.run_detection", "--storage", "minio"] + # (Optional) if you have a proxy/certificates: + # environment: + # - REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt diff --git a/AgCloud/services/weed_detection/migrations/001_init.sql b/AgCloud/services/weed_detection/migrations/001_init.sql new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/weed_detection/models/__init__.py b/AgCloud/services/weed_detection/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/weed_detection/models/dataset.py b/AgCloud/services/weed_detection/models/dataset.py new file mode 100644 index 000000000..93b75ef3f --- /dev/null +++ b/AgCloud/services/weed_detection/models/dataset.py @@ -0,0 +1,159 @@ +import os +from typing import List, Optional, Tuple, Dict +import pandas as pd +import numpy as np +from PIL import Image +import torch +from torch.utils.data import Dataset + +class WeedsFromTables(Dataset): + """ + Supports two modes: + 1) Bounding boxes in tables (image_path + xmin, ymin, xmax, ymax [+ label]) -> builds a mask from boxes. + 2) Without boxes (only Filename/Label) -> loads a corresponding PNG mask file from the masks folder. + + Assumptions: + - If box_cols=None: a masks directory is required with one mask file per image (same basename, PNG). + - If class_col exists and is not binary, the mask values should contain matching class indices. + """ + + def __init__( + self, + root_dir: str, + table_files: List[str], + labels_file: Optional[str] = None, + image_col: str = "image_path", + box_cols: Optional[Tuple[str, str, str, str]] = ("xmin", "ymin", "xmax", "ymax"), + class_col: Optional[str] = "label", + image_transform=None, + mask_transform=None, + masks_dir: str = "masks", # relative to root_dir + ): + self.root_dir = root_dir + self.image_col = image_col + self.box_cols = box_cols + self.class_col = class_col + self.image_transform = image_transform + self.mask_transform = mask_transform + self.masks_dir = os.path.join(root_dir, masks_dir) + + # Load and merge tables + dfs = [] + for path in table_files: + ext = os.path.splitext(path)[1].lower() + if ext in (".xlsx", ".xls"): + df = pd.read_excel(path) + elif ext == ".csv": + df = pd.read_csv(path) + else: + raise ValueError(f"Unsupported table extension: {ext}") + dfs.append(df) + if not dfs: + raise ValueError("No tables loaded.") + df = pd.concat(dfs, ignore_index=True) + df.columns = [str(c).strip() for c in df.columns] + + # Flexibility for common column names: + # If the expected image_col does not exist, try 'Filename'/'filename' + if self.image_col not in df.columns: + for cand in ("Filename", "filename", "file_name", "image", "img"): + if cand in df.columns: + self.image_col = cand + break + if self.image_col not in df.columns: + raise KeyError(f"Missing image column. Expected '{self.image_col}' or a common alternative (e.g., 'Filename').") + + # Drop rows without a valid path/name + df = df.dropna(subset=[self.image_col]).copy() + + # If there are no boxes → use mask files mode + self.use_file_masks = self.box_cols is None + + # Optional label mapping + self.label2id: Optional[Dict[str, int]] = None + if self.class_col and self.class_col in df.columns: + if labels_file: + lex = os.path.splitext(labels_file)[1].lower() + ldf = pd.read_excel(labels_file) if lex in (".xlsx", ".xls") else pd.read_csv(labels_file) + cols = {c.lower(): c for c in ldf.columns} + id_col = cols.get("id") or cols.get("class_id") or list(ldf.columns)[0] + name_col = cols.get("name") or cols.get("label") or cols.get("class") or list(ldf.columns)[1] + self.label2id = {str(r[name_col]): int(r[id_col]) for _, r in ldf.iterrows()} + else: + uniq = sorted(set(map(str, df[self.class_col].unique()))) + self.label2id = {name: i + 1 for i, name in enumerate(uniq)} # 0 = background + + # Normalize image path: if the value is only a filename, search for it under root_dir/images/ + def resolve_path(p: str) -> str: + p = str(p) + if os.path.isabs(p): + return p + cand = os.path.join(self.root_dir, p) + if os.path.exists(cand): + return cand + return os.path.join(self.root_dir, "images", os.path.basename(p)) + + df[self.image_col] = df[self.image_col].map(resolve_path) + + # Save image list and group by image (if boxes exist) + self.images = list(df[self.image_col].unique()) + self.by_image = None + if not self.use_file_masks: + # Ensure bounding box columns exist + for c in (self.box_cols or ()): + if c not in df.columns: + raise KeyError(f"Missing bbox column '{c}' in tables.") + self.by_image = {img: subdf for img, subdf in df.groupby(self.image_col, sort=False)} + + def __len__(self): + return len(self.images) + + @staticmethod + def _clip_box(x1, y1, x2, y2, W, H): + x1 = int(np.clip(x1, 0, W)) + y1 = int(np.clip(y1, 0, H)) + x2 = int(np.clip(x2, 0, W)) + y2 = int(np.clip(y2, 0, H)) + if x2 < x1: x1, x2 = x2, x1 + if y2 < y1: y1, y2 = y2, y1 + return x1, y1, x2, y2 + + def __getitem__(self, idx): + img_path = self.images[idx] + image = Image.open(img_path).convert("RGB") + W, H = image.size + + if self.use_file_masks: + # Load mask from a PNG file with the same name as the image + mask_name = os.path.splitext(os.path.basename(img_path))[0] + ".png" + mask_path = os.path.join(self.masks_dir, mask_name) + if not os.path.exists(mask_path): + raise FileNotFoundError(f"Mask file not found for image: {mask_path}") + mask = Image.open(mask_path).convert("L") + else: + # Build mask from bounding boxes + mask_np = np.zeros((H, W), dtype=np.uint8) + rows = self.by_image[self.images[idx]] + for _, row in rows.iterrows(): + x1, y1, x2, y2 = (row[self.box_cols[0]], row[self.box_cols[1]], + row[self.box_cols[2]], row[self.box_cols[3]]) + x1, y1, x2, y2 = self._clip_box(x1, y1, x2, y2, W, H) + if self.label2id is not None and self.class_col and self.class_col in row: + cls_id = self.label2id.get(str(row[self.class_col]), 1) + else: + cls_id = 1 + mask_np[y1:y2, x1:x2] = cls_id + mask = Image.fromarray(mask_np, mode="L") + + # Transformations + if self.image_transform: + image = self.image_transform(image) + else: + image = torch.from_numpy(np.array(image)).permute(2, 0, 1).float() / 255.0 + + if self.mask_transform: + mask = self.mask_transform(mask) + else: + mask = torch.from_numpy(np.array(mask, dtype=np.uint8)).long() + + return image, mask diff --git a/AgCloud/services/weed_detection/models/evaluate.py b/AgCloud/services/weed_detection/models/evaluate.py new file mode 100644 index 000000000..21016e67d --- /dev/null +++ b/AgCloud/services/weed_detection/models/evaluate.py @@ -0,0 +1,323 @@ +# models/evaluate.py +# --------------------------------------------------------------------- +# Lightweight & robust evaluation for a binary UNet segmentation model. +# - Safe on Windows (no anonymous lambdas; workers=0 by default) +# - Normalizes mask to {0,1} so BCEWithLogitsLoss behaves correctly +# - Matches logits size to mask to avoid shape errors +# - Clamps logits to [-20, 20] to prevent numerical blow-ups in BCE +# - Tiny default IMG_SIZE (16x16) for very fast CPU sanity checks +# - Prints ETA, IoU/Dice, Dice-Loss, one-batch profile, and optional +# best-threshold sweep on the cached validation subset +# --------------------------------------------------------------------- + +import os +import time +import random +import argparse +from typing import Tuple, List, Dict, Any + +import torch +import torch.nn.functional as F +from torch.utils.data import DataLoader, Subset +from torchvision import transforms +from torchvision.transforms import InterpolationMode + +from models.unet_model import UNet +from models.dataset import WeedsFromTables as DeepWeedsDataset + +# Keep machine responsive (mirror your train.py) +torch.set_num_threads(1) + +# ----------------------- Defaults ------------------------------------ + +ROOT = "data" +LABELS_DIR = os.path.join(ROOT, "labels") +MASKS_DIR = "masks" +IMAGE_COL = "Filename" +IMG_SIZE_DEFAULT: Tuple[int, int] = (16, 16) # tiny & fast (matches your train.py) +WEIGHTS_DEFAULT = "models/unet_weedseg_best.pth" +VAL_PREFIX_DEFAULT = "val_subset" + +FAST_DEBUG_DEFAULT = True +SUBSET_SIZE_VAL_DEFAULT = 200 +MAX_STEPS_EVAL_DEFAULT = 100 +PRINT_EVERY_DEFAULT = 10 + +BATCH_SIZE_DEFAULT = 1 # safe/light on CPU +WORKERS_DEFAULT = 0 # Windows-safe (no pickling issues) + +CLAMP_LOGITS: Tuple[float, float] = (-20.0, 20.0) # stabilize BCE + +# ----------------------- Picklable transforms ------------------------- + +class MaskTo01(object): + """ + Convert [1,H,W] uint8 {0,255} mask from PILToTensor to [H,W] long {0,1}. + Kept as a top-level class so it's picklable on Windows. + """ + def __call__(self, t: torch.Tensor) -> torch.Tensor: + t = t.squeeze(0) # [H,W] uint8 + return (t > 0).to(torch.long) # [H,W] long {0,1} + +def build_transforms(img_size: Tuple[int, int]): + image_tf = transforms.Compose([ + transforms.Resize(img_size, interpolation=InterpolationMode.BILINEAR), + transforms.ToTensor(), # [C,H,W] float in [0,1] + ]) + mask_tf = transforms.Compose([ + transforms.Resize(img_size, interpolation=InterpolationMode.NEAREST), + transforms.PILToTensor(), # [1,H,W] uint8 (0 or 255) + MaskTo01(), # [H,W] long {0,1} + ]) + return image_tf, mask_tf + +# ----------------------- Dataset utils -------------------------------- + +def collect_tables(labels_dir: str, prefix: str) -> List[str]: + files = [] + for f in os.listdir(labels_dir): + name = f.lower() + if name.startswith(prefix.lower()) and (name.endswith(".csv") or name.endswith(".xlsx")): + files.append(os.path.join(labels_dir, f)) + if not files: + raise RuntimeError(f"No files with prefix '{prefix}' found in {labels_dir}") + return sorted(files) + +def make_subset(ds, k: int, use_subset: bool, seed: int = 1337): + if not use_subset: + return ds + k = min(k, len(ds)) + rng = random.Random(seed) + idx = rng.sample(range(len(ds)), k) + print(f"FAST_DEBUG: evaluating on subset of {k}/{len(ds)} examples") + return Subset(ds, idx) + +# ----------------------- Core helpers --------------------------------- + +def _match_logits_to_mask(logits: torch.Tensor, mask_hw: Tuple[int, int]) -> torch.Tensor: + """Ensure the logits spatial size matches the target mask.""" + if tuple(logits.shape[-2:]) != tuple(mask_hw): + logits = F.interpolate(logits, size=mask_hw, mode="bilinear", align_corners=False) + return logits + +def iou_dice(pred01: torch.Tensor, tgt01: torch.Tensor): + """pred01,tgt01: [H,W] {0,1} uint8/long.""" + inter = (pred01 & tgt01).sum().item() + union = (pred01 | tgt01).sum().item() + iou = inter / (union + 1e-8) + dice = (2 * inter) / (pred01.sum().item() + tgt01.sum().item() + 1e-8) + return float(iou), float(dice) + +def dice_loss_from_probs(prob: torch.Tensor, target: torch.Tensor, eps: float = 1e-6) -> float: + """ + prob,target: [B,1,H,W] float in [0,1] + Returns mean Dice loss over the batch. + """ + inter = (prob * target).sum(dim=(1, 2, 3)) + denom = prob.sum(dim=(1, 2, 3)) + target.sum(dim=(1, 2, 3)) + dl = 1.0 - (2.0 * inter + eps) / (denom + eps) + return float(dl.mean().item()) + +def find_best_threshold(probs_list: List, masks_list: List) -> Tuple[float, float]: + """Grid-search a good probability threshold on cached samples.""" + import numpy as np + ths = np.linspace(0.1, 0.9, 17) + best_t, best_dice = 0.5, -1.0 + for t in ths: + dices = [] + for p, m in zip(probs_list, masks_list): + pred01 = (p > t).astype("uint8") + inter = (pred01 & m).sum() + dices.append((2 * inter) / (pred01.sum() + m.sum() + 1e-8)) + md = float(np.mean(dices)) if dices else -1.0 + if md > best_dice: + best_dice, best_t = md, float(t) + return best_t, best_dice + +# ----------------------- Profiling ------------------------------------ + +@torch.no_grad() +def profile_one_batch(model, loader, device) -> Dict[str, Any]: + """Quick timing of one batch to sense where the time goes.""" + t0 = time.time() + images, masks = next(iter(loader)) + t1 = time.time() + images = images.to(device) + masks = masks.unsqueeze(1).float().to(device) + t2 = time.time() + logits = model(images) + logits = _match_logits_to_mask(logits, masks.shape[-2:]) + t3 = time.time() + return { + "load_s": t1 - t0, + "to_device_s": t2 - t1, + "forward_s": t3 - t2, + "total_s": t3 - t0, + "img_shape": tuple(images.shape), + "logits_shape": tuple(logits.shape), + "mask_unique": torch.unique(masks[0,0].cpu()) + } + +# ----------------------- Evaluation ----------------------------------- + +@torch.inference_mode() +def evaluate(model: torch.nn.Module, + loader: DataLoader, + device: torch.device, + print_every: int = 10, + max_steps: int | None = None, + do_threshold_sweep: bool = True) -> None: + criterion = torch.nn.BCEWithLogitsLoss() + + total_bce = 0.0 + total_dice_loss = 0.0 + iou_sum, dice_sum = 0.0, 0.0 + n_valid = 0 + + t0 = time.time() + total_steps = len(loader) if max_steps is None else min(len(loader), max_steps) + + # cache a small set for threshold sweep + probs_cache, masks_cache = [], [] + + for step, (imgs, masks) in enumerate(loader, 1): + imgs = imgs.to(device) # [B,3,H,W] + masks = masks.unsqueeze(1).float().to(device) # [B,1,H,W] float {0,1} + + logits = model(imgs) # [B,1,h,w] + logits = _match_logits_to_mask(logits, masks.shape[-2:]) + + if step == 1: + print("shapes:", tuple(imgs.shape), tuple(logits.shape), tuple(masks.shape)) + print("mask unique (should be {0.,1.}):", torch.unique(masks[0,0].cpu())) + # Logits diagnostics + print("logits stats -> min/max/mean/std:", + logits.min().item(), logits.max().item(), + logits.mean().item(), logits.std().item()) + + # Stabilize BCE against extreme logits + logits = torch.clamp(logits, CLAMP_LOGITS[0], CLAMP_LOGITS[1]) + + bce = criterion(logits, masks) + if not torch.isfinite(bce): + print(f" step {step}: non-finite loss -> skipped") + continue + + total_bce += float(bce.item()) + n_valid += 1 + + prob = torch.sigmoid(logits) + total_dice_loss += dice_loss_from_probs(prob, masks) + + # threshold 0.5 metrics + pred01 = (prob > 0.5).to(torch.uint8)[0, 0].cpu() + tgt01 = masks[0, 0].to(torch.uint8).cpu() + iou, dice = iou_dice(pred01, tgt01) + iou_sum += iou; dice_sum += dice + + # cache for sweep + if do_threshold_sweep and len(probs_cache) < (total_steps if max_steps else 200): + probs_cache.append(prob[0,0].cpu().numpy()) + masks_cache.append(tgt01.cpu().numpy()) + + if step == 1 or step % print_every == 0: + elapsed = time.time() - t0 + eta = elapsed / step * (total_steps - step) + print(f" step {step}/{total_steps} | BCE={total_bce/max(1,n_valid):.4f} " + f"| DiceLoss={total_dice_loss/max(1,n_valid):.4f} " + f"| IoU@0.5={iou_sum/max(1,n_valid):.4f} | Dice@0.5={dice_sum/max(1,n_valid):.4f} " + f"| ETA={int(eta//60)}m {int(eta%60)}s") + + if (max_steps is not None) and (step >= max_steps): + break + + if n_valid == 0: + print("No valid samples evaluated.") + return + + print("\n===== VALIDATION SUMMARY =====") + print(f"BCE={total_bce/n_valid:.4f} | DiceLoss={total_dice_loss/n_valid:.4f} " + f"| IoU@0.5={iou_sum/n_valid:.4f} | Dice@0.5={dice_sum/n_valid:.4f} | N={n_valid}") + + # Optional: threshold sweep report + if do_threshold_sweep and probs_cache: + try: + best_t, best_dice = find_best_threshold(probs_cache, masks_cache) + print(f"Best threshold on VAL (grid 0.1..0.9): t={best_t:.2f} | Dice={best_dice:.4f}") + except Exception as e: + print(f"(threshold sweep skipped: {e})") + +# ----------------------- CLI/Main ------------------------------------- + +def parse_args(): + p = argparse.ArgumentParser(description="Lightweight evaluation for UNet weed segmentation.") + p.add_argument("--root", type=str, default=ROOT) + p.add_argument("--labels_dir", type=str, default=LABELS_DIR) + p.add_argument("--val_prefix", type=str, default=VAL_PREFIX_DEFAULT) + p.add_argument("--masks_dir", type=str, default=MASKS_DIR) + p.add_argument("--image_col", type=str, default=IMAGE_COL) + p.add_argument("--img_size", type=int, nargs=2, default=list(IMG_SIZE_DEFAULT)) # H W + p.add_argument("--weights", type=str, default=WEIGHTS_DEFAULT) + + p.add_argument("--fast_debug", action="store_true", default=FAST_DEBUG_DEFAULT) + p.add_argument("--subset", type=int, default=SUBSET_SIZE_VAL_DEFAULT) + p.add_argument("--max_steps", type=int, default=MAX_STEPS_EVAL_DEFAULT) + + p.add_argument("--batch_size", type=int, default=BATCH_SIZE_DEFAULT) + p.add_argument("--workers", type=int, default=WORKERS_DEFAULT) # keep 0 on Windows + p.add_argument("--print_every", type=int, default=PRINT_EVERY_DEFAULT) + p.add_argument("--seed", type=int, default=1337) + p.add_argument("--no_thresh_sweep", action="store_true", help="Disable best-threshold sweep") + return p.parse_args() + +def main(): + args = parse_args() + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print("Device:", device) + + image_tf, mask_tf = build_transforms(tuple(args.img_size)) + val_tables = collect_tables(args.labels_dir, args.val_prefix) + + val_full = DeepWeedsDataset( + root_dir=args.root, + table_files=val_tables, + labels_file=None, + image_col=args.image_col, + box_cols=None, # using mask files + class_col=None, # binary from mask + image_transform=image_tf, + mask_transform=mask_tf, + masks_dir=args.masks_dir, + ) + + val_dataset = make_subset(val_full, args.subset, args.fast_debug, seed=args.seed) + + val_loader = DataLoader( + val_dataset, + batch_size=max(1, args.batch_size), + shuffle=False, + num_workers=max(0, args.workers), # 0 by default → Windows-safe + pin_memory=False, # CPU path + persistent_workers=False, + ) + + model = UNet(in_channels=3, out_channels=1).to(device) + state = torch.load(args.weights, map_location=device) + model.load_state_dict(state, strict=True) + model.eval() + + # one-batch profile (helps detect bottlenecks quickly) + prof = profile_one_batch(model, val_loader, device) + print(f"PROFILE one batch -> load={prof['load_s']:.3f}s | to_device={prof['to_device_s']:.3f}s | " + f"forward={prof['forward_s']:.3f}s | total={prof['total_s']:.3f}s | " + f"img={prof['img_shape']} | logits={prof['logits_shape']} | mask_unique={prof['mask_unique']}") + + evaluate( + model, val_loader, device, + print_every=args.print_every, + max_steps=(args.max_steps if args.fast_debug else None), + do_threshold_sweep=(not args.no_thresh_sweep) + ) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/weed_detection/models/ml_model.py b/AgCloud/services/weed_detection/models/ml_model.py new file mode 100644 index 000000000..5472e192e --- /dev/null +++ b/AgCloud/services/weed_detection/models/ml_model.py @@ -0,0 +1,75 @@ +""" +Optional ML model for weed detection (patch classification / region scoring). +If no weights are available, fallback to heuristic detections is used. +""" + +import torch +import torch.nn as nn +import torchvision.transforms as T +from torchvision.models import mobilenet_v3_small +import numpy as np +import cv2 +from typing import Dict, List, Tuple + +class WeedNet(nn.Module): + def __init__(self, num_classes=2): + super().__init__() + self.backbone = mobilenet_v3_small(weights="DEFAULT") + in_feats = self.backbone.classifier[3].in_features + self.backbone.classifier[3] = nn.Linear(in_feats, num_classes) + + def forward(self, x): + return self.backbone(x) + +class MLWeedDetector: + def __init__(self, weights_path: str | None = None, device: str = "cpu"): + self.device = device + self.model = WeedNet().to(self.device) + self.model.eval() + self.transform = T.Compose([ + T.ToTensor(), + T.Resize((224,224)), + T.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]) + ]) + self.active = False + if weights_path: + try: + state = torch.load(weights_path, map_location=self.device) + self.model.load_state_dict(state, strict=False) + self.active = True + except Exception: + self.active = False # fallback + + @torch.inference_mode() + def score_mask(self, bgr: np.ndarray, coarse_mask: np.ndarray) -> np.ndarray: + """ + Optionally refine heuristic mask by classifying sampled patches. + Returns refined binary mask (uint8). + """ + bgr = np.ascontiguousarray(bgr) + coarse_mask= np.ascontiguousarray(coarse_mask) + if not self.active: + return coarse_mask + mask = coarse_mask.copy() + ys, xs = np.where(coarse_mask > 0) + if len(ys) == 0: + return coarse_mask + + # sample up to N points for refinement + N = min(200, len(ys)) + idx = np.random.choice(len(ys), N, replace=False) + H, W = bgr.shape[:2] + for i in idx: + y, x = ys[i], xs[i] + y0, x0 = max(0, y-16), max(0, x-16) + y1, x1 = min(H, y+16), min(W, x+16) + patch = cv2.cvtColor(bgr[y0:y1, x0:x1], cv2.COLOR_BGR2RGB) + patch = np.ascontiguousarray(patch) + if patch.size == 0: + continue + inp = self.transform(patch).unsqueeze(0).to(self.device) + logits = self.model(inp) + prob = torch.softmax(logits, dim=1)[0,1].item() # class 1 = weed + if prob < 0.5: + mask[y, x] = 0 + return mask diff --git a/AgCloud/services/weed_detection/models/train.py b/AgCloud/services/weed_detection/models/train.py new file mode 100644 index 000000000..e9cd358a5 --- /dev/null +++ b/AgCloud/services/weed_detection/models/train.py @@ -0,0 +1,333 @@ +# models/train.py +# --------------------------------------------------------------------- +# Lightweight UNet training loop (CPU/Windows friendly). +# Key fixes: +# - Masks are normalized to {0,1} (not {0,255}) via a picklable transform. +# - Size-mismatch safety: logits are resized to target HxW before loss. +# - Optional BCE+Dice combined loss (helps class imbalance). +# - Gradient clipping to prevent exploding updates. +# Defaults mirror your "light" config: tiny IMG_SIZE, batch_size=1, workers=0. +# --------------------------------------------------------------------- + +import os +import time +import random +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.utils.data import DataLoader, Subset +from torchvision import transforms +from torchvision.transforms import InterpolationMode + +from models.unet_model import UNet +from models.dataset import WeedsFromTables as DeepWeedsDataset + +print(torch.cuda.is_available()) + +# Keep machine responsive on CPU +torch.set_num_threads(1) + +# ===================== Run Config ===================== +ROOT = "data" +LABELS_DIR = os.path.join(ROOT, "labels") + +# Fast debug to verify pipeline end-to-end +FAST_DEBUG = False # set to False for full training +SUBSET_SIZE_TRAIN = 500 +SUBSET_SIZE_VAL = 200 +MAX_STEPS_TRAIN = 20 +MAX_STEPS_VAL = 100 +PRINT_EVERY = 10 + +# Table column names +IMAGE_COL = "Filename" +BOX_COLS = None +CLASS_COL = None + +# Small input for fast CPU sanity checks (can raise to 128x128 later) +IMG_SIZE = (64, 64) + +# Training knobs +LR = 1e-3 +WEIGHT_DECAY = 1e-4 +GRAD_CLIP_NORM = 5.0 # set None to disable +USE_DICE_MIX = True # True -> (BCE + DiceLoss)/2 +SAVE_DIR = "models" +BEST_WEIGHTS_PATH = os.path.join(SAVE_DIR, "unet_weedseg_best.pth") +LAST_WEIGHTS_PATH = os.path.join(SAVE_DIR, "unet_weedseg_last.pth") +SEED = 1337 +# ====================================================== + +# ------------------- Reproducibility ------------------- +def set_seed(seed: int = 1337): + random.seed(seed) + torch.manual_seed(seed) + torch.cuda.manual_seed_all(seed) +set_seed(SEED) + +# ------------------- Table collection ------------------ +def collect(prefix: str): + files = [] + for f in os.listdir(LABELS_DIR): + name = f.lower() + if name.startswith(prefix.lower()) and (name.endswith(".xlsx") or name.endswith(".csv")): + files.append(os.path.join(LABELS_DIR, f)) + if not files: + raise RuntimeError(f"No {prefix} files found in {LABELS_DIR}") + return sorted(files) + +# ------------------- Subset helper --------------------- +def make_subset(ds, k: int): + n = len(ds) + if k is None or n <= k: + return ds + idxs = random.sample(range(n), k) + return Subset(ds, idxs) + +# ------------------- Transforms ------------------------ +class MaskTo01(object): + """ + Convert [1,H,W] uint8 {0,255} from PILToTensor to [H,W] long {0,1}. + Kept as a top-level class so it's picklable on Windows. + """ + def __call__(self, t: torch.Tensor) -> torch.Tensor: + t = t.squeeze(0) # [H,W] uint8 + return (t > 0).to(torch.long) # [H,W] 0/1 + +image_tf = transforms.Compose([ + transforms.Resize(IMG_SIZE, interpolation=InterpolationMode.BILINEAR), + transforms.ToTensor(), # -> [C,H,W], float in [0,1] +]) + +mask_tf = transforms.Compose([ + transforms.Resize(IMG_SIZE, interpolation=InterpolationMode.NEAREST), + transforms.PILToTensor(), # -> [1,H,W] uint8 (0 or 255) + MaskTo01(), # -> [H,W] long {0,1} +]) + +# ------------------- Datasets -------------------------- +train_tables = collect("train_subset") +val_tables = collect("val_subset") +labels_file = None + +train_dataset_full = DeepWeedsDataset( + root_dir=ROOT, + table_files=train_tables, + labels_file=labels_file, + image_col=IMAGE_COL, + box_cols=BOX_COLS, + class_col=CLASS_COL, + image_transform=image_tf, + mask_transform=mask_tf, + masks_dir="masks", +) +val_dataset_full = DeepWeedsDataset( + root_dir=ROOT, + table_files=val_tables, + labels_file=labels_file, + image_col=IMAGE_COL, + box_cols=BOX_COLS, + class_col=CLASS_COL, + image_transform=image_tf, + mask_transform=mask_tf, + masks_dir="masks", +) + +if FAST_DEBUG: + print("FAST_DEBUG: using small subsets") + train_dataset = make_subset(train_dataset_full, SUBSET_SIZE_TRAIN) + val_dataset = make_subset(val_dataset_full, SUBSET_SIZE_VAL) +else: + train_dataset = train_dataset_full + val_dataset = val_dataset_full + +# ------------------- DataLoaders ----------------------- +# Windows-safe: workers=0; batch_size=1 for low CPU pressure +train_loader = DataLoader( + train_dataset, batch_size=1, shuffle=True, + num_workers=0, pin_memory=False +) +val_loader = DataLoader( + val_dataset, batch_size=1, shuffle=False, + num_workers=0, pin_memory=False +) + +# ------------------- Model & Optimizer ----------------- +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +model = UNet(in_channels=3, out_channels=1).to(device) + +bce = nn.BCEWithLogitsLoss() +optimizer = torch.optim.AdamW(model.parameters(), lr=LR, weight_decay=WEIGHT_DECAY) + +# ------------------- Helpers --------------------------- +def _match_logits_to_mask(logits: torch.Tensor, mask_hw): + """Resize logits to [*,1,H,W] to match target before loss.""" + if logits.shape[-2:] != mask_hw: + logits = F.interpolate(logits, size=mask_hw, mode="bilinear", align_corners=False) + return logits + +def dice_loss_from_logits(logits: torch.Tensor, target01: torch.Tensor, eps: float = 1e-6) -> torch.Tensor: + """ + logits: [B,1,H,W] (raw) + target01: [B,1,H,W] float {0,1} + returns scalar tensor + """ + prob = torch.sigmoid(logits) + inter = (prob * target01).sum(dim=(1,2,3)) + denom = prob.sum(dim=(1,2,3)) + target01.sum(dim=(1,2,3)) + dl = 1.0 - (2.0 * inter + eps) / (denom + eps) + return dl.mean() + +@torch.no_grad() +def pixel_accuracy_from_logits(logits, target01, threshold=0.5): + """ + logits: [B,1,H,W] - פלט המודל לפני sigmoid + target01: [B,1,H,W] - מסכה עם 0/1 + threshold: סף בינארי (לרוב 0.5) + """ + probs = torch.sigmoid(logits) + preds = (probs > threshold).float() + correct = (preds == target01).float().sum() + total = target01.numel() + return (correct / total).item() + +def combined_loss(logits: torch.Tensor, target01: torch.Tensor) -> torch.Tensor: + if USE_DICE_MIX: + return 0.5 * bce(logits, target01) + 0.5 * dice_loss_from_logits(logits, target01) + else: + return bce(logits, target01) + +# ------------------- One-batch profile ----------------- +@torch.no_grad() +def profile_one_batch(model, loader, device): + model.eval() + t0 = time.time() + images, masks = next(iter(loader)) + t1 = time.time() + images = images.to(device) + masks = masks.unsqueeze(1).float().to(device) + t2 = time.time() + logits = model(images) + logits = _match_logits_to_mask(logits, masks.shape[-2:]) + t3 = time.time() + # quick sanity check + print("PROFILE shapes:", tuple(images.shape), tuple(masks.shape), tuple(logits.shape)) + print("mask unique (should be {0.,1.}):", torch.unique(masks[0,0].detach().cpu())) + return { + "load_s": t1 - t0, + "to_device_s": t2 - t1, + "forward_s": t3 - t2, + "total_s": t3 - t0, + "img_shape": tuple(images.shape), + } + +# ------------------- Train / Val loops ----------------- +_printed_debug_shapes = False + +def train_one_epoch(model, loader, optimizer, device, max_steps=None): + global _printed_debug_shapes + model.train() + running = 0.0 + t_start = time.time() + + for step, (images, masks) in enumerate(loader, 1): + if not _printed_debug_shapes: + print("DEBUG shapes:", tuple(images.shape), tuple(masks.shape)) # (B,3,H,W), (B,H,W) + _printed_debug_shapes = True + + images = images.to(device) # [B,3,H,W] + masks = masks.unsqueeze(1).float().to(device) # [B,1,H,W] float {0,1} + + optimizer.zero_grad(set_to_none=True) + logits = model(images) # [B,1,h,w] + logits = _match_logits_to_mask(logits, masks.shape[-2:]) + + loss = combined_loss(logits, masks) + loss.backward() + + if GRAD_CLIP_NORM is not None: + torch.nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP_NORM) + + optimizer.step() + + running += loss.item() + + if step % PRINT_EVERY == 0 or step == 1: + print(f" step {step}/{len(loader)} | loss={running/step:.4f}") + + if (max_steps is not None) and (step >= max_steps): + break + + denom = min(len(loader), max_steps) if max_steps else len(loader) + epoch_time = time.time() - t_start + print(f" epoch avg step: {epoch_time/denom:.3f}s | epoch total: {epoch_time:.1f}s") + return running / max(1, denom) +@torch.no_grad() +def validate(model, loader, device, max_steps=None): + model.eval() + running_loss = 0.0 + running_acc = 0.0 + count = 0 + t_start = time.time() + + for step, (images, masks) in enumerate(loader, 1): + images = images.to(device) + masks = masks.unsqueeze(1).float().to(device) + logits = model(images) + logits = _match_logits_to_mask(logits, masks.shape[-2:]) + loss = combined_loss(logits, masks) + acc = pixel_accuracy_from_logits(logits, masks) + + running_loss += loss.item() + running_acc += acc + count += 1 + + if step % PRINT_EVERY == 0 or step == 1: + print(f" [val] step {step}/{len(loader)} | loss={running_loss/count:.4f} | acc={running_acc/count:.4f}") + + if (max_steps is not None) and (step >= max_steps): + break + + denom = max(1, count) + epoch_time = time.time() - t_start + print(f" [val] epoch avg step: {epoch_time/denom:.3f}s | epoch total: {epoch_time:.1f}s") + avg_loss = running_loss / denom + avg_acc = running_acc / denom + return avg_loss, avg_acc + +# ------------------- Main ------------------------------ +def main(): + epochs = 3 if FAST_DEBUG else 20 + best_val = float("inf") + os.makedirs(SAVE_DIR, exist_ok=True) + + # One-batch profile to understand timing + prof = profile_one_batch(model, train_loader, device) + print(f"PROFILE one batch -> load={prof['load_s']:.3f}s | to_device={prof['to_device_s']:.3f}s | " + f"forward={prof['forward_s']:.3f}s | total={prof['total_s']:.3f}s | shape={prof['img_shape']}") + + for epoch in range(1, epochs + 1): + print(f"\nEpoch {epoch}/{epochs}") + + train_loss = train_one_epoch( + model, train_loader, optimizer, device, + max_steps=(MAX_STEPS_TRAIN if FAST_DEBUG else None) + ) + + val_loss, val_acc = validate( + model, val_loader, device, + max_steps=(MAX_STEPS_VAL if FAST_DEBUG else None) + ) + + print(f"Epoch {epoch:02d} | train_loss={train_loss:.4f} | val_loss={val_loss:.4f} | val_acc={val_acc:.4f}") + + if val_loss < best_val: + best_val = val_loss + torch.save(model.state_dict(), BEST_WEIGHTS_PATH) + print(f"✓ Saved best -> {BEST_WEIGHTS_PATH} (val={best_val:.4f})") + + torch.save(model.state_dict(), LAST_WEIGHTS_PATH) + print(f"✓ Saved last -> {LAST_WEIGHTS_PATH}") + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/weed_detection/models/train_ml_refiner.py b/AgCloud/services/weed_detection/models/train_ml_refiner.py new file mode 100644 index 000000000..4ee5d6749 --- /dev/null +++ b/AgCloud/services/weed_detection/models/train_ml_refiner.py @@ -0,0 +1,107 @@ +""" +Train a small classifier to refine weed mask using pseudo-labels (heuristic or GT). +Saves weights to WEIGHTS_OUT (default: ./weights_refiner.pth). + +Usage: + python -m src.train_ml_refiner +Env (.env): + INPUT_DIR=... # same as batch + GT_DIR=... # optional, if you have GT + USE_GT=0/1 # 1 to use GT masks if available + EPOCHS=3 + BATCH_SIZE=64 + LR=1e-3 + WEIGHTS_OUT=./weights_refiner.pth + SAMPLES_PER_IMAGE=64 + LIMIT_IMAGES=0 # 0 = no limit + MAX_STEPS_PER_EPOCH=0 # 0 = no limit +""" + +import os +from dotenv import load_dotenv +import torch +from torch.utils.data import DataLoader, random_split +import torch.nn as nn +import torch.optim as optim +from tqdm import tqdm + +from .data_ml import PatchRefineDataset +from .ml_model import WeedNet # mobilenet_v3_small head -> 2 classes + + +def main(): + load_dotenv() + images_dir = os.getenv("INPUT_DIR", "./data/images") + gt_dir = os.getenv("GT_DIR", "./data/labels") + use_gt = os.getenv("USE_GT", "0") == "1" + epochs = int(os.getenv("EPOCHS", "3")) + bs = int(os.getenv("BATCH_SIZE", "64")) + lr = float(os.getenv("LR", "1e-3")) + weights_out= os.getenv("WEIGHTS_OUT", "./weights_refiner.pth") + + samples_per_image = int(os.getenv("SAMPLES_PER_IMAGE", "64")) + limit_images = int(os.getenv("LIMIT_IMAGES", "0")) + max_steps_per_epoch = int(os.getenv("MAX_STEPS_PER_EPOCH", "0")) + + # Load dataset + ds = PatchRefineDataset(images_dir, gt_dir, use_gt=use_gt, + samples_per_image=samples_per_image, patch_radius=16) + + if limit_images > 0: + ds.images = ds.images[:limit_images] + + n = len(ds) + n_train = int(0.9 * n) + n_val = n - n_train + train_ds, val_ds = random_split(ds, [n_train, n_val]) + + train_loader = DataLoader(train_ds, batch_size=bs, shuffle=True, num_workers=0, pin_memory=True) + val_loader = DataLoader(val_ds, batch_size=bs, shuffle=False, num_workers=0, pin_memory=True) + + device = "cuda" if torch.cuda.is_available() else "cpu" + model = WeedNet(num_classes=2).to(device) + criterion = nn.CrossEntropyLoss() + optimizer = optim.Adam(model.parameters(), lr=lr) + + best_acc = 0.0 + for epoch in range(1, epochs+1): + model.train() + total = correct = 0 + pbar = tqdm(train_loader, desc=f"Train {epoch}/{epochs}") + for step, (x, y) in enumerate(pbar, start=1): + x, y = x.to(device), y.to(device) + optimizer.zero_grad() + logits = model(x) + loss = criterion(logits, y) + loss.backward() + optimizer.step() + pred = logits.argmax(1) + correct += (pred == y).sum().item() + total += y.numel() + pbar.set_postfix(loss=f"{loss.item():.4f}", acc=f"{(correct/total)*100:.1f}%") + + if max_steps_per_epoch and step >= max_steps_per_epoch: + break + + # validation + model.eval() + v_total = v_correct = 0 + with torch.no_grad(): + for x, y in val_loader: + x, y = x.to(device), y.to(device) + logits = model(x) + pred = logits.argmax(1) + v_correct += (pred == y).sum().item() + v_total += y.numel() + v_acc = v_correct / max(1, v_total) + if v_acc > best_acc: + best_acc = v_acc + torch.save(model.state_dict(), weights_out) + + print(f"[VAL] acc={v_acc:.4f} (best={best_acc:.4f})") + + print(f"[DONE] Saved best weights to: {weights_out}") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/weed_detection/models/unet_model.py b/AgCloud/services/weed_detection/models/unet_model.py new file mode 100644 index 000000000..0952c47b8 --- /dev/null +++ b/AgCloud/services/weed_detection/models/unet_model.py @@ -0,0 +1,54 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F + +class UNet(nn.Module): + def __init__(self, in_channels=3, out_channels=1): + super(UNet, self).__init__() + + # Encoder + self.enc1 = self.conv_block(in_channels, 64) + self.enc2 = self.conv_block(64, 128) + self.enc3 = self.conv_block(128, 256) + self.enc4 = self.conv_block(256, 512) + + # Bottleneck + self.bottleneck = self.conv_block(512, 1024) + + # Decoder + self.dec1 = self.deconv_block(1024, 512) + self.dec2 = self.deconv_block(512, 256) + self.dec3 = self.deconv_block(256, 128) + self.dec4 = self.deconv_block(128, 64) + + # Output layer + self.output = nn.Conv2d(64, out_channels, kernel_size=1) + + def conv_block(self, in_channels, out_channels): + return nn.Sequential( + nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1), + nn.ReLU(inplace=True), + nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1), + nn.ReLU(inplace=True) + ) + + def deconv_block(self, in_channels, out_channels): + return nn.Sequential( + nn.ConvTranspose2d(in_channels, out_channels, kernel_size=2, stride=2), + nn.ReLU(inplace=True) + ) + + def forward(self, x): + enc1 = self.enc1(x) + enc2 = self.enc2(enc1) + enc3 = self.enc3(enc2) + enc4 = self.enc4(enc3) + bottleneck = self.bottleneck(enc4) + + dec1 = self.dec1(bottleneck) + dec2 = self.dec2(dec1) + dec3 = self.dec3(dec2) + dec4 = self.dec4(dec3) + + output = self.output(dec4) + return output diff --git a/AgCloud/services/weed_detection/models/weights_refiner.pth b/AgCloud/services/weed_detection/models/weights_refiner.pth new file mode 100644 index 000000000..05c31cbcd Binary files /dev/null and b/AgCloud/services/weed_detection/models/weights_refiner.pth differ diff --git a/AgCloud/services/weed_detection/requirements.txt b/AgCloud/services/weed_detection/requirements.txt new file mode 100644 index 000000000..0d011903c --- /dev/null +++ b/AgCloud/services/weed_detection/requirements.txt @@ -0,0 +1,10 @@ +--extra-index-url https://pypi.org/simple +# torch==2.9.0 +# torchvision==0.24.0 +kafka-python==2.2.2 +minio==7.2.18 +psycopg2-binary==2.9.11 +opencv-python-headless==4.10.0.84 +Pillow +SQLAlchemy==2.0.36 + diff --git a/AgCloud/services/weed_detection/run_weekly.ps1 b/AgCloud/services/weed_detection/run_weekly.ps1 new file mode 100644 index 000000000..9f10eee66 --- /dev/null +++ b/AgCloud/services/weed_detection/run_weekly.ps1 @@ -0,0 +1,84 @@ +# run_weekly.ps1 +# ------------------------------------------- +# Runs the docker-compose service once a week in a clean and stable manner +# ------------------------------------------- + +# ===== Settings ===== +$ProjectDir = "C:\Users\user\Documents\weed-baseline\AgCloud\weed" # update if needed +$Service = "weed-detector" +$LogFile = "C:\logs\weed-weekly.log" +$LockFile = "C:\temp\weed-weekly.lock" +$WaitForDockerMinutes = 5 + +# Create folders for log/lock +New-Item -ItemType Directory -Force -Path (Split-Path $LogFile) | Out-Null +New-Item -ItemType Directory -Force -Path (Split-Path $LockFile) | Out-Null + +# Prevent overlap +if (Test-Path $LockFile) { exit 0 } +New-Item -ItemType File -Path $LockFile -Force | Out-Null + +# Short logging function with timestamp +function Write-Log($msg) { + $stamp = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') + "$stamp | $msg" | Tee-Object -FilePath $LogFile -Append +} + +try { + Write-Log "JOB START" + + # Start Docker Desktop if it’s not running + if (-not (Get-Process -Name "Docker Desktop" -ErrorAction SilentlyContinue)) { + $dockerExe = "C:\Program Files\Docker\Docker\Docker Desktop.exe" + if (Test-Path $dockerExe) { + Write-Log "Starting Docker Desktop..." + Start-Process $dockerExe | Out-Null + } else { + Write-Log "Docker Desktop not found at $dockerExe" + } + } + + # Wait for Docker engine to start + $deadline = (Get-Date).AddMinutes($WaitForDockerMinutes) + do { + try { docker info | Out-Null; $up=$true } catch { Start-Sleep -Seconds 5 } + } until ($up -or (Get-Date) -gt $deadline) + if (-not $up) { + Write-Log "Docker engine did not become ready within $WaitForDockerMinutes minutes." + exit 98 + } + + # Correct context (harmless if already correct) + docker context use desktop-linux 2>$null | Out-Null + + Push-Location $ProjectDir + + # Header for execution + Write-Log "BEGIN build+run for service '$Service'" + + # Important: do not treat stderr output as a fatal error + $prev = $ErrorActionPreference + $ErrorActionPreference = "Continue" + + # Build and run: returns the container’s own exit code + # --no-deps: only this service; --abort-on-container-exit: stops when the service finishes + "===== $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') :: DOCKER START =====" | Tee-Object -FilePath $LogFile -Append + docker compose up --no-deps --build --abort-on-container-exit --exit-code-from $Service $Service ` + 2>&1 | Tee-Object -FilePath $LogFile -Append + "===== $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') :: DOCKER END =====" | Tee-Object -FilePath $LogFile -Append + + $code = $LASTEXITCODE + $ErrorActionPreference = $prev + + if ($code -ne 0) { + Write-Log "JOB FAILED with exit code $code" + exit $code + } else { + Write-Log "JOB SUCCEEDED (exit code 0)" + } + +} finally { + Pop-Location 2>$null + Remove-Item $LockFile -ErrorAction SilentlyContinue + Write-Log "JOB END" +} diff --git a/AgCloud/services/weed_detection/scripts/cron_job_config.yaml b/AgCloud/services/weed_detection/scripts/cron_job_config.yaml new file mode 100644 index 000000000..55006c170 --- /dev/null +++ b/AgCloud/services/weed_detection/scripts/cron_job_config.yaml @@ -0,0 +1,15 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + name: weed-detection-job +spec: + schedule: "0 0 * * 0" # every Sunday night + jobTemplate: + spec: + template: + spec: + containers: + - name: weed-detection + image: weed-detection-image + command: ["/bin/bash", "-c", "python /scripts/run_detection.py"] + restartPolicy: OnFailure diff --git a/AgCloud/services/weed_detection/scripts/make_masks_auto.py b/AgCloud/services/weed_detection/scripts/make_masks_auto.py new file mode 100644 index 000000000..0c1b03810 --- /dev/null +++ b/AgCloud/services/weed_detection/scripts/make_masks_auto.py @@ -0,0 +1,40 @@ +# scripts/make_masks_auto.py +import os +import cv2 +import numpy as np + +IM_DIR = "data/images" +MASK_DIR = "data/masks" +os.makedirs(MASK_DIR, exist_ok=True) + +def exg_mask(bgr): + b, g, r = cv2.split(bgr.astype(np.float32)) + # Simple Excess Green: ExG = 2G - R - B + exg = 2*g - r - b + exg = cv2.normalize(exg, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) + # Automatic threshold (Otsu) + thr_val, thr = cv2.threshold(exg, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU) + # Cleanup: opening/closing + kernel = np.ones((3,3), np.uint8) + thr = cv2.morphologyEx(thr, cv2.MORPH_OPEN, kernel, iterations=1) + thr = cv2.morphologyEx(thr, cv2.MORPH_CLOSE, kernel, iterations=1) + return (thr > 0).astype(np.uint8) # 0/1 + +def process_one(path): + bgr = cv2.imread(path) + if bgr is None: + print(f"[warn] cannot read: {path}") + return + mask01 = exg_mask(bgr) # [H,W] uint8 0/1 + out = (mask01 * 255).astype(np.uint8) + name = os.path.splitext(os.path.basename(path))[0] + ".png" + cv2.imwrite(os.path.join(MASK_DIR, name), out) + +def main(): + for fn in os.listdir(IM_DIR): + if fn.lower().endswith((".jpg",".jpeg",".png",".bmp","tif","tiff")): + process_one(os.path.join(IM_DIR, fn)) + print("done. masks saved to:", MASK_DIR) + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/weed_detection/scripts/run_detection.py b/AgCloud/services/weed_detection/scripts/run_detection.py new file mode 100644 index 000000000..e5bc9f234 --- /dev/null +++ b/AgCloud/services/weed_detection/scripts/run_detection.py @@ -0,0 +1,137 @@ +""" +run_batch.py + +Purpose: +- Run the disease-detection batch pipeline either from a LOCAL folder of images + or from a MinIO bucket (objects are first downloaded to a local cache dir, + then processed exactly like local files). + +Usage examples: +1) Local folder (backward-compatible): + python -m agri_baseline.scripts.run_batch --storage local --images ./data/images + +2) MinIO (reads config from ENV and optional CLI flags): + python -m agri_baseline.scripts.run_batch --storage minio --minio-prefix "" + +Environment variables (typical .env): +- STORAGE_BACKEND=minio|local +- MINIO_ENDPOINT=127.0.0.1:9000 +- MINIO_ACCESS_KEY=minioadmin +- MINIO_SECRET_KEY=minioadmin +- MINIO_BUCKET=leaves +- MINIO_SECURE=false +- MINIO_PREFIX=mission-123/ (optional) +- MINIO_CACHE_DIR=./data/_minio_cache +""" + +import argparse +import os +from pathlib import Path + +from src.pipeline.logging_setup import setup_logging +from src.pipeline import config +from src.batch_runner import BatchRunner + +# MinIO helpers provided in your project +from services.minio_client import load_minio_config # loads config from ENV +from services.minio_sync import download_prefix_to_dir, ensure_bucket + + +def run_local(images_dir: Path) -> None: + """ + LOCAL mode: + - Run the batch pipeline over a local folder of images. + - This preserves the original behavior for backward compatibility. + """ + runner = BatchRunner() + runner.run_folder(images_dir) + + +def run_minio(prefix: str, cache_dir: Path) -> None: + """ + MINIO mode: + - Pull objects from a MinIO bucket (based on ENV config). + - Download them to a local cache directory. + - Run the batch pipeline over the downloaded files. + """ + cfg = load_minio_config() + ensure_bucket(cfg) # Safety: create the bucket if it doesn't exist + + cache_dir.mkdir(parents=True, exist_ok=True) + + # Download objects under 'prefix' into the local cache folder + downloaded = download_prefix_to_dir(cfg, prefix=prefix, local_dir=cache_dir) + if not downloaded: + raise SystemExit( + f"No objects found in bucket '{cfg.bucket}' with prefix '{prefix}'." + ) + + runner = BatchRunner() + runner.run_folder(cache_dir) + + +def parse_args() -> argparse.Namespace: + """ + Parse CLI arguments and provide sensible defaults from ENV where applicable. + """ + ap = argparse.ArgumentParser(description="Run batch pipeline (local/minio).") + + # Backward-compatible local images folder + ap.add_argument( + "--images", + default=config.IMAGES_DIR, + help="Folder of input images (LOCAL mode)", + ) + + # Storage backend selector + ap.add_argument( + "--storage", + choices=["local", "minio"], + default=os.getenv("STORAGE_BACKEND", "local").lower(), + help="Where to read images from (local|minio).", + ) + + # MinIO options (with ENV fallbacks) + ap.add_argument( + "--minio-prefix", + default=os.getenv("MINIO_PREFIX", ""), + help="Object prefix inside the bucket (e.g. 'mission-123/').", + ) + ap.add_argument( + "--minio-cache", + default=os.getenv("MINIO_CACHE_DIR", "./data/_minio_cache"), + help="Local temp folder used to download MinIO objects before processing.", + ) + + return ap.parse_args() + + +def main() -> None: + """ + Entry point: + - Logs chosen backend. + - Dispatches to local/minio flows. + - Keeps logs concise and informative for CI/ops. + """ + log = setup_logging() + args = parse_args() + + log.info(f"Storage backend: {args.storage}") + + if args.storage == "local": + images_dir = Path(args.images) + log.info(f"Starting batch over LOCAL folder: {images_dir}") + run_local(images_dir) + log.info("Batch done (local).") + else: + cache_dir = Path(args.minio_cache) + log.info( + "Starting batch over MINIO: " + f"bucket from ENV, prefix='{args.minio_prefix}', cache='{cache_dir}'" + ) + run_minio(prefix=args.minio_prefix, cache_dir=cache_dir) + log.info("Batch done (minio).") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/weed_detection/services/io.py b/AgCloud/services/weed_detection/services/io.py new file mode 100644 index 000000000..57014b783 --- /dev/null +++ b/AgCloud/services/weed_detection/services/io.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import json +import logging +from typing import Tuple, Iterable, Dict, Any, List + +import pandas as pd +from sqlalchemy import create_engine, text + +LOGGER = logging.getLogger(__name__) + +# --------------------------------------------------------------------- +# Postgres sources: anomalies / anomaly_types / regions +# --------------------------------------------------------------------- + +_BASE_SQLS: Dict[str, str] = { + "device": """ + SELECT a.ts AS "timestamp", + a.device_id AS entity_id, + at.code AS disease_type, + COALESCE(a.severity::double precision, 0.0) AS severity, + 0.0 AS affected_area + FROM public.anomalies a + JOIN public.anomaly_types at ON at.anomaly_type_id = a.anomaly_type_id + WHERE a.ts IS NOT NULL + {AND_CODE_FILTER} + {AND_TIME_RANGE} + """, + "mission": """ + SELECT a.ts AS "timestamp", + a.mission_id::text AS entity_id, + at.code AS disease_type, + COALESCE(a.severity::double precision, 0.0) AS severity, + 0.0 AS affected_area + FROM public.anomalies a + JOIN public.anomaly_types at ON at.anomaly_type_id = a.anomaly_type_id + WHERE a.ts IS NOT NULL + {AND_CODE_FILTER} + {AND_TIME_RANGE} + """, + "region": """ + SELECT a.ts AS "timestamp", + r.id::text AS entity_id, + at.code AS disease_type, + COALESCE(a.severity::double precision, 0.0) AS severity, + {AREA_EXPR} AS affected_area + FROM public.anomalies a + JOIN public.anomaly_types at ON at.anomaly_type_id = a.anomaly_type_id + JOIN public.regions r ON ST_Contains(r.geom, a.geom) + WHERE a.ts IS NOT NULL AND a.geom IS NOT NULL + {AND_CODE_FILTER} + {AND_TIME_RANGE} + """, +} + + +def _build_sql( + entity_dim: str, + area_strategy: str, + codes: List[str] | None, + start: str | None, + end: str | None, +) -> tuple[str, dict]: + """ + Build parametrized SQL for reading anomalies with chosen entity dimension and area strategy. + """ + sql = _BASE_SQLS[entity_dim] + area_expr = "0.0" + if entity_dim == "region" and area_strategy == "region_area": + area_expr = "ST_Area(r.geom::geography)::double precision" + + and_code = "" + params: Dict[str, Any] = {} + if codes: + and_code = "AND at.code = ANY(:codes)" + params["codes"] = codes + + and_time = "" + if start: + and_time += " AND a.ts >= :start_time" + params["start_time"] = start + if end: + and_time += " AND a.ts < :end_time" + params["end_time"] = end + + sql = ( + sql.replace("{AREA_EXPR}", area_expr) + .replace("{AND_CODE_FILTER}", and_code) + .replace("{AND_TIME_RANGE}", and_time) + ) + return sql, params + + +# --------------------------------------------------------------------- +# Postgres input (canonical) +# --------------------------------------------------------------------- + +def load_inputs_from_postgres(pg_url: str, tz: str, cfg: dict) -> Tuple[pd.DataFrame, pd.DataFrame]: + """ + Load inputs from Postgres (public.anomalies/anomaly_types/regions). + Controlled by cfg['source_mapping'] (entity_dim, area_strategy, filters, codes). + Returns: + det: columns [timestamp, entity_id, disease_type, severity, affected_area] + reg: columns [entity_id, entity_type] + """ + edim = cfg["source_mapping"]["entity_dim"] + area = cfg["source_mapping"].get("area_strategy", "none") + codes = cfg["source_mapping"].get("anomaly_codes") + filters = cfg["source_mapping"].get("filters") or {} + start = filters.get("start_time") + end = filters.get("end_time") + + sql, params = _build_sql(edim, area, codes, start, end) + + eng = create_engine(pg_url) + with eng.begin() as conn: + det = pd.read_sql(text(sql), conn, params=params) + reg = det[["entity_id"]].drop_duplicates().assign(entity_type=edim) + + det["timestamp"] = pd.to_datetime(det["timestamp"], utc=True).dt.tz_convert(tz) + + required = {"timestamp", "entity_id", "disease_type", "severity", "affected_area"} + if not required.issubset(det.columns): + missing = required - set(det.columns) + raise ValueError(f"det: missing {missing}") + if not {"entity_id", "entity_type"}.issubset(reg.columns): + raise ValueError("reg: missing cols") + + return det, reg + + +# --------------------------------------------------------------------- +# Aggregation +# --------------------------------------------------------------------- + +def aggregate(det: pd.DataFrame, freq: str) -> pd.DataFrame: + """ + Aggregate by entity_id + window and compute disease_count, avg_severity, affected_area. + """ + df = det.copy() + + # Normalize tz: drop tz-info to use pandas period-based bucketing safely + if pd.api.types.is_datetime64tz_dtype(df["timestamp"]): + df["timestamp"] = df["timestamp"].dt.tz_convert("UTC").dt.tz_localize(None) + + df["window"] = df["timestamp"].dt.to_period(freq).dt.start_time + grp = df.groupby(["entity_id", "window"], as_index=False).agg( + disease_count=("disease_type", "count"), + avg_severity=("severity", "mean"), + affected_area=("affected_area", "sum"), + ) + grp["window_end"] = grp["window"] + pd.tseries.frequencies.to_offset(freq) + return grp + + +# --------------------------------------------------------------------- +# Alerts: Postgres backend +# --------------------------------------------------------------------- + + +def fetch_open_alerts_pg(pg_url: str) -> pd.DataFrame: + eng = create_engine(pg_url) + sql = """ + SELECT id, entity_id, rule, window_start, window_end, score, + first_seen, last_seen, status, meta_json + FROM public.alerts + WHERE status IN ('OPEN','ACK') + """ + with eng.begin() as conn: + df = pd.read_sql(text(sql), conn) + if not df.empty: + for c in ("first_seen", "last_seen", "window_start", "window_end"): + # make tz-aware UTC then drop tz -> naive UTC + s = pd.to_datetime(df[c], utc=True) + df[c] = s.dt.tz_convert("UTC").dt.tz_localize(None) + + return df + + +def upsert_alerts_pg(pg_url: str, alerts: Iterable[Dict[str, Any]]) -> None: + rows = list(alerts) + if not rows: + return + eng = create_engine(pg_url) + sql = """ + INSERT INTO public.alerts + (entity_id, rule, window_start, window_end, score, + first_seen, last_seen, status, meta_json) + VALUES + (:entity_id, :rule, :window_start, :window_end, :score, + :first_seen, :last_seen, :status, CAST(:meta_json AS jsonb)) + """ + payload = [{ + "entity_id": a["entity_id"], + "rule": a["rule"], + "window_start": a["window_start"], + "window_end": a["window_end"], + "score": float(a["score"]), + "first_seen": a["first_seen"], + "last_seen": a["last_seen"], + "status": a["status"], + "meta_json": json.dumps(a["meta"], ensure_ascii=False), + } for a in rows] + + with eng.begin() as conn: + conn.execute(text(sql), payload) + LOGGER.info("Inserted %d alerts into Postgres.", len(rows)) diff --git a/AgCloud/services/weed_detection/services/minio_client.py b/AgCloud/services/weed_detection/services/minio_client.py new file mode 100644 index 000000000..dd5effd69 --- /dev/null +++ b/AgCloud/services/weed_detection/services/minio_client.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass +from minio import Minio + + +@dataclass(frozen=True) +class MinioConfig: + endpoint: str + access_key: str + secret_key: str + bucket: str + secure: bool + + +def load_minio_config() -> MinioConfig: + endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000") + access_key = os.getenv("MINIO_ACCESS_KEY", "") + secret_key = os.getenv("MINIO_SECRET_KEY", "") + bucket = os.getenv("MINIO_BUCKET", "my-bucket") + secure = os.getenv("MINIO_SECURE", "false").lower() == "true" + + if not access_key or not secret_key: + raise ValueError("Missing MINIO_ACCESS_KEY / MINIO_SECRET_KEY.") + return MinioConfig(endpoint, access_key, secret_key, bucket, secure) + + +def build_client(cfg: MinioConfig) -> Minio: + return Minio( + endpoint=cfg.endpoint, + access_key=cfg.access_key, + secret_key=cfg.secret_key, + secure=cfg.secure, + ) diff --git a/AgCloud/services/weed_detection/services/minio_sync.py b/AgCloud/services/weed_detection/services/minio_sync.py new file mode 100644 index 000000000..2f64195a5 --- /dev/null +++ b/AgCloud/services/weed_detection/services/minio_sync.py @@ -0,0 +1,86 @@ + +# services/minio_sync.py +from __future__ import annotations + +from io import BytesIO +from pathlib import Path +from typing import Iterable + +from .minio_client import MinioConfig, build_client + +ALLOWED_EXTS = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"} + + +def ensure_bucket(cfg: MinioConfig) -> None: + """ + Ensure the target bucket exists; create it if it does not. + """ + client = build_client(cfg) + if not client.bucket_exists(cfg.bucket): + client.make_bucket(cfg.bucket) + + +def download_prefix_to_dir(cfg: MinioConfig, prefix: str, local_dir: Path) -> list[Path]: + """ + Download all objects under the given `prefix` to the local directory. + Filters by ALLOWED_EXTS. Returns a list of local file paths. + """ + client = build_client(cfg) + local_dir.mkdir(parents=True, exist_ok=True) + + print(f"[minio] bucket={cfg.bucket} prefix='{prefix}' -> {local_dir}") + downloaded: list[Path] = [] + listed = 0 + objs = list(client.list_objects(cfg.bucket, prefix=prefix, recursive=True)) + print("ALL OBJECT KEYS (raw):", [o.object_name for o in objs]) + for obj in client.list_objects(cfg.bucket, prefix=prefix, recursive=True): + name = obj.object_name + if not name or name.endswith("/"): + continue # “תיקיות” מדומות + listed += 1 + + suf = Path(name).suffix.lower() + if suf not in ALLOWED_EXTS: + # אפשר להשתיק אם לא רוצים לוגים על סיומות + # print(f"[skip-ext] {name}") + continue + + target = local_dir / Path(name).name + response = client.get_object(cfg.bucket, name) + try: + data = response.read() + finally: + try: + response.close() + response.release_conn() + except Exception: + pass + + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + downloaded.append(target) + print(f"[dl] {name} -> {target}") + + print(f"[minio] listed={listed}, downloaded={len(downloaded)}") + return downloaded + + +def upload_dir_to_prefix(cfg: MinioConfig, local_dir: Path, prefix: str) -> list[str]: + """ + Upload all files from `local_dir` under `prefix`. + Returns a list of object names uploaded. + """ + client = build_client(cfg) + ensure_bucket(cfg) + + uploaded: list[str] = [] + for path in local_dir.rglob("*"): + if not path.is_file(): + continue + rel = path.relative_to(local_dir).as_posix() + object_name = f"{prefix.rstrip('/')}/{rel}" + data = path.read_bytes() + bio = BytesIO(data) + client.put_object(cfg.bucket, object_name, bio, length=len(data)) + uploaded.append(object_name) + return uploaded diff --git a/AgCloud/services/weed_detection/src/__init__.py b/AgCloud/services/weed_detection/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/weed_detection/src/batch_runner.py b/AgCloud/services/weed_detection/src/batch_runner.py new file mode 100644 index 000000000..361e85ac2 --- /dev/null +++ b/AgCloud/services/weed_detection/src/batch_runner.py @@ -0,0 +1,214 @@ +# agri_baseline/src/batch_runner.py +# Max line length: 100 + +from __future__ import annotations + +import json +from dataclasses import asdict, is_dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Tuple + +from src.pipeline.utils import ( + load_image, + image_id_from_path, + clamp_bbox, +) +from src.pipeline.db import ( + get_engine, + INSERT_DET, + INSERT_COUNT, + INSERT_QA, +) +from src.detectors.disease_model import DiseaseDetector + + +class BatchRunner: + """ + End-to-end runner: + - Load image + - Run disease detector + - Normalize detections + - Write anomalies / counts / QA to RelDB + """ + + def __init__(self, mission_id: int = 1, device_id: str = "device-1") -> None: + self.mission_id = mission_id + self.device_id = device_id # TEXT FK per schema v2 + self.engine = get_engine() + self.detector = DiseaseDetector() + + # ---------------------------- + # Public API + # ---------------------------- + + def run_folder(self, folder: Path | str) -> None: + """ + Run pipeline on all images within a folder (non-recursive). + Skips non-image files; prints minimal info. + """ + folder = Path(folder) + assert folder.exists(), f"Folder not found: {folder.resolve()}" + + image_paths = sorted( + p for p in folder.iterdir() if p.suffix.lower() in {".jpg", ".jpeg", ".png"} + ) + + total = 0 + total_dets = 0 + for img_path in image_paths: + try: + n = self.process_image(img_path) + total += 1 + total_dets += n + except Exception as ex: + # Keep output tidy; prefer structured logging in production + print(f"[WARN] Failed on {img_path.name}: {ex}") + + # Record a small QA summary + qa = { + "images_processed": total, + "detections_total": total_dets, + "ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), + } + with self.engine.begin() as conn: + conn.execute(INSERT_QA, {"details": json.dumps(qa)}) + + def process_image(self, img_path: Path | str) -> int: + """ + Run pipeline on a single image, write detections and a simple per-image score. + Returns number of detections written. + """ + img_path = Path(img_path) + img, W, H = load_image(img_path) + + image_id = image_id_from_path(img_path) + dets = self.detector.run(img) + + print(f"{image_id}: found {len(dets)} disease spots") + + # Write detections as anomalies + written = 0 + for d in dets: + x, y, w, h = self._extract_bbox(d) + x, y, w, h = clamp_bbox(int(x), int(y), int(w), int(h), W, H) + cx = x + w / 2.0 + cy = y + h / 2.0 + + area = float(getattr(d, "area", w * h)) + label = str(getattr(d, "label", "disease")) + conf = float(getattr(d, "confidence", 1.0)) + + details = { + "image_id": image_id, + "label": label, + "bbox": [x, y, w, h], + "area": area, + "confidence": conf, + } + if is_dataclass(d): + details["raw_detection"] = asdict(d) + + with self.engine.begin() as conn: + conn.execute( + INSERT_DET, + dict( + mission_id=self.mission_id, + device_id=self.device_id, # TEXT FK + ts=datetime.now(timezone.utc), + anomaly_type_id=1, # seeded below + severity=conf, + details=json.dumps(details), + wkt_geom=f"POINT({cx} {cy})", + ), + ) + written += 1 + + # Per-image score → tile_stats (tile_id TEXT, geom POLYGON) + if dets: + anomaly_score = float(len(dets)) + poly_wkt = self._make_square_polygon_wkt(W / 2.0, H / 2.0, size=1.0) + with self.engine.begin() as conn: + conn.execute( + INSERT_COUNT, + dict( + mission_id=self.mission_id, + tile_id=image_id, # TEXT per schema v2 + anomaly_score=anomaly_score, + wkt_geom=poly_wkt, # POLYGON + ), + ) + + return written + + # ---------------------------- + # Internals + # ---------------------------- + + @staticmethod + def _extract_bbox(d) -> Tuple[float, float, float, float]: + """ + Normalize bbox to (x, y, w, h). Supports: + - d.x, d.y, d.w, d.h + - d.bbox == (x, y, w, h) + - d.xmin, d.ymin, d.xmax, d.ymax + - d.left, d.top, d.width, d.height + """ + if all(hasattr(d, a) for a in ("x", "y", "w", "h")): + return float(d.x), float(d.y), float(d.w), float(d.h) + + if hasattr(d, "bbox"): + bx = list(d.bbox) + if len(bx) != 4: + raise ValueError(f"Unexpected bbox length: {len(bx)} in {bx}") + x, y, w, h = map(float, bx) + return x, y, w, h + + if all(hasattr(d, a) for a in ("xmin", "ymin", "xmax", "ymax")): + x1, y1, x2, y2 = float(d.xmin), float(d.ymin), float(d.xmax), float(d.ymax) + return x1, y1, max(0.0, x2 - x1), max(0.0, y2 - y1) + + if all(hasattr(d, a) for a in ("left", "top", "width", "height")): + return float(d.left), float(d.top), float(d.width), float(d.height) + + raise AttributeError( + "Detection bbox fields missing. Supported: " + "(x,y,w,h) or bbox or (xmin,ymin,xmax,ymax) or (left,top,width,height)." + ) + + @staticmethod + def _make_square_polygon_wkt(cx: float, cy: float, size: float = 1.0) -> str: + """ + Build a tiny square Polygon around (cx, cy) in WKT, closed ring. + PostGIS expects Polygon for tile_stats.geom (SRID 4326). + """ + x1, y1 = cx - size, cy - size + x2, y2 = cx + size, cy + size + return f"POLYGON(({x1} {y1}, {x2} {y1}, {x2} {y2}, {x1} {y2}, {x1} {y1}))" + + +# ------------- CLI helper ------------- + +def main() -> None: + """ + Local runner: + python -m agri_baseline.src.batch_runner --input + """ + import argparse + + parser = argparse.ArgumentParser(description="Run disease detection pipeline.") + parser.add_argument("--input", type=str, required=True, help="Image file or folder") + parser.add_argument("--mission", type=int, default=1, help="Numeric mission ID") + parser.add_argument("--device", type=str, default="device-1", help="Text device ID") + args = parser.parse_args() + + runner = BatchRunner(mission_id=args.mission, device_id=args.device) + in_path = Path(args.input) + if in_path.is_dir(): + runner.run_folder(in_path) + else: + runner.process_image(in_path) + + +if __name__ == "__main__": + main() diff --git a/AgCloud/services/weed_detection/src/detectors/__init__.py b/AgCloud/services/weed_detection/src/detectors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/weed_detection/src/detectors/disease_model.py b/AgCloud/services/weed_detection/src/detectors/disease_model.py new file mode 100644 index 000000000..2428dc4b0 --- /dev/null +++ b/AgCloud/services/weed_detection/src/detectors/disease_model.py @@ -0,0 +1,471 @@ +# /src/detectors/disease_model.py +from __future__ import annotations +from dataclasses import dataclass +from pathlib import Path +from typing import List, Tuple, Union +import os +import numpy as np +import torch +import cv2 + +# If your import path is different (e.g., src.models.ml_model), update here: +from models.ml_model import MLWeedDetector + + +@dataclass +class Detection: + x: float + y: float + w: float + h: float + area: float + confidence: float + label: str = "disease" # You can change to "weed" if that's the desired name + + +class DiseaseDetector: + """ + Flow: + 1) Build a coarse mask + 2) Refine the mask using MLWeedDetector (your MobileNetV3 model) + 3) Convert to detections (bounding boxes) for DB writing + """ + + def __init__(self) -> None: + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + # ---- Load weights ---- + weights_env = os.getenv("WEIGHTS_REFINER", "").strip() + if weights_env: + weights_path = Path(weights_env) + else: + # Smart search inside the models directory + project_root = Path(__file__).resolve().parents[2] + models_dir = project_root / "models" + candidates = [ + models_dir / "weights_refiner.pth", + models_dir / "mobilenetv3_best.pth", + models_dir / "best.pth", + models_dir / "last.pth", + ] + found = [p for p in candidates if p.exists()] + if not found: + raise FileNotFoundError( + f"Could not find weights file. Set WEIGHTS_REFINER or put a weights .pth under {models_dir}" + ) + weights_path = found[0] + + # MLWeedDetector already handles Resize(224) + ImageNet normalization + self.refiner = MLWeedDetector(weights_path=str(weights_path), device=str(self.device)) + + # ---- Parameters configurable via .env ---- + self.min_component_area = int(os.getenv("MIN_COMPONENT_AREA", "200")) # Filter out small connected components + self.min_bbox_area = int(os.getenv("MIN_BBOX_AREA", "150")) # Filter out small bounding boxes + self.bin_thresh = int(os.getenv("REFINED_BIN_THRESH", "128")) # Binarization threshold after refinement + self.conf_area_norm = float(os.getenv("CONF_AREA_NORM", "10000")) # Normalize confidence by area + self.coarse_method = os.getenv("COARSE_METHOD", "OTSU").upper() # OTSU / HSV_GREEN + self.max_infer_side = int(os.getenv("MAX_INFER_SIDE", "0")) # 0 = no global downscale + + + # ---------- Main API ---------- + + def run( + self, + bgr_img: np.ndarray, + return_mask: bool = False + ) -> Union[List[Detection], Tuple[np.ndarray, List[Detection]]]: + """ + :param bgr_img: OpenCV image in BGR format + :param return_mask: If True, also return the refined mask (uint8 0/255) + """ + # Ensure contiguous to prevent negative strides + bgr = np.ascontiguousarray(bgr_img) + + bgr = self._maybe_downscale(bgr) + coarse = self._make_coarse(bgr) + # Ensure contiguous before refinement + coarse = np.ascontiguousarray(coarse) + + refined = self._refine_with_model(bgr, coarse) # 0..255 + refined_bin = self._binarize(refined, self.bin_thresh) # 0/255 + refined_bin = self._remove_small(refined_bin, self.min_component_area) + + detections = self._mask_to_detections(refined_bin) + + if return_mask: + return refined_bin, detections + return detections + + # ---------- Processing helpers ---------- + + def _maybe_downscale(self, bgr: np.ndarray) -> np.ndarray: + if not self.max_infer_side or self.max_infer_side <= 0: + return bgr + h, w = bgr.shape[:2] + m = max(h, w) + if m <= self.max_infer_side: + return bgr + scale = self.max_infer_side / float(m) + new_w, new_h = int(w * scale), int(h * scale) + out = cv2.resize(bgr, (new_w, new_h), interpolation=cv2.INTER_AREA) + return np.ascontiguousarray(out) + + def _make_coarse(self, bgr: np.ndarray) -> np.ndarray: + """ + Coarse mask: + - OTSU (default) + - or HSV_GREEN if COARSE_METHOD=HSV_GREEN + """ + if self.coarse_method == "HSV_GREEN": + hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) + # Generic green range; you can calibrate according to your data: + lower = np.array([35, 40, 20], dtype=np.uint8) + upper = np.array([85, 255, 255], dtype=np.uint8) + mask = cv2.inRange(hsv, lower, upper) # 0/255 + return np.ascontiguousarray(mask) + + # OTSU (default) + gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) + _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + return np.ascontiguousarray(mask) + + def _refine_with_model(self, bgr: np.ndarray, coarse: np.ndarray) -> np.ndarray: + """ + Refinement using your trained model. + MLWeedDetector.score_mask(bgr, coarse) -> mask 0..255 + """ + # Ensure contiguous to avoid negative strides inside the refiner + bgr = np.ascontiguousarray(bgr) + coarse = np.ascontiguousarray(coarse) + + refined = self.refiner.score_mask(bgr, coarse) + if refined.dtype != np.uint8: + refined = np.clip(refined, 0, 255).astype(np.uint8) + return refined + + def _binarize(self, mask: np.ndarray, thresh: int) -> np.ndarray: + if mask.dtype != np.uint8: + mask = np.clip(mask, 0, 255).astype(np.uint8) + _, mask_bin = cv2.threshold(mask, thresh, 255, cv2.THRESH_BINARY) + return np.ascontiguousarray(mask_bin) + + def _remove_small(self, mask_bin: np.ndarray, min_area: int) -> np.ndarray: + if min_area <= 1: + return mask_bin + m01 = (mask_bin > 0).astype(np.uint8) + num_labels, labels = cv2.connectedComponents(m01) + out = np.zeros_like(m01) + for i in range(1, num_labels): # 0 = background + comp = (labels == i) + if int(comp.sum()) >= min_area: + out[comp] = 1 + return (out * 255).astype(np.uint8) + + def _mask_to_detections(self, mask_bin: np.ndarray) -> List[Detection]: + num, labels, stats, _ = cv2.connectedComponentsWithStats( + (mask_bin > 0).astype(np.uint8), connectivity=8 + ) + dets: List[Detection] = [] + for i in range(1, num): # 0 = background + x, y, w, h, area = stats[i] + if area < self.min_component_area: + continue + if (w * h) < self.min_bbox_area: + continue + conf = float(min(1.0, area / max(1.0, self.conf_area_norm))) + dets.append( + Detection( + x=float(x), y=float(y), w=float(w), h=float(h), + area=float(area), confidence=conf, label="disease" + ) + ) + return dets + + +# # /src/detectors/disease_model.py +# from __future__ import annotations +# from dataclasses import dataclass +# from pathlib import Path +# from typing import List, Tuple, Union, Optional, Dict, Any +# import os +# import uuid +# from datetime import datetime, timezone + +# import numpy as np +# import torch +# import cv2 +# import requests + +# # If your import path is different (e.g., src.models.ml_model), update here: +# from models.ml_model import MLWeedDetector + + +# @dataclass +# class Detection: +# x: float +# y: float +# w: float +# h: float +# area: float +# confidence: float +# label: str = "disease" # You can change to "weed" if that's the desired name + + +# class DiseaseDetector: +# """ +# Flow: +# 1) Build a coarse mask +# 2) Refine the mask using MLWeedDetector (your MobileNetV3 model) +# 3) Convert to detections (bounding boxes) for DB writing +# 4) Compute severity from detections and POST JSON alert if severity > threshold +# """ + +# def __init__(self) -> None: +# self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# # ---- Load weights ---- +# weights_env = os.getenv("WEIGHTS_REFINER", "").strip() +# if weights_env: +# weights_path = Path(weights_env) +# else: +# # Smart search inside the models directory +# project_root = Path(__file__).resolve().parents[2] +# models_dir = project_root / "models" +# candidates = [ +# models_dir / "weights_refiner.pth", +# models_dir / "mobilenetv3_best.pth", +# models_dir / "best.pth", +# models_dir / "last.pth", +# ] +# found = [p for p in candidates if p.exists()] +# if not found: +# raise FileNotFoundError( +# f"Could not find weights file. Set WEIGHTS_REFINER or put a weights .pth under {models_dir}" +# ) +# weights_path = found[0] + +# # MLWeedDetector already handles Resize(224) + ImageNet normalization +# self.refiner = MLWeedDetector(weights_path=str(weights_path), device=str(self.device)) + +# # ---- Parameters configurable via .env ---- +# self.min_component_area = int(os.getenv("MIN_COMPONENT_AREA", "200")) # Filter out small connected components +# self.min_bbox_area = int(os.getenv("MIN_BBOX_AREA", "150")) # Filter out small bounding boxes +# self.bin_thresh = int(os.getenv("REFINED_BIN_THRESH", "128")) # Binarization threshold after refinement +# self.conf_area_norm = float(os.getenv("CONF_AREA_NORM", "10000")) # Normalize confidence by area +# self.coarse_method = os.getenv("COARSE_METHOD", "OTSU").upper() # OTSU / HSV_GREEN +# self.max_infer_side = int(os.getenv("MAX_INFER_SIDE", "0")) # 0 = no global downscale + +# # ---- Alerting (JSON POST) ---- +# self.alert_thresh = float(os.getenv("ALERT_SEVERITY_THRESH", "0.3")) +# self.alert_url = os.getenv("ALERT_URL", "http://localhost:8090/alerts") +# self.device_id = os.getenv("DEVICE_ID", "camera-12") +# self.area_name = os.getenv("AREA_NAME", "") or None # optional + +# # ---------- Main API ---------- + +# def run( +# self, +# bgr_img: np.ndarray, +# return_mask: bool = False, +# *, +# # Optional context to include in the alert JSON if available: +# image_url: Optional[str] = None, +# lat: Optional[float] = None, +# lon: Optional[float] = None, +# alert_type: str = "disease_detected", +# meta: Optional[Dict[str, Any]] = None, +# ) -> Union[List[Detection], Tuple[np.ndarray, List[Detection]]]: +# """ +# :param bgr_img: OpenCV image in BGR format +# :param return_mask: If True, also return the refined mask (uint8 0/255) +# :param image_url: Optional image URL for the alert JSON +# :param lat: Optional latitude for the alert JSON +# :param lon: Optional longitude for the alert JSON +# :param alert_type: Alert type string for the alert JSON +# :param meta: Optional metadata dict to include in the alert JSON +# """ +# # Ensure contiguous to prevent negative strides +# bgr = np.ascontiguousarray(bgr_img) + +# bgr = self._maybe_downscale(bgr) +# coarse = self._make_coarse(bgr) +# # Ensure contiguous before refinement +# coarse = np.ascontiguousarray(coarse) + +# refined = self._refine_with_model(bgr, coarse) # 0..255 +# refined_bin = self._binarize(refined, self.bin_thresh) # 0/255 +# refined_bin = self._remove_small(refined_bin, self.min_component_area) + +# detections = self._mask_to_detections(refined_bin) + +# # ---- Compute severity from detections and POST JSON if above threshold ---- +# severity = self._severity_from_detections(detections) +# if severity > self.alert_thresh: +# # For "confidence" field, we send the same scalar as severity by default. +# # Replace if you prefer a different definition. +# self._post_alert_json( +# severity=severity, +# alert_type=alert_type, +# image_url=image_url, +# lat=lat, +# lon=lon, +# confidence=severity, +# meta=meta, +# ) + +# if return_mask: +# return refined_bin, detections +# return detections + +# # ---------- Processing helpers ---------- + +# def _maybe_downscale(self, bgr: np.ndarray) -> np.ndarray: +# if not self.max_infer_side or self.max_infer_side <= 0: +# return bgr +# h, w = bgr.shape[:2] +# m = max(h, w) +# if m <= self.max_infer_side: +# return bgr +# scale = self.max_infer_side / float(m) +# new_w, new_h = int(w * scale), int(h * scale) +# out = cv2.resize(bgr, (new_w, new_h), interpolation=cv2.INTER_AREA) +# return np.ascontiguousarray(out) + +# def _make_coarse(self, bgr: np.ndarray) -> np.ndarray: +# """ +# Coarse mask: +# - OTSU (default) +# - or HSV_GREEN if COARSE_METHOD=HSV_GREEN +# """ +# if self.coarse_method == "HSV_GREEN": +# hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) +# # Generic green range; you can calibrate according to your data: +# lower = np.array([35, 40, 20], dtype=np.uint8) +# upper = np.array([85, 255, 255], dtype=np.uint8) +# mask = cv2.inRange(hsv, lower, upper) # 0/255 +# return np.ascontiguousarray(mask) + +# # OTSU (default) +# gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) +# _, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) +# return np.ascontiguousarray(mask) + +# def _refine_with_model(self, bgr: np.ndarray, coarse: np.ndarray) -> np.ndarray: +# """ +# Refinement using your trained model. +# MLWeedDetector.score_mask(bgr, coarse) -> mask 0..255 +# """ +# # Ensure contiguous to avoid negative strides inside the refiner +# bgr = np.ascontiguousarray(bgr) +# coarse = np.ascontiguousarray(coarse) + +# refined = self.refiner.score_mask(bgr, coarse) +# if refined.dtype != np.uint8: +# refined = np.clip(refined, 0, 255).astype(np.uint8) +# return refined + +# def _binarize(self, mask: np.ndarray, thresh: int) -> np.ndarray: +# if mask.dtype != np.uint8: +# mask = np.clip(mask, 0, 255).astype(np.uint8) +# _, mask_bin = cv2.threshold(mask, thresh, 255, cv2.THRESH_BINARY) +# return np.ascontiguousarray(mask_bin) + +# def _remove_small(self, mask_bin: np.ndarray, min_area: int) -> np.ndarray: +# if min_area <= 1: +# return mask_bin +# m01 = (mask_bin > 0).astype(np.uint8) +# num_labels, labels = cv2.connectedComponents(m01) +# out = np.zeros_like(m01) +# for i in range(1, num_labels): # 0 = background +# comp = (labels == i) +# if int(comp.sum()) >= min_area: +# out[comp] = 1 +# return (out * 255).astype(np.uint8) + +# def _mask_to_detections(self, mask_bin: np.ndarray) -> List[Detection]: +# num, labels, stats, _ = cv2.connectedComponentsWithStats( +# (mask_bin > 0).astype(np.uint8), connectivity=8 +# ) +# dets: List[Detection] = [] +# for i in range(1, num): # 0 = background +# x, y, w, h, area = stats[i] +# if area < self.min_component_area: +# continue +# if (w * h) < self.min_bbox_area: +# continue +# conf = float(min(1.0, area / max(1.0, self.conf_area_norm))) +# dets.append( +# Detection( +# x=float(x), y=float(y), w=float(w), h=float(h), +# area=float(area), confidence=conf, label="disease" +# ) +# ) +# return dets + +# # ---------- Severity & Alert JSON ---------- + +# def _severity_from_detections(self, dets: List[Detection]) -> float: +# """ +# Define severity from detections. +# Default: max confidence over all detections. +# You can change this to sum/mean/etc if needed. +# """ +# return max((d.confidence for d in dets), default=0.0) + +# def _post_alert_json( +# self, +# *, +# severity: float, +# alert_type: str, +# image_url: Optional[str], +# lat: Optional[float], +# lon: Optional[float], +# confidence: Optional[float], +# meta: Optional[Dict[str, Any]], +# ) -> None: +# """ +# POST a JSON alert to the configured Alertmanager URL. +# If your Alertmanager expects a list of alerts, send [payload] instead of payload. +# """ +# payload: Dict[str, Any] = { +# # --- Required fields --- +# "alert_id": str(uuid.uuid4()), +# "alert_type": alert_type, +# "device_id": self.device_id, +# "started_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), +# # --- Optional / dynamics --- +# "severity": float(severity), +# } +# if confidence is not None: +# payload["confidence"] = float(confidence) +# if self.area_name: +# payload["area"] = self.area_name +# if lat is not None: +# payload["lat"] = float(lat) +# if lon is not None: +# payload["lon"] = float(lon) +# if image_url: +# payload["image_url"] = image_url +# if meta: +# payload["meta"] = meta + +# # Some setups require a list of alerts: requests.post(self.alert_url, json=[payload], timeout=5) +# requests.post(self.alert_url, json=payload, timeout=5) + + +# # Optional: quick local test (won't run in production compose) +# if __name__ == "__main__": +# # Minimal smoke test for severity/POST path (won't run inference here). +# dets = [ +# Detection(x=0, y=0, w=10, h=10, area=4000, confidence=0.2), +# Detection(x=15, y=15, w=20, h=20, area=9000, confidence=0.6), +# ] +# dd = DiseaseDetector() +# sev = dd._severity_from_detections(dets) +# print("Severity example:", sev) +# if sev > dd.alert_thresh: +# dd._post_alert_json( +# severity=sev, alert_type="disease_detected", +# image_url=None, lat=None, lon=None, +# confidence=sev, meta={"note": "dry run"}, +# ) diff --git a/AgCloud/services/weed_detection/src/pipeline/__init__.py b/AgCloud/services/weed_detection/src/pipeline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/AgCloud/services/weed_detection/src/pipeline/config.py b/AgCloud/services/weed_detection/src/pipeline/config.py new file mode 100644 index 000000000..18d696e0a --- /dev/null +++ b/AgCloud/services/weed_detection/src/pipeline/config.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +import os +from pathlib import Path + +# Try to load env files both from project root and from agri_baseline/.env +try: + from dotenv import load_dotenv # type: ignore + load_dotenv(dotenv_path=Path("agri_baseline/.env"), override=False) + load_dotenv(override=False) +except Exception: + pass + +# Prefer standard name DATABASE_URL; fallback to DB_URL; finally default to localhost:5432 +DB_URL: str = ( + os.getenv("DATABASE_URL") + or os.getenv("DB_URL") + or "postgresql+psycopg2://missions_user:pg123@localhost:5432/missions_db" +) + +IMAGES_DIR = os.getenv("IMAGES_DIR", "./data/images") +BATCH_SIZE = int(os.getenv("BATCH_SIZE", 64)) +MAX_WORKERS = int(os.getenv("MAX_WORKERS", 4)) +MIN_BBOX_AREA = int(os.getenv("MIN_BBOX_AREA", 60)) +MIN_COMPONENT_AREA = int(os.getenv("MIN_COMPONENT_AREA", 200)) diff --git a/AgCloud/services/weed_detection/src/pipeline/db.py b/AgCloud/services/weed_detection/src/pipeline/db.py new file mode 100644 index 000000000..5fb271655 --- /dev/null +++ b/AgCloud/services/weed_detection/src/pipeline/db.py @@ -0,0 +1,28 @@ +# /src/pipeline/db.py +from __future__ import annotations +import os +from sqlalchemy import create_engine, text + +def get_engine(): + db_url = os.getenv("DB_URL") + if not db_url: + raise RuntimeError("DB_URL is not set in environment") + # echo=False לשקט; אפשר True לדיבוג + return create_engine(db_url, future=True) + +# משפטי INSERT בהתאם לשדות שה-Runner מזין +# הערה: מיועד ל-PostgreSQL + PostGIS. אם SQLite – צריך להתאים (לשמור WKT כ-TEXT). +INSERT_DET = text(""" +INSERT INTO anomalies (mission_id, device_id, ts, anomaly_type_id, severity, details, geom) +VALUES (:mission_id, :device_id, :ts, :anomaly_type_id, :severity, CAST(:details AS JSONB), + ST_GeomFromText(:wkt_geom, 4326)) +""") + +INSERT_COUNT = text(""" +INSERT INTO tile_stats (mission_id, tile_id, anomaly_score, geom) +VALUES (:mission_id, :tile_id, :anomaly_score, ST_GeomFromText(:wkt_geom, 4326)) +""") + +INSERT_QA = text(""" +INSERT INTO qa_runs (details) VALUES (CAST(:details AS JSONB)) +""") diff --git a/AgCloud/services/weed_detection/src/pipeline/logging_setup.py b/AgCloud/services/weed_detection/src/pipeline/logging_setup.py new file mode 100644 index 000000000..9904581c8 --- /dev/null +++ b/AgCloud/services/weed_detection/src/pipeline/logging_setup.py @@ -0,0 +1,11 @@ +# /src/pipeline/logging_setup.py +import logging +import os + +def setup_logging(): + level = os.getenv("LOG_LEVEL", "INFO").upper() + logging.basicConfig( + level=getattr(logging, level, logging.INFO), + format="%(asctime)s | %(levelname)s | %(message)s", + ) + return logging.getLogger("agri-baseline") diff --git a/AgCloud/services/weed_detection/src/pipeline/utils.py b/AgCloud/services/weed_detection/src/pipeline/utils.py new file mode 100644 index 000000000..9dfdc8bb6 --- /dev/null +++ b/AgCloud/services/weed_detection/src/pipeline/utils.py @@ -0,0 +1,25 @@ +# /src/pipeline/utils.py +from __future__ import annotations +from pathlib import Path +import cv2 +import numpy as np + + +def load_image(path: str | Path): + p = Path(path) + img = cv2.imread(str(p), cv2.IMREAD_COLOR) + if img is None: + raise FileNotFoundError(f"Failed to load image: {p}") + img = np.ascontiguousarray(img) + h, w = img.shape[:2] + return img, w, h # זה הפורמט שה-Runner שלך מצפה לו + +def image_id_from_path(p: str | Path) -> str: + return Path(p).stem + +def clamp_bbox(x: int, y: int, w: int, h: int, W: int, H: int): + x = max(0, min(x, W - 1)) + y = max(0, min(y, H - 1)) + w = max(0, min(w, W - x)) + h = max(0, min(h, H - y)) + return x, y, w, h diff --git a/AgCloud/simulators/.env.example b/AgCloud/simulators/.env.example new file mode 100644 index 000000000..429fc70fc --- /dev/null +++ b/AgCloud/simulators/.env.example @@ -0,0 +1,19 @@ +# --- General --- +IMAGES_DIR=/data/images +META_DIR=/data/metadata +CAMERA_ID=drone-01 + +# --- Data broker (Minio bridge) --- +MQTT_HOST_DATA=large-mosquitto +MQTT_PORT_DATA=1885 +MQTT_TOPIC_DATA=MQTT/imagery/air + +# --- Meta broker (kafka bridge) --- +MQTT_HOST_META=mosquitto +MQTT_PORT_META=1883 +MQTT_TOPIC_META=mqtt/aerial/images/metadata + +# --- Publishing behavior --- +INTERVAL_CHECK=10 +INTERVAL_PUBLISH=10 +MQTT_QOS=1 diff --git a/AgCloud/simulators/Dockerfile b/AgCloud/simulators/Dockerfile new file mode 100644 index 000000000..7eeed2e60 --- /dev/null +++ b/AgCloud/simulators/Dockerfile @@ -0,0 +1,23 @@ +# Use official Python slim image +FROM python:3.12-slim + +# Copy the NetFree certificate into the container +COPY certs/*.crt /usr/local/share/ca-certificates/ + +# Install system dependencies, add the certificate, and clean cache +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + update-ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +RUN pip install --no-cache-dir paho-mqtt + +# Set working directory +WORKDIR /app + +# Copy project files +COPY data_publisher.py . + +# Run the Python script +CMD ["python", "-u", "/app/data_publisher.py"] diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081522Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081522Z.jpg new file mode 100644 index 000000000..e455afb5e Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081522Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081532Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081532Z.jpg new file mode 100644 index 000000000..666b88b1a Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081532Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081542Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081542Z.jpg new file mode 100644 index 000000000..041fd4752 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081542Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081552Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081552Z.jpg new file mode 100644 index 000000000..900499f7b Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081552Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081602Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081602Z.jpg new file mode 100644 index 000000000..3795cef26 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081602Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081612Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081612Z.jpg new file mode 100644 index 000000000..659cca8ca Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081612Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081622Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081622Z.jpg new file mode 100644 index 000000000..8fb3b24b3 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081622Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081632Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081632Z.jpg new file mode 100644 index 000000000..762713c06 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081632Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081642Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081642Z.jpg new file mode 100644 index 000000000..3e5df8308 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081642Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081652Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081652Z.jpg new file mode 100644 index 000000000..af2cda331 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081652Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081702Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081702Z.jpg new file mode 100644 index 000000000..347533a27 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081702Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081712Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081712Z.jpg new file mode 100644 index 000000000..9f71df2f7 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081712Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081722Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081722Z.jpg new file mode 100644 index 000000000..5bd2dd3b2 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081722Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081732Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081732Z.jpg new file mode 100644 index 000000000..5fe4d6cb2 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081732Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081742Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081742Z.jpg new file mode 100644 index 000000000..7e974e0a4 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081742Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081752Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081752Z.jpg new file mode 100644 index 000000000..a3e7d6a59 Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081752Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/DRN-482A_20251019T081802Z.jpg b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081802Z.jpg new file mode 100644 index 000000000..c49bd162d Binary files /dev/null and b/AgCloud/simulators/data/air/images/DRN-482A_20251019T081802Z.jpg differ diff --git a/AgCloud/simulators/data/air/images/f123.jpg b/AgCloud/simulators/data/air/images/f123.jpg new file mode 100644 index 000000000..39b028ae8 Binary files /dev/null and b/AgCloud/simulators/data/air/images/f123.jpg differ diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081522Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081522Z.json new file mode 100644 index 000000000..ab64f2775 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081522Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081522Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:15:22Z", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.9681 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081532Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081532Z.json new file mode 100644 index 000000000..331839829 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081532Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081532Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:15:32Z", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.968347 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081542Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081542Z.json new file mode 100644 index 000000000..16ba28f85 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081542Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081542Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:15:42Z", + "gis_origin": { + "latitude": 31.8954, + "longitude": 34.967853 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081552Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081552Z.json new file mode 100644 index 000000000..6d923e2df --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081552Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081552Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:15:52Z", + "gis_origin": { + "latitude": 31.8954, + "longitude": 34.9681 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081602Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081602Z.json new file mode 100644 index 000000000..a21af0638 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081602Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081602Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:16:02Z", + "gis_origin": { + "latitude": 31.8954, + "longitude": 34.968347 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081612Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081612Z.json new file mode 100644 index 000000000..c1ce72ba0 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081612Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081612Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:16:12Z", + "gis_origin": { + "latitude": 31.89519, + "longitude": 34.967853 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081622Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081622Z.json new file mode 100644 index 000000000..31cf9e323 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081622Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081622Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:16:22Z", + "gis_origin": { + "latitude": 31.89519, + "longitude": 34.9681 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081632Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081632Z.json new file mode 100644 index 000000000..0d25cde7a --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081632Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081632Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:16:32Z", + "gis_origin": { + "latitude": 31.89519, + "longitude": 34.968347 + }, + "altitude_m": 120.0, + "done": true +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081642Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081642Z.json new file mode 100644 index 000000000..5a1e829f6 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081642Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081642Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:16:42Z", + "gis_origin": { + "latitude": 31.89691, + "longitude": 34.969653 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081652Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081652Z.json new file mode 100644 index 000000000..40e204264 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081652Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081652Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:16:52Z", + "gis_origin": { + "latitude": 31.89691, + "longitude": 34.9699 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081702Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081702Z.json new file mode 100644 index 000000000..0c8144e0b --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081702Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081702Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:17:02Z", + "gis_origin": { + "latitude": 31.89691, + "longitude": 34.970147 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081712Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081712Z.json new file mode 100644 index 000000000..eac5d8daa --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081712Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081712Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:17:12Z", + "gis_origin": { + "latitude": 31.8967, + "longitude": 34.969653 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081722Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081722Z.json new file mode 100644 index 000000000..c01d65f21 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081722Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081722Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:17:22Z", + "gis_origin": { + "latitude": 31.8967, + "longitude": 34.9699 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081732Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081732Z.json new file mode 100644 index 000000000..b153a4f4e --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081732Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081732Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:17:32Z", + "gis_origin": { + "latitude": 31.8967, + "longitude": 34.970147 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081742Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081742Z.json new file mode 100644 index 000000000..18817e550 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081742Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081742Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:17:42Z", + "gis_origin": { + "latitude": 31.89649, + "longitude": 34.969653 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081752Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081752Z.json new file mode 100644 index 000000000..49ab29568 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081752Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081752Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:17:52Z", + "gis_origin": { + "latitude": 31.89649, + "longitude": 34.9699 + }, + "altitude_m": 119.3, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081802Z.json b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081802Z.json new file mode 100644 index 000000000..d3deb9bc5 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/DRN-482A_20251019T081802Z.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081802Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:18:02Z", + "gis_origin": { + "latitude": 31.89649, + "longitude": 34.970147 + }, + "altitude_m": 119.3, + "done": true +} \ No newline at end of file diff --git a/AgCloud/simulators/data/air/metadata/f123.json b/AgCloud/simulators/data/air/metadata/f123.json new file mode 100644 index 000000000..e6cb22408 --- /dev/null +++ b/AgCloud/simulators/data/air/metadata/f123.json @@ -0,0 +1,11 @@ +{ + "file_name": "DRN-482A_20251019T081512Z.jpg", + "drone_id": "DRN-482A", + "capture_time": "2025-10-19T08:15:12Z", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.967853 + }, + "altitude_m": 120.0, + "done": false +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0021.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0021.jpg new file mode 100644 index 000000000..a612abf35 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0021.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0022.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0022.jpg new file mode 100644 index 000000000..2a8c78282 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0022.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0023.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0023.jpg new file mode 100644 index 000000000..89f217ced Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0023.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0024.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0024.jpg new file mode 100644 index 000000000..5a9c99b7d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0024.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0025.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0025.jpg new file mode 100644 index 000000000..eb493ecc3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0025.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0026.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0026.jpg new file mode 100644 index 000000000..d33eda186 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0026.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0027.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0027.jpg new file mode 100644 index 000000000..eb7a6f9e1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0027.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0028.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0028.jpg new file mode 100644 index 000000000..eb388051a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0028.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0029.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0029.jpg new file mode 100644 index 000000000..1dd8ce557 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0029.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0030.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0030.jpg new file mode 100644 index 000000000..9f5148b82 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0030.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0031.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0031.jpg new file mode 100644 index 000000000..f3c5cdff9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0031.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0032.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0032.jpg new file mode 100644 index 000000000..166d45aac Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0032.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0033.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0033.jpg new file mode 100644 index 000000000..25bb68499 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0033.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0034.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0034.jpg new file mode 100644 index 000000000..86f37a6a9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0034.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0035.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0035.jpg new file mode 100644 index 000000000..434876b35 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0035.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0036.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0036.jpg new file mode 100644 index 000000000..f6060f342 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0036.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0037.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0037.jpg new file mode 100644 index 000000000..d70f6ba88 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0037.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0038.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0038.jpg new file mode 100644 index 000000000..21c19b781 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0038.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0039.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0039.jpg new file mode 100644 index 000000000..919d31311 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0039.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0040.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0040.jpg new file mode 100644 index 000000000..8fb938e74 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0040.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0041.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0041.jpg new file mode 100644 index 000000000..4f29f280e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0041.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0042.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0042.jpg new file mode 100644 index 000000000..32416df4b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0042.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0043.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0043.jpg new file mode 100644 index 000000000..09a55fad6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0043.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0044.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0044.jpg new file mode 100644 index 000000000..f99ad1cad Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0044.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0045.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0045.jpg new file mode 100644 index 000000000..ff5574583 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0045.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0046.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0046.jpg new file mode 100644 index 000000000..6a244097f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0046.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0047.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0047.jpg new file mode 100644 index 000000000..c074b39fb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0047.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0048.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0048.jpg new file mode 100644 index 000000000..cade6b9a6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0048.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0049.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0049.jpg new file mode 100644 index 000000000..41d13e137 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0049.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0050.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0050.jpg new file mode 100644 index 000000000..ab37ac242 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0050.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0051.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0051.jpg new file mode 100644 index 000000000..23249f758 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0051.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0052.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0052.jpg new file mode 100644 index 000000000..8e9a34caf Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0052.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0053.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0053.jpg new file mode 100644 index 000000000..327e2748c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0053.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0054.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0054.jpg new file mode 100644 index 000000000..1cf75e5fa Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0054.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0055.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0055.jpg new file mode 100644 index 000000000..86afad386 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0055.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0056.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0056.jpg new file mode 100644 index 000000000..3a52453ed Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0056.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0057.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0057.jpg new file mode 100644 index 000000000..df1ed93a7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0057.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0058.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0058.jpg new file mode 100644 index 000000000..20d26557b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0058.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0059.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0059.jpg new file mode 100644 index 000000000..40ac46bf1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0059.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0060.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0060.jpg new file mode 100644 index 000000000..e7136dbd0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0060.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0061.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0061.jpg new file mode 100644 index 000000000..3b2c4d9f2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0061.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0062.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0062.jpg new file mode 100644 index 000000000..c8f5a1bae Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0062.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0063.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0063.jpg new file mode 100644 index 000000000..2039846e0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0063.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0064.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0064.jpg new file mode 100644 index 000000000..59360882f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0064.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0065.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0065.jpg new file mode 100644 index 000000000..554f0e6bf Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0065.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0066.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0066.jpg new file mode 100644 index 000000000..92a995ad8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0066.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0067.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0067.jpg new file mode 100644 index 000000000..d31be41ba Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0067.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0068.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0068.jpg new file mode 100644 index 000000000..f19c148f3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0068.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0069.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0069.jpg new file mode 100644 index 000000000..cb0be50db Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0069.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0070.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0070.jpg new file mode 100644 index 000000000..826214668 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0070.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0071.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0071.jpg new file mode 100644 index 000000000..c47e2c1b9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0071.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0072.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0072.jpg new file mode 100644 index 000000000..cf297336e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0072.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0073.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0073.jpg new file mode 100644 index 000000000..2ec065fca Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0073.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0074.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0074.jpg new file mode 100644 index 000000000..0a83701ee Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0074.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0075.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0075.jpg new file mode 100644 index 000000000..4459bddd9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0075.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0076.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0076.jpg new file mode 100644 index 000000000..135946866 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0076.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0077.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0077.jpg new file mode 100644 index 000000000..025ec469d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0077.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0078.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0078.jpg new file mode 100644 index 000000000..142cf2052 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0078.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0079.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0079.jpg new file mode 100644 index 000000000..b55b47a49 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0079.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0080.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0080.jpg new file mode 100644 index 000000000..a7082f98f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0080.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0081.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0081.jpg new file mode 100644 index 000000000..cbda67583 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0081.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0082.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0082.jpg new file mode 100644 index 000000000..c5cb929b8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0082.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0083.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0083.jpg new file mode 100644 index 000000000..83d149871 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0083.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0084.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0084.jpg new file mode 100644 index 000000000..8d6eca44a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0084.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0085.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0085.jpg new file mode 100644 index 000000000..b9f2eb6fe Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0085.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0086.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0086.jpg new file mode 100644 index 000000000..ae9586117 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0086.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0087.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0087.jpg new file mode 100644 index 000000000..6c8441d51 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0087.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0088.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0088.jpg new file mode 100644 index 000000000..2724f1f98 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0088.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0089.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0089.jpg new file mode 100644 index 000000000..b93b03674 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0089.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0090.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0090.jpg new file mode 100644 index 000000000..fa6c38b2d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0090.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0091.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0091.jpg new file mode 100644 index 000000000..94182a9bc Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0091.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0092.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0092.jpg new file mode 100644 index 000000000..bfe8bf79d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0092.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0093.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0093.jpg new file mode 100644 index 000000000..e4df15c22 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0093.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0094.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0094.jpg new file mode 100644 index 000000000..79e7fc184 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0094.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0095.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0095.jpg new file mode 100644 index 000000000..8b2d65469 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0095.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0096.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0096.jpg new file mode 100644 index 000000000..870571acd Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0096.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0097.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0097.jpg new file mode 100644 index 000000000..d1b39845d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0097.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0098.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0098.jpg new file mode 100644 index 000000000..9b2fe354f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0098.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0099.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0099.jpg new file mode 100644 index 000000000..577268cf4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0099.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0100.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0100.jpg new file mode 100644 index 000000000..d49f1aa50 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0100.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0101.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0101.jpg new file mode 100644 index 000000000..8bc9bfd85 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0101.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0102.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0102.jpg new file mode 100644 index 000000000..09465ae1d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0102.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0103.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0103.jpg new file mode 100644 index 000000000..0bae87da1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0103.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0104.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0104.jpg new file mode 100644 index 000000000..73292f824 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0104.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0105.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0105.jpg new file mode 100644 index 000000000..49058d51a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0105.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0106.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0106.jpg new file mode 100644 index 000000000..ab39ef74d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0106.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0107.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0107.jpg new file mode 100644 index 000000000..2738ce92b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0107.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0108.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0108.jpg new file mode 100644 index 000000000..fef66f631 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0108.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0109.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0109.jpg new file mode 100644 index 000000000..c3b8d8151 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0109.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0110.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0110.jpg new file mode 100644 index 000000000..e5cd6fc94 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0110.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0111.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0111.jpg new file mode 100644 index 000000000..856e12e02 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0111.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0112.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0112.jpg new file mode 100644 index 000000000..fe93797c0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0112.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0113.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0113.jpg new file mode 100644 index 000000000..6ce5ebc99 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0113.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0114.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0114.jpg new file mode 100644 index 000000000..017341675 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0114.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0115.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0115.jpg new file mode 100644 index 000000000..9bc80bee0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0115.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0116.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0116.jpg new file mode 100644 index 000000000..70957e3b1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0116.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0117.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0117.jpg new file mode 100644 index 000000000..5c4dd2914 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0117.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0118.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0118.jpg new file mode 100644 index 000000000..81bcf8fc2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0118.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0119.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0119.jpg new file mode 100644 index 000000000..0510f7ad5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0119.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0120.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0120.jpg new file mode 100644 index 000000000..3abfb639f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0120.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0121.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0121.jpg new file mode 100644 index 000000000..7da1e75bd Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0121.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0122.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0122.jpg new file mode 100644 index 000000000..b7f2fa619 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0122.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0123.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0123.jpg new file mode 100644 index 000000000..7fac43f8b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0123.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0124.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0124.jpg new file mode 100644 index 000000000..b2f31f6af Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0124.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0125.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0125.jpg new file mode 100644 index 000000000..8a6478852 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0125.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0126.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0126.jpg new file mode 100644 index 000000000..d0de19ee4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0126.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/images/frame_0127.jpg b/AgCloud/simulators/data/security/cam-central-01/images/frame_0127.jpg new file mode 100644 index 000000000..005104868 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-central-01/images/frame_0127.jpg differ diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0021.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0021.json new file mode 100644 index 000000000..2087dd337 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0021.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043122609, + "file_name": "frame_0021.jpg", + "frame_idx": 12 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0022.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0022.json new file mode 100644 index 000000000..8f4ad8f5a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0022.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043123609, + "file_name": "frame_0022.jpg", + "frame_idx": 13 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0023.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0023.json new file mode 100644 index 000000000..11543e55b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0023.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043124609, + "file_name": "frame_0023.jpg", + "frame_idx": 14 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0024.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0024.json new file mode 100644 index 000000000..8ab01bd36 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0024.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043125609, + "file_name": "frame_0024.jpg", + "frame_idx": 15 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0025.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0025.json new file mode 100644 index 000000000..2a2cdca7f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0025.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043126609, + "file_name": "frame_0025.jpg", + "frame_idx": 16 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0026.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0026.json new file mode 100644 index 000000000..b520c939d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0026.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043127609, + "file_name": "frame_0026.jpg", + "frame_idx": 17 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0027.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0027.json new file mode 100644 index 000000000..d88b91c01 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0027.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043128609, + "file_name": "frame_0027.jpg", + "frame_idx": 18 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0028.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0028.json new file mode 100644 index 000000000..5b0de88cb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0028.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043129609, + "file_name": "frame_0028.jpg", + "frame_idx": 19 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0029.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0029.json new file mode 100644 index 000000000..e41013fed --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0029.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043130609, + "file_name": "frame_0029.jpg", + "frame_idx": 20 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0030.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0030.json new file mode 100644 index 000000000..80ce5568f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0030.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043131609, + "file_name": "frame_0030.jpg", + "frame_idx": 21 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0031.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0031.json new file mode 100644 index 000000000..2ab230a30 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0031.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043132609, + "file_name": "frame_0031.jpg", + "frame_idx": 22 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0032.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0032.json new file mode 100644 index 000000000..812fe0191 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0032.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043133609, + "file_name": "frame_0032.jpg", + "frame_idx": 23 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0033.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0033.json new file mode 100644 index 000000000..34e519801 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0033.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043134609, + "file_name": "frame_0033.jpg", + "frame_idx": 24 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0034.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0034.json new file mode 100644 index 000000000..02fd91ac4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0034.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043135609, + "file_name": "frame_0034.jpg", + "frame_idx": 25 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0035.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0035.json new file mode 100644 index 000000000..e87f32147 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0035.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043136609, + "file_name": "frame_0035.jpg", + "frame_idx": 26 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0036.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0036.json new file mode 100644 index 000000000..de4c2cc3c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0036.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043137609, + "file_name": "frame_0036.jpg", + "frame_idx": 27 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0037.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0037.json new file mode 100644 index 000000000..732a16b77 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0037.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043138609, + "file_name": "frame_0037.jpg", + "frame_idx": 28 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0038.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0038.json new file mode 100644 index 000000000..077e93971 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0038.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043139609, + "file_name": "frame_0038.jpg", + "frame_idx": 29 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0039.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0039.json new file mode 100644 index 000000000..a87485024 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0039.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043140609, + "file_name": "frame_0039.jpg", + "frame_idx": 30 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0040.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0040.json new file mode 100644 index 000000000..5892fdb20 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0040.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043141609, + "file_name": "frame_0040.jpg", + "frame_idx": 31 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0041.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0041.json new file mode 100644 index 000000000..2f3ee499a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0041.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043142609, + "file_name": "frame_0041.jpg", + "frame_idx": 32 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0042.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0042.json new file mode 100644 index 000000000..087573a36 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0042.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043143609, + "file_name": "frame_0042.jpg", + "frame_idx": 33 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0043.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0043.json new file mode 100644 index 000000000..e870acffe --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0043.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043144609, + "file_name": "frame_0043.jpg", + "frame_idx": 34 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0044.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0044.json new file mode 100644 index 000000000..0ace2f17a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0044.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043145609, + "file_name": "frame_0044.jpg", + "frame_idx": 35 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0045.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0045.json new file mode 100644 index 000000000..6c40660ae --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0045.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043146609, + "file_name": "frame_0045.jpg", + "frame_idx": 36 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0046.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0046.json new file mode 100644 index 000000000..bf94d3838 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0046.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043147609, + "file_name": "frame_0046.jpg", + "frame_idx": 37 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0047.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0047.json new file mode 100644 index 000000000..93e7b8e4f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0047.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043148609, + "file_name": "frame_0047.jpg", + "frame_idx": 38 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0048.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0048.json new file mode 100644 index 000000000..250972f41 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0048.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043149609, + "file_name": "frame_0048.jpg", + "frame_idx": 39 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0049.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0049.json new file mode 100644 index 000000000..dbe4134c4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0049.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043150609, + "file_name": "frame_0049.jpg", + "frame_idx": 40 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0050.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0050.json new file mode 100644 index 000000000..ab236b234 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0050.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043151609, + "file_name": "frame_0050.jpg", + "frame_idx": 41 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0051.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0051.json new file mode 100644 index 000000000..ce6ebc7c5 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0051.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043152609, + "file_name": "frame_0051.jpg", + "frame_idx": 42 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0052.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0052.json new file mode 100644 index 000000000..e46b424b9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0052.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043153609, + "file_name": "frame_0052.jpg", + "frame_idx": 43 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0053.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0053.json new file mode 100644 index 000000000..ac8b4dc57 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0053.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043154609, + "file_name": "frame_0053.jpg", + "frame_idx": 44 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0054.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0054.json new file mode 100644 index 000000000..f562fc83f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0054.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043155609, + "file_name": "frame_0054.jpg", + "frame_idx": 45 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0055.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0055.json new file mode 100644 index 000000000..38314ffc0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0055.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043156609, + "file_name": "frame_0055.jpg", + "frame_idx": 46 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0056.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0056.json new file mode 100644 index 000000000..69afa7a9a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0056.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043157609, + "file_name": "frame_0056.jpg", + "frame_idx": 47 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0057.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0057.json new file mode 100644 index 000000000..fa4550478 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0057.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043158609, + "file_name": "frame_0057.jpg", + "frame_idx": 48 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0058.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0058.json new file mode 100644 index 000000000..04aceb638 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0058.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043159609, + "file_name": "frame_0058.jpg", + "frame_idx": 49 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0059.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0059.json new file mode 100644 index 000000000..87df6242e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0059.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043160609, + "file_name": "frame_0059.jpg", + "frame_idx": 50 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0060.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0060.json new file mode 100644 index 000000000..9ffb37d04 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0060.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043161609, + "file_name": "frame_0060.jpg", + "frame_idx": 51 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0061.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0061.json new file mode 100644 index 000000000..5ce85eaec --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0061.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043162609, + "file_name": "frame_0061.jpg", + "frame_idx": 52 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0062.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0062.json new file mode 100644 index 000000000..fae025967 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0062.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043163609, + "file_name": "frame_0062.jpg", + "frame_idx": 53 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0063.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0063.json new file mode 100644 index 000000000..4bdc25309 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0063.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043164609, + "file_name": "frame_0063.jpg", + "frame_idx": 54 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0064.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0064.json new file mode 100644 index 000000000..7f0bb6406 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0064.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043165609, + "file_name": "frame_0064.jpg", + "frame_idx": 55 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0065.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0065.json new file mode 100644 index 000000000..e06dc5b26 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0065.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043166609, + "file_name": "frame_0065.jpg", + "frame_idx": 56 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0066.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0066.json new file mode 100644 index 000000000..49491b582 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0066.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043167609, + "file_name": "frame_0066.jpg", + "frame_idx": 57 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0067.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0067.json new file mode 100644 index 000000000..30a960940 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0067.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043168609, + "file_name": "frame_0067.jpg", + "frame_idx": 58 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0068.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0068.json new file mode 100644 index 000000000..5f1c0fb2b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0068.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043169609, + "file_name": "frame_0068.jpg", + "frame_idx": 59 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0069.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0069.json new file mode 100644 index 000000000..72c711d65 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0069.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043170609, + "file_name": "frame_0069.jpg", + "frame_idx": 60 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0070.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0070.json new file mode 100644 index 000000000..bf7b0f130 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0070.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043171609, + "file_name": "frame_0070.jpg", + "frame_idx": 61 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0071.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0071.json new file mode 100644 index 000000000..f26339eb3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0071.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043172609, + "file_name": "frame_0071.jpg", + "frame_idx": 62 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0072.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0072.json new file mode 100644 index 000000000..2912d430e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0072.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043173609, + "file_name": "frame_0072.jpg", + "frame_idx": 63 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0073.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0073.json new file mode 100644 index 000000000..3d0110364 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0073.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043174609, + "file_name": "frame_0073.jpg", + "frame_idx": 64 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0074.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0074.json new file mode 100644 index 000000000..047561bfd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0074.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043175609, + "file_name": "frame_0074.jpg", + "frame_idx": 65 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0075.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0075.json new file mode 100644 index 000000000..19ac9391c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0075.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043176609, + "file_name": "frame_0075.jpg", + "frame_idx": 66 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0076.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0076.json new file mode 100644 index 000000000..782f061c2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0076.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043177609, + "file_name": "frame_0076.jpg", + "frame_idx": 67 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0077.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0077.json new file mode 100644 index 000000000..9d387f2da --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0077.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043178609, + "file_name": "frame_0077.jpg", + "frame_idx": 68 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0078.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0078.json new file mode 100644 index 000000000..a683ac215 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0078.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043179609, + "file_name": "frame_0078.jpg", + "frame_idx": 69 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0079.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0079.json new file mode 100644 index 000000000..56e412c96 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0079.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043180609, + "file_name": "frame_0079.jpg", + "frame_idx": 70 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0080.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0080.json new file mode 100644 index 000000000..7ce73ea5f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0080.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043181609, + "file_name": "frame_0080.jpg", + "frame_idx": 71 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0081.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0081.json new file mode 100644 index 000000000..9b313709f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0081.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043182609, + "file_name": "frame_0081.jpg", + "frame_idx": 72 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0082.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0082.json new file mode 100644 index 000000000..7f793d13c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0082.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043183609, + "file_name": "frame_0082.jpg", + "frame_idx": 73 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0083.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0083.json new file mode 100644 index 000000000..fc340f907 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0083.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043184609, + "file_name": "frame_0083.jpg", + "frame_idx": 74 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0084.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0084.json new file mode 100644 index 000000000..2dcc417d3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0084.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043185609, + "file_name": "frame_0084.jpg", + "frame_idx": 75 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0085.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0085.json new file mode 100644 index 000000000..c99d3c723 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0085.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043186609, + "file_name": "frame_0085.jpg", + "frame_idx": 76 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0086.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0086.json new file mode 100644 index 000000000..7a5fdbaca --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0086.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043187609, + "file_name": "frame_0086.jpg", + "frame_idx": 77 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0087.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0087.json new file mode 100644 index 000000000..09b1fcfc3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0087.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043188609, + "file_name": "frame_0087.jpg", + "frame_idx": 78 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0088.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0088.json new file mode 100644 index 000000000..4cfdc0ae1 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0088.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043189609, + "file_name": "frame_0088.jpg", + "frame_idx": 79 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0089.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0089.json new file mode 100644 index 000000000..5fb2d5003 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0089.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043190609, + "file_name": "frame_0089.jpg", + "frame_idx": 80 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0090.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0090.json new file mode 100644 index 000000000..33d8b56c1 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0090.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043191609, + "file_name": "frame_0090.jpg", + "frame_idx": 81 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0091.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0091.json new file mode 100644 index 000000000..8590b9f54 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0091.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043192609, + "file_name": "frame_0091.jpg", + "frame_idx": 82 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0092.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0092.json new file mode 100644 index 000000000..0c90c5908 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0092.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043193609, + "file_name": "frame_0092.jpg", + "frame_idx": 83 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0093.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0093.json new file mode 100644 index 000000000..ae58343f7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0093.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043194609, + "file_name": "frame_0093.jpg", + "frame_idx": 84 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0094.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0094.json new file mode 100644 index 000000000..ab0c4d715 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0094.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043195609, + "file_name": "frame_0094.jpg", + "frame_idx": 85 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0095.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0095.json new file mode 100644 index 000000000..ec25f6455 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0095.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043196609, + "file_name": "frame_0095.jpg", + "frame_idx": 86 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0096.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0096.json new file mode 100644 index 000000000..c6be5a9b0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0096.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043197609, + "file_name": "frame_0096.jpg", + "frame_idx": 87 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0097.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0097.json new file mode 100644 index 000000000..afceaf656 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0097.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043198609, + "file_name": "frame_0097.jpg", + "frame_idx": 88 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0098.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0098.json new file mode 100644 index 000000000..182f4ca0a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0098.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043199609, + "file_name": "frame_0098.jpg", + "frame_idx": 89 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0099.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0099.json new file mode 100644 index 000000000..c74bd3a50 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0099.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043200609, + "file_name": "frame_0099.jpg", + "frame_idx": 90 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0100.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0100.json new file mode 100644 index 000000000..45cf5025c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0100.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043201609, + "file_name": "frame_0100.jpg", + "frame_idx": 91 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0101.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0101.json new file mode 100644 index 000000000..51601be48 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0101.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043202609, + "file_name": "frame_0101.jpg", + "frame_idx": 92 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0102.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0102.json new file mode 100644 index 000000000..1a3faec76 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0102.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043203609, + "file_name": "frame_0102.jpg", + "frame_idx": 93 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0103.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0103.json new file mode 100644 index 000000000..544b1b12d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0103.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043204609, + "file_name": "frame_0103.jpg", + "frame_idx": 94 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0104.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0104.json new file mode 100644 index 000000000..77f9b153b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0104.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043205609, + "file_name": "frame_0104.jpg", + "frame_idx": 95 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0105.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0105.json new file mode 100644 index 000000000..c49d99e44 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0105.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043206609, + "file_name": "frame_0105.jpg", + "frame_idx": 96 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0106.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0106.json new file mode 100644 index 000000000..9793be294 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0106.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043207609, + "file_name": "frame_0106.jpg", + "frame_idx": 97 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0107.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0107.json new file mode 100644 index 000000000..da1ce5ec2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0107.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043208609, + "file_name": "frame_0107.jpg", + "frame_idx": 98 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0108.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0108.json new file mode 100644 index 000000000..c668353f6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0108.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043209609, + "file_name": "frame_0108.jpg", + "frame_idx": 99 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0109.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0109.json new file mode 100644 index 000000000..c6cc83b89 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0109.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043210609, + "file_name": "frame_0109.jpg", + "frame_idx": 100 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0110.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0110.json new file mode 100644 index 000000000..94406bbd6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0110.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043211609, + "file_name": "frame_0110.jpg", + "frame_idx": 101 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0111.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0111.json new file mode 100644 index 000000000..a5ea7e5ee --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0111.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043212609, + "file_name": "frame_0111.jpg", + "frame_idx": 102 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0112.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0112.json new file mode 100644 index 000000000..8c3ee3007 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0112.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043213609, + "file_name": "frame_0112.jpg", + "frame_idx": 103 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0113.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0113.json new file mode 100644 index 000000000..ad3277ca7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0113.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043214609, + "file_name": "frame_0113.jpg", + "frame_idx": 104 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0114.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0114.json new file mode 100644 index 000000000..7bc7111f5 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0114.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043215609, + "file_name": "frame_0114.jpg", + "frame_idx": 105 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0115.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0115.json new file mode 100644 index 000000000..8ca3e1e28 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0115.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043216609, + "file_name": "frame_0115.jpg", + "frame_idx": 106 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0116.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0116.json new file mode 100644 index 000000000..dd014917b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0116.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043217609, + "file_name": "frame_0116.jpg", + "frame_idx": 107 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0117.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0117.json new file mode 100644 index 000000000..84036eb03 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0117.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043218609, + "file_name": "frame_0117.jpg", + "frame_idx": 108 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0118.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0118.json new file mode 100644 index 000000000..214ca5ce9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0118.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043219609, + "file_name": "frame_0118.jpg", + "frame_idx": 109 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0119.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0119.json new file mode 100644 index 000000000..744617365 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0119.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043220609, + "file_name": "frame_0119.jpg", + "frame_idx": 110 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0120.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0120.json new file mode 100644 index 000000000..7bdf12598 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0120.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043221609, + "file_name": "frame_0120.jpg", + "frame_idx": 111 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0121.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0121.json new file mode 100644 index 000000000..93901b8fb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0121.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043222609, + "file_name": "frame_0121.jpg", + "frame_idx": 112 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0122.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0122.json new file mode 100644 index 000000000..615d50c5e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0122.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043223609, + "file_name": "frame_0122.jpg", + "frame_idx": 113 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0123.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0123.json new file mode 100644 index 000000000..58ae59307 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0123.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043224609, + "file_name": "frame_0123.jpg", + "frame_idx": 114 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0124.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0124.json new file mode 100644 index 000000000..1ba0a86f9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0124.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043225609, + "file_name": "frame_0124.jpg", + "frame_idx": 115 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0125.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0125.json new file mode 100644 index 000000000..1cdcb163f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0125.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043226609, + "file_name": "frame_0125.jpg", + "frame_idx": 116 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0126.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0126.json new file mode 100644 index 000000000..973a184ab --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0126.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043227609, + "file_name": "frame_0126.jpg", + "frame_idx": 117 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0127.json b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0127.json new file mode 100644 index 000000000..c86969f07 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-central-01/metadata/frame_0127.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-central-01", + "capture_time": 1763043228609, + "file_name": "frame_0127.jpg", + "frame_idx": 118 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0000.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0000.jpg new file mode 100644 index 000000000..f1e61e4e4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0000.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0001.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0001.jpg new file mode 100644 index 000000000..6b9966b25 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0001.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0002.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0002.jpg new file mode 100644 index 000000000..c08366b84 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0002.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0003.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0003.jpg new file mode 100644 index 000000000..ae33a700d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0003.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0004.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0004.jpg new file mode 100644 index 000000000..156ef4875 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0004.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0005.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0005.jpg new file mode 100644 index 000000000..821be5c00 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0005.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0006.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0006.jpg new file mode 100644 index 000000000..9c36dc99e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0006.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0007.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0007.jpg new file mode 100644 index 000000000..8f4130a66 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0007.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0008.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0008.jpg new file mode 100644 index 000000000..af7acb2c6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0008.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0009.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0009.jpg new file mode 100644 index 000000000..b19ff5eb2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0009.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0010.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0010.jpg new file mode 100644 index 000000000..d5ccde146 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0010.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0011.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0011.jpg new file mode 100644 index 000000000..d38fd1e24 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0011.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0012.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0012.jpg new file mode 100644 index 000000000..dd75ba52a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0012.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0013.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0013.jpg new file mode 100644 index 000000000..b85353467 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0013.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0014.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0014.jpg new file mode 100644 index 000000000..017ca1414 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0014.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0015.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0015.jpg new file mode 100644 index 000000000..0d374dcea Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0015.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0016.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0016.jpg new file mode 100644 index 000000000..5c01ea399 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0016.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0017.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0017.jpg new file mode 100644 index 000000000..e41b8cd70 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0017.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0018.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0018.jpg new file mode 100644 index 000000000..943deb9a3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0018.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0019.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0019.jpg new file mode 100644 index 000000000..a2706873b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0019.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0020.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0020.jpg new file mode 100644 index 000000000..f37822f42 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0020.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0021.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0021.jpg new file mode 100644 index 000000000..76eeea29a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0021.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0022.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0022.jpg new file mode 100644 index 000000000..fa50035fd Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0022.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0023.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0023.jpg new file mode 100644 index 000000000..3fb5159cf Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0023.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0024.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0024.jpg new file mode 100644 index 000000000..0729b8248 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0024.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0025.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0025.jpg new file mode 100644 index 000000000..d4d994955 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0025.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0026.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0026.jpg new file mode 100644 index 000000000..0388ebd51 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0026.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0027.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0027.jpg new file mode 100644 index 000000000..65f412ee9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0027.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0028.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0028.jpg new file mode 100644 index 000000000..c5999173c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0028.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0029.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0029.jpg new file mode 100644 index 000000000..59a7fff5a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0029.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0030.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0030.jpg new file mode 100644 index 000000000..c29070e49 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0030.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0031.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0031.jpg new file mode 100644 index 000000000..d55ecdd33 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0031.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0032.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0032.jpg new file mode 100644 index 000000000..77fb807f6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0032.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0033.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0033.jpg new file mode 100644 index 000000000..d3b367f24 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0033.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0034.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0034.jpg new file mode 100644 index 000000000..ea34f5c19 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0034.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0035.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0035.jpg new file mode 100644 index 000000000..2e21690ea Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0035.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0036.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0036.jpg new file mode 100644 index 000000000..9672b5781 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0036.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0037.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0037.jpg new file mode 100644 index 000000000..3fd26b405 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0037.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0038.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0038.jpg new file mode 100644 index 000000000..106de0f73 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0038.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0039.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0039.jpg new file mode 100644 index 000000000..150c4d89d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0039.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0040.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0040.jpg new file mode 100644 index 000000000..49624917c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0040.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0041.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0041.jpg new file mode 100644 index 000000000..01400e9f8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0041.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0042.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0042.jpg new file mode 100644 index 000000000..004892264 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0042.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0043.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0043.jpg new file mode 100644 index 000000000..849a40782 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0043.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0044.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0044.jpg new file mode 100644 index 000000000..fe1dbb196 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0044.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0045.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0045.jpg new file mode 100644 index 000000000..1d926b161 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0045.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0046.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0046.jpg new file mode 100644 index 000000000..7f974e858 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0046.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0047.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0047.jpg new file mode 100644 index 000000000..a950efb92 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0047.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0048.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0048.jpg new file mode 100644 index 000000000..6bb02c7c9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0048.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0049.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0049.jpg new file mode 100644 index 000000000..e4a77ed68 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0049.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0050.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0050.jpg new file mode 100644 index 000000000..74f8c0ead Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0050.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0051.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0051.jpg new file mode 100644 index 000000000..6a4b06500 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0051.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0052.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0052.jpg new file mode 100644 index 000000000..f90774c25 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0052.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0053.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0053.jpg new file mode 100644 index 000000000..6e0ea56d0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0053.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0054.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0054.jpg new file mode 100644 index 000000000..6a231250c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0054.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0055.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0055.jpg new file mode 100644 index 000000000..56abaefa9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0055.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0056.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0056.jpg new file mode 100644 index 000000000..8e5f01534 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0056.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0057.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0057.jpg new file mode 100644 index 000000000..ae699d758 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0057.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0058.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0058.jpg new file mode 100644 index 000000000..6b39c9e30 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0058.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0059.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0059.jpg new file mode 100644 index 000000000..a86106c54 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0059.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0060.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0060.jpg new file mode 100644 index 000000000..da64578d6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0060.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0061.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0061.jpg new file mode 100644 index 000000000..e944946eb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0061.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0062.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0062.jpg new file mode 100644 index 000000000..671d5faf4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0062.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0063.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0063.jpg new file mode 100644 index 000000000..a6426455d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0063.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0064.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0064.jpg new file mode 100644 index 000000000..ef1f99914 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0064.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0065.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0065.jpg new file mode 100644 index 000000000..324c80855 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0065.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0066.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0066.jpg new file mode 100644 index 000000000..f535bb7f0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0066.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0067.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0067.jpg new file mode 100644 index 000000000..932e3682c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0067.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0068.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0068.jpg new file mode 100644 index 000000000..7554c6fd0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0068.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0069.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0069.jpg new file mode 100644 index 000000000..23f72ff6f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0069.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0070.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0070.jpg new file mode 100644 index 000000000..997f1b17f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0070.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0071.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0071.jpg new file mode 100644 index 000000000..abfdbdaae Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0071.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0072.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0072.jpg new file mode 100644 index 000000000..e71fac6ff Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0072.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0073.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0073.jpg new file mode 100644 index 000000000..f190522e5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0073.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0074.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0074.jpg new file mode 100644 index 000000000..52ff3a8b5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0074.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0075.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0075.jpg new file mode 100644 index 000000000..03428b848 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0075.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0076.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0076.jpg new file mode 100644 index 000000000..08bc46718 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0076.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0077.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0077.jpg new file mode 100644 index 000000000..bb6d20da6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0077.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0078.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0078.jpg new file mode 100644 index 000000000..ac91d4f71 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0078.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0079.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0079.jpg new file mode 100644 index 000000000..aeb4c7e36 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0079.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0080.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0080.jpg new file mode 100644 index 000000000..d5d0bcb6a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0080.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0081.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0081.jpg new file mode 100644 index 000000000..a92e3135b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0081.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0082.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0082.jpg new file mode 100644 index 000000000..af3bdba41 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0082.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0083.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0083.jpg new file mode 100644 index 000000000..fd339ddb6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0083.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0084.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0084.jpg new file mode 100644 index 000000000..62dcf8b70 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0084.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0085.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0085.jpg new file mode 100644 index 000000000..962282e34 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0085.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0086.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0086.jpg new file mode 100644 index 000000000..fa9c38e45 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0086.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0087.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0087.jpg new file mode 100644 index 000000000..80e2cd9c6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0087.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0088.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0088.jpg new file mode 100644 index 000000000..137b7d21b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0088.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0089.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0089.jpg new file mode 100644 index 000000000..8ab732edd Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0089.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0090.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0090.jpg new file mode 100644 index 000000000..c8df938c9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0090.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0091.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0091.jpg new file mode 100644 index 000000000..97c5f2bd1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0091.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0092.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0092.jpg new file mode 100644 index 000000000..b37b28f42 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0092.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0093.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0093.jpg new file mode 100644 index 000000000..34a6e639e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0093.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0094.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0094.jpg new file mode 100644 index 000000000..ea54d8566 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0094.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0095.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0095.jpg new file mode 100644 index 000000000..9987bc1e1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0095.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0096.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0096.jpg new file mode 100644 index 000000000..b403c3df9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0096.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0097.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0097.jpg new file mode 100644 index 000000000..0fda6e263 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0097.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0098.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0098.jpg new file mode 100644 index 000000000..728ad07a3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0098.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0099.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0099.jpg new file mode 100644 index 000000000..c213292e6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0099.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0100.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0100.jpg new file mode 100644 index 000000000..faa003b76 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0100.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0101.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0101.jpg new file mode 100644 index 000000000..7ff6a9fd8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0101.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0102.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0102.jpg new file mode 100644 index 000000000..562c8896d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0102.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0103.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0103.jpg new file mode 100644 index 000000000..a38157da2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0103.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0104.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0104.jpg new file mode 100644 index 000000000..51783c087 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0104.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0105.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0105.jpg new file mode 100644 index 000000000..891713a19 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0105.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0106.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0106.jpg new file mode 100644 index 000000000..fa12a3d54 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0106.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0107.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0107.jpg new file mode 100644 index 000000000..630f2583a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0107.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0108.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0108.jpg new file mode 100644 index 000000000..1fd6d47d1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0108.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0109.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0109.jpg new file mode 100644 index 000000000..4b7c8578a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0109.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0110.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0110.jpg new file mode 100644 index 000000000..423670a77 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0110.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0111.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0111.jpg new file mode 100644 index 000000000..810b6e71f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0111.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0112.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0112.jpg new file mode 100644 index 000000000..2095d6394 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0112.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0113.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0113.jpg new file mode 100644 index 000000000..742a59a2a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0113.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0114.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0114.jpg new file mode 100644 index 000000000..29e1335a3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0114.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0115.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0115.jpg new file mode 100644 index 000000000..91a9e09c4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0115.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0116.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0116.jpg new file mode 100644 index 000000000..81217e928 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0116.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0117.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0117.jpg new file mode 100644 index 000000000..6bcf22592 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0117.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0118.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0118.jpg new file mode 100644 index 000000000..87912f63b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0118.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0119.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0119.jpg new file mode 100644 index 000000000..43a9bd8d5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0119.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0120.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0120.jpg new file mode 100644 index 000000000..c1d8c73e5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0120.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0121.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0121.jpg new file mode 100644 index 000000000..b38c02451 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0121.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0122.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0122.jpg new file mode 100644 index 000000000..17709563b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0122.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0123.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0123.jpg new file mode 100644 index 000000000..a621f96ae Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0123.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0124.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0124.jpg new file mode 100644 index 000000000..c6b68148e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0124.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0125.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0125.jpg new file mode 100644 index 000000000..a8c8f7d72 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0125.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0126.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0126.jpg new file mode 100644 index 000000000..7209913a3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0126.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0127.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0127.jpg new file mode 100644 index 000000000..9c3c04822 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0127.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0128.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0128.jpg new file mode 100644 index 000000000..6757cd6e2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0128.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0129.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0129.jpg new file mode 100644 index 000000000..5e8a7febf Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0129.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0130.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0130.jpg new file mode 100644 index 000000000..4184be5c4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0130.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0131.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0131.jpg new file mode 100644 index 000000000..50dc59ed7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0131.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0132.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0132.jpg new file mode 100644 index 000000000..3b55d91df Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0132.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0133.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0133.jpg new file mode 100644 index 000000000..d80c988b8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0133.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0134.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0134.jpg new file mode 100644 index 000000000..97f9c0ead Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0134.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0135.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0135.jpg new file mode 100644 index 000000000..4b78929d6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0135.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0136.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0136.jpg new file mode 100644 index 000000000..db81a0b89 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0136.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0137.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0137.jpg new file mode 100644 index 000000000..7c5759d00 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0137.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0138.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0138.jpg new file mode 100644 index 000000000..9236905de Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0138.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0139.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0139.jpg new file mode 100644 index 000000000..a190e3b58 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0139.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0140.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0140.jpg new file mode 100644 index 000000000..51cb06b35 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0140.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0141.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0141.jpg new file mode 100644 index 000000000..eda174c11 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0141.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0142.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0142.jpg new file mode 100644 index 000000000..1f56dfc85 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0142.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0143.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0143.jpg new file mode 100644 index 000000000..0e467ee47 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0143.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0144.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0144.jpg new file mode 100644 index 000000000..f581ad282 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0144.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0145.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0145.jpg new file mode 100644 index 000000000..afb210861 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0145.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0146.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0146.jpg new file mode 100644 index 000000000..ee1650a36 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0146.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0147.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0147.jpg new file mode 100644 index 000000000..87ea4ede8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0147.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0148.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0148.jpg new file mode 100644 index 000000000..5db55aca3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0148.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0149.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0149.jpg new file mode 100644 index 000000000..960e7fefb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0149.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0150.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0150.jpg new file mode 100644 index 000000000..95f47d7ca Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0150.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0151.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0151.jpg new file mode 100644 index 000000000..3862bf5c5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0151.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0152.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0152.jpg new file mode 100644 index 000000000..941e7fd8b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0152.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0153.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0153.jpg new file mode 100644 index 000000000..7dcd02297 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0153.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0154.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0154.jpg new file mode 100644 index 000000000..00a1eab04 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0154.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0155.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0155.jpg new file mode 100644 index 000000000..1daff2de6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0155.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0156.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0156.jpg new file mode 100644 index 000000000..80cf3436a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0156.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0157.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0157.jpg new file mode 100644 index 000000000..7c249cac6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0157.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0158.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0158.jpg new file mode 100644 index 000000000..3f0ea4ae9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0158.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0159.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0159.jpg new file mode 100644 index 000000000..5a81a468e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0159.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0160.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0160.jpg new file mode 100644 index 000000000..615cdda85 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0160.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0161.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0161.jpg new file mode 100644 index 000000000..51c0d8fe9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0161.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0162.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0162.jpg new file mode 100644 index 000000000..43e4c3f8a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0162.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0163.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0163.jpg new file mode 100644 index 000000000..57ffa6702 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0163.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0164.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0164.jpg new file mode 100644 index 000000000..903665563 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0164.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0165.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0165.jpg new file mode 100644 index 000000000..f6218fda4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0165.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0166.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0166.jpg new file mode 100644 index 000000000..7109026f3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0166.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0167.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0167.jpg new file mode 100644 index 000000000..50f042985 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0167.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0168.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0168.jpg new file mode 100644 index 000000000..4bc3df15e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0168.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0169.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0169.jpg new file mode 100644 index 000000000..bc58bf926 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0169.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0170.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0170.jpg new file mode 100644 index 000000000..08f80125a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0170.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0171.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0171.jpg new file mode 100644 index 000000000..5a87bebbc Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0171.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0172.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0172.jpg new file mode 100644 index 000000000..10e1f2368 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0172.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0173.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0173.jpg new file mode 100644 index 000000000..55730a094 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0173.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0174.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0174.jpg new file mode 100644 index 000000000..9c02e5ec2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0174.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0175.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0175.jpg new file mode 100644 index 000000000..d3c09dec1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0175.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0176.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0176.jpg new file mode 100644 index 000000000..093eab086 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0176.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0177.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0177.jpg new file mode 100644 index 000000000..f052c8353 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0177.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0178.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0178.jpg new file mode 100644 index 000000000..68b520cfb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0178.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0179.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0179.jpg new file mode 100644 index 000000000..b6b1c5934 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0179.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0180.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0180.jpg new file mode 100644 index 000000000..47df4cb23 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0180.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0181.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0181.jpg new file mode 100644 index 000000000..473bf417f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0181.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0182.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0182.jpg new file mode 100644 index 000000000..c14c1fd49 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0182.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0183.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0183.jpg new file mode 100644 index 000000000..c1556d970 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0183.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0184.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0184.jpg new file mode 100644 index 000000000..5be8564d1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0184.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0185.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0185.jpg new file mode 100644 index 000000000..9b16c9b33 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0185.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0186.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0186.jpg new file mode 100644 index 000000000..201afceab Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0186.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0187.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0187.jpg new file mode 100644 index 000000000..ccb13f814 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0187.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0188.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0188.jpg new file mode 100644 index 000000000..12ebe50b0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0188.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0189.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0189.jpg new file mode 100644 index 000000000..824f1fa31 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0189.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0190.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0190.jpg new file mode 100644 index 000000000..6046e6cb4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0190.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0191.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0191.jpg new file mode 100644 index 000000000..98fac7ee9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0191.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0192.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0192.jpg new file mode 100644 index 000000000..99d9c111a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0192.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0193.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0193.jpg new file mode 100644 index 000000000..a447e2741 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0193.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0194.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0194.jpg new file mode 100644 index 000000000..17ab89fc9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0194.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0195.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0195.jpg new file mode 100644 index 000000000..8c2c6885d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0195.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0196.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0196.jpg new file mode 100644 index 000000000..024f26215 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0196.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0197.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0197.jpg new file mode 100644 index 000000000..3c07bdc11 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0197.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0198.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0198.jpg new file mode 100644 index 000000000..8cdd29804 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0198.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0199.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0199.jpg new file mode 100644 index 000000000..a7ed268d7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0199.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0200.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0200.jpg new file mode 100644 index 000000000..4196bb7db Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0200.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0201.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0201.jpg new file mode 100644 index 000000000..4e73d039e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0201.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0202.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0202.jpg new file mode 100644 index 000000000..eddbe91f3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0202.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0203.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0203.jpg new file mode 100644 index 000000000..724526d42 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0203.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0204.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0204.jpg new file mode 100644 index 000000000..5ea114064 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0204.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0205.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0205.jpg new file mode 100644 index 000000000..6ba1d94fa Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0205.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0206.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0206.jpg new file mode 100644 index 000000000..b5ef3c329 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0206.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0207.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0207.jpg new file mode 100644 index 000000000..5446ace33 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0207.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0208.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0208.jpg new file mode 100644 index 000000000..862b97d07 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0208.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0209.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0209.jpg new file mode 100644 index 000000000..0f111fb3c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0209.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0210.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0210.jpg new file mode 100644 index 000000000..503d2ddeb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0210.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0211.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0211.jpg new file mode 100644 index 000000000..05f8c51cf Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0211.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0212.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0212.jpg new file mode 100644 index 000000000..904832a10 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0212.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0213.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0213.jpg new file mode 100644 index 000000000..30de21add Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0213.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0214.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0214.jpg new file mode 100644 index 000000000..9a4eda949 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0214.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0215.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0215.jpg new file mode 100644 index 000000000..cc4c08582 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0215.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0216.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0216.jpg new file mode 100644 index 000000000..ff33af9bc Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0216.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0217.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0217.jpg new file mode 100644 index 000000000..de70071e8 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0217.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0218.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0218.jpg new file mode 100644 index 000000000..e5eae9d9c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0218.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0219.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0219.jpg new file mode 100644 index 000000000..c518542ea Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0219.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0220.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0220.jpg new file mode 100644 index 000000000..af34a4ca4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0220.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0221.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0221.jpg new file mode 100644 index 000000000..04edf33a4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0221.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0222.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0222.jpg new file mode 100644 index 000000000..82f726676 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0222.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0223.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0223.jpg new file mode 100644 index 000000000..acd436c7b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0223.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0224.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0224.jpg new file mode 100644 index 000000000..96232c02e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0224.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0225.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0225.jpg new file mode 100644 index 000000000..832e57c6f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0225.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0226.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0226.jpg new file mode 100644 index 000000000..77b40a6ed Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0226.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0227.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0227.jpg new file mode 100644 index 000000000..65a56d336 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0227.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0228.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0228.jpg new file mode 100644 index 000000000..e0a181a3e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0228.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0229.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0229.jpg new file mode 100644 index 000000000..b1d6ea19a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0229.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0230.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0230.jpg new file mode 100644 index 000000000..bdb0d4b20 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0230.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0231.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0231.jpg new file mode 100644 index 000000000..938d76f25 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0231.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0232.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0232.jpg new file mode 100644 index 000000000..cede61749 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0232.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0233.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0233.jpg new file mode 100644 index 000000000..b2336c649 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0233.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0234.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0234.jpg new file mode 100644 index 000000000..dbcc2aa4a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0234.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0235.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0235.jpg new file mode 100644 index 000000000..47b3cbf90 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0235.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0236.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0236.jpg new file mode 100644 index 000000000..6dc136d52 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0236.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0237.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0237.jpg new file mode 100644 index 000000000..24ff8ee6d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0237.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0238.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0238.jpg new file mode 100644 index 000000000..62d99ee0a Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0238.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0239.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0239.jpg new file mode 100644 index 000000000..2879c5d55 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0239.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0240.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0240.jpg new file mode 100644 index 000000000..03f8e177e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0240.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/images/frame_0241.jpg b/AgCloud/simulators/data/security/cam-south-02/images/frame_0241.jpg new file mode 100644 index 000000000..562793a4f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-south-02/images/frame_0241.jpg differ diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0000.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0000.json new file mode 100644 index 000000000..03ec6eb10 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0000.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971711269, + "file_name": "frame_0000.jpg", + "frame_idx": 0 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0001.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0001.json new file mode 100644 index 000000000..b621ca45d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0001.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971712269, + "file_name": "frame_0001.jpg", + "frame_idx": 1 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0002.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0002.json new file mode 100644 index 000000000..2bb3dee93 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0002.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971713269, + "file_name": "frame_0002.jpg", + "frame_idx": 2 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0003.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0003.json new file mode 100644 index 000000000..b5e262cdd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0003.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971714269, + "file_name": "frame_0003.jpg", + "frame_idx": 3 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0004.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0004.json new file mode 100644 index 000000000..e79d54e26 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0004.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971715269, + "file_name": "frame_0004.jpg", + "frame_idx": 4 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0005.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0005.json new file mode 100644 index 000000000..e6b1a57b0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0005.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971716269, + "file_name": "frame_0005.jpg", + "frame_idx": 5 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0006.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0006.json new file mode 100644 index 000000000..da92ab9e2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0006.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971717269, + "file_name": "frame_0006.jpg", + "frame_idx": 6 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0007.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0007.json new file mode 100644 index 000000000..4bdd31a48 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0007.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971718269, + "file_name": "frame_0007.jpg", + "frame_idx": 7 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0008.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0008.json new file mode 100644 index 000000000..0ff1f896f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0008.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971719269, + "file_name": "frame_0008.jpg", + "frame_idx": 8 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0009.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0009.json new file mode 100644 index 000000000..439e7a462 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0009.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971720269, + "file_name": "frame_0009.jpg", + "frame_idx": 9 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0010.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0010.json new file mode 100644 index 000000000..915ac9967 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0010.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971721269, + "file_name": "frame_0010.jpg", + "frame_idx": 10 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0011.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0011.json new file mode 100644 index 000000000..8a983a27c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0011.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971722269, + "file_name": "frame_0011.jpg", + "frame_idx": 11 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0012.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0012.json new file mode 100644 index 000000000..22b019d90 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0012.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971723269, + "file_name": "frame_0012.jpg", + "frame_idx": 12 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0013.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0013.json new file mode 100644 index 000000000..ae0546b2b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0013.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971724269, + "file_name": "frame_0013.jpg", + "frame_idx": 13 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0014.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0014.json new file mode 100644 index 000000000..b5b437aa0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0014.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971725269, + "file_name": "frame_0014.jpg", + "frame_idx": 14 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0015.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0015.json new file mode 100644 index 000000000..9c925e030 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0015.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971726269, + "file_name": "frame_0015.jpg", + "frame_idx": 15 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0016.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0016.json new file mode 100644 index 000000000..075524103 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0016.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971727269, + "file_name": "frame_0016.jpg", + "frame_idx": 16 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0017.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0017.json new file mode 100644 index 000000000..e6ca5c0c3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0017.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971728269, + "file_name": "frame_0017.jpg", + "frame_idx": 17 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0018.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0018.json new file mode 100644 index 000000000..9480d0ac8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0018.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971729269, + "file_name": "frame_0018.jpg", + "frame_idx": 18 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0019.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0019.json new file mode 100644 index 000000000..51edf8517 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0019.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971730269, + "file_name": "frame_0019.jpg", + "frame_idx": 19 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0020.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0020.json new file mode 100644 index 000000000..292d16c28 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0020.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971731269, + "file_name": "frame_0020.jpg", + "frame_idx": 20 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0021.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0021.json new file mode 100644 index 000000000..e04642aa7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0021.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971732269, + "file_name": "frame_0021.jpg", + "frame_idx": 21 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0022.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0022.json new file mode 100644 index 000000000..9a8582fe0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0022.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971733269, + "file_name": "frame_0022.jpg", + "frame_idx": 22 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0023.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0023.json new file mode 100644 index 000000000..629fe28cc --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0023.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971734269, + "file_name": "frame_0023.jpg", + "frame_idx": 23 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0024.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0024.json new file mode 100644 index 000000000..8dbaf9a6b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0024.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971735269, + "file_name": "frame_0024.jpg", + "frame_idx": 24 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0025.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0025.json new file mode 100644 index 000000000..88774e30b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0025.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971736269, + "file_name": "frame_0025.jpg", + "frame_idx": 25 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0026.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0026.json new file mode 100644 index 000000000..7e45ecd42 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0026.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971737269, + "file_name": "frame_0026.jpg", + "frame_idx": 26 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0027.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0027.json new file mode 100644 index 000000000..cb3ce74bf --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0027.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971738269, + "file_name": "frame_0027.jpg", + "frame_idx": 27 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0028.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0028.json new file mode 100644 index 000000000..1c64e37be --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0028.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971739269, + "file_name": "frame_0028.jpg", + "frame_idx": 28 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0029.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0029.json new file mode 100644 index 000000000..e0b85b58e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0029.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971740269, + "file_name": "frame_0029.jpg", + "frame_idx": 29 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0030.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0030.json new file mode 100644 index 000000000..29526794a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0030.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971741269, + "file_name": "frame_0030.jpg", + "frame_idx": 30 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0031.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0031.json new file mode 100644 index 000000000..a5ea7a320 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0031.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971742269, + "file_name": "frame_0031.jpg", + "frame_idx": 31 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0032.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0032.json new file mode 100644 index 000000000..69bfd4165 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0032.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971743269, + "file_name": "frame_0032.jpg", + "frame_idx": 32 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0033.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0033.json new file mode 100644 index 000000000..8a88d92b5 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0033.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971744269, + "file_name": "frame_0033.jpg", + "frame_idx": 33 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0034.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0034.json new file mode 100644 index 000000000..e6493f426 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0034.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971745269, + "file_name": "frame_0034.jpg", + "frame_idx": 34 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0035.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0035.json new file mode 100644 index 000000000..bdbf52ad7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0035.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971746269, + "file_name": "frame_0035.jpg", + "frame_idx": 35 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0036.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0036.json new file mode 100644 index 000000000..495e6eba0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0036.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971747269, + "file_name": "frame_0036.jpg", + "frame_idx": 36 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0037.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0037.json new file mode 100644 index 000000000..d1e8c9dea --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0037.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971748269, + "file_name": "frame_0037.jpg", + "frame_idx": 37 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0038.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0038.json new file mode 100644 index 000000000..04c33e1c2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0038.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971749269, + "file_name": "frame_0038.jpg", + "frame_idx": 38 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0039.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0039.json new file mode 100644 index 000000000..7f2b2c4f4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0039.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971750269, + "file_name": "frame_0039.jpg", + "frame_idx": 39 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0040.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0040.json new file mode 100644 index 000000000..c86cc938c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0040.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971751269, + "file_name": "frame_0040.jpg", + "frame_idx": 40 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0041.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0041.json new file mode 100644 index 000000000..9550779e8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0041.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971752269, + "file_name": "frame_0041.jpg", + "frame_idx": 41 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0042.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0042.json new file mode 100644 index 000000000..0ffb2a9cd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0042.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971753269, + "file_name": "frame_0042.jpg", + "frame_idx": 42 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0043.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0043.json new file mode 100644 index 000000000..1deab4ee4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0043.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971754269, + "file_name": "frame_0043.jpg", + "frame_idx": 43 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0044.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0044.json new file mode 100644 index 000000000..6fe99174f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0044.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971755269, + "file_name": "frame_0044.jpg", + "frame_idx": 44 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0045.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0045.json new file mode 100644 index 000000000..309ea5cca --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0045.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971756269, + "file_name": "frame_0045.jpg", + "frame_idx": 45 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0046.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0046.json new file mode 100644 index 000000000..70761c01c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0046.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971757269, + "file_name": "frame_0046.jpg", + "frame_idx": 46 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0047.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0047.json new file mode 100644 index 000000000..bfcb511c2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0047.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971758269, + "file_name": "frame_0047.jpg", + "frame_idx": 47 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0048.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0048.json new file mode 100644 index 000000000..fff599152 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0048.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971759269, + "file_name": "frame_0048.jpg", + "frame_idx": 48 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0049.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0049.json new file mode 100644 index 000000000..ebbe06fa3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0049.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971760269, + "file_name": "frame_0049.jpg", + "frame_idx": 49 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0050.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0050.json new file mode 100644 index 000000000..3749da627 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0050.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971761269, + "file_name": "frame_0050.jpg", + "frame_idx": 50 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0051.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0051.json new file mode 100644 index 000000000..a290a28b0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0051.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971762269, + "file_name": "frame_0051.jpg", + "frame_idx": 51 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0052.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0052.json new file mode 100644 index 000000000..8c315f6bb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0052.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971763269, + "file_name": "frame_0052.jpg", + "frame_idx": 52 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0053.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0053.json new file mode 100644 index 000000000..f68a04f13 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0053.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971764269, + "file_name": "frame_0053.jpg", + "frame_idx": 53 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0054.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0054.json new file mode 100644 index 000000000..e221430ab --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0054.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971765269, + "file_name": "frame_0054.jpg", + "frame_idx": 54 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0055.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0055.json new file mode 100644 index 000000000..66f88f68c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0055.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971766269, + "file_name": "frame_0055.jpg", + "frame_idx": 55 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0056.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0056.json new file mode 100644 index 000000000..2636faeee --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0056.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971767269, + "file_name": "frame_0056.jpg", + "frame_idx": 56 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0057.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0057.json new file mode 100644 index 000000000..ae0b0af7f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0057.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971768269, + "file_name": "frame_0057.jpg", + "frame_idx": 57 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0058.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0058.json new file mode 100644 index 000000000..a8923f0e7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0058.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971769269, + "file_name": "frame_0058.jpg", + "frame_idx": 58 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0059.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0059.json new file mode 100644 index 000000000..f41d6b07c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0059.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971770269, + "file_name": "frame_0059.jpg", + "frame_idx": 59 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0060.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0060.json new file mode 100644 index 000000000..67f04c2bd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0060.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971771269, + "file_name": "frame_0060.jpg", + "frame_idx": 60 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0061.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0061.json new file mode 100644 index 000000000..e8758f1a4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0061.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971772269, + "file_name": "frame_0061.jpg", + "frame_idx": 61 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0062.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0062.json new file mode 100644 index 000000000..3925884b0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0062.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971773269, + "file_name": "frame_0062.jpg", + "frame_idx": 62 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0063.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0063.json new file mode 100644 index 000000000..224c483f4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0063.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971774269, + "file_name": "frame_0063.jpg", + "frame_idx": 63 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0064.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0064.json new file mode 100644 index 000000000..7ee135906 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0064.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971775269, + "file_name": "frame_0064.jpg", + "frame_idx": 64 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0065.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0065.json new file mode 100644 index 000000000..6e0a3fb17 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0065.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971776269, + "file_name": "frame_0065.jpg", + "frame_idx": 65 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0066.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0066.json new file mode 100644 index 000000000..5449969b0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0066.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971777269, + "file_name": "frame_0066.jpg", + "frame_idx": 66 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0067.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0067.json new file mode 100644 index 000000000..9b5b4ebf2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0067.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971778269, + "file_name": "frame_0067.jpg", + "frame_idx": 67 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0068.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0068.json new file mode 100644 index 000000000..8dd466154 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0068.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971779269, + "file_name": "frame_0068.jpg", + "frame_idx": 68 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0069.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0069.json new file mode 100644 index 000000000..e0340be3a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0069.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971780269, + "file_name": "frame_0069.jpg", + "frame_idx": 69 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0070.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0070.json new file mode 100644 index 000000000..7e52d296f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0070.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971781269, + "file_name": "frame_0070.jpg", + "frame_idx": 70 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0071.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0071.json new file mode 100644 index 000000000..3488f1eb4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0071.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971782269, + "file_name": "frame_0071.jpg", + "frame_idx": 71 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0072.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0072.json new file mode 100644 index 000000000..a67526f91 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0072.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971783269, + "file_name": "frame_0072.jpg", + "frame_idx": 72 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0073.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0073.json new file mode 100644 index 000000000..855da20af --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0073.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971784269, + "file_name": "frame_0073.jpg", + "frame_idx": 73 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0074.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0074.json new file mode 100644 index 000000000..c71cf8dc3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0074.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971785269, + "file_name": "frame_0074.jpg", + "frame_idx": 74 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0075.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0075.json new file mode 100644 index 000000000..eef1310e8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0075.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971786269, + "file_name": "frame_0075.jpg", + "frame_idx": 75 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0076.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0076.json new file mode 100644 index 000000000..bedc2ce81 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0076.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971787269, + "file_name": "frame_0076.jpg", + "frame_idx": 76 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0077.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0077.json new file mode 100644 index 000000000..315a4ae26 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0077.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971788269, + "file_name": "frame_0077.jpg", + "frame_idx": 77 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0078.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0078.json new file mode 100644 index 000000000..f08f87fb6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0078.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971789269, + "file_name": "frame_0078.jpg", + "frame_idx": 78 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0079.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0079.json new file mode 100644 index 000000000..431fb9e0a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0079.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971790269, + "file_name": "frame_0079.jpg", + "frame_idx": 79 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0080.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0080.json new file mode 100644 index 000000000..9b12f7b0d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0080.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971791269, + "file_name": "frame_0080.jpg", + "frame_idx": 80 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0081.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0081.json new file mode 100644 index 000000000..f358ee556 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0081.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971792269, + "file_name": "frame_0081.jpg", + "frame_idx": 81 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0082.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0082.json new file mode 100644 index 000000000..466472144 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0082.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971793269, + "file_name": "frame_0082.jpg", + "frame_idx": 82 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0083.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0083.json new file mode 100644 index 000000000..3299e5a4e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0083.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971794269, + "file_name": "frame_0083.jpg", + "frame_idx": 83 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0084.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0084.json new file mode 100644 index 000000000..df40f83d8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0084.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971795269, + "file_name": "frame_0084.jpg", + "frame_idx": 84 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0085.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0085.json new file mode 100644 index 000000000..b75189fae --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0085.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971796269, + "file_name": "frame_0085.jpg", + "frame_idx": 85 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0086.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0086.json new file mode 100644 index 000000000..3dee4d138 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0086.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971797269, + "file_name": "frame_0086.jpg", + "frame_idx": 86 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0087.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0087.json new file mode 100644 index 000000000..bff2238b4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0087.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971798269, + "file_name": "frame_0087.jpg", + "frame_idx": 87 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0088.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0088.json new file mode 100644 index 000000000..374a425e3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0088.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971799269, + "file_name": "frame_0088.jpg", + "frame_idx": 88 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0089.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0089.json new file mode 100644 index 000000000..dd1d298bb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0089.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971800269, + "file_name": "frame_0089.jpg", + "frame_idx": 89 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0090.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0090.json new file mode 100644 index 000000000..1f0257f3d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0090.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971801269, + "file_name": "frame_0090.jpg", + "frame_idx": 90 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0091.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0091.json new file mode 100644 index 000000000..ffae682c2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0091.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971802269, + "file_name": "frame_0091.jpg", + "frame_idx": 91 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0092.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0092.json new file mode 100644 index 000000000..1f063820a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0092.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971803269, + "file_name": "frame_0092.jpg", + "frame_idx": 92 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0093.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0093.json new file mode 100644 index 000000000..184654182 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0093.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971804269, + "file_name": "frame_0093.jpg", + "frame_idx": 93 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0094.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0094.json new file mode 100644 index 000000000..8cd61c5a8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0094.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971805269, + "file_name": "frame_0094.jpg", + "frame_idx": 94 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0095.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0095.json new file mode 100644 index 000000000..14200c4f5 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0095.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971806269, + "file_name": "frame_0095.jpg", + "frame_idx": 95 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0096.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0096.json new file mode 100644 index 000000000..97c6bbf94 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0096.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971807269, + "file_name": "frame_0096.jpg", + "frame_idx": 96 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0097.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0097.json new file mode 100644 index 000000000..40dc5901b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0097.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971808269, + "file_name": "frame_0097.jpg", + "frame_idx": 97 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0098.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0098.json new file mode 100644 index 000000000..fab3f7d18 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0098.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971809269, + "file_name": "frame_0098.jpg", + "frame_idx": 98 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0099.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0099.json new file mode 100644 index 000000000..fbf0a5fff --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0099.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971810269, + "file_name": "frame_0099.jpg", + "frame_idx": 99 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0100.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0100.json new file mode 100644 index 000000000..23f2a586f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0100.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971811269, + "file_name": "frame_0100.jpg", + "frame_idx": 100 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0101.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0101.json new file mode 100644 index 000000000..4d4689bbc --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0101.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971812269, + "file_name": "frame_0101.jpg", + "frame_idx": 101 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0102.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0102.json new file mode 100644 index 000000000..10331f857 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0102.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971813269, + "file_name": "frame_0102.jpg", + "frame_idx": 102 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0103.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0103.json new file mode 100644 index 000000000..feedb88b8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0103.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971814269, + "file_name": "frame_0103.jpg", + "frame_idx": 103 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0104.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0104.json new file mode 100644 index 000000000..fe47c6011 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0104.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971815269, + "file_name": "frame_0104.jpg", + "frame_idx": 104 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0105.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0105.json new file mode 100644 index 000000000..df89930a2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0105.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971816269, + "file_name": "frame_0105.jpg", + "frame_idx": 105 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0106.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0106.json new file mode 100644 index 000000000..a0dc90894 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0106.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971817269, + "file_name": "frame_0106.jpg", + "frame_idx": 106 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0107.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0107.json new file mode 100644 index 000000000..e296fe4cd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0107.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971818269, + "file_name": "frame_0107.jpg", + "frame_idx": 107 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0108.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0108.json new file mode 100644 index 000000000..d26f3880d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0108.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971819269, + "file_name": "frame_0108.jpg", + "frame_idx": 108 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0109.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0109.json new file mode 100644 index 000000000..f0f259df0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0109.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971820269, + "file_name": "frame_0109.jpg", + "frame_idx": 109 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0110.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0110.json new file mode 100644 index 000000000..1dd2404a7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0110.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971821269, + "file_name": "frame_0110.jpg", + "frame_idx": 110 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0111.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0111.json new file mode 100644 index 000000000..7e0ec5c41 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0111.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971822269, + "file_name": "frame_0111.jpg", + "frame_idx": 111 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0112.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0112.json new file mode 100644 index 000000000..32564d8a8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0112.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971823269, + "file_name": "frame_0112.jpg", + "frame_idx": 112 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0113.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0113.json new file mode 100644 index 000000000..4417b0315 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0113.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971824269, + "file_name": "frame_0113.jpg", + "frame_idx": 113 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0114.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0114.json new file mode 100644 index 000000000..352c7ce06 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0114.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971825269, + "file_name": "frame_0114.jpg", + "frame_idx": 114 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0115.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0115.json new file mode 100644 index 000000000..1f2df5555 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0115.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971826269, + "file_name": "frame_0115.jpg", + "frame_idx": 115 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0116.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0116.json new file mode 100644 index 000000000..e9f54e60a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0116.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971827269, + "file_name": "frame_0116.jpg", + "frame_idx": 116 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0117.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0117.json new file mode 100644 index 000000000..e902e1d8d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0117.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971828269, + "file_name": "frame_0117.jpg", + "frame_idx": 117 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0118.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0118.json new file mode 100644 index 000000000..69a190520 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0118.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971829269, + "file_name": "frame_0118.jpg", + "frame_idx": 118 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0119.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0119.json new file mode 100644 index 000000000..50ecf6ded --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0119.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971830269, + "file_name": "frame_0119.jpg", + "frame_idx": 119 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0120.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0120.json new file mode 100644 index 000000000..7f8f22d75 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0120.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971831269, + "file_name": "frame_0120.jpg", + "frame_idx": 120 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0121.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0121.json new file mode 100644 index 000000000..1f2f74a9e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0121.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971832269, + "file_name": "frame_0121.jpg", + "frame_idx": 121 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0122.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0122.json new file mode 100644 index 000000000..4a7ed3239 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0122.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971833269, + "file_name": "frame_0122.jpg", + "frame_idx": 122 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0123.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0123.json new file mode 100644 index 000000000..f398496e8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0123.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971834269, + "file_name": "frame_0123.jpg", + "frame_idx": 123 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0124.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0124.json new file mode 100644 index 000000000..e07c9a888 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0124.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971835269, + "file_name": "frame_0124.jpg", + "frame_idx": 124 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0125.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0125.json new file mode 100644 index 000000000..89b7bb798 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0125.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971836269, + "file_name": "frame_0125.jpg", + "frame_idx": 125 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0126.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0126.json new file mode 100644 index 000000000..c1bcb291e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0126.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971837269, + "file_name": "frame_0126.jpg", + "frame_idx": 126 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0127.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0127.json new file mode 100644 index 000000000..afc7f883c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0127.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971838269, + "file_name": "frame_0127.jpg", + "frame_idx": 127 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0128.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0128.json new file mode 100644 index 000000000..f207041c4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0128.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971839269, + "file_name": "frame_0128.jpg", + "frame_idx": 128 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0129.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0129.json new file mode 100644 index 000000000..dfe377054 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0129.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971840269, + "file_name": "frame_0129.jpg", + "frame_idx": 129 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0130.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0130.json new file mode 100644 index 000000000..f86254a47 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0130.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971841269, + "file_name": "frame_0130.jpg", + "frame_idx": 130 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0131.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0131.json new file mode 100644 index 000000000..07a614ff9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0131.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971842269, + "file_name": "frame_0131.jpg", + "frame_idx": 131 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0132.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0132.json new file mode 100644 index 000000000..28d25f095 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0132.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971843269, + "file_name": "frame_0132.jpg", + "frame_idx": 132 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0133.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0133.json new file mode 100644 index 000000000..621e5755a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0133.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971844269, + "file_name": "frame_0133.jpg", + "frame_idx": 133 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0134.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0134.json new file mode 100644 index 000000000..deb5c58fb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0134.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971845269, + "file_name": "frame_0134.jpg", + "frame_idx": 134 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0135.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0135.json new file mode 100644 index 000000000..db0d78a91 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0135.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971846269, + "file_name": "frame_0135.jpg", + "frame_idx": 135 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0136.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0136.json new file mode 100644 index 000000000..9286ca1de --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0136.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971847269, + "file_name": "frame_0136.jpg", + "frame_idx": 136 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0137.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0137.json new file mode 100644 index 000000000..d47d406c8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0137.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971848269, + "file_name": "frame_0137.jpg", + "frame_idx": 137 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0138.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0138.json new file mode 100644 index 000000000..2806f20df --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0138.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971849269, + "file_name": "frame_0138.jpg", + "frame_idx": 138 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0139.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0139.json new file mode 100644 index 000000000..581321b1f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0139.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971850269, + "file_name": "frame_0139.jpg", + "frame_idx": 139 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0140.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0140.json new file mode 100644 index 000000000..2bc3cc718 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0140.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971851269, + "file_name": "frame_0140.jpg", + "frame_idx": 140 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0141.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0141.json new file mode 100644 index 000000000..1366475a0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0141.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971852269, + "file_name": "frame_0141.jpg", + "frame_idx": 141 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0142.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0142.json new file mode 100644 index 000000000..30644131a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0142.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971853269, + "file_name": "frame_0142.jpg", + "frame_idx": 142 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0143.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0143.json new file mode 100644 index 000000000..076db992f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0143.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971854269, + "file_name": "frame_0143.jpg", + "frame_idx": 143 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0144.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0144.json new file mode 100644 index 000000000..77a2d8723 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0144.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971855269, + "file_name": "frame_0144.jpg", + "frame_idx": 144 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0145.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0145.json new file mode 100644 index 000000000..a054e2e7f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0145.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971856269, + "file_name": "frame_0145.jpg", + "frame_idx": 145 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0146.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0146.json new file mode 100644 index 000000000..6e66c4451 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0146.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971857269, + "file_name": "frame_0146.jpg", + "frame_idx": 146 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0147.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0147.json new file mode 100644 index 000000000..5fdf1f0ca --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0147.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971858269, + "file_name": "frame_0147.jpg", + "frame_idx": 147 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0148.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0148.json new file mode 100644 index 000000000..53f801443 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0148.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971859269, + "file_name": "frame_0148.jpg", + "frame_idx": 148 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0149.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0149.json new file mode 100644 index 000000000..c3958b44c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0149.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971860269, + "file_name": "frame_0149.jpg", + "frame_idx": 149 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0150.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0150.json new file mode 100644 index 000000000..a8fc0b172 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0150.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971861269, + "file_name": "frame_0150.jpg", + "frame_idx": 150 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0151.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0151.json new file mode 100644 index 000000000..c77a8c838 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0151.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971862269, + "file_name": "frame_0151.jpg", + "frame_idx": 151 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0152.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0152.json new file mode 100644 index 000000000..0583d973f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0152.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971863269, + "file_name": "frame_0152.jpg", + "frame_idx": 152 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0153.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0153.json new file mode 100644 index 000000000..9c2afb304 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0153.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971864269, + "file_name": "frame_0153.jpg", + "frame_idx": 153 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0154.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0154.json new file mode 100644 index 000000000..c1717226a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0154.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971865269, + "file_name": "frame_0154.jpg", + "frame_idx": 154 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0155.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0155.json new file mode 100644 index 000000000..03f535d2c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0155.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971866269, + "file_name": "frame_0155.jpg", + "frame_idx": 155 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0156.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0156.json new file mode 100644 index 000000000..98af9078d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0156.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971867269, + "file_name": "frame_0156.jpg", + "frame_idx": 156 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0157.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0157.json new file mode 100644 index 000000000..bb900af7f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0157.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971868269, + "file_name": "frame_0157.jpg", + "frame_idx": 157 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0158.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0158.json new file mode 100644 index 000000000..d3b9370e4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0158.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971869269, + "file_name": "frame_0158.jpg", + "frame_idx": 158 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0159.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0159.json new file mode 100644 index 000000000..a8627a442 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0159.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971870269, + "file_name": "frame_0159.jpg", + "frame_idx": 159 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0160.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0160.json new file mode 100644 index 000000000..f5380292e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0160.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971871269, + "file_name": "frame_0160.jpg", + "frame_idx": 160 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0161.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0161.json new file mode 100644 index 000000000..690b15b31 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0161.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971872269, + "file_name": "frame_0161.jpg", + "frame_idx": 161 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0162.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0162.json new file mode 100644 index 000000000..72e9fb486 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0162.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971873269, + "file_name": "frame_0162.jpg", + "frame_idx": 162 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0163.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0163.json new file mode 100644 index 000000000..8a8a8f3cd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0163.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971874269, + "file_name": "frame_0163.jpg", + "frame_idx": 163 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0164.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0164.json new file mode 100644 index 000000000..cb5bc4036 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0164.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971875269, + "file_name": "frame_0164.jpg", + "frame_idx": 164 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0165.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0165.json new file mode 100644 index 000000000..a34beac1f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0165.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971876269, + "file_name": "frame_0165.jpg", + "frame_idx": 165 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0166.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0166.json new file mode 100644 index 000000000..129775639 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0166.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971877269, + "file_name": "frame_0166.jpg", + "frame_idx": 166 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0167.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0167.json new file mode 100644 index 000000000..17c0412a6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0167.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971878269, + "file_name": "frame_0167.jpg", + "frame_idx": 167 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0168.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0168.json new file mode 100644 index 000000000..15ae1c6ee --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0168.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971879269, + "file_name": "frame_0168.jpg", + "frame_idx": 168 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0169.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0169.json new file mode 100644 index 000000000..8071da9c1 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0169.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971880269, + "file_name": "frame_0169.jpg", + "frame_idx": 169 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0170.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0170.json new file mode 100644 index 000000000..710ae1ea6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0170.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971881269, + "file_name": "frame_0170.jpg", + "frame_idx": 170 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0171.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0171.json new file mode 100644 index 000000000..787bdae24 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0171.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971882269, + "file_name": "frame_0171.jpg", + "frame_idx": 171 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0172.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0172.json new file mode 100644 index 000000000..cd66fe579 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0172.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971883269, + "file_name": "frame_0172.jpg", + "frame_idx": 172 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0173.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0173.json new file mode 100644 index 000000000..ca3034ce4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0173.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971884269, + "file_name": "frame_0173.jpg", + "frame_idx": 173 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0174.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0174.json new file mode 100644 index 000000000..5b0935bc3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0174.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971885269, + "file_name": "frame_0174.jpg", + "frame_idx": 174 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0175.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0175.json new file mode 100644 index 000000000..6a336b94c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0175.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971886269, + "file_name": "frame_0175.jpg", + "frame_idx": 175 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0176.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0176.json new file mode 100644 index 000000000..4447c1cd0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0176.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971887269, + "file_name": "frame_0176.jpg", + "frame_idx": 176 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0177.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0177.json new file mode 100644 index 000000000..9165e029e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0177.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971888269, + "file_name": "frame_0177.jpg", + "frame_idx": 177 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0178.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0178.json new file mode 100644 index 000000000..33fd9479f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0178.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971889269, + "file_name": "frame_0178.jpg", + "frame_idx": 178 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0179.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0179.json new file mode 100644 index 000000000..ce52cfc26 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0179.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971890269, + "file_name": "frame_0179.jpg", + "frame_idx": 179 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0180.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0180.json new file mode 100644 index 000000000..93ae04272 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0180.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971891269, + "file_name": "frame_0180.jpg", + "frame_idx": 180 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0181.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0181.json new file mode 100644 index 000000000..1c2458c00 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0181.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971892269, + "file_name": "frame_0181.jpg", + "frame_idx": 181 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0182.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0182.json new file mode 100644 index 000000000..4a8fc2718 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0182.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971893269, + "file_name": "frame_0182.jpg", + "frame_idx": 182 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0183.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0183.json new file mode 100644 index 000000000..f496488f8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0183.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971894269, + "file_name": "frame_0183.jpg", + "frame_idx": 183 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0184.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0184.json new file mode 100644 index 000000000..d18d516d4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0184.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971895269, + "file_name": "frame_0184.jpg", + "frame_idx": 184 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0185.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0185.json new file mode 100644 index 000000000..7215963e4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0185.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971896269, + "file_name": "frame_0185.jpg", + "frame_idx": 185 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0186.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0186.json new file mode 100644 index 000000000..887d6d68e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0186.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971897269, + "file_name": "frame_0186.jpg", + "frame_idx": 186 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0187.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0187.json new file mode 100644 index 000000000..8e01dd24c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0187.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971898269, + "file_name": "frame_0187.jpg", + "frame_idx": 187 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0188.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0188.json new file mode 100644 index 000000000..692b84f59 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0188.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971899269, + "file_name": "frame_0188.jpg", + "frame_idx": 188 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0189.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0189.json new file mode 100644 index 000000000..ee8159fa4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0189.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971900269, + "file_name": "frame_0189.jpg", + "frame_idx": 189 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0190.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0190.json new file mode 100644 index 000000000..d998455df --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0190.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971901269, + "file_name": "frame_0190.jpg", + "frame_idx": 190 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0191.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0191.json new file mode 100644 index 000000000..c25bc11b6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0191.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971902269, + "file_name": "frame_0191.jpg", + "frame_idx": 191 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0192.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0192.json new file mode 100644 index 000000000..d58ddeb94 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0192.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971903269, + "file_name": "frame_0192.jpg", + "frame_idx": 192 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0193.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0193.json new file mode 100644 index 000000000..bd7d0001f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0193.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971904269, + "file_name": "frame_0193.jpg", + "frame_idx": 193 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0194.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0194.json new file mode 100644 index 000000000..f5683bb6d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0194.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971905269, + "file_name": "frame_0194.jpg", + "frame_idx": 194 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0195.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0195.json new file mode 100644 index 000000000..440ee19d2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0195.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971906269, + "file_name": "frame_0195.jpg", + "frame_idx": 195 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0196.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0196.json new file mode 100644 index 000000000..67ea61d2e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0196.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971907269, + "file_name": "frame_0196.jpg", + "frame_idx": 196 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0197.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0197.json new file mode 100644 index 000000000..b52957e7f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0197.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971908269, + "file_name": "frame_0197.jpg", + "frame_idx": 197 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0198.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0198.json new file mode 100644 index 000000000..9bd12b009 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0198.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971909269, + "file_name": "frame_0198.jpg", + "frame_idx": 198 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0199.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0199.json new file mode 100644 index 000000000..30769f758 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0199.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971910269, + "file_name": "frame_0199.jpg", + "frame_idx": 199 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0200.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0200.json new file mode 100644 index 000000000..f8bbd5bee --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0200.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971911269, + "file_name": "frame_0200.jpg", + "frame_idx": 200 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0201.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0201.json new file mode 100644 index 000000000..1704053c9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0201.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971912269, + "file_name": "frame_0201.jpg", + "frame_idx": 201 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0202.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0202.json new file mode 100644 index 000000000..9eefe7744 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0202.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971913269, + "file_name": "frame_0202.jpg", + "frame_idx": 202 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0203.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0203.json new file mode 100644 index 000000000..44339fcbb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0203.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971914269, + "file_name": "frame_0203.jpg", + "frame_idx": 203 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0204.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0204.json new file mode 100644 index 000000000..7d0b0b145 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0204.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971915269, + "file_name": "frame_0204.jpg", + "frame_idx": 204 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0205.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0205.json new file mode 100644 index 000000000..6cdec35bc --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0205.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971916269, + "file_name": "frame_0205.jpg", + "frame_idx": 205 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0206.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0206.json new file mode 100644 index 000000000..98cacd1b2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0206.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971917269, + "file_name": "frame_0206.jpg", + "frame_idx": 206 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0207.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0207.json new file mode 100644 index 000000000..85b28c144 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0207.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971918269, + "file_name": "frame_0207.jpg", + "frame_idx": 207 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0208.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0208.json new file mode 100644 index 000000000..3ab50d99c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0208.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971919269, + "file_name": "frame_0208.jpg", + "frame_idx": 208 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0209.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0209.json new file mode 100644 index 000000000..76b4a79aa --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0209.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971920269, + "file_name": "frame_0209.jpg", + "frame_idx": 209 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0210.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0210.json new file mode 100644 index 000000000..0d14a5533 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0210.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971921269, + "file_name": "frame_0210.jpg", + "frame_idx": 210 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0211.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0211.json new file mode 100644 index 000000000..01ab0f223 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0211.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971922269, + "file_name": "frame_0211.jpg", + "frame_idx": 211 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0212.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0212.json new file mode 100644 index 000000000..e7dbfade7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0212.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971923269, + "file_name": "frame_0212.jpg", + "frame_idx": 212 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0213.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0213.json new file mode 100644 index 000000000..b5b3cacdf --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0213.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971924269, + "file_name": "frame_0213.jpg", + "frame_idx": 213 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0214.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0214.json new file mode 100644 index 000000000..6f549ae47 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0214.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971925269, + "file_name": "frame_0214.jpg", + "frame_idx": 214 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0215.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0215.json new file mode 100644 index 000000000..e85c66178 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0215.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971926269, + "file_name": "frame_0215.jpg", + "frame_idx": 215 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0216.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0216.json new file mode 100644 index 000000000..d70f2329e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0216.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971927269, + "file_name": "frame_0216.jpg", + "frame_idx": 216 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0217.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0217.json new file mode 100644 index 000000000..0c979ac9b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0217.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971928269, + "file_name": "frame_0217.jpg", + "frame_idx": 217 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0218.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0218.json new file mode 100644 index 000000000..fa89bdd32 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0218.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971929269, + "file_name": "frame_0218.jpg", + "frame_idx": 218 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0219.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0219.json new file mode 100644 index 000000000..48f781d05 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0219.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971930269, + "file_name": "frame_0219.jpg", + "frame_idx": 219 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0220.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0220.json new file mode 100644 index 000000000..52288d30a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0220.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971931269, + "file_name": "frame_0220.jpg", + "frame_idx": 220 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0221.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0221.json new file mode 100644 index 000000000..f5b876dd2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0221.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971932269, + "file_name": "frame_0221.jpg", + "frame_idx": 221 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0222.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0222.json new file mode 100644 index 000000000..7567b04e3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0222.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971933269, + "file_name": "frame_0222.jpg", + "frame_idx": 222 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0223.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0223.json new file mode 100644 index 000000000..da8b077c6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0223.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971934269, + "file_name": "frame_0223.jpg", + "frame_idx": 223 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0224.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0224.json new file mode 100644 index 000000000..2d4089377 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0224.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971935269, + "file_name": "frame_0224.jpg", + "frame_idx": 224 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0225.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0225.json new file mode 100644 index 000000000..ae11747e7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0225.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971936269, + "file_name": "frame_0225.jpg", + "frame_idx": 225 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0226.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0226.json new file mode 100644 index 000000000..c379cd693 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0226.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971937269, + "file_name": "frame_0226.jpg", + "frame_idx": 226 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0227.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0227.json new file mode 100644 index 000000000..c3ac5c8f6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0227.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971938269, + "file_name": "frame_0227.jpg", + "frame_idx": 227 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0228.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0228.json new file mode 100644 index 000000000..7212fc516 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0228.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971939269, + "file_name": "frame_0228.jpg", + "frame_idx": 228 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0229.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0229.json new file mode 100644 index 000000000..493a71662 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0229.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971940269, + "file_name": "frame_0229.jpg", + "frame_idx": 229 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0230.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0230.json new file mode 100644 index 000000000..4ac1d405e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0230.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971941269, + "file_name": "frame_0230.jpg", + "frame_idx": 230 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0231.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0231.json new file mode 100644 index 000000000..45a3fe23e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0231.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971942269, + "file_name": "frame_0231.jpg", + "frame_idx": 231 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0232.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0232.json new file mode 100644 index 000000000..e3c3461db --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0232.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971943269, + "file_name": "frame_0232.jpg", + "frame_idx": 232 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0233.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0233.json new file mode 100644 index 000000000..6c913c0a4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0233.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971944269, + "file_name": "frame_0233.jpg", + "frame_idx": 233 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0234.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0234.json new file mode 100644 index 000000000..d7579dea7 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0234.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971945269, + "file_name": "frame_0234.jpg", + "frame_idx": 234 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0235.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0235.json new file mode 100644 index 000000000..28e979215 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0235.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971946269, + "file_name": "frame_0235.jpg", + "frame_idx": 235 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0236.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0236.json new file mode 100644 index 000000000..cf2f7bc8b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0236.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971947269, + "file_name": "frame_0236.jpg", + "frame_idx": 236 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0237.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0237.json new file mode 100644 index 000000000..e8c6ffb2a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0237.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971948269, + "file_name": "frame_0237.jpg", + "frame_idx": 237 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0238.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0238.json new file mode 100644 index 000000000..fae298763 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0238.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971949269, + "file_name": "frame_0238.jpg", + "frame_idx": 238 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0239.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0239.json new file mode 100644 index 000000000..10ffeca71 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0239.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971950269, + "file_name": "frame_0239.jpg", + "frame_idx": 239 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0240.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0240.json new file mode 100644 index 000000000..6b10ab329 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0240.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971951269, + "file_name": "frame_0240.jpg", + "frame_idx": 240 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0241.json b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0241.json new file mode 100644 index 000000000..bc2349562 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-south-02/metadata/frame_0241.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-south-02", + "capture_time": 1762971952269, + "file_name": "frame_0241.jpg", + "frame_idx": 241 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0011.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0011.jpg new file mode 100644 index 000000000..38f310df6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0011.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0012.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0012.jpg new file mode 100644 index 000000000..c6380198e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0012.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0013.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0013.jpg new file mode 100644 index 000000000..c12122bc1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0013.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0014.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0014.jpg new file mode 100644 index 000000000..8ca3730d2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0014.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0015.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0015.jpg new file mode 100644 index 000000000..5ab1a9176 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0015.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0016.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0016.jpg new file mode 100644 index 000000000..ceee91e79 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0016.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0017.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0017.jpg new file mode 100644 index 000000000..d3505d1a4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0017.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0018.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0018.jpg new file mode 100644 index 000000000..1fd8b9b36 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0018.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0019.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0019.jpg new file mode 100644 index 000000000..0a7e27960 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0019.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0020.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0020.jpg new file mode 100644 index 000000000..da4ec8cf7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0020.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0021.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0021.jpg new file mode 100644 index 000000000..be8127389 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0021.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0022.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0022.jpg new file mode 100644 index 000000000..25e23e63d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0022.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0023.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0023.jpg new file mode 100644 index 000000000..ac6e562bc Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0023.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0024.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0024.jpg new file mode 100644 index 000000000..9a0ba07a0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0024.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0025.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0025.jpg new file mode 100644 index 000000000..109fab5dd Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0025.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0026.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0026.jpg new file mode 100644 index 000000000..459d790e4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0026.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0027.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0027.jpg new file mode 100644 index 000000000..248d48e46 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0027.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0028.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0028.jpg new file mode 100644 index 000000000..d704d99d6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0028.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0029.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0029.jpg new file mode 100644 index 000000000..413d039e2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0029.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0030.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0030.jpg new file mode 100644 index 000000000..1e3809ce3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0030.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0031.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0031.jpg new file mode 100644 index 000000000..d6cbc40cc Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0031.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0032.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0032.jpg new file mode 100644 index 000000000..36d730d26 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0032.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0033.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0033.jpg new file mode 100644 index 000000000..e5ef8113d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0033.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0034.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0034.jpg new file mode 100644 index 000000000..8469d76fb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0034.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0035.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0035.jpg new file mode 100644 index 000000000..df4933530 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0035.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0036.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0036.jpg new file mode 100644 index 000000000..312d2bfe9 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0036.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0037.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0037.jpg new file mode 100644 index 000000000..876d501b4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0037.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0038.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0038.jpg new file mode 100644 index 000000000..5c3f809dd Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0038.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0039.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0039.jpg new file mode 100644 index 000000000..d9a66f386 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0039.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0040.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0040.jpg new file mode 100644 index 000000000..49ea7990f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0040.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0041.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0041.jpg new file mode 100644 index 000000000..2bdc6f3f7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0041.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0042.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0042.jpg new file mode 100644 index 000000000..1a0159232 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0042.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0043.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0043.jpg new file mode 100644 index 000000000..4c1656137 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0043.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0044.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0044.jpg new file mode 100644 index 000000000..a81e2a601 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0044.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0045.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0045.jpg new file mode 100644 index 000000000..e89ef3c16 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0045.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0046.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0046.jpg new file mode 100644 index 000000000..85467c19d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0046.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0047.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0047.jpg new file mode 100644 index 000000000..9e87166aa Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0047.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0048.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0048.jpg new file mode 100644 index 000000000..92d1c666f Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0048.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0049.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0049.jpg new file mode 100644 index 000000000..7cfaed9ae Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0049.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0050.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0050.jpg new file mode 100644 index 000000000..9473bd25c Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0050.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0051.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0051.jpg new file mode 100644 index 000000000..133d2c031 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0051.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0052.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0052.jpg new file mode 100644 index 000000000..a956b9933 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0052.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0053.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0053.jpg new file mode 100644 index 000000000..afc484308 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0053.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0054.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0054.jpg new file mode 100644 index 000000000..dac710dce Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0054.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0055.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0055.jpg new file mode 100644 index 000000000..d4f4a53f7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0055.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0056.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0056.jpg new file mode 100644 index 000000000..b6ca1f9cb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0056.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0057.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0057.jpg new file mode 100644 index 000000000..958c26359 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0057.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0058.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0058.jpg new file mode 100644 index 000000000..936ce2953 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0058.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0059.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0059.jpg new file mode 100644 index 000000000..79b2715bb Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0059.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0060.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0060.jpg new file mode 100644 index 000000000..206484e7b Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0060.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0061.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0061.jpg new file mode 100644 index 000000000..2e67c80b3 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0061.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0062.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0062.jpg new file mode 100644 index 000000000..2f7d613a5 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0062.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0063.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0063.jpg new file mode 100644 index 000000000..b67010f21 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0063.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0064.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0064.jpg new file mode 100644 index 000000000..b9ce58bd2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0064.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0065.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0065.jpg new file mode 100644 index 000000000..556d4571d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0065.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0066.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0066.jpg new file mode 100644 index 000000000..c7b3f7264 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0066.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0067.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0067.jpg new file mode 100644 index 000000000..bdf26f155 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0067.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0068.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0068.jpg new file mode 100644 index 000000000..d1596bc8e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0068.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0069.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0069.jpg new file mode 100644 index 000000000..487432f4d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0069.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0070.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0070.jpg new file mode 100644 index 000000000..943d0c1d4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0070.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0071.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0071.jpg new file mode 100644 index 000000000..6a5492e38 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0071.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0072.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0072.jpg new file mode 100644 index 000000000..d8454c007 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0072.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0073.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0073.jpg new file mode 100644 index 000000000..f6026deb2 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0073.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0074.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0074.jpg new file mode 100644 index 000000000..291bb4891 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0074.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0075.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0075.jpg new file mode 100644 index 000000000..d076ffe56 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0075.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0076.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0076.jpg new file mode 100644 index 000000000..440f373ff Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0076.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0077.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0077.jpg new file mode 100644 index 000000000..9ce2c1ec0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0077.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0078.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0078.jpg new file mode 100644 index 000000000..524a645bc Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0078.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0079.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0079.jpg new file mode 100644 index 000000000..bf1241cc7 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0079.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0080.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0080.jpg new file mode 100644 index 000000000..3b78184d6 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0080.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0081.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0081.jpg new file mode 100644 index 000000000..397f129e1 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0081.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0082.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0082.jpg new file mode 100644 index 000000000..75b3376b4 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0082.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0083.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0083.jpg new file mode 100644 index 000000000..825b9ef57 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0083.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0084.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0084.jpg new file mode 100644 index 000000000..3f34d540d Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0084.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0085.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0085.jpg new file mode 100644 index 000000000..7665e6100 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0085.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0086.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0086.jpg new file mode 100644 index 000000000..62e6a645e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0086.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0087.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0087.jpg new file mode 100644 index 000000000..1b3ee79db Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0087.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0088.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0088.jpg new file mode 100644 index 000000000..26a04ae3e Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0088.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/images/frame_0089.jpg b/AgCloud/simulators/data/security/cam-west-02/images/frame_0089.jpg new file mode 100644 index 000000000..1f20e5ea0 Binary files /dev/null and b/AgCloud/simulators/data/security/cam-west-02/images/frame_0089.jpg differ diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0011.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0011.json new file mode 100644 index 000000000..2f140a8ee --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0011.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970534189, + "file_name": "frame_0011.jpg", + "frame_idx": 11 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0012.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0012.json new file mode 100644 index 000000000..f353d2659 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0012.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970535189, + "file_name": "frame_0012.jpg", + "frame_idx": 12 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0013.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0013.json new file mode 100644 index 000000000..ad35fea86 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0013.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970536189, + "file_name": "frame_0013.jpg", + "frame_idx": 13 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0014.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0014.json new file mode 100644 index 000000000..5c951f950 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0014.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970537189, + "file_name": "frame_0014.jpg", + "frame_idx": 14 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0015.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0015.json new file mode 100644 index 000000000..5da9db3aa --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0015.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970538189, + "file_name": "frame_0015.jpg", + "frame_idx": 15 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0016.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0016.json new file mode 100644 index 000000000..c6b5339f8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0016.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970539189, + "file_name": "frame_0016.jpg", + "frame_idx": 16 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0017.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0017.json new file mode 100644 index 000000000..dc8a15bec --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0017.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970540189, + "file_name": "frame_0017.jpg", + "frame_idx": 17 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0018.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0018.json new file mode 100644 index 000000000..495107151 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0018.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970541189, + "file_name": "frame_0018.jpg", + "frame_idx": 18 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0019.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0019.json new file mode 100644 index 000000000..d2b20bb2c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0019.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970542189, + "file_name": "frame_0019.jpg", + "frame_idx": 19 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0020.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0020.json new file mode 100644 index 000000000..f3e23d1cc --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0020.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970543189, + "file_name": "frame_0020.jpg", + "frame_idx": 20 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0021.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0021.json new file mode 100644 index 000000000..3b86ec499 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0021.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970544189, + "file_name": "frame_0021.jpg", + "frame_idx": 21 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0022.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0022.json new file mode 100644 index 000000000..e82ade0bd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0022.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970545189, + "file_name": "frame_0022.jpg", + "frame_idx": 22 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0023.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0023.json new file mode 100644 index 000000000..868f5a0bf --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0023.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970546189, + "file_name": "frame_0023.jpg", + "frame_idx": 23 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0024.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0024.json new file mode 100644 index 000000000..a0e44c50c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0024.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970547189, + "file_name": "frame_0024.jpg", + "frame_idx": 24 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0025.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0025.json new file mode 100644 index 000000000..7f3f38b8f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0025.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970548189, + "file_name": "frame_0025.jpg", + "frame_idx": 25 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0026.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0026.json new file mode 100644 index 000000000..27330f5ff --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0026.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970549189, + "file_name": "frame_0026.jpg", + "frame_idx": 26 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0027.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0027.json new file mode 100644 index 000000000..da4d9d524 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0027.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970550189, + "file_name": "frame_0027.jpg", + "frame_idx": 27 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0028.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0028.json new file mode 100644 index 000000000..f03c8ceda --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0028.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970551189, + "file_name": "frame_0028.jpg", + "frame_idx": 28 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0029.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0029.json new file mode 100644 index 000000000..8272a4153 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0029.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970552189, + "file_name": "frame_0029.jpg", + "frame_idx": 29 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0030.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0030.json new file mode 100644 index 000000000..a8a256e00 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0030.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970553189, + "file_name": "frame_0030.jpg", + "frame_idx": 30 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0031.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0031.json new file mode 100644 index 000000000..c7e2d6478 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0031.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970554189, + "file_name": "frame_0031.jpg", + "frame_idx": 31 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0032.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0032.json new file mode 100644 index 000000000..31817be0d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0032.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970555189, + "file_name": "frame_0032.jpg", + "frame_idx": 32 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0033.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0033.json new file mode 100644 index 000000000..aca2e1e2b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0033.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970556189, + "file_name": "frame_0033.jpg", + "frame_idx": 33 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0034.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0034.json new file mode 100644 index 000000000..b2da56088 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0034.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970557189, + "file_name": "frame_0034.jpg", + "frame_idx": 34 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0035.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0035.json new file mode 100644 index 000000000..bf4eb74f2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0035.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970558189, + "file_name": "frame_0035.jpg", + "frame_idx": 35 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0036.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0036.json new file mode 100644 index 000000000..ffc19aa44 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0036.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970559189, + "file_name": "frame_0036.jpg", + "frame_idx": 36 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0037.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0037.json new file mode 100644 index 000000000..12d669494 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0037.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970560189, + "file_name": "frame_0037.jpg", + "frame_idx": 37 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0038.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0038.json new file mode 100644 index 000000000..3d75c41f9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0038.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970561189, + "file_name": "frame_0038.jpg", + "frame_idx": 38 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0039.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0039.json new file mode 100644 index 000000000..5ee6249e0 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0039.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970562189, + "file_name": "frame_0039.jpg", + "frame_idx": 39 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0040.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0040.json new file mode 100644 index 000000000..15f45aedd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0040.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970563189, + "file_name": "frame_0040.jpg", + "frame_idx": 40 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0041.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0041.json new file mode 100644 index 000000000..dd0b3422c --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0041.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970564189, + "file_name": "frame_0041.jpg", + "frame_idx": 41 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0042.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0042.json new file mode 100644 index 000000000..8ba017e80 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0042.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970565189, + "file_name": "frame_0042.jpg", + "frame_idx": 42 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0043.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0043.json new file mode 100644 index 000000000..06a69ee39 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0043.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970566189, + "file_name": "frame_0043.jpg", + "frame_idx": 43 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0044.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0044.json new file mode 100644 index 000000000..b85d70ebd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0044.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970567189, + "file_name": "frame_0044.jpg", + "frame_idx": 44 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0045.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0045.json new file mode 100644 index 000000000..0a05269fc --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0045.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970568189, + "file_name": "frame_0045.jpg", + "frame_idx": 45 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0046.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0046.json new file mode 100644 index 000000000..44d8761bd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0046.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970569189, + "file_name": "frame_0046.jpg", + "frame_idx": 46 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0047.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0047.json new file mode 100644 index 000000000..006d55bcf --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0047.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970570189, + "file_name": "frame_0047.jpg", + "frame_idx": 47 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0048.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0048.json new file mode 100644 index 000000000..0b173fa9f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0048.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970571189, + "file_name": "frame_0048.jpg", + "frame_idx": 48 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0049.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0049.json new file mode 100644 index 000000000..5116a4a0b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0049.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970572189, + "file_name": "frame_0049.jpg", + "frame_idx": 49 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0050.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0050.json new file mode 100644 index 000000000..a67bcaa5d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0050.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970573189, + "file_name": "frame_0050.jpg", + "frame_idx": 50 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0051.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0051.json new file mode 100644 index 000000000..4c4cdab09 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0051.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970574189, + "file_name": "frame_0051.jpg", + "frame_idx": 51 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0052.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0052.json new file mode 100644 index 000000000..e4a5ce9a4 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0052.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970575189, + "file_name": "frame_0052.jpg", + "frame_idx": 52 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0053.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0053.json new file mode 100644 index 000000000..6fb1bf9bd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0053.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970576189, + "file_name": "frame_0053.jpg", + "frame_idx": 53 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0054.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0054.json new file mode 100644 index 000000000..a4d0f5e34 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0054.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970577189, + "file_name": "frame_0054.jpg", + "frame_idx": 54 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0055.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0055.json new file mode 100644 index 000000000..127b10ce6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0055.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970578189, + "file_name": "frame_0055.jpg", + "frame_idx": 55 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0056.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0056.json new file mode 100644 index 000000000..36fb64a49 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0056.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970579189, + "file_name": "frame_0056.jpg", + "frame_idx": 56 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0057.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0057.json new file mode 100644 index 000000000..d9d229668 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0057.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970580189, + "file_name": "frame_0057.jpg", + "frame_idx": 57 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0058.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0058.json new file mode 100644 index 000000000..edcba21c8 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0058.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970581189, + "file_name": "frame_0058.jpg", + "frame_idx": 58 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0059.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0059.json new file mode 100644 index 000000000..ce7724b39 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0059.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970582189, + "file_name": "frame_0059.jpg", + "frame_idx": 59 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0060.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0060.json new file mode 100644 index 000000000..8359bf670 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0060.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970583189, + "file_name": "frame_0060.jpg", + "frame_idx": 60 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0061.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0061.json new file mode 100644 index 000000000..8adcc1c15 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0061.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970584189, + "file_name": "frame_0061.jpg", + "frame_idx": 61 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0062.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0062.json new file mode 100644 index 000000000..28e16bd95 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0062.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970585189, + "file_name": "frame_0062.jpg", + "frame_idx": 62 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0063.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0063.json new file mode 100644 index 000000000..364dd77f3 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0063.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970586189, + "file_name": "frame_0063.jpg", + "frame_idx": 63 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0064.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0064.json new file mode 100644 index 000000000..1073f9e04 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0064.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970587189, + "file_name": "frame_0064.jpg", + "frame_idx": 64 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0065.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0065.json new file mode 100644 index 000000000..65d9d087b --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0065.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970588189, + "file_name": "frame_0065.jpg", + "frame_idx": 65 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0066.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0066.json new file mode 100644 index 000000000..70f293d0e --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0066.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970589189, + "file_name": "frame_0066.jpg", + "frame_idx": 66 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0067.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0067.json new file mode 100644 index 000000000..f67cc2bcd --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0067.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970590189, + "file_name": "frame_0067.jpg", + "frame_idx": 67 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0068.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0068.json new file mode 100644 index 000000000..82ece425d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0068.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970591189, + "file_name": "frame_0068.jpg", + "frame_idx": 68 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0069.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0069.json new file mode 100644 index 000000000..1888911db --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0069.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970592189, + "file_name": "frame_0069.jpg", + "frame_idx": 69 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0070.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0070.json new file mode 100644 index 000000000..3152e0fad --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0070.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970593189, + "file_name": "frame_0070.jpg", + "frame_idx": 70 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0071.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0071.json new file mode 100644 index 000000000..6cd2c2f97 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0071.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970594189, + "file_name": "frame_0071.jpg", + "frame_idx": 71 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0072.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0072.json new file mode 100644 index 000000000..b06d07951 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0072.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970595189, + "file_name": "frame_0072.jpg", + "frame_idx": 72 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0073.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0073.json new file mode 100644 index 000000000..ac82e2861 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0073.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970596189, + "file_name": "frame_0073.jpg", + "frame_idx": 73 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0074.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0074.json new file mode 100644 index 000000000..4f21638b2 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0074.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970597189, + "file_name": "frame_0074.jpg", + "frame_idx": 74 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0075.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0075.json new file mode 100644 index 000000000..189680382 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0075.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970598189, + "file_name": "frame_0075.jpg", + "frame_idx": 75 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0076.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0076.json new file mode 100644 index 000000000..02ae4d36f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0076.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970599189, + "file_name": "frame_0076.jpg", + "frame_idx": 76 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0077.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0077.json new file mode 100644 index 000000000..00c02904f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0077.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970600189, + "file_name": "frame_0077.jpg", + "frame_idx": 77 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0078.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0078.json new file mode 100644 index 000000000..1dd65f10f --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0078.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970601189, + "file_name": "frame_0078.jpg", + "frame_idx": 78 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0079.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0079.json new file mode 100644 index 000000000..4064925bf --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0079.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970602189, + "file_name": "frame_0079.jpg", + "frame_idx": 79 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0080.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0080.json new file mode 100644 index 000000000..1dd0f7775 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0080.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970603189, + "file_name": "frame_0080.jpg", + "frame_idx": 80 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0081.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0081.json new file mode 100644 index 000000000..7854729a6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0081.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970604189, + "file_name": "frame_0081.jpg", + "frame_idx": 81 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0082.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0082.json new file mode 100644 index 000000000..8def46369 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0082.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970605189, + "file_name": "frame_0082.jpg", + "frame_idx": 82 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0083.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0083.json new file mode 100644 index 000000000..3e76046eb --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0083.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970606189, + "file_name": "frame_0083.jpg", + "frame_idx": 83 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0084.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0084.json new file mode 100644 index 000000000..5373d0257 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0084.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970607189, + "file_name": "frame_0084.jpg", + "frame_idx": 84 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0085.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0085.json new file mode 100644 index 000000000..f14cdc6c9 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0085.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970608189, + "file_name": "frame_0085.jpg", + "frame_idx": 85 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0086.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0086.json new file mode 100644 index 000000000..221ab5f35 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0086.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970609189, + "file_name": "frame_0086.jpg", + "frame_idx": 86 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0087.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0087.json new file mode 100644 index 000000000..f07dd87d6 --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0087.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970610189, + "file_name": "frame_0087.jpg", + "frame_idx": 87 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0088.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0088.json new file mode 100644 index 000000000..d1d27de0a --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0088.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970611189, + "file_name": "frame_0088.jpg", + "frame_idx": 88 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0089.json b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0089.json new file mode 100644 index 000000000..c814c502d --- /dev/null +++ b/AgCloud/simulators/data/security/cam-west-02/metadata/frame_0089.json @@ -0,0 +1,6 @@ +{ + "camera_id": "cam-west-02", + "capture_time": 1762970612189, + "file_name": "frame_0089.jpg", + "frame_idx": 89 +} \ No newline at end of file diff --git a/AgCloud/simulators/data/sound/metadata/mic-1_20250916T191623Z.json b/AgCloud/simulators/data/sound/metadata/mic-1_20250916T191623Z.json new file mode 100644 index 000000000..113596ed9 --- /dev/null +++ b/AgCloud/simulators/data/sound/metadata/mic-1_20250916T191623Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-1_20250916T191623Z.wav", + "device_id": "mic-1", + "capture_time": "2025-09-16T19:16:23Z", + "duration_sec": 5.0, + "done": false, + "sample_rate_hz": 16000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.8459 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/sound/metadata/mic-1_20250917T162528Z.json b/AgCloud/simulators/data/sound/metadata/mic-1_20250917T162528Z.json new file mode 100644 index 000000000..607d82e3e --- /dev/null +++ b/AgCloud/simulators/data/sound/metadata/mic-1_20250917T162528Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-1_20250917T162528Z.wav", + "device_id": "mic-1", + "capture_time": "2025-09-17T16:25:28Z", + "duration_sec": 5.0, + "done": false, + "sample_rate_hz": 44100, + "channels": 2, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.8459 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/sound/metadata/mic-2_20250917T182119Z.json b/AgCloud/simulators/data/sound/metadata/mic-2_20250917T182119Z.json new file mode 100644 index 000000000..b3f104c42 --- /dev/null +++ b/AgCloud/simulators/data/sound/metadata/mic-2_20250917T182119Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-2_20250917T182119Z.wav", + "device_id": "mic-2", + "capture_time": "2025-09-17T18:21:19Z", + "duration_sec": 5.0, + "done": false, + "sample_rate_hz": 16000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.8459 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/sound/metadata/mic-33_20250917T162407Z.json b/AgCloud/simulators/data/sound/metadata/mic-33_20250917T162407Z.json new file mode 100644 index 000000000..8dde5f02d --- /dev/null +++ b/AgCloud/simulators/data/sound/metadata/mic-33_20250917T162407Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-33_20250917T162407Z.wav", + "device_id": "mic-33", + "capture_time": "2025-09-17T16:24:07Z", + "duration_sec": 50.0, + "done": false, + "sample_rate_hz": 44100, + "channels": 2, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 31.89561, + "longitude": 34.8459 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/sound/sounds/mic-1_20250916T191623Z.wav b/AgCloud/simulators/data/sound/sounds/mic-1_20250916T191623Z.wav new file mode 100644 index 000000000..ec92fed17 Binary files /dev/null and b/AgCloud/simulators/data/sound/sounds/mic-1_20250916T191623Z.wav differ diff --git a/AgCloud/simulators/data/sound/sounds/mic-1_20250917T162528Z.wav b/AgCloud/simulators/data/sound/sounds/mic-1_20250917T162528Z.wav new file mode 100644 index 000000000..16161e7ec Binary files /dev/null and b/AgCloud/simulators/data/sound/sounds/mic-1_20250917T162528Z.wav differ diff --git a/AgCloud/simulators/data/sound/sounds/mic-2_20250917T182119Z.wav b/AgCloud/simulators/data/sound/sounds/mic-2_20250917T182119Z.wav new file mode 100644 index 000000000..6752a67fe Binary files /dev/null and b/AgCloud/simulators/data/sound/sounds/mic-2_20250917T182119Z.wav differ diff --git a/AgCloud/simulators/data/sound/sounds/mic-33_20250917T162407Z.wav b/AgCloud/simulators/data/sound/sounds/mic-33_20250917T162407Z.wav new file mode 100644 index 000000000..2c40326b8 Binary files /dev/null and b/AgCloud/simulators/data/sound/sounds/mic-33_20250917T162407Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251003T120500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251003T120500Z.json new file mode 100644 index 000000000..43ce7d2a8 --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251003T120500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251003T120500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-10-03T12:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251101T120500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251101T120500Z.json new file mode 100644 index 000000000..d6ca0d3fc --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251101T120500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251101T120500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-11-01T12:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251102T120500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251102T120500Z.json new file mode 100644 index 000000000..d5a7b2360 --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251102T120500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251102T120500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-11-02T12:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251102T140500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251102T140500Z.json new file mode 100644 index 000000000..fdae67d8b --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251102T140500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251102T140500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-11-02T14:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251103T120500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251103T120500Z.json new file mode 100644 index 000000000..51251b6f5 --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251103T120500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251103T120500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-11-03T12:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251104T120500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251104T120500Z.json new file mode 100644 index 000000000..268fa727e --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251104T120500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251104T120500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-11-04T12:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251105T120500Z.json b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251105T120500Z.json new file mode 100644 index 000000000..41fc118b5 --- /dev/null +++ b/AgCloud/simulators/data/ultra-sound/metadata/mic-u-2_20251105T120500Z.json @@ -0,0 +1,14 @@ +{ + "file_name": "mic-u-2_20251105T120500Z.wav", + "device_id": "mic-u-2", + "capture_time": "2025-11-05T12:05:00Z", + "duration_sec": 0.002, + "done": false, + "sample_rate_hz": 500000, + "channels": 1, + "content_type": "audio/wav", + "gis_origin": { + "latitude": 32.89561, + "longitude": 30.9681 + } +} \ No newline at end of file diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251003T120500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251003T120500Z.wav new file mode 100644 index 000000000..6e9edf1e3 Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251003T120500Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251101T120500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251101T120500Z.wav new file mode 100644 index 000000000..e7b83b3eb Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251101T120500Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251102T120500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251102T120500Z.wav new file mode 100644 index 000000000..576ca0159 Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251102T120500Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251102T140500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251102T140500Z.wav new file mode 100644 index 000000000..3e2cad0f2 Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251102T140500Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251103T120500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251103T120500Z.wav new file mode 100644 index 000000000..f527d8120 Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251103T120500Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251104T120500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251104T120500Z.wav new file mode 100644 index 000000000..4d0c9f01c Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251104T120500Z.wav differ diff --git a/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251105T120500Z.wav b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251105T120500Z.wav new file mode 100644 index 000000000..cf0edce9e Binary files /dev/null and b/AgCloud/simulators/data/ultra-sound/sounds/mic-u-2_20251105T120500Z.wav differ diff --git a/AgCloud/simulators/data_publisher.py b/AgCloud/simulators/data_publisher.py new file mode 100644 index 000000000..0144d28f1 --- /dev/null +++ b/AgCloud/simulators/data_publisher.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +import os +import json +import time +import uuid +import hashlib +import mimetypes +from datetime import datetime, timezone +import paho.mqtt.client as mqtt + +# ---- Configuration ---- +IMAGES_DIR = os.getenv("IMAGES_DIR", "/data/images") +META_DIR = os.getenv("META_DIR", "/data/metadata") + +MQTT_HOST_DATA = os.getenv("MQTT_HOST_DATA", "large-mosquitto") +MQTT_PORT_DATA = int(os.getenv("MQTT_PORT_DATA", "1885")) +MQTT_TOPIC_DATA = os.getenv("MQTT_TOPIC_DATA", "MQTT/imagery/air") + +MQTT_HOST_META = os.getenv("MQTT_HOST_META", "mosquitto") +MQTT_PORT_META = int(os.getenv("MQTT_PORT_META", "1883")) +MQTT_TOPIC_META = os.getenv("MQTT_TOPIC_META", "mqtt/aerial/images/metadata") + +CAMERA_ID = os.getenv("CAMERA_ID", "DRN-482A") +INTERVAL_CHECK = int(os.getenv("INTERVAL_CHECK", "10")) +INTERVAL_PUBLISH = int(os.getenv("INTERVAL_PUBLISH", "10")) +QOS = int(os.getenv("MQTT_QOS", "1")) + +# ---- MQTT Setup ---- +client_images = mqtt.Client(client_id=f"drone-simulator-img-{uuid.uuid4().hex[:6]}") +client_images.connect(MQTT_HOST_DATA, MQTT_PORT_DATA, keepalive=60) +client_images.loop_start() + +client_meta = mqtt.Client(client_id=f"drone-simulator-meta-{uuid.uuid4().hex[:6]}") +print(f"[MQTT] DATA -> {MQTT_HOST_DATA}:{MQTT_PORT_DATA}") +print(f"[MQTT] META -> {MQTT_HOST_META}:{MQTT_PORT_META}") +client_meta.connect(MQTT_HOST_META, MQTT_PORT_META, keepalive=60) +client_meta.loop_start() + +# ---- Helpers ---- +def sha256_hex(path: str): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + +def iso_utc(): + return datetime.utcnow().replace(tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + +def load_metadata_for(img_name): + base = os.path.splitext(os.path.basename(img_name))[0] + meta_path = os.path.join(META_DIR, f"{base}.json") + if os.path.exists(meta_path): + with open(meta_path, "r", encoding="utf-8") as f: + return json.load(f) + return {} + +def generate_new_name(ext=".jpg"): + timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + return f"{CAMERA_ID}_{timestamp}{ext}" + +# ---- Core ---- +def publish_image(image_path): + ext = os.path.splitext(image_path)[1].lower() + meta_data = load_metadata_for(image_path) + new_file_name = generate_new_name(ext) + meta_data["file_name"] = new_file_name + meta_data["capture_time"] = iso_utc() + + with open(image_path, "rb") as f: + data = f.read() + + timestamp_ms = int(time.time() * 1000) + + # Automatically detect content type based on file extension + guessed_type, _ = mimetypes.guess_type(image_path) + if guessed_type: + content_type = guessed_type.replace("/", "_") # e.g. image/jpeg → image_jpeg + else: + content_type = "application_octet-stream" + + topic = f"{MQTT_TOPIC_DATA}/{timestamp_ms}/{content_type}/{new_file_name}" + client_images.publish(topic, payload=data, qos=QOS) + + payload = json.dumps(meta_data, ensure_ascii=False) + client_meta.publish(MQTT_TOPIC_META, payload, qos=QOS) + + print(f"Published file: {new_file_name} | topic: {topic} | type: {guessed_type}") + +def get_all_images(): + exts = {".jpg", ".jpeg", ".png", ".tif", + ".wav", ".mp3", ".flac", ".ogg", ".m4a"} + return [os.path.join(IMAGES_DIR, f) + for f in sorted(os.listdir(IMAGES_DIR)) + if os.path.splitext(f)[1].lower() in exts] + +def main(): + print("Drone simulator started") + print(f" Images broker: {MQTT_HOST_DATA}:{MQTT_PORT_DATA} | topic: {MQTT_TOPIC_DATA}") + print(f" Metadata broker: {MQTT_HOST_META}:{MQTT_PORT_META} | topic: {MQTT_TOPIC_META}") + sent_hashes = set() + + while True: + all_imgs = get_all_images() + new_imgs = [p for p in all_imgs if sha256_hex(p) not in sent_hashes] + + if not new_imgs: + print("No new images. Checking again...") + sent_hashes.clear() + time.sleep(INTERVAL_CHECK) + continue + + for img in new_imgs: + publish_image(img) + sent_hashes.add(sha256_hex(img)) + time.sleep(INTERVAL_PUBLISH) + + print("Cycle completed. Restarting...") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("Stopped manually.") + client_images.loop_stop() + client_images.disconnect() + client_meta.loop_stop() + client_meta.disconnect() diff --git a/AgCloud/simulators/docker-compose.yml b/AgCloud/simulators/docker-compose.yml new file mode 100644 index 000000000..8a9fd8028 --- /dev/null +++ b/AgCloud/simulators/docker-compose.yml @@ -0,0 +1,32 @@ +services: + data-publisher: + build: . + container_name: data-publisher + env_file: .env + volumes: + - ./data/air/images:/data/images:ro + - ./data/air/metadata:/data/metadata:ro + command: ["python", "-u", "/app/data_publisher.py"] + + sound-publisher: + build: . + container_name: sound-publisher + env_file: .env.sound + volumes: + - ./data/sound/sounds:/data/sound/sounds:ro + - ./data/sound/metadata:/data/sound/metadata:ro + command: ["python", "-u", "/app/data_publisher.py"] + + ultra-sound-publisher: + build: . + container_name: ultra-sound-publisher + env_file: .env.ultra + volumes: + - ./data/ultra-sound/sounds:/data/ultra-sound/sounds:ro + - ./data/ultra-sound/metadata:/data/ultra-sound/metadata:ro + command: ["python", "-u", "/app/data_publisher.py"] + +networks: + default: + external: true + name: ag_cloud diff --git a/AgCloud/simulators/readme b/AgCloud/simulators/readme new file mode 100644 index 000000000..d49d81d35 --- /dev/null +++ b/AgCloud/simulators/readme @@ -0,0 +1,37 @@ +# README – Running the Device Simulator + +## Steps to Run + +1. **Start all project services** + ```bash + docker compose up -d + ``` + +2. **Start the device simulator** + From the simulator’s directory: + ```bash + docker compose up -d + ``` + +--- + +## Verifying the System + +### 1. View uploaded images in MinIO +Open in your browser: +``` +http://localhost:9002 +``` + +**Default credentials:** +``` +MINIO_ROOT_USER = minioadmin +MINIO_ROOT_PASSWORD = minioadmin123 +``` + +--- + +### 2. Monitor Kafka messages +Run the following command to view messages sent to the metadata topic: +```bash +docker exec -it kafka /opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic aerial_images_metadata --from-beginning``` diff --git a/AgCloud/simulators/security/Dockerfile b/AgCloud/simulators/security/Dockerfile new file mode 100644 index 000000000..1118fa4a4 --- /dev/null +++ b/AgCloud/simulators/security/Dockerfile @@ -0,0 +1,27 @@ +# Use official Python slim image +FROM python:3.12-slim + +# Copy the NetFree certificate into the container + +# COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt + +# COPY certs/*.crt /usr/local/share/ca-certificates/ + + +# Install system dependencies, add the certificate, and clean cache +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + update-ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +RUN pip install --no-cache-dir paho-mqtt + +# Set working directory +WORKDIR /app + +# Copy project files +COPY data_publisher.py . + +# Run the Python script +CMD ["python", "-u", "/app/data_publisher.py"] diff --git a/AgCloud/simulators/security/data_publisher.py b/AgCloud/simulators/security/data_publisher.py new file mode 100644 index 000000000..fe87a141e --- /dev/null +++ b/AgCloud/simulators/security/data_publisher.py @@ -0,0 +1,302 @@ +# #!/usr/bin/env python3 +# import os +# import json +# import time +# import uuid +# import hashlib +# import mimetypes +# from datetime import datetime, timezone +# import paho.mqtt.client as mqtt + +# # ---- Configuration ---- +# IMAGES_DIR = os.getenv("IMAGES_DIR", "/data/images") +# META_DIR = os.getenv("META_DIR", "/data/metadata") + +# MQTT_HOST_DATA = os.getenv("MQTT_HOST_DATA", "large-mosquitto") +# MQTT_PORT_DATA = int(os.getenv("MQTT_PORT_DATA", "1885")) +# MQTT_TOPIC_DATA = os.getenv("MQTT_TOPIC_DATA", "MQTT/imagery/security") + +# MQTT_HOST_META = os.getenv("MQTT_HOST_META", "mosquitto") +# MQTT_PORT_META = int(os.getenv("MQTT_PORT_META", "1883")) +# MQTT_TOPIC_META = os.getenv("MQTT_TOPIC_META", "dev-security-images-keys") + +# CAMERA_ID = os.getenv("CAMERA_ID", "CAM-482A") +# INTERVAL_CHECK = int(os.getenv("INTERVAL_CHECK", "10")) +# INTERVAL_PUBLISH = int(os.getenv("INTERVAL_PUBLISH", "10")) +# QOS = int(os.getenv("MQTT_QOS", "1")) + +# # ---- MQTT Setup ---- +# client_images = mqtt.Client(client_id=f"camera-simulator-img-{uuid.uuid4().hex[:6]}") +# client_images.connect(MQTT_HOST_DATA, MQTT_PORT_DATA, keepalive=60) +# client_images.loop_start() + +# client_meta = mqtt.Client(client_id=f"camera-simulator-meta-{uuid.uuid4().hex[:6]}") +# client_meta.connect(MQTT_HOST_META, MQTT_PORT_META, keepalive=60) +# client_meta.loop_start() + +# # ---- Helpers ---- +# def sha256_hex(path: str): +# with open(path, "rb") as f: +# return hashlib.sha256(f.read()).hexdigest() + +# def iso_utc(): +# return datetime.utcnow().replace(tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + +# def load_metadata_for(img_name): +# base = os.path.splitext(os.path.basename(img_name))[0] +# meta_path = os.path.join(META_DIR, f"{base}.json") +# if os.path.exists(meta_path): +# with open(meta_path, "r", encoding="utf-8") as f: +# return json.load(f) +# return {} + +# def generate_new_name(ext=".jpg",camera_id = CAMERA_ID): +# timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") +# return f"{camera_id}_{timestamp}{ext}" + +# # ---- Core ---- +# def publish_image(image_path): +# ext = os.path.splitext(image_path)[1].lower() +# meta_data = load_metadata_for(image_path) +# camera_id = meta_data["camera_id"] or CAMERA_ID +# new_file_name = generate_new_name(ext,camera_id) +# meta_data["file_name"] = new_file_name +# meta_data["capture_time"] = iso_utc() + +# with open(image_path, "rb") as f: +# data = f.read() + +# timestamp_ms = int(time.time() * 1000) + +# # Automatically detect content type based on file extension +# guessed_type, _ = mimetypes.guess_type(image_path) +# if guessed_type: +# content_type = guessed_type.replace("/", "_") # e.g. image/jpeg → image_jpeg +# else: +# content_type = "application_octet-stream" + +# topic = f"{MQTT_TOPIC_DATA}/{timestamp_ms}/{content_type}/{new_file_name}" +# client_images.publish(topic, payload=data, qos=QOS) +# payload = json.dumps(meta_data, ensure_ascii=False) +# client_meta.publish(MQTT_TOPIC_META, payload, qos=QOS) + +# print(f"Published image: {new_file_name} | topic: {topic} | type: {guessed_type}") + +# def get_all_images(): +# exts = {".jpg", ".jpeg", ".png", ".tif"} +# return [os.path.join(IMAGES_DIR, f) +# for f in sorted(os.listdir(IMAGES_DIR)) +# if os.path.splitext(f)[1].lower() in exts] + +# def main(): +# print("Camera simulator started") +# print(f" Images broker: {MQTT_HOST_DATA}:{MQTT_PORT_DATA} | topic: {MQTT_TOPIC_DATA}") +# print(f" Metadata broker: {MQTT_HOST_META}:{MQTT_PORT_META} | topic: {MQTT_TOPIC_META}") +# sent_hashes = set() + +# while True: +# all_imgs = get_all_images() +# new_imgs = [p for p in all_imgs if sha256_hex(p) not in sent_hashes] + +# if not new_imgs: +# print("No new images. Checking again...") +# sent_hashes.clear() +# time.sleep(INTERVAL_CHECK) +# continue + +# for img in new_imgs: +# publish_image(img) +# sent_hashes.add(sha256_hex(img)) +# time.sleep(INTERVAL_PUBLISH) + +# print("Cycle completed. Restarting...") + +# if __name__ == "__main__": +# try: +# main() +# except KeyboardInterrupt: +# print("Stopped manually.") +# client_images.loop_stop() +# client_images.disconnect() +# client_meta.loop_stop() +# client_meta.disconnect() + + + + +#!/usr/bin/env python3 +import os +import json +import time +import uuid +import hashlib +import mimetypes +import threading +from datetime import datetime, timezone +import paho.mqtt.client as mqtt + +# ───────────────────────────────────────────── +# Configuration +# ───────────────────────────────────────────── +SECURITY_DIR = os.getenv("SECURITY_DIR", "/security") + +MQTT_HOST_DATA = os.getenv("MQTT_HOST_DATA", "large-mosquitto") +MQTT_PORT_DATA = int(os.getenv("MQTT_PORT_DATA", "1885")) +MQTT_TOPIC_DATA = os.getenv("MQTT_TOPIC_DATA", "MQTT/imagery/security") + +MQTT_HOST_META = os.getenv("MQTT_HOST_META", "mosquitto") +MQTT_PORT_META = int(os.getenv("MQTT_PORT_META", "1883")) +MQTT_TOPIC_META = os.getenv("MQTT_TOPIC_META", "dev-security-images-keys") + +INTERVAL_PUBLISH = int(os.getenv("INTERVAL_PUBLISH", "10")) +QOS = int(os.getenv("MQTT_QOS", "1")) + + +# ───────────────────────────────────────────── +# Helpers +# ───────────────────────────────────────────── +def sha256_hex(path: str): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def iso_utc(): + return datetime.utcnow().replace(tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def generate_new_name(ext=".jpg", camera_id="UNKNOWN"): + timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + return f"{camera_id}_{timestamp}{ext}" + + +def load_metadata_for(img_path, metadata_dir): + base = os.path.splitext(os.path.basename(img_path))[0] + meta_path = os.path.join(metadata_dir, f"{base}.json") + if os.path.exists(meta_path): + with open(meta_path, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + +def get_all_images(images_dir): + exts = {".jpg", ".jpeg", ".png", ".tif"} + return [ + os.path.join(images_dir, f) + for f in sorted(os.listdir(images_dir)) + if os.path.splitext(f)[1].lower() in exts + ] + + +# ───────────────────────────────────────────── +# Publishing logic +# ───────────────────────────────────────────── +def publish_image(image_path, metadata_dir, camera_id, client_images, client_meta): + ext = os.path.splitext(image_path)[1].lower() + meta_data = load_metadata_for(image_path, metadata_dir) + meta_data["camera_id"] = camera_id + new_file_name = generate_new_name(ext, camera_id) + meta_data["file_name"] = new_file_name + meta_data["capture_time"] = iso_utc() + + with open(image_path, "rb") as f: + data = f.read() + + timestamp_ms = int(time.time() * 1000) + + guessed_type, _ = mimetypes.guess_type(image_path) + if guessed_type: + content_type = guessed_type.replace("/", "_") + else: + content_type = "application_octet-stream" + + topic = f"{MQTT_TOPIC_DATA}/{timestamp_ms}/{content_type}/{new_file_name}" + client_images.publish(topic, payload=data, qos=QOS) + + payload = json.dumps(meta_data, ensure_ascii=False) + client_meta.publish(MQTT_TOPIC_META, payload, qos=QOS) + + print(f"[{camera_id}] Published image: {new_file_name} | topic: {topic} | type: {guessed_type}") + + +# ───────────────────────────────────────────── +# Per-camera worker (single run) +# ───────────────────────────────────────────── +def camera_worker(camera_path): + camera_id = os.path.basename(camera_path) + images_dir = os.path.join(camera_path, "images") + metadata_dir = os.path.join(camera_path, "metadata") + + if not os.path.isdir(images_dir): + print(f"[{camera_id}] Skipping: no /images folder found") + return + if not os.path.isdir(metadata_dir): + print(f"[{camera_id}] Warning: no /metadata folder found") + + # Create separate MQTT clients per camera + client_images = mqtt.Client(client_id=f"{camera_id}-img-{uuid.uuid4().hex[:6]}") + client_images.connect(MQTT_HOST_DATA, MQTT_PORT_DATA, keepalive=60) + client_images.loop_start() + + client_meta = mqtt.Client(client_id=f"{camera_id}-meta-{uuid.uuid4().hex[:6]}") + client_meta.connect(MQTT_HOST_META, MQTT_PORT_META, keepalive=60) + client_meta.loop_start() + + print(f"[{camera_id}] Started publishing thread") + + try: + all_imgs = get_all_images(images_dir) + if not all_imgs: + print(f"[{camera_id}] No images found, skipping.") + else: + for img in all_imgs: + publish_image(img, metadata_dir, camera_id, client_images, client_meta) + time.sleep(INTERVAL_PUBLISH) + + print(f"[{camera_id}] Finished publishing {len(all_imgs)} images.") + except Exception as e: + print(f"[{camera_id}] Error: {e}") + finally: + client_images.loop_stop() + client_images.disconnect() + client_meta.loop_stop() + client_meta.disconnect() + + +# ───────────────────────────────────────────── +# Main entry point +# ───────────────────────────────────────────── +def main(): + print("Camera simulator (single-run mode, multi-camera parallel)") + print(f"Root directory: {SECURITY_DIR}") + + if not os.path.isdir(SECURITY_DIR): + print(f"❌ SECURITY_DIR does not exist: {SECURITY_DIR}") + return + + camera_dirs = [ + os.path.join(SECURITY_DIR, d) + for d in os.listdir(SECURITY_DIR) + if os.path.isdir(os.path.join(SECURITY_DIR, d)) + ] + + if not camera_dirs: + print("❌ No camera directories found.") + return + + threads = [] + for cam_dir in camera_dirs: + t = threading.Thread(target=camera_worker, args=(cam_dir,)) + t.start() + threads.append(t) + + # Wait for all camera threads to finish + for t in threads: + t.join() + + print("✅ All cameras finished publishing. Exiting.") + + +if __name__ == "__main__": + main() + + diff --git a/AgCloud/simulators/security/docker-compose.yml b/AgCloud/simulators/security/docker-compose.yml new file mode 100644 index 000000000..a9b525fdb --- /dev/null +++ b/AgCloud/simulators/security/docker-compose.yml @@ -0,0 +1,24 @@ +services: + + data-publisher: + build: . + container_name: security-publisher + # env_file: .env + volumes: + - ../data/security:/security:ro + command: ["python", "-u", "/app/data_publisher.py"] + + # sound-publisher: + # build: . + # # image: data-publisher:latest # or 'build: .' again if you prefer + # container_name: sound-publisher + # env_file: .env.sound # separate env file for sound topics/ids + # volumes: + # - ./data/sound/sounds:/data/sound/sounds:ro + # - ./data/sound/metadata:/data/sound/metadata:ro + # command: ["python", "-u", "/app/data_publisher.py"] + +networks: + default: + external: true + name: agcloud_ag_cloud diff --git a/AgCloud/simulators/sounds/data_upload_simulator.py b/AgCloud/simulators/sounds/data_upload_simulator.py new file mode 100644 index 000000000..be2ad9a4a --- /dev/null +++ b/AgCloud/simulators/sounds/data_upload_simulator.py @@ -0,0 +1,144 @@ +import os +import time +from minio import Minio +from minio.error import S3Error +import paho.mqtt.client as mqtt + +# --- MinIO connection details --- +MINIO_ENDPOINT = "localhost:9001" # MinIO server address (change if remote) +ACCESS_KEY = "minioadmin" +SECRET_KEY = "minioadmin123" +BUCKET_NAME = "audio-files" # The MinIO bucket where we will upload files + +# --- Connecting to MinIO --- +minio_client = Minio( + MINIO_ENDPOINT, + access_key=ACCESS_KEY, + secret_key=SECRET_KEY, + secure=False # Set to True if using TLS/SSL +) + +# --- MQTT connection details --- +MQTT_BROKER = "localhost" # Address of the MQTT broker (Mosquitto) +MQTT_PORT = 1883 # Default MQTT port + +# --- MQTT Topics --- +MQTT_AUDIO_TOPIC = "audio-files/audio/uploaded" +MQTT_JSON_TOPIC = "audio-files/json/uploaded" + +# --- Ensure the bucket exists, if not, create it --- +if not minio_client.bucket_exists(BUCKET_NAME): + minio_client.make_bucket(BUCKET_NAME) + +# --- Local directories --- +LOCAL_AUDIO_DIR = "./mqtt_images/data/real_images/audio" # Folder where the audio files are stored +LOCAL_JSON_DIR = "./mqtt_images/data/real_images/json" # Folder where the JSON files are stored + +# --- Valid file formats --- +valid_audio_formats = [".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"] +valid_json_formats = [".json"] + +# --- Function to check if the file has a valid format --- +def is_valid_audio_format(file_name): + return any(file_name.endswith(ext) for ext in valid_audio_formats) + +def is_valid_json_format(file_name): + return any(file_name.endswith(ext) for ext in valid_json_formats) + +# --- MQTT Client Setup --- +mqtt_client = mqtt.Client() + +# --- Connect to MQTT Broker --- +mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60) + +# --- MQTT Callbacks --- +def on_connect(client, userdata, flags, rc): + print(f"Connected with result code {rc}") + +mqtt_client.on_connect = on_connect +mqtt_client.loop_start() # Start MQTT loop to keep connection active + +# --- Function to send MQTT message --- +def send_mqtt_message(file_name, file_type="audio"): + if file_type == "audio": + topic = MQTT_AUDIO_TOPIC + elif file_type == "json": + topic = MQTT_JSON_TOPIC + else: + raise ValueError("Invalid file type specified.") + + message = f"New {file_type} file uploaded: {file_name}" + result = mqtt_client.publish(topic, message) + if result.rc == mqtt.MQTT_ERR_SUCCESS: + print(f"Sent MQTT message: {message}") + else: + print(f"Failed to send message: {message}") + +# --- Function to upload a file --- +def upload_file(file_path, file_type="audio"): + file = os.path.basename(file_path) + object_name = f"{file_type}/{int(time.time())}_{file}" + + # Define content type based on file type + if file_type == "audio": + content_type = "audio/wav" if file.endswith(".wav") else "audio/mpeg" + elif file_type == "json": + content_type = "application/json" + else: + raise ValueError("Invalid file type specified.") + + try: + # Upload the file to MinIO + minio_client.fput_object(BUCKET_NAME, object_name, file_path, content_type=content_type) + print(f"File uploaded: {object_name}") + + # Notify via MQTT + send_mqtt_message(object_name, file_type) + + # After uploading, delete the file from the local folder + os.remove(file_path) + print(f"File {file} removed from the local folder.") + return True + except S3Error as e: + print(f"Error uploading {file}: {e}") + return False + +# --- Simulation loop --- +def run_simulation(): + empty_checks = 0 # Tracks the number of consecutive failed checks + + while True: + # Get all valid files from the audio and json directories + audio_files = [f for f in os.listdir(LOCAL_AUDIO_DIR) if is_valid_audio_format(f)] + json_files = [f for f in os.listdir(LOCAL_JSON_DIR) if is_valid_json_format(f)] + + # Upload audio files if there are any + if audio_files: + empty_checks = 0 # Reset empty counter + print(f"Found {len(audio_files)} audio file(s). Uploading...") + for file in audio_files: + file_path = os.path.join(LOCAL_AUDIO_DIR, file) + upload_file(file_path, file_type="audio") # Upload the audio file + + # Upload JSON files if there are any + if json_files: + empty_checks = 0 # Reset empty counter + print(f"Found {len(json_files)} JSON file(s). Uploading...") + for file in json_files: + file_path = os.path.join(LOCAL_JSON_DIR, file) + upload_file(file_path, file_type="json") # Upload the json file + + # Sleep after checking both directories + if audio_files or json_files: + print("All files uploaded. Sleeping for 5 minutes...") + time.sleep(300) # Wait 5 minutes before checking again + else: + empty_checks += 1 + print(f"No valid files found (check {empty_checks}/6). Sleeping 10 minutes...") + if empty_checks >= 2: + print("No valid files added for 1 hour. Stopping the script.") + break + time.sleep(600) # Wait 10 minutes before checking again + +if __name__ == "__main__": + run_simulation() diff --git a/AgCloud/storage_with_mqtt/.gitignore b/AgCloud/storage_with_mqtt/.gitignore new file mode 100644 index 000000000..cf956e5a1 --- /dev/null +++ b/AgCloud/storage_with_mqtt/.gitignore @@ -0,0 +1,49 @@ +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Virtualenv +.venv/ +venv/ +.env +.env.* + +# Certificates +*.crt +*.pem +*.key + +# Docker +*.log +.dockerignore +docker-compose.override.yml + +# IDE/Editor +.vscode/ +.idea/ + +# System +*.DS_Store +Thumbs.db + +# Test / Coverage +.pytest_cache/ +.coverage +htmlcov/ + +# Build / Dist +build/ +dist/ +*.egg-info/ + +# Project specific +mqtt_images/outbox/ +mqtt_images/data/real_images/ +storage/minio-storage/data/ +storage/combined_minio_setup/config/ +storage/monitoring/grafana-data/ + +# Windows ADS metadata (downloaded files) +*:Zone.Identifier diff --git a/AgCloud/storage_with_mqtt/README.md b/AgCloud/storage_with_mqtt/README.md new file mode 100644 index 000000000..362a1480a --- /dev/null +++ b/AgCloud/storage_with_mqtt/README.md @@ -0,0 +1,94 @@ +# MQTT → MinIO Hot/Cold Ingest (with ILM Tiering) + +This project sets up a local development environment for an MQTT → MinIO (Hot) pipeline +with automatic tiering to MinIO (Cold). It includes automatic ILM bootstrap, +a sample publisher, and monitoring (Prometheus/Grafana). + +--- + +## Prerequisites +- Docker + Docker Compose installed +- Open ports: + - `1883` (MQTT broker) + - `9000/9001` (MinIO Hot server + console) + - `9100/9101` (MinIO Cold server + console) + - `3000` (Grafana) + - `9090` (Prometheus) + +--- + +## Quick Start + +```bash +docker compose up -d --build +``` + +Default credentials: +- `MINIO_ROOT_USER=minioadmin` +- `MINIO_ROOT_PASSWORD=minioadmin123` + +--- + +## Services + +- **MinIO Hot Console:** http://localhost:9002 +- **MinIO Cold Console:** http://localhost:9102 +- **MQTT Broker:** tcp://localhost:1883 +- **Grafana Dashboard:** http://localhost:3000 +- **Prometheus Metrics:** http://localhost:9090 + +--- + +## What the bootstrap (`init.sh`) does + +- Configures `mc` aliases for Hot/Cold +- Ensures buckets `imagery` and `sound` exist +- Enables Versioning on those buckets +- Creates remote tiers: `COLD_IMAGERY`, `COLD_SOUND` +- Applies default ILM (7 days → Cold) + +--- + +## Quick Checks + +List buckets and tiers: +```bash +docker compose exec mc-bootstrap sh -lc 'mc ls hot && mc admin tier ls hot' +``` + +Check ILM: +```bash +docker compose exec mc-bootstrap sh -lc 'mc ilm ls hot/imagery && mc ilm ls hot/sound' +``` + +Upload a test file: +```bash +docker compose exec mc-bootstrap sh -lc 'echo "hello" > /tmp/test.txt && mc cp /tmp/test.txt hot/imagery/' +``` + +Check file status: +```bash +docker compose exec mc-bootstrap sh -lc 'mc stat hot/imagery/test.txt' +``` + +--- + + +## Reset Rules + +```bash +docker compose exec mc-bootstrap sh -lc ' +for b in imagery sound; do + ids=$(mc ilm export hot/$b 2>/dev/null | sed -n "s/.*\"ID\" *: *\"\([^\"]\+\)\".*/\1/p" || true) + for id in $ids; do mc ilm rule rm hot/$b --id "$id" || true; done +done +' +docker compose restart mc-bootstrap +``` + +--- + +## Troubleshooting + +- **"Invalid credentials"** → check `MINIO_ROOT_USER`/`MINIO_ROOT_PASSWORD` +- **File remains STANDARD** → wait or verify ILM is set to 0 days diff --git a/AgCloud/storage_with_mqtt/docker-compose.yml b/AgCloud/storage_with_mqtt/docker-compose.yml new file mode 100644 index 000000000..31bb24ae7 --- /dev/null +++ b/AgCloud/storage_with_mqtt/docker-compose.yml @@ -0,0 +1,168 @@ +services: + large-mosquitto: + image: eclipse-mosquitto:2 + restart: unless-stopped + volumes: + - ./mqtt_images/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf + ports: + - "1885:1885" + + # ===== MinIO: hot + cold + bootstrap ===== + minio-hot: + env_file: + - ./storage/combined_minio_setup/.env + build: + context: ./storage/minio-storage + environment: + - MINIO_PROMETHEUS_AUTH_TYPE=public + ports: + - "9001:9000" # HOT S3 (host:9001 -> container:9000) + - "9002:9001" # HOT Console (host:9002 -> container:9001) + networks: [minionet] + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready"] + interval: 3s + timeout: 2s + retries: 40 + volumes: + - minio-hot-data:/data + + minio-cold: + env_file: + - ./storage/combined_minio_setup/.env + build: + context: ./storage/minio-storage + environment: + - MINIO_PROMETHEUS_AUTH_TYPE=public + ports: + - "9101:9000" # COLD S3 + - "9102:9001" # COLD Console + networks: [minionet] + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready"] + interval: 3s + timeout: 2s + retries: 40 + volumes: + - minio-cold-data:/data + + mc-bootstrap: + env_file: + - ./storage/combined_minio_setup/.env + build: + context: ./storage/Lifecycle_rules/minio-bootstrap + volumes: + - ./storage/combined_minio_setup/config:/config:ro + - ./data/config:/config + depends_on: + minio-hot: + condition: service_healthy + minio-cold: + condition: service_healthy + command: ["/bin/bash","-lc","/entrypoint/init.sh; tail -f /dev/null"] + environment: + MC_ALIAS_HOT: hot + MC_ALIAS_COLD: cold + HOT_ENDPOINT: http://minio-hot:9000 + COLD_ENDPOINT: http://minio-cold:9000 + networks: [minionet] + restart: unless-stopped + + + mqtt_ingest: + build: + context: ./mqtt_images/mqtt_ingest + env_file: + - ./storage/combined_minio_setup/.env + environment: + MINIO_ENDPOINT: http://minio-hot:9000 + S3_BUCKET: imagery + MULTIPART_THRESHOLD_BYTES: 5242880 + PART_SIZE_BYTES: 5242880 + MULTIPART_MAX_CONCURRENCY: 32 + MQTT_BROKER: large-mosquitto + MQTT_PORT: 1885 + MQTT_TOPIC: MQTT/imagery/# + MQTT_PUB_TOPIC: imagery/ingested + DUMMY_DB: 0 + DB_API_BASE: http://host.docker.internal:8080 + DB_API_TOKEN: dev-token + OUTBOX_DIR: /app/outbox + INGEST_WORKERS: 8 + + volumes: + - ./mqtt_images/outbox:/app/outbox + depends_on: + large-mosquitto: + condition: service_started + minio-hot: + condition: service_healthy + mc-bootstrap: + condition: service_started + networks: + - default + - minionet + restart: unless-stopped + + + mqtt_publisher: + build: + context: ./mqtt_images/mqtt_publisher + environment: + - MQTT_HOST=large-mosquitto + - MQTT_PORT=1885 + - MQTT_TOPIC_BASE=MQTT/imagery + - IMAGES_DIR=/images + - CAMERA_ID=camera-01 + - LIMIT=0 + - SHUFFLE=1 + - MQTT_QOS=2 + - PUBLISH_DELAY_MS=10 + volumes: + - ./mqtt_images/data/real_images:/images:ro + depends_on: + - large-mosquitto + restart: "no" + + # ===== Monitoring ===== + prometheus: + image: prom/prometheus:latest + container_name: monitoring-prometheus + command: ["--config.file=/etc/prometheus/prometheus.yml"] + volumes: + - ./storage/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9090:9090" + networks: [minionet] + depends_on: + - minio-hot + - minio-cold + + grafana: + image: grafana/grafana:latest + container_name: monitoring-grafana + ports: + - "3000:3000" + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + depends_on: + - prometheus + networks: [minionet] + volumes: + - ./storage/monitoring/provisioning:/etc/grafana/provisioning:ro + - ./storage/monitoring/grafana-dashboard.json:/var/lib/grafana/dashboards/minio-dashboard.json:ro + # --NetFree-- + - ./certs/company-ca.crt:/usr/local/share/ca-certificates/company-ca.crt:ro + command: ["/bin/sh","-c","update-ca-certificates && /run.sh"] + +networks: + minionet: + driver: bridge + +volumes: + minio-hot-data: {} + minio-cold-data: {} + + + diff --git a/AgCloud/storage_with_mqtt/mqtt_images/latency_now.md b/AgCloud/storage_with_mqtt/mqtt_images/latency_now.md new file mode 100644 index 000000000..453024d7e --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/latency_now.md @@ -0,0 +1,43 @@ +# Latency & Throughput Report + +**Setup:** Publisher → Mosquitto → mqtt_ingest → MinIO (S3 API); ~3,000 images, ~1 MiB each. + +## Run A — Single PUT (no multipart) +- **count:** 813 +- **latency (ms):** p50=4884.00, p95=8489.00, p99=8753.00, avg=4717.61 +- **throughput:** 74.19 imgs/s (~4451 imgs/min) +- **data volume:** 1240.83 MB total (113.22 MB/s) + +## Run B — Multipart PUT +- **count:** 1609 +- **latency (ms):** p50=5069.00, p95=8823.00, p99=9172.00, avg=4939.08 +- **throughput:** 40.34 imgs/s (~2420 imgs/min) +- **data volume:** 2454.98 MB total (61.55 MB/s) + +## Run C — Current run (latest) +- **count:** 3000 (S3) / 3000 (CSV) +- **elapsed (publisher):** 49.59 s +- **throughput:** 60.5 imgs/s (~3,630 imgs/min) +- **broker logs:** no drops/disconnects observed +- **latency quantiles:** not computed here (CSV available with 3,000 rows) +- **data rate, assuming 1 MiB/image:** ~60.5 MB/s (~3.0 GB total) +- **data rate, assuming 1.53 MB/image (as in A/B):** ~92.6 MB/s (~4.59 GB total) + +## Comparison +- **A vs B:** Δp95 = +334 ms (+3.9%), Δthroughput = −33.85 imgs/s (−45.6%), ΔMB/s = −51.68 MB/s +- **C vs A:** throughput −18.5% (60.5 vs 74.19 imgs/s) +- **C vs B:** throughput +50% (60.5 vs 40.34 imgs/s) + +## Metric Glossary +- **count:** number of images included in the metric. +- **latency (ms):** end-to-end time from publisher timestamp in the MQTT topic to successful object upload to S3/MinIO, measured in milliseconds. +- **latency quantiles (p50/p95/p99):** the 50th/95th/99th percentile of latencies; e.g., p95 means 95% of images were faster than this value. +- **avg:** arithmetic mean latency across all images in the run. +- **throughput (imgs/s, imgs/min):** how many images are processed per second (or minute), computed as count ÷ elapsed time. +- **data volume (MB total):** total payload bytes uploaded, converted to megabytes. +- **data rate (MB/s):** data volume ÷ elapsed time. +- **elapsed (publisher):** wall-clock time the publisher spent sending the batch; it approximates but is not identical to end-to-end ingest time. +- **Single PUT vs Multipart:** Single PUT uploads the whole object in one request; Multipart splits large objects into parts and uploads in parallel—useful for very large files or high-latency links, but it can add overhead for ~1 MiB objects. + +## Notes +- Current run quantiles can be computed from `out/latency.csv` (3,000 rows) later if needed. diff --git a/AgCloud/storage_with_mqtt/mqtt_images/mosquitto/mosquitto.conf b/AgCloud/storage_with_mqtt/mqtt_images/mosquitto/mosquitto.conf new file mode 100644 index 000000000..c73d64f34 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/mosquitto/mosquitto.conf @@ -0,0 +1,13 @@ +listener 1885 0.0.0.0 +allow_anonymous true + +persistence true +persistence_location /mosquitto/data/ + +max_packet_size 104857600 +max_inflight_bytes 0 +max_queued_messages 0 +max_queued_bytes 0 +message_size_limit 0 + + diff --git a/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/Dockerfile b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/Dockerfile new file mode 100644 index 000000000..bcdb44863 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim + +# Build argument +ARG USE_NETFREE=true + +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* + +# Install NetFree certificates only if USE_NETFREE=true +RUN if [ "$USE_NETFREE" = "true" ]; then \ + echo "Configuring NetFree certificates..."; \ + # Check if the certificate file exists + if [ -n "$(ls ./*.crt 2>/dev/null)" ]; then \ + cp ./*.crt /usr/local/share/ca-certificates/; \ + chmod 644 /usr/local/share/ca-certificates/*.crt; \ + update-ca-certificates; \ + else \ + echo "No NetFree certificate found, skipping"; \ + fi; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + paho-mqtt boto3 requests + +WORKDIR /app +COPY app.py . +COPY config.py . +ENV OUT_DIR=/out +VOLUME ["/out"] +CMD ["python","-u","/app/app.py"] diff --git a/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/app.py b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/app.py new file mode 100644 index 000000000..8fc9344d4 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/app.py @@ -0,0 +1,476 @@ +# ---------- Imports ---------- +import os, io, time, hashlib, threading, queue, signal, json, uuid, errno, pathlib, mimetypes +from datetime import datetime, timezone +from typing import Tuple +from urllib.parse import quote + +import boto3 +from botocore.config import Config as BotoConfig +from boto3.s3.transfer import TransferConfig + +import paho.mqtt.client as mqtt +from paho.mqtt.client import CallbackAPIVersion + +import requests +from requests.adapters import HTTPAdapter +from urllib3.util.retry import Retry + +try: + from mqtt_ingest.config import cfg +except ModuleNotFoundError: + from config import cfg + +# ---------- ENV ---------- +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "http://minio:9000") +AWS_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +AWS_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin123") +BUCKET = os.getenv("S3_BUCKET", "imagery") + +MQTT_BROKER = os.getenv("MQTT_BROKER", "large-mosquitto") +CLIENT_ID = os.getenv("MQTT_CLIENT_ID", "mqtt_ingest") +DEFAULT_PREFIX = os.getenv("DEFAULT_PREFIX", "camera-01") + +FORCE_DEVICE_ID = (os.getenv("DEVICE_ID", "").strip() or None) + +DB_API_BASE = os.getenv("DB_API_BASE", "").strip() +DB_API_TOKEN = os.getenv("DB_API_TOKEN", "").strip() +DB_API_AUTH_MODE = os.getenv("DB_API_AUTH_MODE", "service").lower() +DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/app/secret/db_api_token") +DB_API_SERVICE_NAME= os.getenv("DB_API_SERVICE_NAME", "mqtt_ingest").strip() or "mqtt_ingest" +OUTBOX_DIR = os.getenv("OUTBOX_DIR", "/app/outbox") +DUMMY_DB = os.getenv("DUMMY_DB", "0") == "1" + +INGEST_QUEUE_MAXSIZE = int(os.getenv("INGEST_QUEUE_MAXSIZE", "1000")) + +# ---------- NUMERIC FROM CONFIG ---------- +MP_THRESHOLD = cfg.MP_THRESHOLD +PART_SIZE = cfg.PART_SIZE +MAX_CONC = cfg.MAX_CONC +MQTT_PORT = cfg.MQTT_PORT +INGEST_WORKERS = cfg.INGEST_WORKERS + +# ---------- MQTT SUBSCRIBE TOPIC ---------- +MQTT_TOPIC = os.getenv("MQTT_TOPIC", "MQTT/imagery/#") + +# Add at the top with other ENV variables: + +# ---------- Media Prefixes ---------- +CAMERA_PREFIX = os.getenv("CAMERA_PREFIX", "camera") +MICROPHONE_PREFIX = os.getenv("MICROPHONE_PREFIX", "microphone") +ULTRA_DIR_PREFIX = os.getenv("ULTRA_DIR_PREFIX", "plants") + +# ---------- S3 ---------- +s3 = boto3.client( + "s3", + endpoint_url=MINIO_ENDPOINT, + aws_access_key_id=AWS_ACCESS_KEY, + aws_secret_access_key=AWS_SECRET_KEY, + config=BotoConfig( + signature_version="s3v4", + s3={"addressing_style": "path"}, + max_pool_connections=max(64, MAX_CONC * 2), + retries={"max_attempts": 3, "mode": "standard"}, + ), +) + +tx_cfg = TransferConfig( + multipart_threshold=MP_THRESHOLD, + multipart_chunksize=PART_SIZE, + max_concurrency=MAX_CONC, + use_threads=True, +) + +# ---------- Helpers ---------- +mimetypes.init() + +def get_s3_etag(bucket: str, key: str) -> str | None: + try: + resp = s3.head_object(Bucket=bucket, Key=key) + etag = resp.get("ETag") + if isinstance(etag, str): + return etag.strip('"') + except Exception: + pass + return None + +def now_ms() -> int: + return int(time.time() * 1000) + +def iso_utc(ts_ms: int | None = None) -> str: + if ts_ms is None: + return datetime.utcnow().replace(tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + return datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + +def sha256_hex(b: bytes) -> str: + h = hashlib.sha256(); h.update(b); return h.hexdigest() + +def stem(filename: str) -> str: + base = os.path.basename(filename) + root, _ = os.path.splitext(base) + return root or uuid.uuid4().hex + +def normalize_content_type(ctype: str, filename: str) -> str: + if ctype and ctype != "application/octet-stream": + return ctype + guess, _ = mimetypes.guess_type(filename) + return guess or "application/octet-stream" + +def parse_topic(topic: str) -> dict: + parts = [p for p in topic.split("/") if p] + parts_lower = [p.lower() for p in parts] + now = now_ms() + result = { + "camera": DEFAULT_PREFIX, + "publish_ts_ms": now, + "content_type": "application/octet-stream", + "filename": f"{now}.bin", + "media_type": "image", + } + + # --- detect namespace and offsets --- + ns = None + idx = -1 + # for cand in ("imagery", "sounds"): + # if cand in parts: + # ns, idx = cand, parts.index(cand) + # break + if "imagery" in parts_lower: + ns, idx = "imagery", parts_lower.index("imagery") + elif any(p.startswith("sounds_ultra") for p in parts_lower): + ns, idx = "sounds_ultra", next(i for i, p in enumerate(parts_lower) if p.startswith("sounds_ultra")) + elif "sounds" in parts_lower: + ns, idx = "sounds", parts_lower.index("sounds") + + if ns == "imagery": + # format: MQTT/imagery//// + if len(parts) > idx + 1 and parts[idx + 1]: + result["camera"] = parts[idx + 1] + if len(parts) > idx + 2 and parts[idx + 2]: + try: + ts = int(parts[idx + 2]) + if ts > 0: + result["publish_ts_ms"] = ts + except ValueError: + pass + if len(parts) > idx + 3 and parts[idx + 3]: + result["content_type"] = parts[idx + 3].replace("_", "/") + if len(parts) > idx + 4 and parts[idx + 4]: + result["filename"] = parts[idx + 4] + + elif ns in ("sounds", "sounds_ultra"): + if len(parts) > idx + 1 and parts[idx + 1]: + try: + ts = int(parts[idx + 1]) + if ts > 0: + result["publish_ts_ms"] = ts + except ValueError: + pass + if len(parts) > idx + 2 and parts[idx + 2]: + result["content_type"] = parts[idx + 2].replace("_", "/") + if len(parts) > idx + 3 and parts[idx + 3]: + result["filename"] = parts[idx + 3] + + # normalize + media type detect + result["content_type"] = normalize_content_type(result["content_type"], result["filename"]) + ctype = result["content_type"].lower() + if ctype.startswith("image/"): + result["media_type"] = "image" + elif ctype.startswith("video/"): + result["media_type"] = "image" + elif ctype.startswith("audio/") or "sounds" in ctype or "wav" in ctype or "mp3" in ctype: + result["media_type"] = "sounds" + else: + ext = result["filename"].lower().rsplit(".", 1)[-1] if "." in result["filename"] else "" + if ext in ("jpg","jpeg","png","gif","bmp","tiff","webp"): + result["media_type"] = "image" + elif ext in ("wav","mp3","ogg","flac","aac","m4a"): + result["media_type"] = "sounds" + else: + result["media_type"] = "image" + + date_part = datetime.fromtimestamp(result["publish_ts_ms"] / 1000, tz=timezone.utc).strftime("%Y-%m-%d") + device_id = result["camera"] + + if result["media_type"] == "sounds": + if device_id.startswith(f"{CAMERA_PREFIX}-"): + device_name = device_id.replace(f"{CAMERA_PREFIX}-", f"{MICROPHONE_PREFIX}-", 1) + elif device_id.startswith(f"{MICROPHONE_PREFIX}-"): + device_name = device_id + else: + device_name = f"{MICROPHONE_PREFIX}-{device_id}" + else: + if device_id.startswith(f"{CAMERA_PREFIX}-"): + device_name = device_id + elif device_id.startswith(f"{MICROPHONE_PREFIX}-"): + device_name = device_id.replace(f"{MICROPHONE_PREFIX}-", f"{CAMERA_PREFIX}-", 1) + else: + device_name = f"{CAMERA_PREFIX}-{device_id}" + + # key = f"{result['media_type']}/{device_name}/{date_part}/{result['publish_ts_ms']}/{result['filename']}" + is_ultra = ns == "sounds_ultra" + topdir = ULTRA_DIR_PREFIX if is_ultra else result["media_type"] + key = f"{topdir}/{device_name}/{date_part}/{result['publish_ts_ms']}/{result['filename']}" + + result["key"] = key + result["device_id"] = device_name + result["image_id"] = stem(result["filename"]) or uuid.uuid4().hex + result["capture_ts_iso"] = iso_utc(result["publish_ts_ms"]) + return result + +# ---------- Uploader ---------- +def upload_bytes(key: str, data: bytes, content_type: str) -> str: + checksum = sha256_hex(data) + extra_args = {"Metadata": {"checksum-sha256": checksum}, "ContentType": content_type} + if len(data) >= MP_THRESHOLD: + bio = io.BytesIO(data) + s3.upload_fileobj(bio, BUCKET, key, ExtraArgs=extra_args, Config=tx_cfg) + else: + s3.put_object(Bucket=BUCKET, Key=key, Body=data, **extra_args) + return checksum + +# ---------- Outbox ---------- +def _ensure_dir(path: str): + try: + os.makedirs(path, exist_ok=True) + except OSError as e: + if e.errno != errno.EEXIST: + raise + +def _safe_file_id(meta: dict) -> str: + image_id = meta.get("image_id") or meta.get("metadata", {}).get("image_id") + if not image_id: + image_id = stem(meta.get("object_key", "") or uuid.uuid4().hex) + return str(image_id).replace("/", "_") + +def save_to_outbox(meta: dict) -> None: + try: + _ensure_dir(OUTBOX_DIR) + file_id = _safe_file_id(meta) + path = os.path.join(OUTBOX_DIR, f"{file_id}.json") + with open(path, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False) + print(f"[OUTBOX] saved {path}", flush=True) + except Exception as e: + print(f"[OUTBOX][ERROR] {e}", flush=True) + +# ---------- Token Bootstrap ---------- +def _safe_join_url(base: str, path: str) -> str: + return f"{base.rstrip('/')}/{path.lstrip('/')}" + +def _read_token_from_file(path: str) -> str | None: + try: + p = pathlib.Path(path) + if p.exists(): + t = p.read_text(encoding="utf-8").strip() + return t or None + except Exception: + pass + return None + +def _write_token_to_file(path: str, token: str) -> None: + p = pathlib.Path(path) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(token, encoding="utf-8") + +def _fetch_token_via_dev_bootstrap(base: str, retries: int = 3, backoff: float = 0.8) -> str | None: + print(f"[BOOTSTRAP] fetching service token from {base}", flush=True) + url = _safe_join_url(base, "/auth/_dev_bootstrap") + payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": True} + for attempt in range(1, retries + 1): + try: + r = requests.post(url, json=payload, timeout=10) + if r.status_code not in (200, 201): + time.sleep(backoff * attempt) + continue + data = r.json() + raw = (data.get("service_account", {}) or {}).get("raw_token") \ + or (data.get("service_account", {}) or {}).get("token") + if raw and isinstance(raw, str) and raw.strip() and "***" not in raw: + return raw.strip() + except Exception: + time.sleep(backoff * attempt) + return None + +def get_or_bootstrap_token() -> str | None: + if DB_API_TOKEN and DB_API_TOKEN.lower() != "auto": + print("[BOOTSTRAP] using DB_API_TOKEN from env", flush=True) + return DB_API_TOKEN + if not DB_API_BASE: + print("[BOOTSTRAP][WARN] DB_API_BASE not set; cannot bootstrap token.", flush=True) + return None + token = _read_token_from_file(DB_API_TOKEN_FILE) + if token: + print(f"[BOOTSTRAP] using service token from {DB_API_TOKEN_FILE}", flush=True) + + return token + token = _fetch_token_via_dev_bootstrap(DB_API_BASE) + if token: + _write_token_to_file(DB_API_TOKEN_FILE, token) + print(f"[BOOTSTRAP] wrote service token to {DB_API_TOKEN_FILE}", flush=True) + return token + print("[BOOTSTRAP][ERROR] Failed to obtain service token (dev bootstrap).", flush=True) + return None + +# ---------- Web Service client ---------- +_http = requests.Session() +svc_token = get_or_bootstrap_token() + +if svc_token: + if DB_API_AUTH_MODE == "service": + _http.headers.update({"X-Service-Token": svc_token}) + else: + _http.headers.update({"Authorization": f"Bearer {svc_token}"}) +_http.headers.update({"Content-Type": "application/json"}) +_http.mount("http://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) +_http.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) + +def write_db(meta: dict) -> bool: + if DUMMY_DB: + print( + f"[DB-DUMMY] would POST to {DB_API_BASE or 'N/A'}: {json.dumps(meta, ensure_ascii=False)}", + flush=True, + ) + return True + + if not DB_API_BASE: + print("[DB][WARN] DB_API_BASE not set; skipping DB write.", flush=True) + return False + + base = DB_API_BASE.rstrip("/") + try: + r = _http.post(f"{base}/api/files", json=meta, timeout=10) + if 200 <= r.status_code < 300: + print("[DB] POST ok", flush=True) + return True + if r.status_code == 409: + print("[DB] POST conflict, trying PUT...", flush=True) + ok_key = quote(meta["object_key"], safe="/") + url = f"{base}/api/files/{meta['bucket']}/{ok_key}" + r = _http.put(url, json=meta, timeout=10) + ok = 200 <= r.status_code < 300 + print(f"[DB] PUT {'ok' if ok else r.status_code}", flush=True) + return ok + + print(f"[DB] POST failed: {r.status_code} {r.text[:200]}", flush=True) + return False + + except requests.ConnectionError as e: + print(f"[DB][WARN] API not reachable ({base}): {e}", flush=True) + return False + + except requests.Timeout as e: + print(f"[DB][WARN] API timeout ({base}): {e}", flush=True) + return False + + except requests.RequestException as e: + print(f"[DB][ERROR] {e}", flush=True) + return False + + +# ---------- Worker Queue ---------- +q_in: "queue.Queue[Tuple[str, bytes, int]]" = queue.Queue(maxsize=INGEST_QUEUE_MAXSIZE) +_shutdown = threading.Event() + +def worker(): + while not _shutdown.is_set(): + try: + topic, payload, _ = q_in.get(timeout=0.5) + except queue.Empty: + continue + info = parse_topic(topic) + key = info["key"] + try: + checksum = upload_bytes(key, payload, info["content_type"]) + s3_uri = f"s3://{BUCKET}/{key}" + etag_real = get_s3_etag(BUCKET, key) + device_id = FORCE_DEVICE_ID or None + db_row = { + "bucket": BUCKET, + "object_key": key, + "content_type": info["content_type"], + "size_bytes": len(payload), + "etag": etag_real or checksum, + "device_id": device_id, + "mission_id": info.get("mission_id"), + "tile_id": info.get("tile_id"), + "footprint": info.get("footprint_wkt"), + "metadata": { + "image_id": info["image_id"], + "s3_uri": s3_uri, + "sha256": checksum, + "capture_ts": info["capture_ts_iso"], + "ingest_ts": iso_utc(), + "source_topic": topic, + "extra": info.get("extra"), + }, + } + + ok = write_db(db_row) + if not ok and not DUMMY_DB: + save_to_outbox(db_row) + except Exception as e: + print(f"[ERROR] upload failed for key={key}: {e}", flush=True) + finally: + q_in.task_done() + +# ---------- MQTT Callbacks (API v2) ---------- +def on_connect(client, userdata, flags, reason_code, properties): + if reason_code == 0: + client.subscribe(MQTT_TOPIC, qos=1) + print(f"Subscribed to {MQTT_TOPIC} at {MQTT_BROKER}:{MQTT_PORT}", flush=True) + else: + print(f"[ERROR] MQTT connect reason_code={reason_code}", flush=True) + +def on_message(client, userdata, msg): + print(f"[MQTT] received message: {msg.topic}, {len(msg.payload)} bytes") + q_in.put((msg.topic, msg.payload, 0)) + +def on_disconnect(client, userdata, disconnect_flags, reason_code, properties): + print(f"MQTT disconnected reason_code={reason_code}", flush=True) + +# ---------- Main ---------- +def main(): + try: + s3.head_bucket(Bucket=BUCKET) + except Exception: + s3.create_bucket(Bucket=BUCKET) + + for _ in range(max(2, INGEST_WORKERS)): + threading.Thread(target=worker, daemon=True).start() + + client = mqtt.Client( + CallbackAPIVersion.VERSION2, + client_id=CLIENT_ID, + protocol=mqtt.MQTTv5, + ) + client.max_inflight_messages_set(1000) + client.max_queued_messages_set(0) + client.reconnect_delay_set(min_delay=1, max_delay=8) + client.on_connect = on_connect + client.on_message = on_message + client.on_disconnect = on_disconnect + client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60) + client.loop_start() + + print( + f"INGEST ready. mp_threshold={MP_THRESHOLD}, part={PART_SIZE}, conc={MAX_CONC}, workers={INGEST_WORKERS}", + flush=True + ) + + def _stop(*_): + _shutdown.set() + client.loop_stop() + client.disconnect() + + signal.signal(signal.SIGINT, _stop) + signal.signal(signal.SIGTERM, _stop) + + try: + while not _shutdown.is_set(): + time.sleep(0.2) + finally: + q_in.join() + print("INGEST stopped.", flush=True) + +if __name__ == "__main__": + main() diff --git a/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/config.py b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/config.py new file mode 100644 index 000000000..6900c0f85 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_ingest/config.py @@ -0,0 +1,77 @@ +# mqtt_ingest/config.py +from dataclasses import dataclass +from pathlib import Path +import os, typing as t + +try: + import yaml +except Exception: + yaml = None + +@dataclass(frozen=True) +class Config: + # ingest + MP_THRESHOLD: int = 5 * 1024 * 1024 + PART_SIZE: int = 5 * 1024 * 1024 + MAX_CONC: int = 32 + MQTT_PORT: int = 1883 + INGEST_WORKERS: int = 8 + + # publisher + LIMIT: int = 0 + PUBLISH_QOS: int = 2 + PUBLISH_DELAY_MS: int = 10 + +def _coerce(v, to_type): + if to_type is int: return int(v) + if to_type is float: return float(v) + if to_type is bool: return str(v).lower() in ("1","true","yes","on") + return v + + +_ENV_ALIASES = { + "MP_THRESHOLD": ["MULTIPART_THRESHOLD_BYTES"], + "PART_SIZE": ["PART_SIZE_BYTES"], + "MAX_CONC": ["MULTIPART_MAX_CONCURRENCY"], + "MQTT_PORT": ["MQTT_PORT"], + "INGEST_WORKERS": ["INGEST_WORKERS"], + "LIMIT": ["LIMIT"], + "PUBLISH_QOS": ["MQTT_QOS"], + "PUBLISH_DELAY_MS": ["PUBLISH_DELAY_MS"], +} + +def load_config(path: t.Optional[t.Union[str, Path]] = None) -> Config: + base = Config() + + data = {} + root = Path(__file__).resolve().parents[1] + if path is None: + for name in ("config.yaml", "config.yml"): + p = root / name + if p.exists(): + path = p + break + if path and yaml: + data = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {} + + + env = os.environ + out = {} + for f in base.__dataclass_fields__.values(): + key = f.name + val = getattr(base, key) + if key in data: + try: val = _coerce(data[key], f.type) + except Exception: pass + for alias in _ENV_ALIASES.get(key, []): + if alias in env: + try: val = _coerce(env[alias], f.type) + except Exception: pass + out[key] = val + return Config(**out) + +cfg = load_config() + + + + diff --git a/AgCloud/storage_with_mqtt/mqtt_images/mqtt_publisher/Dockerfile b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_publisher/Dockerfile new file mode 100644 index 000000000..b980af88d --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_publisher/Dockerfile @@ -0,0 +1,36 @@ +# syntax=docker/dockerfile:1 +FROM python:3.12-slim + +# Build argument +ARG USE_NETFREE=true + +# Install basic packages +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* + +# Install NetFree certificates only if USE_NETFREE=true +RUN if [ "$USE_NETFREE" = "true" ]; then \ + echo "Configuring NetFree certificates..."; \ + # Check if the certificate file exists + if [ -f ./*.crt ]; then \ + cp ./*.crt /usr/local/share/ca-certificates/; \ + chmod 644 /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ + else \ + echo "No NetFree certificate found, skipping"; \ + fi; \ + fi + +ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt +ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt +ENV PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN pip install --no-cache-dir \ + --trusted-host pypi.org --trusted-host files.pythonhosted.org \ + paho-mqtt + +WORKDIR /app +COPY app.py . + +ENV IMAGES_DIR=/images + +CMD ["bash", "-c", "sleep 5 && python -u /app/app.py"] diff --git a/AgCloud/storage_with_mqtt/mqtt_images/mqtt_publisher/app.py b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_publisher/app.py new file mode 100644 index 000000000..86d653f35 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/mqtt_publisher/app.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import os, time, glob, random, pathlib +import paho.mqtt.client as mqtt +from paho.mqtt.client import CallbackAPIVersion +import json +import random + +HOST = os.getenv("MQTT_HOST", "large-mosquitto") +PORT = int(os.getenv("MQTT_PORT", "1885")) +TOPIC_BASE = os.getenv("MQTT_TOPIC_BASE", "MQTT/imagery") +IMAGES_DIR = os.getenv("IMAGES_DIR", "/images") +CAMERA_ID = os.getenv("CAMERA_ID", "camera-01") +LIMIT = int(os.getenv("LIMIT", "0")) +SHUFFLE = os.getenv("SHUFFLE", "1") in ("1","true","yes","on") +QOS = int(os.getenv("MQTT_QOS", "2")) +DELAY_MS = int(os.getenv("PUBLISH_DELAY_MS", "10")) + +def content_type_for(path: str) -> str: + ext = pathlib.Path(path).suffix.lower() + if ext in (".jpg", ".jpeg"): return "image/jpeg" + if ext == ".png": return "image/png" + if ext == ".bmp": return "image/bmp" + if ext == ".gif": return "image/gif" + if ext == ".mp4": return "video/mp4" + if ext == ".mov": return "video/quicktime" + if ext == ".mkv": return "video/x-matroska" + if ext == ".avi": return "video/x-msvideo" + return "application/octet-stream" + +def ctype_to_safe(ctype: str) -> str: + return ctype.replace("/", "_") + +def main(): + paths = sorted(glob.glob(f"{IMAGES_DIR}/**/*.*", recursive=True)) + if SHUFFLE: + random.shuffle(paths) + if LIMIT > 0: + paths = paths[:LIMIT] + + client = mqtt.Client( + CallbackAPIVersion.VERSION2, + client_id=f"pub-{int(time.time())}", + protocol=mqtt.MQTTv5, + ) + client.max_inflight_messages_set(1000) + client.max_queued_messages_set(0) + client.reconnect_delay_set(min_delay=1, max_delay=8) + + def on_connect(client, userdata, flags, reason_code, properties): + print(f"Publisher connected rc={reason_code}, files={len(paths)}", flush=True) + + client.on_connect = on_connect + client.connect(HOST, PORT, keepalive=60) + client.loop_start() + + t0 = time.time() + sent = 0 + for p in paths: + try: + with open(p, "rb") as f: + data = f.read() + ctype = content_type_for(p) + ctype_safe = ctype_to_safe(ctype) + ts_ms = int(time.time() * 1000) + filename = pathlib.Path(p).name + topic = f"{TOPIC_BASE}/{CAMERA_ID}/{ts_ms}/{ctype_safe}/{filename}" + r = client.publish(topic, payload=data, qos=QOS) + r.wait_for_publish() + sent += 1 + # --- Telemetry publish (same ts_ms & sensor) --- + # Configurable base position via env (optional): + BASE_LAT = float(os.getenv("BASE_LAT", "32.061")) + BASE_LON = float(os.getenv("BASE_LON", "34.772")) + BASE_HDG = float(os.getenv("BASE_HDG", "180")) + BASE_SPD = float(os.getenv("BASE_SPD", "2.0")) + + def _jitter(base: float, j: float = 0.0005) -> float: + # ~50m random jitter around the base point + import random + return base + random.uniform(-j, j) + + telemetry = { + "lat": round(_jitter(BASE_LAT), 6), + "lon": round(_jitter(BASE_LON), 6), + "heading": round(BASE_HDG + __import__("random").uniform(-10, 10), 1), + "speed_mps": round(max(0.0, __import__("random").gauss(BASE_SPD, 0.5)), 2), + } + telemetry_topic = f"{TOPIC_BASE.replace('imagery', 'telemetry')}/{CAMERA_ID}/{ts_ms}" + client.publish(telemetry_topic, payload=json.dumps(telemetry).encode("utf-8"), qos=QOS) + # --- end telemetry publish --- + + if DELAY_MS > 0: + time.sleep(DELAY_MS / 1000.0) + except Exception as e: + print(f"[WARN] failed to publish {p}: {e}", flush=True) + + elapsed = time.time() - t0 + rate = sent / elapsed if elapsed > 0 else sent + print(f"Publisher done. Sent={sent}, elapsed={elapsed:.2f}s, rate={rate:.1f} imgs/s", flush=True) + + client.loop_stop() + client.disconnect() + +if __name__ == "__main__": + main() + diff --git a/AgCloud/storage_with_mqtt/mqtt_images/tests/test_csv_and_queue.py b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_csv_and_queue.py new file mode 100644 index 000000000..27e19da95 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_csv_and_queue.py @@ -0,0 +1,37 @@ +import pathlib, sys, csv + +# Ensure project root on path +sys.path.append(str(pathlib.Path(__file__).resolve().parents[1])) + +from mqtt_ingest import app + +def test_ensure_csv_and_write(tmp_path): + path = tmp_path / "latency.csv" + app.ensure_csv(str(path)) + # header exists + with open(path) as f: + header = f.readline().strip().split(",") + assert header == app._csv_header + + # append a row + app.write_csv_row(str(path), [1, 2, 1, 100, "k"]) + with open(path) as f: + rows = list(csv.reader(f)) + assert rows[1] == ["1", "2", "1", "100", "k"] + +def test_on_message_enqueues(monkeypatch): + events = [] + class FakeQueue: + def put(self, item): events.append(item) + monkeypatch.setattr(app, "q_in", FakeQueue()) + + class Msg: + topic = "MQTT/imagery/camX/1730000000000/image_jpeg/a.jpg" + payload = b"abc" + + app.on_message(None, None, Msg) + assert len(events) == 1 + t, payload, zero = events[0] + assert "imagery" in t + assert payload == b"abc" + assert zero == 0 diff --git a/AgCloud/storage_with_mqtt/mqtt_images/tests/test_integration_docker.py b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_integration_docker.py new file mode 100644 index 000000000..152cf13f1 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_integration_docker.py @@ -0,0 +1,40 @@ +import os, subprocess, time, pathlib, sys, pytest + +@pytest.mark.integration +def test_docker_end_to_end(): + if not os.environ.get("RUN_DOCKER_IT"): + pytest.skip("Set RUN_DOCKER_IT=1 to run docker-compose integration test") + + def run(cmd): + res = subprocess.run(cmd, shell=True, capture_output=True, text=True) + assert res.returncode == 0, f"Command failed:\n{cmd}\nSTDOUT:\n{res.stdout}\nSTDERR:\n{res.stderr}" + return res.stdout.strip() + + # Clean & start core services + run("docker compose down -v || true") + run("rm -f out/latency.csv || true") + run("docker compose up -d --build mosquitto minio mc mqtt_ingest") + + # Give services a moment to come up + time.sleep(5) + + # Run publisher (blocks until done) + run("docker compose up --build mqtt_publisher") + + # Count S3 objects via mc + out = run("""\ +docker exec -i mqtt-mc-1 sh -c ' + mc alias set local http://minio:9000 minioadmin minioadmin >/dev/null 2>&1 + mc ls --recursive --json local/images | wc -l +'""" ) + s3_count = int(out.strip() or "0") + + # Count CSV rows (minus header) + csv_path = pathlib.Path("out/latency.csv") + csv_rows = 0 + if csv_path.exists(): + with csv_path.open() as f: + csv_rows = max(0, sum(1 for _ in f) - 1) + + assert s3_count > 0, "No objects found in MinIO bucket" + assert s3_count == csv_rows, f"S3 objects ({s3_count}) != CSV rows ({csv_rows})" diff --git a/AgCloud/storage_with_mqtt/mqtt_images/tests/test_large_and_mime.py b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_large_and_mime.py new file mode 100644 index 000000000..5a96e8c32 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_large_and_mime.py @@ -0,0 +1,56 @@ +# tests/test_large_and_mime.py +import os +import hashlib +import importlib.util +from pathlib import Path + +import boto3 +import pytest +from moto import mock_aws + +ROOT = Path(__file__).resolve().parents[1] +APP_PATH = ROOT / "mqtt_ingest" / "app.py" + +def _load_app(): + spec = importlib.util.spec_from_file_location("app", str(APP_PATH)) + app = importlib.util.module_from_spec(spec) + spec.loader.exec_module(app) + return app + +def _mk_s3_and_patch(monkeypatch, app): + s3 = boto3.client("s3", region_name="us-east-1") + s3.create_bucket(Bucket=app.BUCKET) + monkeypatch.setattr(app, "s3", s3) + return s3 + +def _sha256_hex(b: bytes) -> str: + h = hashlib.sha256(); h.update(b); return h.hexdigest() + +RUN_STRESS = os.getenv("RUN_STRESS") == "1" + +@pytest.mark.skipif(not RUN_STRESS, reason="set RUN_STRESS=1 to enable heavy test") +@mock_aws +def test_very_large_upload_multipart(monkeypatch): + app = _load_app() + s3 = _mk_s3_and_patch(monkeypatch, app) + + + size_mib = int(os.getenv("STRESS_SIZE_MIB", "64")) + data = (b"x" * (1024 * 1024)) * size_mib + key = "camera-01/2025-01-01/huge.bin" + + app.upload_bytes(key, data, "application/octet-stream") + + head = s3.head_object(Bucket=app.BUCKET, Key=key) + + assert head["Metadata"]["checksum-sha256"] == _sha256_hex(data) + + + reported = head["ContentLength"] + if reported != len(data): + assert reported >= len(data) + assert (reported - len(data)) <= 2048 + else: + assert reported == len(data) + + diff --git a/AgCloud/storage_with_mqtt/mqtt_images/tests/test_parse_topic.py b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_parse_topic.py new file mode 100644 index 000000000..f02c00d88 --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_parse_topic.py @@ -0,0 +1,25 @@ +import pathlib, sys +import pytest + +# Ensure project root on path +sys.path.append(str(pathlib.Path(__file__).resolve().parents[1])) + +from mqtt_ingest.app import parse_topic, DEFAULT_PREFIX + +def test_parse_topic_valid(): + topic = "MQTT/imagery/camera-01/1730000000000/image_jpeg/img001.jpg" + info = parse_topic(topic) + assert info["camera"] == "camera-01" + assert info["publish_ts_ms"] == 1730000000000 + assert info["content_type"] == "image/jpeg" + assert info["filename"] == "img001.jpg" + assert info["key"].startswith("camera-01/") + assert info["key"].endswith("/img001.jpg") + +def test_parse_topic_fallback(): + info = parse_topic("MQTT/weird/topic") + assert info["camera"] == DEFAULT_PREFIX + assert isinstance(info["publish_ts_ms"], int) + assert info["content_type"] == "application/octet-stream" + assert info["filename"].endswith(".bin") + assert info["key"].startswith(f"{DEFAULT_PREFIX}/") diff --git a/AgCloud/storage_with_mqtt/mqtt_images/tests/test_upload_bytes.py b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_upload_bytes.py new file mode 100644 index 000000000..b390c420a --- /dev/null +++ b/AgCloud/storage_with_mqtt/mqtt_images/tests/test_upload_bytes.py @@ -0,0 +1,58 @@ +import os, hashlib, importlib.util +import boto3 +from moto import mock_aws + + +def _load_app(): + here = os.path.dirname(__file__) + app_path = os.path.join(here, "..", "mqtt_ingest", "app.py") + spec = importlib.util.spec_from_file_location("app", app_path) + app = importlib.util.module_from_spec(spec) + spec.loader.exec_module(app) + return app + +def _mk_s3_and_patch_app(monkeypatch, app): + s3 = boto3.client("s3", region_name="us-east-1") + s3.create_bucket(Bucket=app.BUCKET) + monkeypatch.setattr(app, "s3", s3) + return s3 + +def _sha256_hex(b: bytes) -> str: + h = hashlib.sha256(); h.update(b); return h.hexdigest() + +@mock_aws +def test_upload_bytes_small_put(monkeypatch): + app = _load_app() + s3 = _mk_s3_and_patch_app(monkeypatch, app) + + data = b"x" * (256 * 1024) + key = "camera-01/2025-01-01/small.bin" + app.upload_bytes(key, data, "application/octet-stream") + + head = s3.head_object(Bucket=app.BUCKET, Key=key) + assert head["ContentType"] == "application/octet-stream" + assert head["Metadata"]["checksum-sha256"] == _sha256_hex(data) + assert head["ContentLength"] == len(data) + +@mock_aws +def test_upload_bytes_large_multipart(monkeypatch): + app = _load_app() + s3 = _mk_s3_and_patch_app(monkeypatch, app) + + data = b"x" * (6 * 1024 * 1024) + key = "camera-01/2025-01-01/big.bin" + app.upload_bytes(key, data, "application/octet-stream") + + head = s3.head_object(Bucket=app.BUCKET, Key=key) + assert head["ContentType"] == "application/octet-stream" + + assert head["Metadata"]["checksum-sha256"] == _sha256_hex(data) + + + reported = head["ContentLength"] + if reported != len(data): + + assert reported >= len(data) + assert (reported - len(data)) <= 1024 + else: + assert reported == len(data) diff --git a/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/data/config/lifecycle-imagery.json b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/data/config/lifecycle-imagery.json new file mode 100644 index 000000000..b431a52df --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/data/config/lifecycle-imagery.json @@ -0,0 +1,11 @@ +{ + "Rules": [ + { + "ID": "seven-days-imagery", + "Status": "Enabled", + "Filter": { "Prefix": "" }, + "Transition": { "Days": 7, "StorageClass": "COLD_IMAGERY" } + } + ] + } + \ No newline at end of file diff --git a/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/data/config/lifecycle-sound.json b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/data/config/lifecycle-sound.json new file mode 100644 index 000000000..b9aab2eb2 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/data/config/lifecycle-sound.json @@ -0,0 +1,15 @@ +{ + "Rules": [ + { + "ID": "seven-days-sound", + "Status": "Enabled", + "Filter": { + "Prefix": "" + }, + "Transition": { + "Days": 7, + "StorageClass": "COLD_SOUND" + } + } + ] +} diff --git a/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap/Dockerfile b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap/Dockerfile new file mode 100644 index 000000000..d5b4d94f9 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap/Dockerfile @@ -0,0 +1,27 @@ +# ============================ +# Stage 1: copy mc binary +# ============================ +FROM minio/mc:latest AS mc-source +# ============================ +# Stage 2: main image +# ============================ +FROM alpine:3.19 +# ===== Install dependencies ===== +# include dos2unix so line endings are normalized automatically +RUN apk add --no-cache bash curl ca-certificates netcat-openbsd dos2unix && \ + update-ca-certificates +# ===== Add NetFree CA ===== +COPY certs/*.crt /usr/local/share/ca-certificates/ +RUN update-ca-certificates +# ===== Copy mc from the official image ===== +COPY --from=mc-source /usr/bin/mc /usr/local/bin/mc +RUN chmod +x /usr/local/bin/mc +# ===== Create directories ===== +RUN mkdir -p /entrypoint /config +# ===== Copy init script ===== +COPY entrypoint/init.sh /entrypoint/init.sh +# ===== Normalize and ensure execution permissions ===== +# (this guarantees LF endings + execute permission for everyone) +RUN dos2unix /entrypoint/init.sh && chmod 755 /entrypoint/init.sh +# ===== Entry point ===== +CMD ["/entrypoint/init.sh"] diff --git a/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap/entrypoint/init.sh b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap/entrypoint/init.sh new file mode 100644 index 000000000..16b388157 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/Lifecycle_rules/minio-bootstrap/entrypoint/init.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${MINIO_ROOT_USER:?Missing MINIO_ROOT_USER}" +: "${MINIO_ROOT_PASSWORD:?Missing MINIO_ROOT_PASSWORD}" +: "${MC_ALIAS_HOT:=hot}" +: "${MC_ALIAS_COLD:=cold}" +: "${HOT_ENDPOINT:=http://minio-hot:9000}" +: "${COLD_ENDPOINT:=http://minio-cold:9000}" + +mc alias set "${MC_ALIAS_HOT}" "${HOT_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" || true +mc alias set "${MC_ALIAS_COLD}" "${COLD_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" || true + +echo "[bootstrap] Checking HTTP availability..." +until curl -sf "${HOT_ENDPOINT}/minio/health/live" >/dev/null; do sleep 1; done +until curl -sf "${COLD_ENDPOINT}/minio/health/live" >/dev/null; do sleep 1; done + +echo "[bootstrap] Waiting for HOT (${HOT_ENDPOINT})..." +until mc ls "${MC_ALIAS_HOT}" >/dev/null 2>&1; do sleep 2; done +echo "[bootstrap] Waiting for COLD (${COLD_ENDPOINT})..." +until mc ls "${MC_ALIAS_COLD}" >/dev/null 2>&1; do sleep 2; done + +echo "[bootstrap] Creating buckets..." +mc mb "${MC_ALIAS_HOT}/imagery" || true +mc mb "${MC_ALIAS_HOT}/sound" || true +mc mb "${MC_ALIAS_COLD}/imagery" || true +mc mb "${MC_ALIAS_COLD}/sound" || true + +echo "[bootstrap] Enabling versioning..." +mc version enable "${MC_ALIAS_HOT}/imagery" || true +mc version enable "${MC_ALIAS_HOT}/sound" || true + +echo "[bootstrap] Waiting for Kafka broker..." +until nc -z kafka 9092 >/dev/null 2>&1; do + echo "[bootstrap] Kafka not ready, retrying..." + sleep 3 +done +echo "[bootstrap] Kafka is accessible." + +echo "[bootstrap] Configuring all Kafka notifiers..." + + +# Configure IMAGE notifiers +echo "[bootstrap] → aerial" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:aerial \ + brokers="kafka:9092" \ + topic="image.new.aerial" + +echo "[bootstrap] → air" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:air \ + brokers="kafka:9092" \ + topic="image.new.air" + +echo "[bootstrap] → fruits" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:fruits \ + brokers="kafka:9092" \ + topic="image.new.fruits" + +echo "[bootstrap] → leaves" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:leaves \ + brokers="kafka:9092" \ + topic="image.new.leaves" + +echo "[bootstrap] → ground" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:ground \ + brokers="kafka:9092" \ + topic="image.new.ground" + +echo "[bootstrap] → field" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:field \ + brokers="kafka:9092" \ + topic="image.new.field" + +echo "[bootstrap] → security" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:security \ + brokers="kafka:9092" \ + topic="image.new.security" + + +# Configure SOUND notifiers +echo "[bootstrap] → plants" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:plants \ + brokers="kafka:9092" \ + topic="sound.new.plants" + +echo "[bootstrap] → sounds" +mc admin config set "${MC_ALIAS_HOT}" notify_kafka:sounds \ + brokers="kafka:9092" \ + topic="sound.new.sounds" + +echo "[bootstrap] ✅ All 7 notifiers configured" +echo "[bootstrap] ⚠️ Restarting MinIO to apply notifier changes..." +mc admin service restart "${MC_ALIAS_HOT}" --json || true + +# Wait for MinIO restart with retry instead of fixed sleep +echo "[bootstrap] Waiting for MinIO to come back online (with retries)..." +max_retries=${MAX_MINIO_RETRIES:-60} # Default: 60 attempts +retry_interval=${MINIO_RETRY_INTERVAL:-5} # Default: 5 seconds +i=0 +until mc ls "${MC_ALIAS_HOT}" >/dev/null 2>&1; do + i=$((i+1)) + if [ "$i" -ge "$max_retries" ]; then + echo "[bootstrap] ERROR: MinIO did not become ready after $((max_retries * retry_interval)) seconds" + break + fi + echo "[bootstrap] MinIO not ready, attempt $i/$max_retries (waiting ${retry_interval}s)..." + sleep "$retry_interval" +done + +if mc ls "${MC_ALIAS_HOT}" >/dev/null 2>&1; then + echo "[bootstrap] ✅ MinIO is back online" +else + echo "[bootstrap] ⚠️ Continuing even though MinIO did not fully recover (check logs)" +fi + +echo "[bootstrap] Verifying Kafka notifiers..." +mc admin config get "${MC_ALIAS_HOT}" notify_kafka + +echo "[bootstrap] Ensuring remote tiers exist in HOT..." +if ! mc ilm tier ls "${MC_ALIAS_HOT}" --json | grep -q '"Name":"COLD_IMAGERY"'; then + mc ilm tier add s3 "${MC_ALIAS_HOT}" COLD_IMAGERY \ + --endpoint "${COLD_ENDPOINT}" \ + --access-key "${MINIO_ROOT_USER}" \ + --secret-key "${MINIO_ROOT_PASSWORD}" \ + --bucket imagery \ + --region us-east-1 || true +fi + +if ! mc ilm tier ls "${MC_ALIAS_HOT}" --json | grep -q '"Name":"COLD_SOUND"'; then + mc ilm tier add s3 "${MC_ALIAS_HOT}" COLD_SOUND \ + --endpoint "${COLD_ENDPOINT}" \ + --access-key "${MINIO_ROOT_USER}" \ + --secret-key "${MINIO_ROOT_PASSWORD}" \ + --bucket sound \ + --region us-east-1 || true +fi + +echo "[bootstrap] Applying lifecycle policies..." +mc ilm rule rm "${MC_ALIAS_HOT}/imagery" --all --force || true +if [ -s "/config/lifecycle-imagery.json" ]; then + mc ilm import "${MC_ALIAS_HOT}/imagery" < "/config/lifecycle-imagery.json" || true +else + mc ilm rule add "${MC_ALIAS_HOT}/imagery" \ + --transition-days 7 --transition-tier COLD_IMAGERY || true +fi + +mc ilm rule rm "${MC_ALIAS_HOT}/sound" --all --force || true +if [ -s "/config/lifecycle-sound.json" ]; then + mc ilm import "${MC_ALIAS_HOT}/sound" < "/config/lifecycle-sound.json" || true +else + mc ilm rule add "${MC_ALIAS_HOT}/sound" \ + --transition-days 7 --transition-tier COLD_SOUND || true +fi + +echo "[bootstrap] Removing old event rules..." +mc event remove "${MC_ALIAS_HOT}/imagery" --force >/dev/null 2>&1 || true +mc event remove "${MC_ALIAS_HOT}/sound" --force >/dev/null 2>&1 || true + +sleep 3 +echo "[bootstrap] Adding Kafka event rules for IMAGERY..." +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::aerial:kafka \ + --event put \ + --prefix "aerial/" + +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::aerial:kafka \ + --event put \ + --prefix "image/camera-air/" + +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::fruits:kafka \ + --event put \ + --prefix "fruits/" + +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::leaves:kafka \ + --event put \ + --prefix "leaves/" + +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::ground:kafka \ + --event put \ + --prefix "ground/" + +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::field:kafka \ + --event put \ + --prefix "field/" + +mc event add "${MC_ALIAS_HOT}/imagery" \ + arn:minio:sqs::security:kafka \ + --event put \ + --prefix "security/" + +echo "[bootstrap] Adding Kafka event rules for SOUND..." +mc event add "${MC_ALIAS_HOT}/sound" \ + arn:minio:sqs::plants:kafka \ + --event put \ + --prefix "plants/" + +mc event add "${MC_ALIAS_HOT}/sound" \ + arn:minio:sqs::sounds:kafka \ + --event put \ + --prefix "sounds/" + +echo "[bootstrap] Validating event rules..." +mc event list "${MC_ALIAS_HOT}/imagery" || true +mc event list "${MC_ALIAS_HOT}/sound" || true +echo "[bootstrap] ✅ Done." +echo "[bootstrap] Keeping container alive..." +tail -f /dev/null diff --git a/AgCloud/storage_with_mqtt/storage/minio-storage/Dockerfile b/AgCloud/storage_with_mqtt/storage/minio-storage/Dockerfile new file mode 100644 index 000000000..0013d18ba --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/minio-storage/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim + +# Tools+certs first +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# MinIO server binary +RUN curl -k -L -o /usr/local/bin/minio https://dl.min.io/server/minio/release/linux-amd64/minio \ + && chmod +x /usr/local/bin/minio + +# Python SDK +RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org --no-cache-dir minio + +# App +WORKDIR /app +COPY create_buckets.py /app/create_buckets.py +RUN mkdir -p /data + +EXPOSE 9000 9001 + +CMD sh -c 'minio server /data --console-address :9001 & \ + echo "Waiting for MinIO..." && sleep 5 && \ + python3 /app/create_buckets.py && \ + tail -f /dev/null' \ No newline at end of file diff --git a/AgCloud/storage_with_mqtt/storage/minio-storage/create_buckets.py b/AgCloud/storage_with_mqtt/storage/minio-storage/create_buckets.py new file mode 100644 index 000000000..0f5722c83 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/minio-storage/create_buckets.py @@ -0,0 +1,37 @@ +from minio import Minio +from minio.error import S3Error +import os +import time + +# Reading from ENV +endpoint = os.getenv("MINIO_ENDPOINT", "localhost:9000") +access_key = os.getenv("MINIO_ROOT_USER", "minioadmin") +secret_key = os.getenv("MINIO_ROOT_PASSWORD", "minioadmin123") +secure = os.getenv("MINIO_SECURE", "0") == "1" + +# Initialize MinIO client +client = Minio( + endpoint, + access_key=access_key, + secret_key=secret_key, + secure=secure +) +# Waiting for MinIO be ready +for i in range(20): + try: + client.list_buckets() + print("✅ MinIO is ready.") + break + except Exception: + print("⏳ Waiting for MinIO to be ready...") + time.sleep(1) +else: + raise Exception("MinIO not ready after waiting") + +# Creating the buckets +for bucket in ["imagery", "sound"]: + if not client.bucket_exists(bucket): + client.make_bucket(bucket) + print(f"✅ Created bucket: {bucket}") + else: + print(f"ℹ️ Bucket already exists: {bucket}") diff --git a/AgCloud/storage_with_mqtt/storage/minio-storage/mc-bootstrap.sh b/AgCloud/storage_with_mqtt/storage/minio-storage/mc-bootstrap.sh new file mode 100644 index 000000000..1a7b0f4e8 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/minio-storage/mc-bootstrap.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -e + +echo "🪄 Waiting for MinIO to be ready..." +until curl -s http://minio-hot:9000/minio/health/ready >/dev/null; do + sleep 2 +done + +echo "✅ MinIO is ready. Configuring bucket events..." + +mc alias set local http://minio-hot:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" + +if ! mc ls local | grep -q "imagery"; then + mc mb local/imagery +fi + +mc event add local/imagery arn:minio:sqs::primary:kafka_primary --event put --prefix "sound/" +mc event add local/imagery arn:minio:sqs::primary:kafka_images --event put --prefix "image/" + +echo "Event configuration completed." diff --git a/AgCloud/storage_with_mqtt/storage/monitoring/dashbaord.png b/AgCloud/storage_with_mqtt/storage/monitoring/dashbaord.png new file mode 100644 index 000000000..22d4f54de Binary files /dev/null and b/AgCloud/storage_with_mqtt/storage/monitoring/dashbaord.png differ diff --git a/AgCloud/storage_with_mqtt/storage/monitoring/docker-compose.yml b/AgCloud/storage_with_mqtt/storage/monitoring/docker-compose.yml new file mode 100644 index 000000000..21c1bcd54 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/monitoring/docker-compose.yml @@ -0,0 +1,45 @@ +services: + minio: + build: + context: ../storage/minio-bootstrap + dockerfile: Dockerfile + container_name: minio + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + MINIO_PROMETHEUS_AUTH_TYPE: public + HOT_ADDRESS: ":9000" + HOT_CONSOLE: ":9001" + COLD_ADDRESS: ":9100" + COLD_CONSOLE: ":9101" + HOT_ENDPOINT: "http://127.0.0.1:9000" + COLD_ENDPOINT: "http://127.0.0.1:9100" + MC_ALIAS_HOT: "hot" + MC_ALIAS_COLD: "cold" + MC_HOST_hot: "http://minio:minioadmin123@127.0.0.1:9000" + MC_HOST_cold: "http://minio:minioadmin123@127.0.0.1:9100" + ports: + - "9000:9000" + - "9001:9001" + - "9100:9100" + - "9101:9101" + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + command: ["--config.file=/etc/prometheus/prometheus.yml"] + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9090:9090" + depends_on: [minio] + + grafana: + image: grafana/grafana:latest + container_name: grafana + ports: + - "3000:3000" + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + depends_on: [prometheus] diff --git a/AgCloud/storage_with_mqtt/storage/monitoring/grafana-dashboard.json b/AgCloud/storage_with_mqtt/storage/monitoring/grafana-dashboard.json new file mode 100644 index 000000000..acea070bc --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/monitoring/grafana-dashboard.json @@ -0,0 +1,244 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Dashboard showing MinIO bucket usage and PUT request latency\n", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "bew9cih96fx8gb" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "code", + "expr": "minio_s3_requests_ttfb_seconds_distribution{api=\"putobject\"}", + "fullMetaSearch": false, + "includeNullMetadata": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "PUT latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "bew9cih96fx8gb" + }, + "description": "minio_cluster_usage_total_bytes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.1.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "bew9cih96fx8gb" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "minio_cluster_usage_total_bytes", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Bucket size", + "type": "timeseries" + } + ], + "preload": false, + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "MinIO Monitoring", + "uid": "83c84814-bdf4-4a16-b3a0-b2cf9ae50de8", + "version": 3 + } \ No newline at end of file diff --git a/AgCloud/storage_with_mqtt/storage/monitoring/prometheus.yml b/AgCloud/storage_with_mqtt/storage/monitoring/prometheus.yml new file mode 100644 index 000000000..60dcb8c88 --- /dev/null +++ b/AgCloud/storage_with_mqtt/storage/monitoring/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 5s + +scrape_configs: + - job_name: minio-hot + metrics_path: /minio/v2/metrics/cluster + static_configs: + - targets: ['minio-hot:9000'] + - job_name: minio-cold + metrics_path: /minio/v2/metrics/cluster + static_configs: + - targets: ['minio-cold:9000'] diff --git a/AgCloud/streaming/flink/Dockerfile.flink-py b/AgCloud/streaming/flink/Dockerfile.flink-py new file mode 100644 index 000000000..60664e730 --- /dev/null +++ b/AgCloud/streaming/flink/Dockerfile.flink-py @@ -0,0 +1,19 @@ +FROM flink:1.18-scala_2.12-java11 + +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 python3-pip python3-venv curl && \ + rm -rf /var/lib/apt/lists/* + +RUN python3 -m pip install --no-cache-dir \ + "apache-flink==1.18.1" \ + aiohttp==3.9.5 \ + minio==7.2.7 \ + requests==2.32.3 \ + protobuf==3.20.3 \ + cloudpickle==2.2.1 + +ENV PYFLINK_CLIENT_EXECUTABLE=/usr/bin/python3 +ENV FLINK_PYTHON=/usr/bin/python3 + +USER flink diff --git a/AgCloud/streaming/flink/README_inference_integration_full.md b/AgCloud/streaming/flink/README_inference_integration_full.md new file mode 100644 index 000000000..1d23e13d8 --- /dev/null +++ b/AgCloud/streaming/flink/README_inference_integration_full.md @@ -0,0 +1,253 @@ +# README – Integrating a New Model Service into the Inference Flow (Kafka → Flink → HTTP) + +This document explains how to integrate a new model inference service into the generic flow: **Kafka → Flink Dispatcher → HTTP Inference Service**, without creating any new Dockerfiles. + +--- + +## Table of Contents +1. Overview +2. Existing Components +3. Recommended Directory Structure +4. Step-by-Step Integration Guide +5. Adapter Template +6. Updating the Model Registry +7. Adding Services to docker-compose +8. Kafka Input Message Schema +9. Health and Integration Testing +10. Common Troubleshooting +11. Quick Checklist for New Teams +12. FAQ + +--- + +## 1) Overview +- The **generic HTTP inference service** under `services/inference_http` exposes the endpoint `/infer_json`, which accepts `{bucket, key}` and returns the model’s output. +- The **Flink Dispatcher** consumes from the topic `imagery.new.`, performs HTTP requests, and routes results to success or DLQ topics. +- New teams **do not create new Dockerfiles**. Instead, they only: + 1) Add a new Adapter (Runner) for their model. + 2) Add a new case to `model_registry.py`. + 3) Add two compose services: `-inference-http` and `flink-dispatcher-`. + 4) Optionally update `requirements.txt` if extra dependencies are needed. + +--- + +## 2) Existing Components +- `services/inference_http/app.py` – FastAPI app exposing `/infer_json`, which retrieves objects from MinIO and runs the correct runner based on `TEAM`. +- `services/inference_http/model_registry.py` – Function `get_model_runner(team)` returning the proper runner instance. +- `services/inference_http/adapters/fruit_defect_runner.py` – Example adapter for loading weights and running inference. +- `streaming/flink/jobs/http_dispatcher.py` – PyFlink job that consumes Kafka messages and performs HTTP inference calls. +- Example compose services: `fruit-inference-http` (HTTP) and `flink-dispatcher-fruit` (Dispatcher). + +--- + +## 3) Recommended Directory Structure +``` +services/ + inference_http/ + app.py + model_registry.py + requirements.txt + adapters/ + fruit_defect_runner.py + _runner.py # your new runner + models/ + fruit_defect/... # example + /... # your model code (without weights) + weights/ + fruit_cls_best.ts + _best.pt # your model weights (mounted as volume) +``` +> Model weights are loaded from `/app/weights` inside the container. Read their path from the `WEIGHTS_PATH` environment variable. + +--- + +## 4) Step-by-Step Integration Guide +1. **Add model code** to `services/inference_http/models//...` (exclude weights). +2. **Create an Adapter (Runner)** at `services/inference_http/adapters/_runner.py` (see template below). +3. **Update the Model Registry** by adding a case for ``. +4. **Place your weights** in `services/inference_http/weights/` and set `WEIGHTS_PATH` if the filename differs. +5. **Dependencies** – if new Python packages are required, add them to `services/inference_http/requirements.txt`. Prefer wheels. If system (apt) packages are required, Dockerfile changes may be necessary (rare case). +6. **Add HTTP service** to compose as `-inference-http` with `TEAM=` and a volume for weights. +7. **Add Flink Dispatcher** as `flink-dispatcher-` consuming from `imagery.new.` and calling `http://-inference-http:8000/infer_json`. +8. **Check health** using `/healthz`. +9. **Test inference manually** via `curl`. +10. **Send a Kafka test message** to verify full flow. + +--- + +## 5) Adapter Template +`services/inference_http/adapters/_runner.py` +```python +from typing import Any, Dict, Optional + +class TeamXRunner: + def __init__(self, weights_path: Optional[str] = None, model_tag: Optional[str] = None): + import os + self.weights_path = weights_path or os.getenv("WEIGHTS_PATH", "/app/weights/_best.pt") + self.model_tag = model_tag + # TODO: Load model, e.g. torch.load(self.weights_path) + # self.model = load_model(self.weights_path) + + def run(self, image_bytes_or_uri: Any, model_tag: Optional[str] = None, extra: Optional[Dict] = None) -> Dict: + # image_bytes_or_uri may be raw image bytes or URI (e.g. s3://bucket/key) + # TODO: Preprocess, infer, postprocess + return { + "label": "example_label", + "score": 0.99, + "confidence": 0.99, + "latency_ms_model": 12 + } +``` +> **Recommended output keys:** `label`, `score`, `confidence`, `latency_ms_model`. + +--- + +## 6) Updating the Model Registry +`services/inference_http/model_registry.py` +```python +from adapters.fruit_defect_runner import FruitRunner +from adapters._runner import TeamXRunner # new + +def get_model_runner(team: str): + t = (team or "").lower() + if t == "fruit": + return FruitRunner() + if t == "": + return TeamXRunner() + raise ValueError(f"unknown TEAM {t}") +``` + +--- + +## 7) Adding Services to docker-compose + +### 7.1 HTTP Service +```yaml + -inference-http: + build: + context: ./services/inference_http + dockerfile: Dockerfile + environment: + - TEAM= + - WEIGHTS_PATH=/app/weights/_best.pt # optional + # - MINIO_ENDPOINT=minio-hot:9000 + # - MINIO_ACCESS_KEY=minioadmin + # - MINIO_SECRET_KEY=minioadmin123 + # - MINIO_SECURE=0 + volumes: + - ./services/inference_http/weights:/app/weights:ro + depends_on: + - minio-hot + networks: [ag_cloud] + restart: unless-stopped + # ports: + # - "8001:8000" # optional for local testing +``` + +### 7.2 Flink Dispatcher Service +```yaml + flink-dispatcher-: + image: agcloud-flink-py:1.18 + container_name: flink-dispatcher- + depends_on: + flink-jobmanager: { condition: service_started } + flink-taskmanager: { condition: service_started } + -inference-http: { condition: service_started } + networks: [ag_cloud] + environment: + - KAFKA_BOOTSTRAP=kafka:9092 + - INPUT_TOPIC=imagery.new. + - TEAM= + - HTTP_URL=http://-inference-http:8000/infer_json + - DLQ_TOPIC=dlq.inference.http + - GROUP_ID=http-dispatcher- + - PARALLELISM=2 + - PYFLINK_CLIENT_EXECUTABLE=/usr/bin/python3 + volumes: + - ./streaming/flink/jobs:/opt/flink/jobs:ro + - ./streaming/flink/connectors:/opt/flink/lib:ro + command: [ + "bash","-lc", + "set -e;\n\n echo 'Waiting for JobManager...';\n until /opt/flink/bin/flink list --jobmanager flink-jobmanager:8081 >/dev/null 2>&1; do\n echo 'still waiting...'; sleep 3;\n done;\n echo 'JobManager ready!';\n\n /opt/flink/bin/flink run \ + -Dpython.client.executable=/usr/bin/python3 \ + -Dpython.executable=/usr/bin/python3 \ + --jobmanager flink-jobmanager:8081 \ + --detached \ + --python /opt/flink/jobs/http_dispatcher.py \ + -- \ + --bootstrap ${KAFKA_BOOTSTRAP} \ + --input-topic ${INPUT_TOPIC} \ + --team ${TEAM} \ + --http-url ${HTTP_URL} \ + --group-id ${GROUP_ID} \ + --dlq-topic ${DLQ_TOPIC};\n tail -f /dev/null" + ] +``` + +--- + +## 8) Kafka Input Message Schema +Input topic: `imagery.new.` +```json +{ + "bucket": "imagery-hot", + "key": "path/to/image.jpg", + "camera": "", + "publish_ts_ms": 1699999999999 +} +``` +**Required fields:** `bucket` and `key`. + +--- + +## 9) Health and Integration Testing +1. **Check HTTP Health**: + ```bash + curl http://-inference-http:8000/healthz + ``` + Expected: `{ "ok": true, "team": "" }` + +2. **Manual Inference Test**: + ```bash + curl -X POST http://-inference-http:8000/infer_json -H 'Content-Type: application/json' -d '{"bucket":"imagery-hot","key":"path/to/image.jpg"}' + ``` + +3. **End-to-End Flow Test**: + Send a Kafka message to `imagery.new.` and confirm results or DLQ entries. + +--- + +## 10) Common Troubleshooting +- **HTTP service fails to start** → check `TEAM` env and `model_registry.py` entry. +- **Weights not found** → verify `WEIGHTS_PATH` and volume mapping. +- **ImportError in Adapter** → add dependencies to `requirements.txt` and rebuild. +- **HTTP timeout** → verify network `ag_cloud`, correct `HTTP_URL`, and resource load. +- **Kafka serialization/auth** → ensure connector JAR versions match and are mounted correctly. + +--- + +## 11) Quick Checklist for New Teams +- [ ] Add model under `models/` +- [ ] Create adapter `adapters/_runner.py` +- [ ] Add case to `model_registry.py` +- [ ] Place weights in `weights/` +- [ ] Update `requirements.txt` if needed +- [ ] Add `-inference-http` to compose +- [ ] Add `flink-dispatcher-` to compose +- [ ] Verify `/healthz` and `/infer_json` +- [ ] Send Kafka test message + +--- + +## 12) FAQ +**Q: Do we need a new Dockerfile?** +A: No, unless you require non-Python system dependencies. + +**Q: Where do weights go?** +A: In `services/inference_http/weights`, mounted to `/app/weights`. Define `WEIGHTS_PATH` if the filename differs. + +**Q: Which fields are required in Kafka messages?** +A: `bucket` and `key`. + +**Q: How can I test quickly?** +A: Use `/healthz`, send a test `/infer_json`, then a Kafka message to `imagery.new.`. diff --git a/AgCloud/streaming/flink/connectors/flink-connector-kafka-3.2.0-1.18.jar b/AgCloud/streaming/flink/connectors/flink-connector-kafka-3.2.0-1.18.jar new file mode 100644 index 000000000..f4f9345b2 Binary files /dev/null and b/AgCloud/streaming/flink/connectors/flink-connector-kafka-3.2.0-1.18.jar differ diff --git a/AgCloud/streaming/flink/connectors/flink-json-1.18.1.jar b/AgCloud/streaming/flink/connectors/flink-json-1.18.1.jar new file mode 100644 index 000000000..5e12e7bbb Binary files /dev/null and b/AgCloud/streaming/flink/connectors/flink-json-1.18.1.jar differ diff --git a/AgCloud/streaming/flink/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar b/AgCloud/streaming/flink/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar new file mode 100644 index 000000000..a46bcb565 Binary files /dev/null and b/AgCloud/streaming/flink/connectors/flink-sql-connector-kafka-3.2.0-1.18.jar differ diff --git a/AgCloud/streaming/flink/connectors/kafka-clients-3.2.3.jar b/AgCloud/streaming/flink/connectors/kafka-clients-3.2.3.jar new file mode 100644 index 000000000..76a9d2ad6 Binary files /dev/null and b/AgCloud/streaming/flink/connectors/kafka-clients-3.2.3.jar differ diff --git a/AgCloud/streaming/flink/connectors/lz4-java-1.8.0.jar b/AgCloud/streaming/flink/connectors/lz4-java-1.8.0.jar new file mode 100644 index 000000000..89c644b8e Binary files /dev/null and b/AgCloud/streaming/flink/connectors/lz4-java-1.8.0.jar differ diff --git a/AgCloud/streaming/flink/connectors/snappy-java-1.1.10.5.jar b/AgCloud/streaming/flink/connectors/snappy-java-1.1.10.5.jar new file mode 100644 index 000000000..7707e5878 Binary files /dev/null and b/AgCloud/streaming/flink/connectors/snappy-java-1.1.10.5.jar differ diff --git a/AgCloud/streaming/flink/jobs/http_dispatcher.py b/AgCloud/streaming/flink/jobs/http_dispatcher.py new file mode 100644 index 000000000..5fcdfb553 --- /dev/null +++ b/AgCloud/streaming/flink/jobs/http_dispatcher.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +""" +Flink 1.18 PyFlink — HTTP dispatcher (Kafka -> HTTP /infer_json) +- DataStream API: KafkaSource / KafkaSink +- WatermarkStrategy from pyflink.common (compatible with 1.18) +- Splits successful requests to inference.dispatched. and errors to DLQ +""" + +import os +import json +import uuid +import asyncio +import argparse +from typing import Any, Dict + +import aiohttp + +# PyFlink core +from pyflink.datastream import StreamExecutionEnvironment, RuntimeExecutionMode +from pyflink.datastream.functions import MapFunction, RuntimeContext + +# WatermarkStrategy/Types – in Flink 1.18 imported from pyflink.common +from pyflink.common import WatermarkStrategy, Types +from pyflink.common.serialization import SimpleStringSchema + +# Kafka connectors +from pyflink.datastream.connectors.kafka import ( + KafkaSource, + KafkaSink, + KafkaOffsetsInitializer, + KafkaRecordSerializationSchema, +) + +# ----------------------------- +# Args & Config +# ----------------------------- +def parse_args(): + p = argparse.ArgumentParser(description="HTTP dispatcher for imagery events") + p.add_argument("--bootstrap", default=os.getenv("KAFKA_BOOTSTRAP", "kafka:9092")) + p.add_argument("--input-topic", default=os.getenv("INPUT_TOPIC", "imagery.new.fruit")) + p.add_argument("--team", default=os.getenv("TEAM", "fruit")) + p.add_argument("--http-url", + default=os.getenv("HTTP_URL", "http://fruit-inference-http:8004/infer_json")) + p.add_argument("--dlq-topic", default=os.getenv("DLQ_TOPIC", "dlq.inference.http")) + p.add_argument("--group-id", default=os.getenv("GROUP_ID", "http-dispatcher-fruit")) + + # tuning + p.add_argument("--parallelism", type=int, default=int(os.getenv("PARALLELISM", "2"))) + p.add_argument("--http-connect-timeout", type=float, + default=float(os.getenv("HTTP_CONNECT_TIMEOUT", "2.0"))) + p.add_argument("--http-read-timeout", type=float, + default=float(os.getenv("HTTP_READ_TIMEOUT", "5.0"))) + p.add_argument("--http-max-retries", type=int, + default=int(os.getenv("HTTP_MAX_RETRIES", "5"))) + p.add_argument("--http-retry-backoff-s", type=float, + default=float(os.getenv("HTTP_RETRY_BACKOFF_S", "0.5"))) + return p.parse_args() + + +# ----------------------------- +# MapFunction – HTTP POST with retry +# ----------------------------- +class HttpMap(MapFunction): + def __init__(self, http_url: str, connect_timeout: float, read_timeout: float, + max_retries: int, retry_backoff_s: float, team: str): + self.http_url = http_url + self.connect_timeout = connect_timeout + self.read_timeout = read_timeout + self.max_retries = max_retries + self.retry_backoff_s = retry_backoff_s + self.team = team + self.loop = None + self.session = None + + def open(self, runtime_context: RuntimeContext): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + timeout = aiohttp.ClientTimeout(total=None, + connect=self.connect_timeout, + sock_read=self.read_timeout) + self.session = self.loop.run_until_complete(self._make_session(timeout)) + + async def _make_session(self, timeout: aiohttp.ClientTimeout) -> aiohttp.ClientSession: + return aiohttp.ClientSession(timeout=timeout) + + async def _post_once(self, url: str, payload: Dict[str, Any], headers: Dict[str, str]): + async with self.session.post(url, json=payload, headers=headers) as resp: + return resp.status, await resp.text() + + async def _post_with_retry(self, url: str, payload: Dict[str, Any], headers: Dict[str, str]): + attempt = 0 + while True: + try: + status, text = await self._post_once(url, payload, headers) + if 200 <= status < 300: + return {"ok": True, "status": status, "body": text} + # Do not retry most 4xx except 408/429 + if 400 <= status < 500 and status not in (408, 429): + return {"ok": False, "status": status, "body": text, "retry": False} + attempt += 1 + if attempt > self.max_retries: + return {"ok": False, "status": status, "body": text, "retry": False} + await asyncio.sleep(self.retry_backoff_s * attempt) + except Exception as e: + attempt += 1 + if attempt > self.max_retries: + return {"ok": False, "status": 599, "body": str(e), "retry": False} + await asyncio.sleep(self.retry_backoff_s * attempt) + + def map(self, s: str) -> str: + # 1) Parse JSON + try: + event = json.loads(s) + except Exception as e: + return json.dumps( + {"ok": False, "status": 422, "body": f"bad json: {e}", "raw": s, "stage": "parse"}, + ensure_ascii=False + ) + + # 2) Generate idempotency key + event_id = event.get("event_id") or str(uuid.uuid4()) + + # 3) Validate fields: must have bucket+key; image_uri not allowed + if "image_uri" in event: + return json.dumps( + {"ok": False, "status": 422, + "body": "image_uri not supported; use {bucket,key} only", + "event": event, "stage": "validate"}, + ensure_ascii=False + ) + + bucket = event.get("bucket") + key = event.get("key") + if not bucket or not key: + return json.dumps( + {"ok": False, "status": 422, + "body": "missing required fields: bucket and key", + "event": event, "stage": "validate"}, + ensure_ascii=False + ) + + # 4) Prepare headers and payload: send only bucket/key to the inference service + headers = { + "Content-Type": "application/json", + "Idempotency-Key": event_id, + "X-Correlation-ID": event_id, + } + payload = {"bucket": bucket, "key": key} + + # 5) Execute HTTP POST with retry logic + res = self.loop.run_until_complete(self._post_with_retry(self.http_url, payload, headers)) + + # 6) Wrap output into a consistent JSON response + out = { + "event_id": event_id, + "team": self.team, + "http_url": self.http_url, + **res, + "event": event + } + return json.dumps(out, ensure_ascii=False) + + def close(self): + try: + if self.session is not None: + self.loop.run_until_complete(self.session.close()) + finally: + if self.loop is not None: + self.loop.close() + + +# ----------------------------- +# Flink Topology +# ----------------------------- +def build_env(parallelism: int) -> StreamExecutionEnvironment: + env = StreamExecutionEnvironment.get_execution_environment() + env.set_runtime_mode(RuntimeExecutionMode.STREAMING) + env.set_parallelism(parallelism) + return env + + +def build_source(env: StreamExecutionEnvironment, bootstrap: str, group_id: str, topic: str): + src = ( + KafkaSource.builder() + .set_bootstrap_servers(bootstrap) + .set_group_id(group_id) + .set_topics(topic) + .set_starting_offsets(KafkaOffsetsInitializer.earliest()) + .set_value_only_deserializer(SimpleStringSchema()) + .build() + ) + return env.from_source(src, WatermarkStrategy.no_watermarks(), "kafka-source") + + +def build_sink(bootstrap: str, topic: str): + return ( + KafkaSink.builder() + .set_bootstrap_servers(bootstrap) + .set_record_serializer( + KafkaRecordSerializationSchema.builder() + .set_topic(topic) + .set_value_serialization_schema(SimpleStringSchema()) + .build() + ) + .build() + ) + + +def main(): + args = parse_args() + + env = build_env(args.parallelism) + ds = build_source(env, args.bootstrap, args.group_id, args.input_topic) + + mapper = HttpMap( + http_url=args.http_url, + connect_timeout=args.http_connect_timeout, + read_timeout=args.http_read_timeout, + max_retries=args.http_max_retries, + retry_backoff_s=args.http_retry_backoff_s, + team=args.team, + ) + dispatched = ds.map(mapper, output_type=Types.STRING()) + + def _is_ok(s: str) -> bool: + try: + return bool(json.loads(s).get("ok")) + except Exception: + return False + + ok_stream = dispatched.filter(_is_ok) + bad_stream = dispatched.filter(lambda s: not _is_ok(s)) + + ok_topic = f"inference.dispatched.{args.team}" + ok_stream.sink_to(build_sink(args.bootstrap, ok_topic)) + bad_stream.sink_to(build_sink(args.bootstrap, args.dlq_topic)) + + env.execute(f"http-dispatcher-{args.team}") + + +if __name__ == "__main__": + main() diff --git a/AgCloud/templates/templates.yml b/AgCloud/templates/templates.yml new file mode 100644 index 000000000..228971b89 --- /dev/null +++ b/AgCloud/templates/templates.yml @@ -0,0 +1,93 @@ +templates: + smoke_detected: + category: environmental + summary: "🚨 Smoke detected by ${device_id} near ${area} (confidence ${confidence})" + recommendation: "Inspect the ${area} immediately. If fire is confirmed, contact emergency services." + + fruit_ripeness_high: + category: agriculture + summary: "🍓 High fruit ripeness detected by ${device_id} (${confidence} ≥ ${threshold})" + recommendation: "Harvest or inspect the plantation area. Ripe/overripe fruits ratio: ${description}" + + fence_hole: + category: security + summary: "🚧 Fence hole detected by ${device_id} in ${area} (conf ${confidence})" + recommendation: "Dispatch a patrol to ${area} and review recent footage." + + + plant_drought_detected: + category: agriculture + summary: "🌿 Plant drought detected by ${device_id} (severity ${severity}, confidence ${confidence})" + recommendation: "Check irrigation in ${area}. Status: ${watering_status}. Audio file: ${file}" + + suspicious_sound-predatory_animals: + category: security + summary: "⚠️ Predatory animal sound detected by ${device_id} (confidence ${confidence})" + recommendation: "Stay alert and avoid the area until confirmed safe." + + suspicious_sound-non_predatory_animals: + category: environment + summary: "🐾 Animal sounds detected by ${device_id}" + recommendation: "No immediate action required. Monitor if sounds persist." + + suspicious_sound-birds: + category: environment + summary: "🐦 Bird activity detected by ${device_id}" + recommendation: "Normal environmental sounds. No action needed." + + suspicious_sound-fire: + category: emergency + summary: "🔥 Possible fire sounds detected by ${device_id} (confidence ${confidence})" + recommendation: "Inspect the area for signs of fire and alert authorities if confirmed." + + suspicious_sound-footsteps: + category: security + summary: "👣 Footsteps detected by ${device_id} at ${timestamp}" + recommendation: "Check surveillance feed or nearby activity." + + suspicious_sound-insects: + category: environment + summary: "🐝 Insect sounds detected by ${device_id}" + recommendation: "Low concern. Suitable for ecological monitoring." + + suspicious_sound-screaming: + category: emergency + summary: "🚨 Screaming detected by ${device_id}" + recommendation: "Verify immediately. Contact security or emergency services if needed." + + suspicious_sound-shotgun: + category: security + summary: "🔫 Possible gunshot detected by ${device_id}" + recommendation: "Treat as high-priority event. Notify authorities immediately." + + suspicious_sound-stormy_weather: + category: environmental + summary: "🌩️ Storm sounds detected by ${device_id}" + recommendation: "Monitor weather alerts and secure outdoor equipment." + + suspicious_sound-streaming_water: + category: environmental + summary: "💧 Flowing water detected by ${device_id}" + recommendation: "Check for flooding or irrigation system leaks." + + suspicious_sound-vehicle: + category: movement + summary: "🚗 Vehicle detected by ${device_id} at ${timestamp}" + recommendation: "Verify authorized vehicle access in ${area}." + + + masked_person: + category: security + summary: "Person wearing a mask detected by ${device_id}" + recommendation: "Verify the person’s authorization using the live feed." + + "intruding animal": + category: security + summary: "🐾 Intruding ${subject} detected by ${device_id}" + recommendation: "Assess whether the ${subject} poses a threat or damage risk. Consider activating deterrent systems or notifying farm personnel." + + climbing_fence: + category: perimeter + summary: "🧗 Fence-climbing activity of ${subject} detected by ${device_id}" + recommendation: "Inspect the fence section immediately. Possible intruder or wildlife breach attempt." + diff --git a/AgCloud/tools/send_alert_kafka.py b/AgCloud/tools/send_alert_kafka.py new file mode 100644 index 000000000..02ecf9b8f --- /dev/null +++ b/AgCloud/tools/send_alert_kafka.py @@ -0,0 +1,54 @@ +import json, uuid, argparse, datetime +from kafka import KafkaProducer + +# --------- CLI --------- +p = argparse.ArgumentParser(description="Send alert to Kafka") +p.add_argument("--bootstrap", default="localhost:9092") +p.add_argument("--topic", default="alerts") +p.add_argument("--alert-type", required=True) +p.add_argument("--device-id", required=True) +p.add_argument("--started-at", default=None, help="ISO8601; default=now UTC") +p.add_argument("--lat", type=float, default=None) +p.add_argument("--lon", type=float, default=None) +p.add_argument("--image-url", default=None) +p.add_argument("--severity", type=int, default=3) +p.add_argument("--area", default="north_field") +args = p.parse_args() + +# --------- Build payload --------- +# NOTE: Comments are NOT sent; kept here for clarity +payload = { + # --- Required fields --- + "alert_id": str(uuid.uuid4()), + "alert_type": args.alert_type, # e.g. "fence_hole" or "smoke_detected" + "device_id": args.device_id, # e.g. "camera-01" + "started_at": ( + args.started_at + if args.started_at + else datetime.datetime.now(datetime.timezone.utc).isoformat() + ), + # --- Optional fields --- + "confidence": None, # fill by your detector if available + "severity": args.severity, + "area": args.area, + "lat": args.lat, + "lon": args.lon, + "image_url": args.image_url, + "meta": {}, # you can enrich with bucket/key, etc. +} + +# Remove None fields to keep payload clean +payload = {k: v for k, v in payload.items() if v is not None} + +# --------- Send --------- +producer = KafkaProducer( + bootstrap_servers=args.bootstrap, + value_serializer=lambda v: json.dumps(v, ensure_ascii=False).encode("utf-8"), + linger_ms=50, + retries=5, +) +fut = producer.send(args.topic, payload) +rec = fut.get(timeout=10) +producer.flush() +print(f"OK: topic={args.topic} partition={rec.partition} offset={rec.offset}") +print(json.dumps(payload, ensure_ascii=False))